Files
vyndr/web/src/app/globals.css
T
builtbykev c3d469e964 M3.1 follow-up: ShareCard grade-B billboard → Design white; drop stale comment hexes
The share/OG card (a billboard) still painted grade B blue (#4A9EFF) on canvas;
Design's B is neutral-bright white. Canvas needs a literal, so #F0F0F0 direct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:23:56 -04:00

1446 lines
50 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@import "tailwindcss";
/* ─────────────────────────────────────────────────────────
VYNDR Design System
Bloomberg terminal + ESPN broadcast + glitch-in-the-matrix.
"The terminal at sportsbook prices."
───────────────────────────────────────────────────────── */
:root {
/* Surfaces — Design package exact ramp (HANDOFF): void/card/elevated +
deep-panel + row-hairline. (2026-07-16 token alignment — Design wins.) */
--bg-0: #06060B; /* void */
--bg-1: #0E0E14; /* card (was #0E0E16) */
--bg-2: #14141E; /* elevated (was #15151F) */
--bg-3: #1A1A26;
--bg-deep: #0A0A10; /* deep panel */
--hairline: #101018; /* row hairline */
/* Borders — Design exact */
--border: #1E1E2A; /* was #1E1E2E */
--border-hi: #2A2A38; /* was #2A2A3E */
/* Text — Design exact ramp F0F0F0 / B8BCC8 / 707080 (+ 4a4a58 micro) */
--text-0: #F0F0F0; /* was #E8E8F0 */
--text-1: #B8BCC8; /* was #7A7A8E — Design's secondary is far brighter */
--text-2: #707080; /* was #4A4A5E */
--text-3: #4a4a58; /* micro-labels */
/* Brand accent — deep forest, button green, glow */
--acc-0: #0F3D2E;
--acc-1: #1A5A42;
--acc-glow: rgba(15, 61, 46, 0.25);
/* Grade tiers — the most important colors. Design package (HANDOFF): A-tier
green+glow · B neutral-bright WHITE · C muted GREY · D/F red. The old
B=blue / C=amber violated DESIGN-SPEC v2's own "B neutral-bright, C muted"
— aligned to Design 2026-07-16. Amber stays its own token (--amber). */
--grade-aplus: #00FFB8;
--grade-a: #00D4A0;
--grade-b: #F0F0F0; /* neutral-bright white (was blue) */
--grade-c: #B8BCC8; /* was #FFB347 (amber) */
--grade-d: #FF4757; /* was #FF5252 */
/* Sport tints */
--nba: #E94B3C;
--mlb: #1E90FF;
--wnba: #F7944A;
/* Special — Vendetta crimson; kill conditions only */
--crimson: #8B0000;
/* Signals */
--danger: var(--grade-d);
--warning: var(--amber); /* decoupled from grade-C (now grey) — warning is amber */
--success: var(--grade-a);
/* Geometry */
--diagonal-angle: -2deg;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
/* Glitch intensity — 0..1, can be tuned per-section.
VYNDR 2.0 (§2) sets this to 1 — full intensity is the design baseline. */
--glitch: 1;
--scan-opacity: calc(0.04 + 0.06 * var(--glitch));
--grain-opacity: calc(0.02 + 0.02 * var(--glitch));
--rgb-shift: calc(0.5px + 1.5px * var(--glitch));
--slash-opacity: calc(0.06 + 0.08 * var(--glitch));
--sweep-opacity: calc(0.08 + 0.12 * var(--glitch));
/* ─────────────────────────────────────────────────────────
VYNDR 2.0 DESIGN TOKENS (§2 — exact values from vyndr.css).
The new canonical short-name token set. Legacy aliases below
keep every existing component resolving during the migration.
───────────────────────────────────────────────────────── */
/* Grade tokens (the product's core color language) */
--g-ap: #00ffb8; /* A+ rarest, max glow */
--g-a: #00d4a0; /* A workhorse green / primary CTA */
--g-b: #F0F0F0; /* B neutral-bright white (Design) */
--g-c: #B8BCC8; /* C muted grey (Design; was amber) */
--g-d: #FF4757; /* D red (Design; was #ff5252) */
/* Sport tokens */
--s-nba: #e94b3c;
--s-mlb: #1e90ff;
--s-wnba: #f7944a;
--s-soccer: #3ddc84;
/* Amber / system glow (LOADING, LIVE, 404, warnings) */
--amber: #ffb347;
--amber-glow: 0 0 12px rgba(255, 179, 71, 0.6);
/* Semantic */
--live: #ff3b3b;
--hit: #00d4a0;
--miss: #FF4757; /* Design red (was #ff5252) */
/* Scanline base opacity + hero grade color */
--scan-op: 0.04;
--grade-hero: var(--g-a);
/* Motion tokens (DS5 · DESIGN-SPEC Part 4 — ALIVE IS PUNCTUATED STILLNESS).
Every duration in the living layer resolves from one of these — no one-off
ms literals. IDLE = the single proof-of-life pulse; TRANSITION is sub-200ms
(perceived instantaneity); DATA is the change-only green pulse; TICKER-HOLD
is how long the ticker RESTS on each item so it can be read (≥4s). */
--motion-idle: 1.4s;
--motion-transition: 180ms;
--motion-data: 300ms;
--ticker-hold: 4200ms;
/* Type — Inter for chrome/UI, JetBrains Mono for ALL data.
Session 41: --font-* come from next/font (self-hosted, set on <html>).
Literal family names kept as fallbacks for any non-next/font context. */
--sans: var(--font-sans), 'Inter', system-ui, sans-serif;
--mono: var(--font-mono), 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;
--ibm-mono: var(--font-ibm), var(--font-mono), 'IBM Plex Mono', 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;
/* ── Legacy aliases — every existing component still resolves ── */
--bg-primary: var(--bg-0);
--bg-surface: var(--bg-1);
--bg-surface-hover: var(--bg-2);
--bg-elevated: var(--bg-3);
--border-focus: var(--border-hi);
--text-primary: var(--text-0);
--text-secondary: var(--text-1);
--text-tertiary: var(--text-2);
--accent: var(--acc-0);
--accent-light: var(--acc-1);
--accent-glow: var(--acc-glow);
--bg: var(--bg-0);
--card: var(--bg-1);
--card-hover: var(--bg-2);
--border-light: var(--border-hi);
--text: var(--text-0);
--text-muted: var(--text-1);
--text-dim: var(--text-2);
--cyan: var(--grade-a);
--cyan-hover: var(--grade-aplus);
--cyan-dim: rgba(0, 212, 160, 0.10);
--kill: var(--crimson);
--forest: var(--acc-0);
--forest-dark: #0A2A20;
--forest-light: var(--acc-1);
}
/* ─────────────────────────────────────────────────────────
Base
───────────────────────────────────────────────────────── */
* {
box-sizing: border-box;
}
html {
background: var(--bg-primary);
color-scheme: dark;
}
body {
background: var(--bg-0);
color: var(--text-0);
font-family: var(--sans);
font-weight: 400;
line-height: 1.6;
letter-spacing: -0.01em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--sans);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--text-0);
}
.font-mono,
.mono {
font-family: var(--mono);
font-feature-settings: 'tnum' 1;
font-variant-numeric: tabular-nums;
}
.num {
font-family: var(--mono);
font-variant-numeric: tabular-nums;
font-weight: 700;
}
.lbl {
font-family: var(--mono);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-2);
}
::selection {
background: var(--accent);
color: var(--text-primary);
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-focus);
}
/* ─────────────────────────────────────────────────────────
The Diagonal Cut — signature visual motif
───────────────────────────────────────────────────────── */
.diagonal-cut {
position: relative;
isolation: isolate;
}
.diagonal-cut::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(var(--diagonal-angle), transparent 0%, var(--accent-glow) 100%);
pointer-events: none;
z-index: 0;
border-radius: inherit;
}
.diagonal-cut > * {
position: relative;
z-index: 1;
}
.diagonal-cut-strong::before {
background: linear-gradient(var(--diagonal-angle), transparent 0%, rgba(26, 74, 58, 0.20) 100%);
}
/* Radial accent glow — for hero backgrounds */
.radial-glow {
background:
radial-gradient(ellipse 60% 50% at 50% 30%, var(--accent-glow), transparent 70%),
var(--bg-primary);
}
/* ─────────────────────────────────────────────────────────
Surfaces
───────────────────────────────────────────────────────── */
.surface {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
transition: transform 200ms ease, border-color 200ms ease, background 200ms ease;
}
.surface-hover:hover {
background: var(--bg-surface-hover);
border-color: var(--border-focus);
transform: translateY(-2px);
}
.surface-elevated {
background: var(--bg-elevated);
border: 1px solid var(--border-focus);
border-radius: var(--radius-lg);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
/* ─────────────────────────────────────────────────────────
Buttons
───────────────────────────────────────────────────────── */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
background: var(--accent);
color: var(--text-primary);
border: 1px solid var(--accent-light);
border-radius: var(--radius-md);
font-weight: 600;
font-size: 14px;
letter-spacing: 0.01em;
cursor: pointer;
transition: background 200ms ease, transform 100ms ease, box-shadow 200ms ease;
text-decoration: none;
}
.btn-primary:hover {
background: var(--accent-light);
box-shadow: 0 4px 16px var(--accent-glow);
}
.btn-primary:active {
transform: scale(0.98);
}
.btn-primary:disabled {
opacity: 0.4;
cursor: not-allowed;
background: var(--accent);
box-shadow: none;
}
.btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
background: transparent;
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-weight: 500;
font-size: 14px;
cursor: pointer;
transition: border-color 200ms ease, background 200ms ease;
text-decoration: none;
}
.btn-ghost:hover {
border-color: var(--border-focus);
background: var(--bg-surface);
}
/* ─────────────────────────────────────────────────────────
Inputs
───────────────────────────────────────────────────────── */
.input-field {
width: 100%;
padding: 12px 16px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-primary);
font-family: inherit;
font-size: 14px;
transition: border-color 200ms ease, background 200ms ease;
}
.input-field::placeholder {
color: var(--text-tertiary);
}
.input-field:focus {
outline: none;
border-color: var(--accent-light);
background: var(--bg-elevated);
}
/* ─────────────────────────────────────────────────────────
Grade tier helpers
───────────────────────────────────────────────────────── */
.grade-a-tier { color: var(--grade-a); }
.grade-b-tier { color: var(--grade-b); }
.grade-c-tier { color: var(--grade-c); }
.grade-d-tier { color: var(--grade-d); }
.grade-a-bg { background: rgba(0, 200, 150, 0.10); border-color: rgba(0, 200, 150, 0.30); }
.grade-b-bg { background: rgba(74, 158, 255, 0.10); border-color: rgba(74, 158, 255, 0.30); }
.grade-c-bg { background: rgba(255, 179, 71, 0.10); border-color: rgba(255, 179, 71, 0.30); }
.grade-d-bg { background: rgba(255, 107, 107, 0.10); border-color: rgba(255, 107, 107, 0.30); }
/* ─────────────────────────────────────────────────────────
Animations
───────────────────────────────────────────────────────── */
@keyframes grade-reveal {
0% { transform: scale(0.85); opacity: 0; }
60% { transform: scale(1.04); opacity: 1; }
100% { transform: scale(1.00); opacity: 1; }
}
.animate-grade {
animation: grade-reveal 400ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes blur-pulse {
0%, 100% { opacity: 0.65; }
50% { opacity: 0.85; }
}
.animate-blur-pulse {
animation: blur-pulse 3s ease-in-out 1;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.shimmer-loading {
background: linear-gradient(
90deg,
var(--accent) 0%,
var(--accent-light) 50%,
var(--accent) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s linear infinite;
}
/* DS1 (§4 — kill loading walls) — the ONE skeleton primitive. Layout-matched
dark shimmer blocks replace every text-wall loader. Tokenized (no one-off
CSS); the global prefers-reduced-motion rule freezes the sweep to a static,
still-visible surface — arrives-then-rests, honored globally. */
.vyndr-skeleton {
background: linear-gradient(
90deg,
var(--bg-surface) 0%,
var(--bg-surface-hover) 50%,
var(--bg-surface) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s linear infinite;
border-radius: 8px;
}
/* S6 (A1 board, LCP) — floored at a VISIBLE state (.6), matching the S33
entrance-keyframe rule (a paused frame is never invisible). Starting at
opacity 0 also delayed the hero heading's first contentful paint — an
invisible element isn't an LCP candidate until it becomes visible, so the
old 0-start pushed the landing LCP by the animation delay + duration. */
@keyframes fade-up {
0% { opacity: 0.6; transform: translateY(8px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes skeleton-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.7; }
}
@keyframes phosphor-pulse {
0%, 100% { opacity: 0.6; transform: scaleX(0.6); }
50% { opacity: 1; transform: scaleX(1); }
}
.animate-fade-up {
animation: fade-up 400ms ease-out both;
}
.stagger-1 { animation-delay: 50ms; }
.stagger-2 { animation-delay: 100ms; }
.stagger-3 { animation-delay: 150ms; }
.stagger-4 { animation-delay: 200ms; }
.stagger-5 { animation-delay: 250ms; }
.stagger-6 { animation-delay: 300ms; }
@keyframes ticker-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.animate-ticker {
animation: ticker-scroll 60s linear infinite;
}
.animate-ticker:hover {
animation-play-state: paused;
}
/* ─────────────────────────────────────────────────────────
Tier blur (the conversion engine)
───────────────────────────────────────────────────────── */
.tier-locked {
position: relative;
filter: blur(8px);
user-select: none;
pointer-events: none;
animation: blur-pulse 3s ease-in-out 1;
}
.tier-locked-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: linear-gradient(180deg, transparent 0%, rgba(10, 10, 15, 0.6) 60%, rgba(10, 10, 15, 0.85) 100%);
border-radius: inherit;
z-index: 2;
padding: 24px;
text-align: center;
}
/* ─────────────────────────────────────────────────────────
Utilities
───────────────────────────────────────────────────────── */
.text-balance { text-wrap: balance; }
.text-pretty { text-wrap: pretty; }
.divider {
height: 1px;
background: var(--border);
margin: 16px 0;
}
/* ─────────────────────────────────────────────────────────
Texture layers — seasoning, not the meal
───────────────────────────────────────────────────────── */
.tex-scan {
position: relative;
isolation: isolate;
}
.tex-scan::after {
content: "";
position: absolute; inset: 0;
pointer-events: none;
background-image: repeating-linear-gradient(
to bottom,
rgba(255,255,255,var(--scan-opacity)) 0px,
rgba(255,255,255,var(--scan-opacity)) 1px,
transparent 1px,
transparent 3px
);
z-index: 2;
mix-blend-mode: screen;
}
.tex-sweep {
position: relative;
overflow: hidden;
isolation: isolate;
}
.tex-sweep::before {
content: "";
position: absolute;
left: 0; right: 0;
height: 140px;
top: -140px;
pointer-events: none;
background: linear-gradient(
180deg,
transparent,
rgba(0, 255, 184, var(--sweep-opacity)) 50%,
transparent
);
z-index: 1;
animation: crt-sweep 7s ease-in-out infinite;
mix-blend-mode: screen;
}
@keyframes crt-sweep {
0% { transform: translateY(0); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(120vh); opacity: 0; }
}
.tex-vignette {
position: relative;
}
.tex-vignette::after {
content: "";
position: absolute; inset: 0;
pointer-events: none;
background: radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.45) 100%);
z-index: 3;
}
.tex-slash {
position: relative;
}
.tex-slash::before {
content: "";
position: absolute; inset: 0;
pointer-events: none;
background: linear-gradient(
-2deg,
transparent 0%,
rgba(0, 212, 160, var(--slash-opacity)) 50%,
transparent 100%
);
z-index: 1;
}
.tex-grain::after {
content: "";
position: fixed; inset: 0;
pointer-events: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.9 0 0 0 0 0.9 0 0 0 0 1 0 0 0 0.45 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
opacity: var(--grain-opacity);
z-index: 999;
mix-blend-mode: overlay;
}
/* Page-level CRT vignette, scales with --glitch */
body.tex-grain::before {
content: "";
position: fixed; inset: 0;
pointer-events: none;
background: radial-gradient(ellipse 110% 90% at 50% 50%, transparent 50%, rgba(0,0,0,calc(0.15 + 0.20 * var(--glitch))) 100%);
z-index: 998;
}
/* ─────────────────────────────────────────────────────────
RGB split — broadcast misalignment effect
───────────────────────────────────────────────────────── */
.rgb-split {
position: relative;
display: inline-block;
}
.rgb-split::before,
.rgb-split::after {
content: attr(data-text);
position: absolute; inset: 0;
pointer-events: none;
}
.rgb-split::before {
color: rgba(255, 60, 60, 0.55);
transform: translateX(calc(-1 * var(--rgb-shift)));
mix-blend-mode: screen;
}
.rgb-split::after {
color: rgba(60, 120, 255, 0.55);
transform: translateX(var(--rgb-shift));
mix-blend-mode: screen;
}
/* ─────────────────────────────────────────────────────────
Wordmark — VYNDR with RGB-split letters, green-glow R, blinking cursor
───────────────────────────────────────────────────────── */
.wordmark {
font-family: var(--ibm-mono);
font-weight: 800;
letter-spacing: 0.10em;
display: inline-flex;
align-items: center;
line-height: 1;
position: relative;
}
.wm-letter {
position: relative;
display: inline-block;
}
.wm-letter::before,
.wm-letter::after {
content: attr(data-text);
position: absolute; inset: 0;
pointer-events: none;
display: inline-block;
}
.wm-letter::before {
color: rgba(255, 60, 60, 0.55);
transform: translateX(calc(-1px - var(--rgb-shift)));
mix-blend-mode: screen;
}
.wm-letter::after {
color: rgba(60, 120, 255, 0.55);
transform: translateX(calc(1px + var(--rgb-shift)));
mix-blend-mode: screen;
}
.wordmark .vynd { color: var(--text-0); }
.wordmark .r {
color: var(--grade-a);
text-shadow: 0 0 calc(var(--wm-size, 32px) * 0.25) rgba(0, 212, 160, 0.6);
position: relative;
}
.wordmark .r::before { color: rgba(255, 60, 60, 0.45); }
.wordmark .r::after { color: rgba(0, 255, 184, 0.60); }
.wm-cursor {
display: inline-block;
width: 0.16em;
height: 0.9em;
margin-left: 0.18em;
background: var(--grade-a);
box-shadow: 0 0 calc(var(--wm-size, 32px) * 0.3) rgba(0, 212, 160, 0.8);
animation: cursor-blink 1.1s steps(2) infinite;
}
@keyframes cursor-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.wm-anim { animation: wm-tear 5.5s ease-in-out infinite; }
@keyframes wm-tear {
0%, 4%, 100% { transform: translateX(0); }
5% { transform: translateX(2px) skewX(-2deg); filter: hue-rotate(20deg); }
6% { transform: translateX(-1px); }
7% { transform: translateX(0); filter: none; }
47% { transform: translateX(0); }
48% { transform: translateX(-2px) skewX(1deg); }
49% { transform: translateX(1px); }
50% { transform: translateX(0); }
}
/* ─────────────────────────────────────────────────────────
Glitch text — occasional horizontal tear, broadcast tinted ghosts
───────────────────────────────────────────────────────── */
.glitch-text {
position: relative;
display: inline-block;
}
.glitch-text::before,
.glitch-text::after {
content: attr(data-text);
position: absolute; inset: 0;
pointer-events: none;
}
.glitch-text::before {
color: rgba(255, 60, 60, 0.7);
transform: translateX(calc(-1px - var(--rgb-shift)));
mix-blend-mode: screen;
animation: glitch-shift-r 4s steps(40) infinite;
}
.glitch-text::after {
color: rgba(60, 180, 255, 0.7);
transform: translateX(calc(1px + var(--rgb-shift)));
mix-blend-mode: screen;
animation: glitch-shift-b 4s steps(40) infinite reverse;
}
@keyframes glitch-shift-r {
0%, 100% { clip-path: inset(0 0 0 0); transform: translateX(calc(-1px - var(--rgb-shift))); }
92% { clip-path: inset(20% 0 60% 0); transform: translateX(-3px); }
94% { clip-path: inset(60% 0 10% 0); transform: translateX(2px); }
96% { clip-path: inset(0 0 0 0); }
}
@keyframes glitch-shift-b {
0%, 100% { clip-path: inset(0 0 0 0); }
93% { clip-path: inset(45% 0 35% 0); transform: translateX(3px); }
95% { clip-path: inset(10% 0 70% 0); transform: translateX(-2px); }
97% { clip-path: inset(0 0 0 0); }
}
/* ─────────────────────────────────────────────────────────
Transmission badge, frequency badge, phosphor underline
───────────────────────────────────────────────────────── */
.transmission {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 10px 4px 4px;
font-family: var(--ibm-mono);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--grade-a);
background: rgba(0, 212, 160, 0.06);
border: 1px solid rgba(0, 212, 160, 0.30);
border-radius: 3px;
}
.transmission .dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--grade-a);
box-shadow: 0 0 8px var(--grade-a);
animation: pulse-live 1.4s ease-in-out infinite;
}
@keyframes pulse-live {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(1.3); }
}
.freq-badge {
display: inline-flex; align-items: center; gap: 10px;
padding: 6px 10px;
background: var(--bg-1);
border: 1px solid var(--border);
font-family: var(--ibm-mono);
font-size: 10px;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-1);
}
.freq-badge .ch { color: var(--grade-a); font-weight: 700; }
.phosphor-line {
height: 2px;
background: linear-gradient(90deg, transparent, var(--grade-a), transparent);
box-shadow: 0 0 8px rgba(0, 212, 160, 0.60);
width: 100%;
}
/* The V watermark — Vendetta nod, hero background */
.v-watermark {
position: absolute;
font-family: var(--ibm-mono);
font-weight: 800;
font-size: 70vmin;
line-height: 0.8;
color: var(--acc-0);
opacity: 0.06;
letter-spacing: -0.05em;
user-select: none;
pointer-events: none;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
z-index: 0;
}
/* CRT phosphor glow utility for grade letters */
.grade-glow-aplus { color: var(--grade-aplus); text-shadow: 0 0 18px var(--grade-aplus), 0 0 36px rgba(0, 255, 184, 0.40); }
.grade-glow-a { color: var(--grade-a); text-shadow: 0 0 14px rgba(0, 212, 160, 0.70), 0 0 30px rgba(0, 212, 160, 0.30); }
.grade-glow-b { color: var(--grade-b); text-shadow: 0 0 12px rgba(74, 158, 255, 0.60), 0 0 24px rgba(74, 158, 255, 0.20); }
.grade-glow-c { color: var(--grade-c); text-shadow: 0 0 12px rgba(255, 179, 71, 0.55); }
.grade-glow-d { color: var(--grade-d); text-shadow: 0 0 12px rgba(255, 82, 82, 0.55); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* ─────────────────────────────────────────────────────────
RTL support (Session 12 — Arabic).
Toggled by <html dir="rtl">. The root layout server-
resolves the locale and sets the attribute; these rules
flip the few directional surfaces (nav flex direction,
sidebar drawers, list markers) without inverting the
whole grid (the Bloomberg-style data tables stay LTR
even in RTL mode — numbers read left-to-right
regardless).
───────────────────────────────────────────────────────── */
[dir="rtl"] body {
text-align: right;
}
[dir="rtl"] .nav-links,
[dir="rtl"] .nav-desktop {
flex-direction: row-reverse;
}
[dir="rtl"] .mono,
[dir="rtl"] [class*="font-mono"] {
/* Monospace numeric blocks stay LTR — financial data reads
left-to-right in every locale by convention. */
direction: ltr;
text-align: right;
unicode-bidi: isolate;
}
[dir="rtl"] .pricing-grid {
direction: rtl;
}
[dir="rtl"] .btn-primary,
[dir="rtl"] .btn-ghost {
/* Buttons stay LTR so chevrons / arrows render predictably. */
unicode-bidi: isolate;
}
/* Session 14 — shimmer keyframe used by Slate skeleton placeholders
and any other loading surface that wants the same Bloomberg-style
subtle motion. The animation runs over a 200%-wide gradient
background; advancing -100% → 100% slides the highlight band
across the element. */
@keyframes vyndr-shimmer {
0% { background-position: -100% 0; }
100% { background-position: 100% 0; }
}
/* ============================================================
VYNDR 2.0 DESIGN SYSTEM (§4 — ported verbatim from vyndr.css)
Glitch + animation system. Applied ONLY to chrome (wordmark,
headers, dividers, loaders, the living layer). DATA NEVER
GLITCHES. Where keyframe/class names collide with the legacy
block above, these definitions are authoritative (later wins).
============================================================ */
/* ===================== SCANLINE TEXTURE ===================== */
.scanlines, .scanlines-bg { position: relative; }
.scanlines::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(0deg, transparent 50%, rgba(0,0,0,1) 50%);
background-size: 100% 2px;
opacity: var(--scan-op);
z-index: 1;
}
/* Intelligence surface (Level 3) — green + drifting scanlines: "VYNDR is speaking" */
.intel-surface {
position: relative;
background: linear-gradient(160deg, var(--acc-0), var(--acc-1));
overflow: hidden;
}
.intel-surface::after {
content: "";
position: absolute;
inset: -4px 0 -4px 0;
pointer-events: none;
background: repeating-linear-gradient(0deg, transparent 0 2px, rgba(0,0,0,0.85) 2px 4px);
opacity: calc(var(--scan-op) * 1.25 + 0.01);
animation: scan-drift 4s linear infinite;
z-index: 1;
}
@keyframes scan-drift {
from { transform: translateY(0); }
to { transform: translateY(4px); }
}
/* ===================== WORDMARK GLITCH ===================== */
.wm {
position: relative;
display: inline-block;
flex: none;
white-space: nowrap;
font-family: var(--sans);
font-weight: 800;
letter-spacing: 0.04em;
color: var(--text-0);
text-shadow: 0 0 1px rgba(232,232,240,.25);
animation: wm-tear 5s steps(1, end) infinite;
}
.wm::before, .wm::after {
content: attr(data-text);
position: absolute;
top: 0; left: 0;
width: 100%;
pointer-events: none;
opacity: calc(0.92 * var(--glitch));
}
.wm::before {
color: #ff2d6f;
transform: translateX(calc(-1.9px * var(--glitch)));
animation: glitch-shift-r 5s steps(1, end) infinite;
mix-blend-mode: screen;
}
.wm::after {
color: #2dffd5;
transform: translateX(calc(1.9px * var(--glitch)));
animation: glitch-shift-b 5s steps(1, end) infinite;
mix-blend-mode: screen;
}
/* the locked green R — permanent phosphor highlight */
.wm-r {
position: relative;
z-index: 4;
color: var(--g-a);
text-shadow: 0 0 9px rgba(0,212,160,.7), 0 0 22px rgba(0,212,160,.38);
}
/* live terminal caret after the R — alive, mid-type flicker */
.wm-cursor {
position: relative;
z-index: 4;
display: inline-block;
width: 0.11em;
height: 0.82em;
margin-left: 0.14em;
transform: translateY(0.04em);
background: var(--g-a);
box-shadow: 0 0 9px rgba(0,212,160,.9), 0 0 18px rgba(0,212,160,.4);
animation: wm-caret 1.15s steps(1, end) infinite;
}
@keyframes wm-caret {
0%, 44% { opacity: 1; }
45%, 55% { opacity: 0.12; }
56%, 70% { opacity: 1; }
71%, 100% { opacity: 0.12; }
}
/* BETA clearance pill */
.wm-beta {
font-family: var(--mono);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.2em;
color: var(--g-a);
border: 1px solid color-mix(in srgb, var(--g-a) 50%, transparent);
background: rgba(0,212,160,.08);
padding: 3px 7px 3px 9px;
border-radius: 5px;
line-height: 1;
box-shadow: 0 0 16px -5px rgba(0,212,160,.6);
}
@keyframes wm-tear {
0%, 4%, 100% { transform: translate(0); }
5% { filter: hue-rotate(40deg) brightness(1.15); transform: translate(calc(2.4px*var(--glitch))) skew(calc(-3deg*var(--glitch))); }
6% { filter: brightness(1.05); transform: translate(calc(-1.4px*var(--glitch))) skew(calc(1deg*var(--glitch))); }
7% { filter: none; transform: translate(0); }
47% { transform: translate(0); }
48% { filter: hue-rotate(-30deg) brightness(1.12); transform: translate(calc(-2.4px*var(--glitch))) skew(calc(1.5deg*var(--glitch))); }
49% { transform: translate(calc(1.4px*var(--glitch))); }
50% { filter: none; transform: translate(0); }
}
@keyframes glitch-shift-r {
0%,4%,8%,46%,52%,100% { clip-path: inset(0 0 0 0); transform: translateX(calc(-1.5px*var(--glitch))); }
5% { clip-path: inset(10% 0 60% 0); transform: translateX(calc(-4px*var(--glitch))); }
6% { clip-path: inset(40% 0 20% 0); transform: translateX(calc(3px*var(--glitch))); }
48% { clip-path: inset(20% 0 55% 0); transform: translateX(calc(-3px*var(--glitch))); }
49% { clip-path: inset(60% 0 10% 0); transform: translateX(calc(2px*var(--glitch))); }
}
@keyframes glitch-shift-b {
0%,4%,8%,46%,52%,100% { clip-path: inset(0 0 0 0); transform: translateX(calc(1.5px*var(--glitch))); }
5% { clip-path: inset(55% 0 15% 0); transform: translateX(calc(4px*var(--glitch))); }
6% { clip-path: inset(15% 0 50% 0); transform: translateX(calc(-3px*var(--glitch))); }
48% { clip-path: inset(50% 0 25% 0); transform: translateX(calc(3px*var(--glitch))); }
49% { clip-path: inset(5% 0 70% 0); transform: translateX(calc(-2px*var(--glitch))); }
}
/* Section-header glitch on hover */
.glitch-hover { position: relative; display: inline-block; }
.glitch-hover:hover { animation: head-tear 0.32s steps(1,end) 1; }
@keyframes head-tear {
0% { transform: translate(0); text-shadow: none; }
25% { transform: translate(calc(2px*var(--glitch))) skew(calc(-3deg*var(--glitch)));
text-shadow: calc(-2px*var(--glitch)) 0 #ff2d6f, calc(2px*var(--glitch)) 0 #2dffd5; }
50% { transform: translate(calc(-1px*var(--glitch))); }
75% { transform: translate(calc(1px*var(--glitch))); text-shadow: calc(1px*var(--glitch)) 0 #2dffd5; }
100% { transform: translate(0); text-shadow: none; }
}
/* ===================== CRT SWEEP ===================== */
.crt-sweep {
position: fixed;
left: 0; right: 0; top: 0;
height: 38vh;
pointer-events: none;
z-index: 9999;
background: linear-gradient(180deg,
transparent 0%,
rgba(0,212,160,0.04) 40%,
rgba(0,255,184,0.14) 50%,
rgba(255,255,255,0.10) 51%,
rgba(0,212,160,0.04) 60%,
transparent 100%);
animation: crt-sweep 0.7s ease-in 1;
}
@keyframes crt-sweep {
0% { opacity: 0; transform: translateY(-40vh); }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; transform: translateY(120vh); }
}
/* Local sweep within a card (grade-card reveal) */
.crt-sweep-local {
position: absolute;
left: 0; right: 0; top: 0;
height: 60%;
pointer-events: none;
z-index: 6;
background: linear-gradient(180deg,
transparent, rgba(0,255,184,0.12) 48%, rgba(255,255,255,0.10) 50%,
rgba(0,255,184,0.08) 52%, transparent);
animation: crt-sweep-local 0.65s ease-in 1;
}
@keyframes crt-sweep-local {
0% { opacity: 0; transform: translateY(-60%); }
15% { opacity: 1; }
100% { opacity: 0; transform: translateY(220%); }
}
/* ===================== PHOSPHOR PULSE ===================== */
.phosphor-cursor {
display: inline-block;
width: 9px; height: 1.05em;
background: var(--g-a);
margin-left: 2px;
vertical-align: text-bottom;
box-shadow: 0 0 8px rgba(0,212,160,0.8);
animation: phosphor-pulse 1.05s ease-in-out infinite;
}
.phosphor-cursor.amber { background: var(--amber); box-shadow: var(--amber-glow); }
@keyframes phosphor-pulse {
0%,100% { opacity: 0.5; transform: scaleX(0.6); }
50% { opacity: 1; transform: scaleX(1); }
}
/* ===================== GRADE REVEAL ===================== */
@keyframes grade-reveal {
0% { transform: scale(0.82); }
60% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.grade-reveal { animation: grade-reveal 0.42s cubic-bezier(.2,1.3,.4,1) both; }
/* ===================== TICKER (DS5 — punctuated stillness) =====================
The ticker RESTS. It no longer marquee-scrolls (constant motion reads cheap,
Part 0 law #1). It holds each item for --ticker-hold (≥4s) so it can be READ,
and the ONLY motion is a change-only cross-fade + a brief data-update pulse
when the item advances. The legacy .ticker-track marquee is retired. */
@keyframes ticker-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.ticker-track {
display: inline-flex;
white-space: nowrap;
animation: ticker-scroll 38s linear infinite;
}
/* The resting strip — a static ranked row, no idle animation. */
.ticker-rest {
display: flex;
align-items: center;
min-width: 0;
overflow: hidden;
white-space: nowrap;
}
/* Change-only cross-fade as the held item advances (sub-200ms, weighted). */
@keyframes ticker-swap { from { opacity: 0.55; } to { opacity: 1; } }
.ticker-item-enter { animation: ticker-swap var(--motion-transition) ease-out both; }
/* Change-only green pulse — the terminal reacting to a fresh head event. */
@keyframes ticker-pulse {
0% { background: color-mix(in srgb, var(--g-a) 22%, transparent); }
100% { background: transparent; }
}
.ticker-pulse { animation: ticker-pulse var(--motion-data) ease-out 1; }
/* ===================== LIVE PULSE ===================== */
.live-dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
background: var(--live);
box-shadow: 0 0 0 0 rgba(255,59,59,0.7);
animation: live-pulse 1.4s ease-out infinite;
}
@keyframes live-pulse {
0% { box-shadow: 0 0 0 0 rgba(255,59,59,0.6); }
70% { box-shadow: 0 0 0 7px rgba(255,59,59,0); }
100% { box-shadow: 0 0 0 0 rgba(255,59,59,0); }
}
/* ===================== DESK SHOWCASE (DS5 pricing hero) ===================== */
/* Balanced two-column at desktop (pitch | real feature visuals), single column
on mobile — kills the dead right-half of the old pricing page (#8). */
.desk-showcase { grid-template-columns: 1fr; }
@media (min-width: 900px) {
.desk-showcase { grid-template-columns: 1.05fr 1fr; }
}
/* ===================== LINE FLASH (odds cell changes) ===================== */
@keyframes flash-up { 0% { background: rgba(0,212,160,0.45); } 100% { background: transparent; } }
@keyframes flash-down { 0% { background: rgba(255,82,82,0.45); } 100% { background: transparent; } }
.flash-up { animation: flash-up 0.6s ease-out 1; }
.flash-down { animation: flash-down 0.6s ease-out 1; }
/* ===================== UTILITY ===================== */
.mono { font-family: var(--mono); }
.label {
font-family: var(--mono);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.12em;
color: var(--text-1);
}
.amber-glow { color: var(--amber); text-shadow: var(--amber-glow); }
/* ===================== LIVING LAYER (the brain) ===================== */
/* EKG heartbeat strip */
@keyframes ekg-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } }
/* DS5 — the EKG is now a STATIC readout trace (no idle scroll). The header's
ONE idle proof-of-life is the single SIGNAL-LIVE live-dot; the heartbeat
waveform is a frozen instrument face, not constant motion (Part 4). */
.ekg-track { display: inline-flex; }
/* Neural node pulse */
@keyframes node-pulse {
0%, 100% { opacity: 0.35; r: 2.4; }
50% { opacity: 1; r: 3.4; }
}
.brain-node { animation: node-pulse 2.6s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
/* Synapse traveling pulse along connections */
@keyframes synapse-travel { from { stroke-dashoffset: 26; } to { stroke-dashoffset: 0; } }
.brain-link { stroke-dasharray: 3 23; animation: synapse-travel 2.2s linear infinite; }
/* Number tick pop when a counter increments */
@keyframes count-tick {
0% { transform: translateY(0); color: var(--text-0); }
30% { transform: translateY(-2px); color: var(--g-a); text-shadow: 0 0 10px rgba(0,212,160,.6); }
100% { transform: translateY(0); }
}
.count-tick { animation: count-tick 0.5s ease-out 1; }
/* Factor ignite — sequential light-up during grade processing */
@keyframes factor-ignite {
0% { opacity: 0; transform: translateX(-6px); }
100% { opacity: 1; transform: translateX(0); }
}
.factor-ignite { animation: factor-ignite 0.34s ease-out both; }
/* Signal feed item entering */
@keyframes signal-in {
0% { opacity: 0; transform: translateY(-8px); background: rgba(0,212,160,.16); }
100% { opacity: 1; transform: translateY(0); background: transparent; }
}
.signal-in { animation: signal-in 0.6s ease-out 1; }
/* Processing scan bar (grade thinking) */
@keyframes proc-scan {
0% { transform: translateY(-100%); opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { transform: translateY(280%); opacity: 0; }
}
.proc-scan { animation: proc-scan 1.1s ease-in-out infinite; }
/* Soft data-stream blink */
@keyframes data-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
.data-blink { animation: data-blink 1.6s ease-in-out infinite; }
/* ── Entrance keyframes — each FLOORS at the visible state so a paused/
backgrounded frame never renders invisible (learned the hard way, §4). ── */
@keyframes toast-in { 0% { transform: translateX(24px); } 100% { transform: translateX(0); } }
.toast-in { animation: toast-in 0.26s cubic-bezier(.2,1,.4,1) both; }
@keyframes sheet-up { 0% { transform: translateY(40px); } 100% { transform: translateY(0); } }
.sheet-up { animation: sheet-up 0.24s ease-out both; }
@keyframes cmd-in { 0% { transform: translateY(-10px); } 100% { transform: translateY(0); } }
.cmd-in { animation: cmd-in 0.18s ease-out both; }
@keyframes fade-in { from { opacity: 0.6; } to { opacity: 1; } }
.fade-in { animation: fade-in 0.2s ease-out both; }
/* Reduced-motion: kill the living layer + glitch chrome outright */
@media (prefers-reduced-motion: reduce) {
.wm, .wm::before, .wm::after, .ticker-track, .live-dot,
.ticker-item-enter, .ticker-pulse,
.intel-surface::after, .phosphor-cursor,
.ekg-track, .brain-node, .brain-link, .proc-scan, .data-blink { animation: none !important; }
}
/* ============================================================
ACCESSIBILITY + PREFERENCES (§10 — driven by <html data-*>)
============================================================ */
:focus-visible { outline: 2px solid var(--g-a); outline-offset: 2px; border-radius: 3px; }
/* Reduce motion (manual toggle, independent of OS setting) */
html[data-motion="reduced"] *,
html[data-motion="reduced"] *::before,
html[data-motion="reduced"] *::after {
animation-duration: 0.001s !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001s !important;
scroll-behavior: auto !important;
}
/* High contrast */
html[data-contrast="high"] {
--text-0: #ffffff;
--text-1: #cdcde0;
--text-2: #a0a0bc;
--border: #4c4c6a;
--border-hi: #70709a;
--bg-0: #000005;
--bg-1: #0c0c16;
--bg-2: #16161f;
}
html[data-contrast="high"] .scanlines::after,
html[data-contrast="high"] .intel-surface::after { opacity: 0.015 !important; }
/* Text size — zoom scales the whole UI predictably */
html[data-text="sm"] body { zoom: 0.92; }
html[data-text="lg"] body { zoom: 1.12; }
html[data-text="xl"] body { zoom: 1.25; }
/* Colorblind-safe — retunes red→orange; meaning never rests on color alone */
html[data-cb="1"] {
--miss: #fe6100;
--g-d: #fe6100;
--live: #fe6100;
--s-nba: #ff6f3c;
}
html[data-cb="1"] .cb-neg { text-decoration: underline; text-underline-offset: 2px; }
html[data-cb="1"] .cb-pos { text-decoration: underline; text-underline-offset: 2px; }
/* Readable font — high-legibility stack for dyslexia/low-vision */
html[data-font="readable"] {
--sans: "Atkinson Hyperlegible", "Verdana", "Trebuchet MS", system-ui, sans-serif;
--mono: "Atkinson Hyperlegible Mono", "DejaVu Sans Mono", ui-monospace, monospace;
letter-spacing: 0.01em;
}
html[data-font="readable"] .wm::before,
html[data-font="readable"] .wm::after { opacity: 0.45 !important; }
/* ============================================================
MOBILE PARITY (§6, §F — Session 37)
Mobile is the primary (PWA) layout; desktop is the enhancement.
============================================================ */
/* The 5-tab bar is mobile-only — hidden once the desktop nav has room. */
@media (min-width: 768px) {
.mobile-tab-bar { display: none !important; }
}
/* Clear the fixed bottom tab bar (64px + iOS safe area) so content isn't
hidden behind it. Desktop keeps the standard 80px footer breathing room. */
@media (max-width: 767px) {
main {
padding-bottom: calc(84px + env(safe-area-inset-bottom, 0px)) !important;
}
}
/* Grade hero zone: full-width, slightly smaller letter on phones. */
@media (max-width: 640px) {
.grade-hero { font-size: 80px !important; }
/* Session 43 — player-profile hero name was clipping (e.g. "Wembanyam")
at 390px. Shrink + wrap instead of overflowing the flex row. */
.player-hero-name { font-size: 24px !important; overflow-wrap: anywhere; }
}
/* Book-line tables scroll horizontally on small screens with a sticky
first (team/book) column so the matchup stays anchored while prices scroll. */
@media (max-width: 640px) {
.game-lines-grid {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.game-lines-grid .team-col {
position: sticky;
left: 0;
z-index: 1;
background: var(--bg-1);
}
}
/* Terminal's multi-column grid stacks on mobile. */
@media (max-width: 768px) {
.terminal-grid { grid-template-columns: 1fr !important; }
/* Pitcher-identity (Wave 5B): identity + arsenal columns stack; the arsenal
table keeps its own internal grid. */
.parsenal-grid { grid-template-columns: 1fr !important; }
}
/* ── Session 59 (work-order 3.2) — overflow containment at 390px ──────── */
/* Horizontal scroll is CONTAINED to strips (tabs, chips, book tables) —
the document itself never scrolls sideways. */
@media (max-width: 767px) {
html, body { overflow-x: hidden; }
}
/* Card meta: venue drops FIRST on phones so time/pitchers never truncate. */
@media (max-width: 639px) {
.gc-venue { display: none !important; }
}
/* GAME LINES: full 8-book table on desktop; on phones it collapses to the
best-line summary row (.gl-summary) with an "N BOOKS ⌄" expander that
reveals the full table (.gl-full.gl-expanded). */
.gl-summary { display: none; }
@media (max-width: 639px) {
.gl-summary { display: flex !important; }
.gl-full:not(.gl-expanded) { display: none !important; }
.gl-full.gl-expanded { margin-top: 10px; }
}
/* ============================================================
DESIGN COMPLETION TRAIN — M1 MOBILE FOUNDATION (2026-07-16)
Now RECONCILED against Design's drawn mobile (specs/design-reference/
MOBILE-SPEC.md — Design DREW 390×844, so these implement it, not infer it).
Design agrees: no EKG on mobile, ≥44px tap targets. Design OVERRIDES the
header shape — mobile is an app bar (logo + sync clock) + breadth strip +
bottom wire, not the collapsed heartbeat bar; that full recomposition is
M1b (the board/shell surface). Built to Design's mobile spec, but still
VISUALLY UNVERIFIED at 390px (WSL2↔Chrome unreachable — Chrome audit is eyes).
============================================================ */
/* Mobile app-bar clock (M1b) — Design's mobile drops SIGNAL LIVE + EKG + graded
+ the SYNC label and shows a single right-aligned clock that RESTS as a wall
clock and REACTS to amber/STALE off the real freshness tier (Hybrid: clock =
stillness, STALE = punctuation; never silently stale on mobile). Full app-bar
recomposition (merge into the logo row + breadth strip + bottom wire) is the
next M1b shell increment. */
@media (max-width: 767px) {
.hb-ekg,
.hb-desktop { display: none !important; }
.hb-mobile-clock { display: inline-flex !important; }
.heartbeat-bar {
height: 28px !important;
gap: 10px !important;
padding: 0 12px !important;
justify-content: flex-end;
}
.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;
}
}
/* (removed .m-hero — RECONCILE 2026-07-16: Design's mobile uses CONTEXT-
SPECIFIC hero sizes, not one generic clamp: 74px grade tier, 40px live
tier, 24px /u stats. Those land per-surface in M1b, not as a global class.) */
/* ============================================================
Session 60 (night2/E) — §7 reveal choreography.
Stamp slam + 90ms staggered context panels. Entrance keyframes floor at
a VISIBLE state (Phase-0 rule: a paused frame is never invisible), and
reduced-motion kills the choreography entirely (ProcessingGrade also
skips straight to the card).
============================================================ */
@keyframes stamp-in {
0% { opacity: .5; transform: rotate(-7deg) scale(1.7); }
60% { opacity: 1; transform: rotate(-7deg) scale(0.96); }
100% { opacity: 1; transform: rotate(-7deg) scale(1); }
}
.stamp-in { animation: stamp-in .34s cubic-bezier(.2,1.6,.4,1) both; }
@keyframes panel-in {
from { opacity: .55; transform: translateY(7px); }
to { opacity: 1; transform: translateY(0); }
}
.grade-reveal > div > * { animation: panel-in .3s ease-out both; }
.grade-reveal > div > *:nth-child(2) { animation-delay: 90ms; }
.grade-reveal > div > *:nth-child(3) { animation-delay: 180ms; }
.grade-reveal > div > *:nth-child(4) { animation-delay: 270ms; }
.grade-reveal > div > *:nth-child(5) { animation-delay: 360ms; }
.grade-reveal > div > *:nth-child(6) { animation-delay: 450ms; }
.grade-reveal > div > *:nth-child(7) { animation-delay: 540ms; }
.grade-reveal > div > *:nth-child(n+8) { animation-delay: 630ms; }
@media (prefers-reduced-motion: reduce) {
.stamp-in, .grade-reveal > div > * { animation: none !important; }
}