Design completion train — import authoritative mockup + M1a mobile foundation

SETUP (required first step): overwrote specs/design-reference/ with the CURRENT
authoritative mockup ("Vyndr System.dc.html" from claude.ai/design, the version
with the combat card / pitcher identity / live grade-shift / correlation builder
/ FREE|PRO pricing / TRANSMISSION QUIET). 12 surfaces. Confirmed it has NO media
queries — desktop-only, so the 390px expression is a deliberate design decision,
not a shrink.

M1a — mobile foundation (built to spec, VISUALLY UNVERIFIED at 390px; WSL2↔Chrome
unreachable, the Chrome audit is the eyes):
- Header-zone collapse (the concrete audit finding: ticker + SIGNAL LIVE + STALE
  stacking in ~110px). The decorative EKG is dropped <768px so the heartbeat
  reads as ONE clean status line; signal pulse + ticker are the single animated
  element (DESIGN-SPEC §4). LiveLayer gains .heartbeat-bar / .hb-ekg hooks.
- Primary CTA (.vbtn) meets the 44px touch target on mobile; dense `small`
  buttons opt out (density is a feature).
- .m-hero mono-hero clamp for the one-figure-per-card grammar at 390px.
- M4 test-lock: vyndrParityQA asserts the header collapse, tap target, overflow
  containment, and the honest "UNVERIFIED at 390px" label are all in source.

