+ THE WIRE
+
+ --:--:--
+ Session open — 312 props graded across 47 games.
+ LOG ▸
+
+
@@ -110,9 +140,22 @@
-
VYNDR
-
-
SPORTS INTELLIGENCE TERMINAL
+
+
+
+
+
+
+ VYNDR
+
+
+ SPORTS INTELLIGENCE TERMINAL
+
@@ -143,13 +186,13 @@
-
VISUAL SYSTEM · v2
+
01 · THE TERMINAL · VISUAL SYSTEM v2
The instrument, not the sportsbook.
Dense but ranked. Mostly still, then it reacts — a number pulses the instant a line moves, a card breathes when its game goes live. One hero per screen.
-
+
TONIGHT'S SLATE
@@ -270,6 +313,19 @@
▲opened 25.5
+
+
+
+
+ OPEN 25.5 · 9:02 AM
+ STEAM 4:40 PM ▲
+ NOW 27.5
+
+
@@ -322,8 +378,13 @@
-
-
+
+
+ 02
+
THE SYSTEM
Five components + 74 marks. Every other page is assembled from these.
+
+
+
COMPONENT SYSTEMTHE PARTS EVERY SURFACE INHERITS
@@ -467,7 +528,7 @@
-
+
ARCHETYPE GLYPHSFULL SYSTEM · 74 UNIQUE MARKS · 8 SPORTS
@@ -525,7 +586,13 @@
-
+
+
+ 03
+
THE SURFACES
What gets posted. The timeline is customer #1 — these stop a scroll.
+
+
+
STREAKSA BLOOMBERG ALERT, NOT A LOG LINE
@@ -719,7 +786,10 @@
- VYNDR
+
+
+ VYNDR
+ JUL 11
@@ -761,7 +831,10 @@
- VYNDR
+
+
+ VYNDR
+ CLV-VERIFIED
@@ -787,7 +860,13 @@
-
+
+
+ 04
+
THE INTELLIGENCE
One analysis, many grades — style matchups, arsenal reads, correlation math.
COMPONENTS · 74 GLYPHS · EDGE BOARD · /u · SHARE · COMBAT · LIVE GRADE · PRO · STATESVYNDR VISUAL SYSTEM · v2
@@ -1239,6 +1318,30 @@ class Component extends DCLogic {
clock(){ const d=new Date(); const p=n=>String(n).padStart(2,'0'); return p(d.getMinutes())+':'+p(d.getSeconds()); }
+ setupScroll(){
+ if(!this.rootNode) return;
+ // arrival: sections + act dividers boot in once when reached
+ const sections=[...this.rootNode.querySelectorAll('[data-screen-label],[data-reveal]')];
+ sections.forEach(s=>{ if(!s.hasAttribute('data-reveal')) s.setAttribute('data-reveal',''); });
+ this._io=new IntersectionObserver((ents)=>{
+ ents.forEach(en=>{ if(en.isIntersecting){ en.target.classList.add('vy-in'); this._io.unobserve(en.target); } });
+ },{threshold:.08});
+ sections.forEach(s=>this._io.observe(s));
+ // safety: anything above the fold reveals immediately
+ setTimeout(()=>{ sections.forEach(s=>{ if(s.getBoundingClientRect().top{
+ ents.forEach(en=>{
+ if(!en.isIntersecting) return;
+ const id=en.target.id;
+ this.rootNode.querySelectorAll('.vy-navdot').forEach(a=>a.classList.toggle('on', a.getAttribute('data-nav')===id));
+ });
+ },{rootMargin:'-30% 0px -55% 0px'});
+ map.forEach(id=>{ const el=this.rootNode.querySelector('#'+id); if(el) this._so.observe(el); });
+ }
+
setupParlay(){
if(!this.parlayLegs) return;
const legs=[...this.parlayLegs.querySelectorAll('[data-leg]')];
@@ -1445,6 +1548,7 @@ class Component extends DCLogic {
this.buildGradeHistory();
this.setupBoardHover();
this.setupParlay();
+ this.setupScroll();
this._key=(e)=>{
const k=(e.key||'').toLowerCase();
if((e.metaKey||e.ctrlKey) && k==='k'){ e.preventDefault(); if(this.state.cmdOpen){ this.setState({cmdOpen:false}); } else { this.openCmdBar(); } }
@@ -1452,6 +1556,30 @@ class Component extends DCLogic {
};
document.addEventListener('keydown',this._key);
+ // THE WIRE — session log: one entry, holds, then the next
+ this._wireEntries=[
+ ['STEAM','Jokić o27.5 PTS — line 25.5 → 27.5 since open. Grade holds A+.','#00D4A0'],
+ ['GRADE','Skenes o6.5 K firmed to A — CHC whiff rate confirms the read.','#00D4A0'],
+ ['LIVE','Edwards o5.5 AST pacing ▲ — usage +6% with Gobert off.','#00D4A0'],
+ ['FLAG','Vlad Jr. streak correlated with team total — stake trimmed.','#FFB347'],
+ ['CLV','Portfolio closing-line value tonight: +2.8% across 14 edges.','#B8BCC8'],
+ ['FADE','LaVine o22.5 drifting — MIL perimeter D. Graded D, avoid.','#FF4757']
+ ];
+ this._wi=0;
+ const wireTick=()=>{
+ if(!this.rootNode) return;
+ const w=this.rootNode.querySelector('[data-wire]');
+ const t=this.rootNode.querySelector('[data-wire-time]');
+ if(!w) return;
+ const [tag,msg,col]=this._wireEntries[this._wi % this._wireEntries.length]; this._wi++;
+ w.innerHTML=''+tag+' · '+msg;
+ if(t){const d=new Date();const p=n=>String(n).padStart(2,'0');t.textContent=p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds());}
+ w.style.animation='none'; void w.offsetWidth;
+ w.style.animation='vy-wirein 6.4s ease-out';
+ };
+ this._wire=setInterval(wireTick,6500);
+ setTimeout(wireTick,1800);
+
// live grade-shift micro-view — grade reacts to game state
this._lg=setInterval(()=>{
if(this.props.reactionsLive===false || !this._booted || !this.rootNode) return;
@@ -1563,6 +1691,9 @@ class Component extends DCLogic {
clearInterval(this._rx);
clearTimeout(this._bootTimer);
clearInterval(this._lg);
+ clearInterval(this._wire);
+ if(this._io) this._io.disconnect();
+ if(this._so) this._so.disconnect();
if(this._key) document.removeEventListener('keydown',this._key);
}
}
diff --git a/tests/unit/vyndrParityQA.test.js b/tests/unit/vyndrParityQA.test.js
index dad1db0..f2a0c3d 100644
--- a/tests/unit/vyndrParityQA.test.js
+++ b/tests/unit/vyndrParityQA.test.js
@@ -140,3 +140,34 @@ describe('QA.22 — honest waiting copy everywhere (no passive mystery)', () =>
expect(read(rel)).toContain('nextRunLabelET');
});
});
+
+// ── DESIGN COMPLETION TRAIN — M1 mobile foundation (2026-07-16) ──────────
+// Structural mobile rules become failing tests (M4 "test-lock what's
+// lockable"). These assert the RULES exist in source — they do NOT prove
+// visual correctness at 390px (that needs the Chrome audit).
+describe('M1 — mobile foundation is wired (structural, not visual)', () => {
+ const css = read('app/globals.css');
+
+ it('header collapses on phones: the decorative EKG is hidden <768px', () => {
+ // The audit flagged ticker + SIGNAL LIVE + STALE stacking in ~110px.
+ expect(css).toMatch(/\.hb-ekg\s*\{[^}]*display:\s*none/);
+ // and the hook actually exists on the component
+ expect(read('components/vyndr/LiveLayer.tsx')).toMatch(/hb-ekg/);
+ expect(read('components/vyndr/LiveLayer.tsx')).toMatch(/heartbeat-bar/);
+ });
+
+ it('primary CTA meets a 44px touch target on mobile', () => {
+ expect(css).toMatch(/\.vbtn\s*\{[^}]*min-height:\s*44px/);
+ // full-size VBtn carries the class; dense `small` buttons opt out
+ expect(read('components/vyndr/VBtn.tsx')).toMatch(/small\s*\?\s*undefined\s*:\s*'vbtn'/);
+ });
+
+ it('the M1 mobile-foundation block is present and honestly labelled unverified', () => {
+ expect(css).toMatch(/DESIGN COMPLETION TRAIN — M1 MOBILE FOUNDATION/);
+ expect(css).toMatch(/VISUALLY UNVERIFIED at 390px/);
+ });
+
+ it('document never scrolls sideways on phones (overflow contained)', () => {
+ expect(css).toMatch(/@media\s*\(max-width:\s*767px\)\s*\{[^]*?html,\s*body\s*\{[^}]*overflow-x:\s*hidden/);
+ });
+});
diff --git a/web/src/app/globals.css b/web/src/app/globals.css
index 9c73df8..92c5f4b 100644
--- a/web/src/app/globals.css
+++ b/web/src/app/globals.css
@@ -1361,6 +1361,51 @@ html[data-font="readable"] .wm::after { opacity: 0.45 !important; }
.gl-full.gl-expanded { margin-top: 10px; }
}
+/* ============================================================
+ DESIGN COMPLETION TRAIN — M1 MOBILE FOUNDATION (2026-07-16)
+ The mockup (specs/design-reference/vyndr-system.html) is DESKTOP-ONLY.
+ These rules are the deliberate 390px expression of its grammar — not a
+ shrink: ONE animated element in the header (DESIGN-SPEC §4), one bold
+ mono hero per card with muted context (§3/§5), ≥44px primary tap targets,
+ zero horizontal overflow. BUILT TO SPEC, VISUALLY UNVERIFIED at 390px
+ (WSL2↔Chrome unreachable here — a true-390px Chrome audit is the eyes).
+ ============================================================ */
+
+/* Header-zone collapse (audit: ticker + SIGNAL LIVE + STALE all stacking in
+ ~110px on narrow). The EKG is decorative proof-of-life — dropped on phones
+ so the heartbeat reads as ONE clean status line, and the ticker + live-dot
+ become the single animated element in the header. */
+@media (max-width: 767px) {
+ .hb-ekg { display: none !important; }
+ .heartbeat-bar {
+ height: 28px !important;
+ gap: 10px !important;
+ padding: 0 12px !important;
+ }
+ /* The status line is one line by contract — never let it wrap into the
+ "stacked" state the audit flagged. */
+ .heartbeat-bar > span { white-space: nowrap; }
+}
+
+/* Tap targets — the PRIMARY button component meets the 44px touch minimum.
+ Scoped to .vbtn only (not dense in-row data chips — density is a feature;
+ per-surface tap-target review rides with each M1b surface). */
+@media (max-width: 767px) {
+ .vbtn {
+ min-height: 44px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
+
+/* Mono hero grammar (§3 "one bold hero figure among muted context"). Card
+ hero numbers opt in via .m-hero; they scale up on phones so the ONE figure
+ that matters dominates the stacked card. Context stays demoted. */
+@media (max-width: 640px) {
+ .m-hero { font-size: clamp(28px, 12vw, 56px) !important; line-height: 1; }
+}
+
/* ============================================================
Session 60 (night2/E) — §7 reveal choreography.
Stamp slam + 90ms staggered context panels. Entrance keyframes floor at
diff --git a/web/src/components/vyndr/LiveLayer.tsx b/web/src/components/vyndr/LiveLayer.tsx
index 1a115f3..3e0e091 100644
--- a/web/src/components/vyndr/LiveLayer.tsx
+++ b/web/src/components/vyndr/LiveLayer.tsx
@@ -118,7 +118,7 @@ export function HeartbeatBar() {
const syncColor = tier === 'stale' ? 'var(--miss)' : tier === 'amber' ? 'var(--amber)' : 'var(--text-2)';
return (
SIGNAL LIVE
- {/* EKG strip */}
-
+ {/* EKG strip — decorative proof-of-life. Hidden <768px (mobile header
+ collapses to ONE status line; the signal pulse + ticker are the single
+ animated element there — DESIGN-SPEC §4 "one animated element"). */}
+