Per-surface stacking (M1b), billboards (M2), desktop parity (M3) continue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-16 05:09:14 -04:00
parent 520548d3f7
commit a10227ee5a
5 changed files with 228 additions and 16 deletions
+144 -13
View File
@@ -34,6 +34,18 @@
@keyframes vy-eventin{0%{opacity:0;transform:translateY(-3px)}12%,80%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-3px)}}
@keyframes vy-cmdin{0%{opacity:0;transform:translateY(-10px) scale(.98)}100%{opacity:1;transform:none}}
@keyframes vy-detailin{0%{opacity:0;transform:translateY(-2px)}100%{opacity:1;transform:none}}
@keyframes vy-wirein{0%{opacity:0;transform:translateY(4px)}8%,86%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-3px)}}
@keyframes vy-cursor{0%,49%{opacity:1}50%,100%{opacity:0}}
@keyframes vy-markflick{0%,88%,100%{opacity:1;filter:none}89%{opacity:.55;filter:drop-shadow(1.5px 0 rgba(255,71,87,.8)) drop-shadow(-1.5px 0 rgba(0,212,160,.8))}91%{opacity:1;filter:none}94%{opacity:.7;filter:drop-shadow(-1px 0 rgba(255,71,87,.7))}95%{opacity:1;filter:none}}
[data-reveal]{opacity:0;transform:translateY(16px);transition:opacity .6s cubic-bezier(.2,.8,.2,1),transform .6s cubic-bezier(.2,.8,.2,1)}
[data-reveal].vy-in{opacity:1;transform:none}
html{scroll-behavior:smooth}
.vy-navdot{display:flex;align-items:center;gap:9px;text-decoration:none;padding:3px 0}
.vy-navdot span.d{width:6px;height:6px;border-radius:2px;background:#2A2A38;transition:background .2s,box-shadow .2s}
.vy-navdot span.l{font-family:'JetBrains Mono',monospace;font-size:8.5px;letter-spacing:.16em;color:#4a4a58;opacity:0;transform:translateX(-4px);transition:opacity .2s,transform .2s,color .2s}
.vy-navdot:hover span.l{opacity:1;transform:none}
.vy-navdot.on span.d{background:#00D4A0;box-shadow:0 0 8px rgba(0,212,160,.6)}
.vy-navdot.on span.l{opacity:1;transform:none;color:#00D4A0}
.rx-up{animation:vy-rxup .75s ease-out}
.rx-down{animation:vy-rxdown .75s ease-out}
.vy-scanwrap{position:absolute;inset:0;overflow:hidden;pointer-events:none;border-radius:inherit}
@@ -44,6 +56,24 @@
<div class="vy-vignette" aria-hidden="true"></div>
<div class="vy-grain" aria-hidden="true"></div>
<!-- ============ JUMP RAIL ============ -->
<nav style="position:fixed;right:22px;top:50%;transform:translateY(-50%);z-index:46;display:flex;flex-direction:column;gap:4px;align-items:flex-end">
<a class="vy-navdot" href="#terminal" data-nav="terminal"><span class="l">TERMINAL</span><span class="d"></span></a>
<a class="vy-navdot" href="#system" data-nav="system"><span class="l">SYSTEM</span><span class="d"></span></a>
<a class="vy-navdot" href="#glyphs" data-nav="glyphs"><span class="l">GLYPHS</span><span class="d"></span></a>
<a class="vy-navdot" href="#surfaces" data-nav="surfaces"><span class="l">SURFACES</span><span class="d"></span></a>
<a class="vy-navdot" href="#intel" data-nav="intel"><span class="l">INTELLIGENCE</span><span class="d"></span></a>
</nav>
<!-- ============ THE WIRE — session log, one entry at a time ============ -->
<div style="position:fixed;left:0;right:0;bottom:0;z-index:45;display:flex;align-items:center;gap:14px;height:36px;padding:0 40px;background:rgba(6,6,11,.88);backdrop-filter:blur(14px);border-top:1px solid #14141E">
<span class="mono" style="font-size:9px;letter-spacing:.26em;color:#00D4A0;font-weight:800;flex:none">THE WIRE</span>
<span style="width:1px;height:14px;background:#1E1E2A;flex:none"></span>
<span class="mono" data-wire-time style="font-size:10px;color:#707080;letter-spacing:.06em;flex:none">--:--:--</span>
<span class="mono" data-wire style="font-size:11px;color:#B8BCC8;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1">Session open — 312 props graded across 47 games.</span>
<span class="mono" style="font-size:9px;letter-spacing:.16em;color:#4a4a58;flex:none">LOG ▸</span>
</div>
<!-- ============ ⌘K COMMAND BAR ============ -->
<sc-if value="{{ cmdOpen }}" hint-placeholder-val="{{ false }}">
<div onClick="{{ closeCmd }}" style="position:fixed;inset:0;z-index:60;background:rgba(4,4,8,.72);backdrop-filter:blur(7px);display:flex;align-items:flex-start;justify-content:center;padding-top:11vh">
@@ -110,9 +140,22 @@
<!-- ============ TOP BAR ============ -->
<div style="position:sticky;top:0;z-index:40;display:flex;align-items:center;justify-content:space-between;padding:14px 40px;background:rgba(6,6,11,.82);backdrop-filter:blur(14px);border-bottom:1px solid #14141E">
<div style="display:flex;align-items:center;gap:14px">
<div class="vy-glitch mono" style="font-weight:800;font-size:19px;letter-spacing:.32em;color:#F0F0F0;animation:vy-glitch 6.5s steps(1) infinite">VYND<span style="color:#00D4A0">R</span></div>
<div style="width:1px;height:16px;background:#2A2A38"></div>
<div class="mono" style="font-size:10.5px;letter-spacing:.34em;color:#707080">SPORTS INTELLIGENCE TERMINAL</div>
<!-- logo mark: signal V — a caret intercepting the line -->
<div style="position:relative;width:30px;height:30px;flex:none;animation:vy-markflick 6.5s steps(1) infinite">
<svg width="30" height="30" viewBox="0 0 32 32" fill="none" style="display:block">
<rect x="1" y="1" width="30" height="30" rx="7" fill="#0E0E14" stroke="#2A2A38" stroke-width="1"></rect>
<path d="M8 9 L16 24 L24 9" fill="none" stroke="#F0F0F0" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M16 24 L24 9" fill="none" stroke="#00D4A0" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"></path>
<circle cx="24" cy="9" r="2" fill="#00D4A0"></circle>
</svg>
</div>
<div style="display:flex;flex-direction:column;gap:1px">
<div style="display:flex;align-items:center;gap:2px">
<span class="vy-glitch mono" style="font-weight:800;font-size:19px;letter-spacing:.32em;color:#F0F0F0;animation:vy-glitch 6.5s steps(1) infinite;line-height:1">VYND<span style="color:#00D4A0">R</span></span>
<span style="width:8px;height:17px;background:#00D4A0;margin-left:2px;animation:vy-cursor 1.1s steps(1) infinite"></span>
</div>
<span class="mono" style="font-size:8px;letter-spacing:.42em;color:#707080;line-height:1">SPORTS INTELLIGENCE TERMINAL</span>
</div>
</div>
<div style="display:flex;align-items:center;gap:20px">
<span class="mono" data-event style="font-size:10.5px;letter-spacing:.1em;color:#00D4A0;opacity:0;min-width:0;white-space:nowrap"></span>
@@ -143,13 +186,13 @@
<!-- ============ MASTHEAD ============ -->
<div style="padding:52px 0 30px">
<div class="mono" style="font-size:11px;letter-spacing:.34em;color:#00D4A0;margin-bottom:16px">VISUAL SYSTEM · v2</div>
<div class="mono" style="font-size:11px;letter-spacing:.34em;color:#00D4A0;margin-bottom:16px">01 · THE TERMINAL · VISUAL SYSTEM v2</div>
<h1 style="font-size:44px;font-weight:800;letter-spacing:-.02em;line-height:1.02;max-width:760px">The instrument, not the sportsbook.</h1>
<p style="margin-top:16px;max-width:560px;color:#B8BCC8;font-size:15px;line-height:1.55">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.</p>
</div>
<!-- ============ HERO ASSEMBLY ============ -->
<div data-screen-label="Hero assembly" style="margin-top:14px">
<div id="terminal" data-screen-label="Hero assembly" style="margin-top:14px">
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:14px">
<div style="display:flex;align-items:center;gap:12px">
<span class="mono" style="font-size:12px;letter-spacing:.28em;color:#F0F0F0;font-weight:700">TONIGHT'S SLATE</span>
@@ -270,6 +313,19 @@
<span class="mono" style="font-size:12px;color:#00D4A0"></span>
<span class="mono" style="font-size:10.5px;color:#707080;letter-spacing:.1em">opened 25.5</span>
</div>
<!-- steam sparkline: the story of the line -->
<div style="margin-top:12px">
<svg width="100%" height="34" viewBox="0 0 220 34" preserveAspectRatio="none" style="display:block">
<polyline points="0,26 30,26 55,24 80,25 105,18 130,17 155,10 185,8 214,5" fill="none" stroke="#00D4A0" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" opacity=".9"></polyline>
<polyline points="0,26 30,26 55,24 80,25 105,18 130,17 155,10 185,8 214,5 214,34 0,34" fill="rgba(0,212,160,.07)" stroke="none"></polyline>
<circle cx="214" cy="5" r="2.6" fill="#00D4A0" style="animation:vy-livedot 1.5s ease-in-out infinite"></circle>
</svg>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:4px">
<span class="mono" style="font-size:8.5px;letter-spacing:.12em;color:#4a4a58">OPEN 25.5 · 9:02 AM</span>
<span class="mono" style="font-size:8.5px;letter-spacing:.12em;color:#00D4A0">STEAM 4:40 PM ▲</span>
<span class="mono" style="font-size:8.5px;letter-spacing:.12em;color:#4a4a58">NOW 27.5</span>
</div>
</div>
</div>
</div>
@@ -322,8 +378,13 @@
</div>
</div>
<!-- ============ COMPONENT GALLERY ============ -->
<div data-screen-label="Component gallery" style="margin-top:64px">
<!-- ============ ACT II ============ -->
<div data-reveal style="margin-top:110px;display:flex;align-items:baseline;gap:18px;border-bottom:1px solid #14141E;padding-bottom:18px">
<span class="mono" style="font-size:64px;font-weight:800;line-height:.8;color:#14141E;-webkit-text-stroke:1px #2A2A38">02</span>
<div><div class="mono" style="font-size:13px;letter-spacing:.3em;color:#F0F0F0;font-weight:800">THE SYSTEM</div><div style="font-size:12.5px;color:#707080;margin-top:5px">Five components + 74 marks. Every other page is assembled from these.</div></div>
</div>
<div id="system" data-screen-label="Component gallery" style="margin-top:44px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px">
<span class="mono" style="font-size:12px;letter-spacing:.28em;color:#F0F0F0;font-weight:700">COMPONENT SYSTEM</span>
<span class="mono" style="font-size:10.5px;letter-spacing:.18em;color:#707080">THE PARTS EVERY SURFACE INHERITS</span>
@@ -467,7 +528,7 @@
</div>
<!-- ============ ARCHETYPE GLYPHS ============ -->
<div data-screen-label="Archetype glyphs" style="margin-top:64px">
<div id="glyphs" data-screen-label="Archetype glyphs" style="margin-top:64px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px">
<span class="mono" style="font-size:12px;letter-spacing:.28em;color:#F0F0F0;font-weight:700">ARCHETYPE GLYPHS</span>
<span class="mono" style="font-size:10.5px;letter-spacing:.18em;color:#707080">FULL SYSTEM · 74 UNIQUE MARKS · 8 SPORTS</span>
@@ -525,7 +586,13 @@
</div>
<!-- ============ STREAKS BILLBOARD ============ -->
<div data-screen-label="Streaks billboard" style="margin-top:64px">
<!-- ============ ACT III ============ -->
<div data-reveal style="margin-top:110px;display:flex;align-items:baseline;gap:18px;border-bottom:1px solid #14141E;padding-bottom:18px">
<span class="mono" style="font-size:64px;font-weight:800;line-height:.8;color:#14141E;-webkit-text-stroke:1px #2A2A38">03</span>
<div><div class="mono" style="font-size:13px;letter-spacing:.3em;color:#F0F0F0;font-weight:800">THE SURFACES</div><div style="font-size:12.5px;color:#707080;margin-top:5px">What gets posted. The timeline is customer #1 — these stop a scroll.</div></div>
</div>
<div id="surfaces" data-screen-label="Streaks billboard" style="margin-top:44px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px">
<span class="mono" style="font-size:12px;letter-spacing:.28em;color:#F0F0F0;font-weight:700">STREAKS</span>
<span class="mono" style="font-size:10.5px;letter-spacing:.18em;color:#707080">A BLOOMBERG ALERT, NOT A LOG LINE</span>
@@ -719,7 +786,10 @@
<div style="width:360px;height:450px;border-radius:14px;overflow:hidden;border:1px solid #2A2A38;box-shadow:0 24px 60px -24px rgba(0,0,0,.8)">
<div style="width:1080px;height:1350px;transform:scale(.3333);transform-origin:top left;background:radial-gradient(700px 500px at 80% 0%,rgba(0,212,160,.09),transparent 60%),#06060B;color:#F0F0F0;padding:72px 68px;display:flex;flex-direction:column">
<div style="display:flex;align-items:center;justify-content:space-between">
<span class="mono" style="font-size:34px;font-weight:800;letter-spacing:.3em">VYND<span style="color:#00D4A0">R</span></span>
<span style="display:flex;align-items:center;gap:16px">
<svg width="52" height="52" viewBox="0 0 32 32" fill="none"><rect x="1" y="1" width="30" height="30" rx="7" fill="#0E0E14" stroke="#2A2A38" stroke-width="1"></rect><path d="M8 9 L16 24 L24 9" fill="none" stroke="#F0F0F0" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M16 24 L24 9" fill="none" stroke="#00D4A0" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"></path><circle cx="24" cy="9" r="2" fill="#00D4A0"></circle></svg>
<span class="mono" style="font-size:34px;font-weight:800;letter-spacing:.3em">VYND<span style="color:#00D4A0">R</span></span>
</span>
<span class="mono" style="font-size:22px;letter-spacing:.1em;color:#707080">JUL 11</span>
</div>
<div style="margin-top:56px">
@@ -761,7 +831,10 @@
<div style="width:540px;height:283px;border-radius:14px;overflow:hidden;border:1px solid #2A2A38;box-shadow:0 24px 60px -24px rgba(0,0,0,.8)">
<div style="width:1200px;height:630px;transform:scale(.45);transform-origin:top left;background:radial-gradient(700px 500px at 78% 0%,rgba(0,212,160,.1),transparent 60%),#06060B;color:#F0F0F0;padding:56px 60px;display:flex;flex-direction:column">
<div style="display:flex;align-items:center;justify-content:space-between">
<span class="mono" style="font-size:30px;font-weight:800;letter-spacing:.3em">VYND<span style="color:#00D4A0">R</span></span>
<span style="display:flex;align-items:center;gap:14px">
<svg width="46" height="46" viewBox="0 0 32 32" fill="none"><rect x="1" y="1" width="30" height="30" rx="7" fill="#0E0E14" stroke="#2A2A38" stroke-width="1"></rect><path d="M8 9 L16 24 L24 9" fill="none" stroke="#F0F0F0" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M16 24 L24 9" fill="none" stroke="#00D4A0" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"></path><circle cx="24" cy="9" r="2" fill="#00D4A0"></circle></svg>
<span class="mono" style="font-size:30px;font-weight:800;letter-spacing:.3em">VYND<span style="color:#00D4A0">R</span></span>
</span>
<span class="mono" style="display:inline-flex;align-items:center;gap:12px;font-size:22px;letter-spacing:.14em;color:#00D4A0"><span style="width:11px;height:11px;border-radius:50%;background:#00D4A0"></span>CLV-VERIFIED</span>
</div>
<div style="display:flex;align-items:center;gap:48px;margin-top:auto;margin-bottom:auto">
@@ -787,7 +860,13 @@
</div>
<!-- ============ COMBAT HEAD-TO-HEAD ============ -->
<div data-screen-label="Combat head-to-head" style="margin-top:64px">
<!-- ============ ACT IV ============ -->
<div data-reveal style="margin-top:110px;display:flex;align-items:baseline;gap:18px;border-bottom:1px solid #14141E;padding-bottom:18px">
<span class="mono" style="font-size:64px;font-weight:800;line-height:.8;color:#14141E;-webkit-text-stroke:1px #2A2A38">04</span>
<div><div class="mono" style="font-size:13px;letter-spacing:.3em;color:#F0F0F0;font-weight:800">THE INTELLIGENCE</div><div style="font-size:12.5px;color:#707080;margin-top:5px">One analysis, many grades — style matchups, arsenal reads, correlation math.</div></div>
</div>
<div id="intel" data-screen-label="Combat head-to-head" style="margin-top:44px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px">
<span class="mono" style="font-size:12px;letter-spacing:.28em;color:#F0F0F0;font-weight:700">STYLE MATCHUP</span>
<span class="mono" style="font-size:10.5px;letter-spacing:.18em;color:#707080">COMBAT · STYLES MAKE FIGHTS · FIGHT-WEEK WEAPON</span>
@@ -1062,7 +1141,7 @@
</div>
<div data-screen-label="footer" style="margin-top:64px">
<div style="padding-top:22px;border-top:1px solid #14141E;display:flex;align-items:center;justify-content:space-between">
<div style="padding-top:22px;border-top:1px solid #14141E;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px 24px">
<span class="mono" style="font-size:10px;letter-spacing:.16em;color:#707080">COMPONENTS · 74 GLYPHS · EDGE BOARD · /u · SHARE · COMBAT · LIVE GRADE · PRO · STATES</span>
<span class="mono" style="font-size:10px;letter-spacing:.16em;color:#00D4A0">VYNDR VISUAL SYSTEM · v2</span>
</div>
@@ -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<window.innerHeight) s.classList.add('vy-in'); }); },300);
// rail highlighting
const map=['terminal','system','glyphs','surfaces','intel'];
this._so=new IntersectionObserver((ents)=>{
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='<span style="color:'+col+';font-weight:700">'+tag+'</span><span style="color:#2A2A38"> · </span>'+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);
}
}
+31
View File
@@ -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/);
});
});
+45
View File
@@ -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
+5 -3
View File
@@ -118,7 +118,7 @@ export function HeartbeatBar() {
const syncColor = tier === 'stale' ? 'var(--miss)' : tier === 'amber' ? 'var(--amber)' : 'var(--text-2)';
return (
<div
className="scanlines"
className="scanlines heartbeat-bar"
style={{
display: 'flex',
alignItems: 'center',
@@ -136,8 +136,10 @@ export function HeartbeatBar() {
<span className="mono" style={{ color: 'var(--g-a)', fontWeight: 700, letterSpacing: '0.12em' }}>SIGNAL LIVE</span>
</span>
{/* EKG strip */}
<div style={{ flex: 1, overflow: 'hidden', height: 22, maskImage: 'linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent)' }}>
{/* 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"). */}
<div className="hb-ekg" style={{ flex: 1, overflow: 'hidden', height: 22, maskImage: 'linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent)' }}>
<div className="ekg-track" style={{ height: 22 }}>
{[0, 1].map((k) => (
<svg key={k} width="200" height="22" viewBox="0 0 200 22" style={{ display: 'block' }} aria-hidden>
+3
View File
@@ -54,6 +54,9 @@ export default function VBtn({
type={type}
disabled={disabled}
onClick={onClick}
/* full-size CTAs meet the 44px mobile touch target (globals.css M1);
dense `small` buttons keep their compact size (density is a feature). */
className={small ? undefined : 'vbtn'}
style={{ ...base, ...VARIANTS[variant], ...style }}
onMouseEnter={(e) => {
if (variant === 'outline') e.currentTarget.style.borderColor = 'var(--g-a)';