/* OMNI tokens. Load order: styles.css, omni-tokens.css, omni-shell.css.
   Re-skins the site by overriding legacy custom properties; no markup changes. */

:root {
  /* ---------- Surfaces ---------- */
  --bg:           #0a0712;
  --bg-2:         #0f0a1d;
  --surface:      #15101f;
  --surface-2:    #1c1530;
  --surface-3:    #2a2042;
  --surface-hi:   #36284f;

  /* ---------- Text ---------- */
  --text:         #f4f1ff;
  --text-2:       rgba(244,241,255,0.72);
  --text-3:       rgba(244,241,255,0.48);
  --text-4:       rgba(244,241,255,0.375);

  /* ---------- Lines ----------
     Three roles, and the role decides the ratio — not the visual weight.
     --line / --line-2 are DECORATIVE dividers: they separate content that is
     already distinguishable some other way (a gap, a background step, a
     heading), so WCAG sets no floor on them and they stay hairline-faint.
     --line-ctl is a CONTROL BOUNDARY: the edge of a text input or a ghost
     button is the ONLY thing that says "this is a control", which is exactly
     the case WCAG 1.4.11 puts a 3:1 floor on. At the old --line-2 (1.49:1)
     an empty input was invisible until focused. --line-hi sits above
     --line-ctl so hover still reads as a change, not as the rest state. */
  --line:         rgba(255,255,255,0.07);   /* decorative divider — no floor */
  --line-2:       rgba(255,255,255,0.14);   /* decorative divider — no floor */
  --line-ctl:     rgba(255,255,255,0.355);  /* control boundary — 3.04:1 worst surface */
  --line-hi:      rgba(255,255,255,0.42);   /* hover / emphasis — 3.66:1, a step above --line-ctl */

  /* ---------- Brand ----------
     THE FILL/INK SPLIT. On this palette one violet cannot do both jobs: a
     violet light enough to read as body text on the dark surfaces (4.5:1)
     is too light to carry white text (white on the old #8b6cff measured
     3.69:1 — the primary CTA, DM bubbles, active chips and the mobile FAB
     all failed). The two jobs are therefore split by token:

       --violet    = FILL. Something light sits ON it. White ink measures
                     4.56:1 here. It still holds 4.09:1 as a graphical mark
                     on --surface, so borders/spinners/underlines are fine.
       --violet-2  = INK. Violet text ON a dark surface. 7.57:1.

     Which token moved is decided by which role has consumers outside this
     file: violet's FILL role is painted from profile.html, library.html,
     music.css and the shell's DM bubbles, so the fix has to ride the token
     they already reference. Rose is the mirror case — its only fill is
     .pill-live in this file while --rose-as-ink is used from three other
     lanes' files — so --rose keeps its ink value and the one fill gets
     --rose-fill. Same principle both times: the shared token carries
     whichever role is painted outside this writer's files.

     If you need violet TEXT on a dark surface, use --violet-2. Do not raise
     --violet back up to "fix" it — that re-breaks every white-on-violet
     surface listed above. */
  --violet:       #7a56ff;   /* FILL — white ink 4.56:1; as a mark 4.09:1 on --surface */
  --violet-2:     #b094ff;   /* INK  — 7.57:1 on --surface */
  --violet-dim:   rgba(122,86,255,0.14);
  --violet-glow:  rgba(122,86,255,0.45);

  --orange:       #ff7849;
  --orange-2:     #ffa07a;
  --orange-dim:   rgba(255,120,73,0.14);

  --teal:         #2dd4bf;   /* signature: verified / supporter / sovereign */
  --teal-2:       #5eead4;
  --teal-dim:     rgba(45,212,191,0.14);

  --gold:         #fbbf24;   /* founder */
  --rose:         #f43f5e;   /* live / hot — INK role (5.44:1 on --bg). See the fill/ink note above. */
  --rose-fill:    #e11d48;   /* FILL role — white ink 4.70:1. Use whenever light text sits ON rose. */
  --ok:           #22c55e;
  --warn:         #f59e0b;
  --fail:         #ef4444;

  /* brand gradients — the one carry-over from the earlier polish layer */
  --grad-brand:   linear-gradient(135deg, var(--violet) 0%, #6b4cff 55%, var(--teal) 160%);
  --grad-heat:    linear-gradient(135deg, var(--orange) 0%, var(--gold) 100%);

  /* ---------- Type ---------- */
  --display:   'Bricolage Grotesque','Inter Tight',system-ui,sans-serif;
  --sans:      'Inter',system-ui,-apple-system,sans-serif;
  --mono:      'JetBrains Mono',ui-monospace,'SF Mono',Menlo,monospace;
  --editorial: 'Instrument Serif','Iowan Old Style',Georgia,serif;

  /* ---------- Radii ---------- */
  --r-xs: 6px; --r-sm: 10px; --r-md: 14px;
  --r-lg: 20px; --r-xl: 28px; --r-pill: 999px;

  /* ---------- Shadows ---------- */
  --shadow-card: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 24px rgba(0,0,0,0.35);
  --shadow-pop:  0 20px 60px rgba(0,0,0,0.55), 0 0 0 1px var(--line-2);
  --shadow-glow: 0 0 32px var(--violet-glow);

  /* ---------- Layout ---------- */
  --nav-h:       60px;
  --player-h:    80px;   /* desktop now-playing bar */
  --player-h-sm: 64px;   /* mobile mini bar */
  --tabbar-h:    56px;   /* mobile bottom tabs */
  --sidebar-w:   240px;  /* desktop left rail */
  --rail-w-icon: 76px;   /* tablet icon rail */
  --rail-w-right:320px;  /* trending / now-playing rail */
  --max-w:       1280px;
  --col-feed:    620px;  /* X-width single feed column */
  --col-wide:    1080px; /* music / discover grids */
  --gutter:      20px;

  /* ---------- Glass — retuned for this palette ---------- */
  --glass-bg:     rgba(21,16,31,0.78);
  --glass-blur:   blur(18px) saturate(160%);
  --glass-line:   var(--line-2);
  --glass-shadow: 0 -8px 28px rgba(0,0,0,0.42);

  /* ---------- Motion ---------- */
  --dur-1: .12s; --dur-2: .18s; --dur-3: .28s; --dur-4: .42s;
  --ease-out:   cubic-bezier(.2,.8,.2,1);
  --ease-inout: cubic-bezier(.4,0,.2,1);

  /* ---------- Z-index scale — single source for stacking order ---------- */
  --z-content: 1;
  --z-rail:    60;
  --z-tabbar:  70;
  --z-player:  80;
  --z-sheet:   90;
  --z-modal:   100;
  --z-toast:   110;
}

/* Legacy custom-property bridge — maps the older variable names
   onto the current palette so shipped markup needs no edits. */
:root {
  --panel:     var(--surface);
  --panel-2:   var(--surface-2);
  --ink:       var(--text);
  --ink-dim:   var(--text-3);
  --brand:     var(--violet);
  --brand-ink: #ffffff;
  --radius:    var(--r-md);
  /* --line, --bg, --ok, --warn keep their names with the values above */
  --err:       var(--fail);
}

/* Base page background and type — applies to every page. The shell-only
   layout resets below are scoped to body.omni-shell. */
body {
  background:
    radial-gradient(1200px 620px at 50% -140px, rgba(139,108,255,0.10), transparent 62%),
    var(--bg);
  color: var(--text);
  font-family: var(--sans);
}
body.omni-shell { display: block; align-items: initial; padding: 0; }

/* Kill the phone-narrow 440px cage once the shell is on. */
body.omni-shell .card,
body.omni-shell .wrap,
body.omni-shell .foot { max-width: none; }
body.omni-shell .nav { display: none; }        /* replaced by the rail / tabbar */
body.omni-shell .brand { display: none; }      /* wordmark lives in the rail now */

/* Primitives — drop-in component classes. */
.btn-primary {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 16px; border-radius: var(--r-pill); border: 1px solid transparent;
  font: 600 14px/1 var(--sans); letter-spacing: -.005em; cursor: pointer;
  background: linear-gradient(180deg, var(--violet) 0%, #6b4cff 100%); color: #fff;
  box-shadow: 0 4px 16px rgba(139,108,255,.35), inset 0 1px 0 rgba(255,255,255,.2);
  transition: box-shadow var(--dur-2) var(--ease-out), transform var(--dur-1) var(--ease-out);
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(139,108,255,.5), inset 0 1px 0 rgba(255,255,255,.2); }
/* A ghost button has no fill, so its border is the only thing that says it is
   a control — the 1.4.11 case. It takes --line-ctl, not --line-2. */
.btn-ghost {
  display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px;
  border-radius: var(--r-pill); background: transparent; color: var(--text);
  border: 1px solid var(--line-ctl); font: 600 14px/1 var(--sans); cursor: pointer;
  transition: background var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out);
}
.btn-ghost:hover { background: var(--surface); border-color: var(--line-hi); }
.btn-sm { padding: 6px 12px; font-size: 13px; }
.btn-lg { padding: 14px 22px; font-size: 15px; }

.pill {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
  border-radius: var(--r-pill); font: 700 11px/1 var(--sans);
  letter-spacing: .06em; text-transform: uppercase;
  background: var(--surface-2); color: var(--text-2); border: 1px solid var(--line);
}
.pill-violet { background: var(--violet-dim); color: var(--violet-2); border-color: rgba(139,108,255,.30); }
.pill-orange { background: var(--orange-dim); color: var(--orange-2); border-color: rgba(255,120,73,.30); }
.pill-teal   { background: var(--teal-dim);   color: var(--teal-2);   border-color: rgba(45,212,191,.30); }
.pill-gold   { background: rgba(251,191,36,.12); color: var(--gold);  border-color: rgba(251,191,36,.30); }
.pill-rose   { background: rgba(244,63,94,.14); color: #fda4af;       border-color: rgba(244,63,94,.30); }
/* white ink, so this one takes the FILL token — --rose itself stays the ink
   value the delta/like/sticker text in other lanes' files reads from */
.pill-live   { background: var(--rose-fill); color: #fff; border-color: transparent; box-shadow: 0 0 0 4px rgba(225,29,72,.20); }
.pill-live::before { content:''; width:6px; height:6px; border-radius:50%; background:#fff; animation: omni-pulse 1.4s infinite; }
@keyframes omni-pulse { 0%,100%{opacity:1} 50%{opacity:.4} }

/* The avatar hue is registered as a number so a caller cannot break the disc:
   an unregistered custom property accepts any token, and a non-numeric one
   would make BOTH background declarations invalid at computed-value time,
   which unsets the property and paints a transparent disc with white letters
   on it. Registered, a bad value falls back to this initial hue instead. */
@property --avatar-h {
  syntax: "<number>";
  initial-value: 270;
  inherits: true;
}

/* Initials avatar — geometry and type only. The colour pair lives in the
   shared rule below so every disc paints from one declaration. */
.avatar {
  display:inline-flex; align-items:center; justify-content:center;
  font: 700 14px/1 var(--display); border-radius:50%; width:36px; height:36px;
  text-transform:uppercase; user-select:none; object-fit:cover;
}
/* Disc colour contract. The generated background varies by HUE per person but
   is pinned to a FIXED perceptual lightness, so identity varies and contrast
   does not. Two ratios an edit here must hold: white ink on the generated
   background never below 4.5:1, and the disc itself never below 3:1 against
   the page surface — the disc is a graphical object, not text.
   Do NOT generate this background from HSL lightness. HSL lightness is not
   perceptual luminance: at one saturation/lightness pair the white-ink ratio
   swings 1.38:1 (hue 60) to 7.55:1 (hue 240) and 293 of 360 hues fail AA.
   Clamping the lightness does not fix it — the floor is still 1.43:1 at 50%
   and 2.48:1 at 38% — because the problem is the colour space, not the number.
   The plain declaration below is the fallback for engines with no oklch().
   It keeps per-user hue AND readable initials — 5.29:1 white-ink minimum, at
   hue 60 — but it cannot also hold the disc edge on the dark surface (1.33:1
   at hue 240). No saturation/lightness pair holds both across the wheel above
   10% saturation, which is colourless, so legible ink wins the trade.
   The selector list also covers the older disc classes so both markups paint
   identically; the two-class form is listed so this white ink outranks the
   earlier two-class rule that tints the letters. That earlier stylesheet keeps
   the older disc's size, font and centering — only the colour pair and the
   inset shading are owned here. On photo discs the background is the loading
   and failure backdrop; the image paints over it. */
.avatar, .av, .av.ph {
  background: hsl(var(--avatar-h, 270) 55% 28%); color:#fff;
  box-shadow: inset 0 -2px 8px rgba(0,0,0,.25), inset 0 1px 0 rgba(255,255,255,.18);
}
/* Fixed-lightness generated background, measured across all 360 hues: white
   ink holds a 4.93:1 minimum (max 5.92:1, spread 0.99), and the disc holds
   3.15:1 on the darkest app surface and 4.32:1 on the lightest. Chroma stays
   at 0.12; 0.16 is more vivid but drops the white-ink minimum to 4.59:1,
   which leaves almost nothing over the 4.5:1 floor. Both ratios that matter
   are internal to the component — ink against its own background — so they do
   not depend on the page behind it and ONE value serves both themes. That is
   why no per-theme avatar background override exists; adding one would
   re-introduce the per-hue swing this rule removes. */
@supports (color: oklch(0 0 0)) {
  .avatar, .av, .av.ph { background: oklch(0.52 0.12 var(--avatar-h, 270)); }
}
.avatar-xs{width:24px;height:24px;font-size:11px}
.avatar-sm{width:32px;height:32px;font-size:12px}
.avatar-md{width:48px;height:48px;font-size:18px}
.avatar-lg{width:64px;height:64px;font-size:24px}
.avatar-xl{width:96px;height:96px;font-size:36px}
.avatar-2xl{width:128px;height:128px;font-size:48px}
.avatar-3xl{width:160px;height:160px;font-size:60px}
.avatar-ring { padding:3px; border-radius:50%;
  background: conic-gradient(from 0deg, var(--violet), var(--teal), var(--orange), var(--violet)); }
.avatar-ring .avatar { border: 2px solid var(--bg); }
/* seen state — ring goes flat, matches IG semantics */
.avatar-ring[data-seen="1"] { background: var(--line-2); }

.verified { display:inline-flex; width:14px; height:14px; vertical-align:-2px;
  background: var(--teal); border-radius:50%; align-items:center; justify-content:center; color:#06160f; }
.verified svg { width:9px; height:9px; }

.stat-num   { font: 700 32px/1 var(--mono); letter-spacing:-.02em; }
.stat-label { font: 600 11px/1 var(--sans); letter-spacing:.1em; text-transform:uppercase; color: var(--text-3); }
.eyebrow    { font: 700 11px/1 var(--sans); letter-spacing:.12em; text-transform:uppercase; color: var(--text-3); }
.h1 { font: 700 clamp(34px,5vw,56px)/1.05 var(--display); letter-spacing:-.02em; margin:0; }
.h2 { font: 700 32px/1.1 var(--display); letter-spacing:-.015em; margin:0; }
.h3 { font: 700 22px/1.2 var(--display); letter-spacing:-.01em; margin:0; }
.h4 { font: 700 17px/1.25 var(--sans); margin:0; }
.muted { color: var(--text-3); } .dim { color: var(--text-2); }
.mono { font-family: var(--mono); } .editorial { font-family: var(--editorial); }

/* Restyle hooks for existing component classes. */
body.omni-shell .item,
body.omni-shell .card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--shadow-card);
}
body.omni-shell .item:hover { border-color: var(--line-2); }
body.omni-shell .btn { background: linear-gradient(180deg, var(--violet) 0%, #6b4cff 100%);
  color:#fff; border-radius: var(--r-pill); box-shadow: 0 4px 16px rgba(139,108,255,.35); }
body.omni-shell .btn.ghost { background: transparent; color: var(--text-2); border:1px solid var(--line-ctl); }
/* Element-level, so this outranks a lane's own single-class input rule
   (0,1,2 vs 0,1,0) and the control boundary is corrected app-wide from one
   place — no lane has to edit its own stylesheet to clear 1.4.11. */
body.omni-shell input, body.omni-shell select, body.omni-shell textarea {
  background: var(--surface-2); border:1px solid var(--line-ctl); border-radius: var(--r-md); color: var(--text); }
body.omni-shell input:focus, body.omni-shell textarea:focus, body.omni-shell select:focus {
  outline:none; border-color: var(--violet); box-shadow: 0 0 0 3px var(--violet-dim); }
body.omni-shell .tag, body.omni-shell .badge, body.omni-shell .pf-role {
  background: var(--surface-2); color: var(--text-3); border:1px solid var(--line); border-radius: var(--r-pill); }
/* supporter badge uses the signature accent, distinct from the action accent */
body.omni-shell .sup-badge { color: var(--teal-2); background: var(--teal-dim); border-color: rgba(45,212,191,.3); }
/* awarded cosmetics stay gold + square-ish — the earned/bought distinction survives the reskin */
body.omni-shell .award-badge { color: var(--gold); background: rgba(251,191,36,.12); border:1px solid rgba(251,191,36,.3); border-radius: var(--r-xs); }
body.omni-shell .act-btn { background: transparent; border-color: transparent; color: var(--text-3); }
body.omni-shell .act-btn:hover { background: var(--surface-2); color: var(--text); }
body.omni-shell .act-btn.on { color: var(--teal-2); border-color: transparent; background: var(--teal-dim); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; }
}

/* Additive tokens: art-gradients, card-hover, scrollbar. */

:root {
  /* ---- Additive layer: no conflict with the values above ---- */

  /* art-placeholder gradients for empty covers — they give a grid
     variety and rhythm when no artwork has been uploaded. */
  --art-grad-1: linear-gradient(135deg, #1a1060 0%, #6c63ff 100%);
  --art-grad-2: linear-gradient(135deg, #0d3040 0%, #00c2ff 100%);
  --art-grad-3: linear-gradient(135deg, #3d0a2e 0%, #ff4fa3 100%);
  --art-grad-4: linear-gradient(135deg, #0a2e15 0%, #4fcf7a 100%);
  --art-grad-5: linear-gradient(135deg, #2e1a00 0%, #f7a64f 100%);
  --art-grad-6: linear-gradient(135deg, #1a0a2e 0%, #b44fff 100%);

  /* one hover wash for every card surface, so the value is not
     re-hard-coded per component */
  --card-hover: rgba(255,255,255,0.03);

  /* scrollbar width, used by the shell's scrollbar rules */
  --scrollbar-w: 6px;
}

/* scrollbar — additive, applies only under the shell */
body.omni-shell ::-webkit-scrollbar { width: var(--scrollbar-w); height: var(--scrollbar-w); }
body.omni-shell ::-webkit-scrollbar-track { background: transparent; }
body.omni-shell ::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 3px; }
body.omni-shell ::-webkit-scrollbar-thumb:hover { background: var(--line-hi); }
body.omni-shell { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }

/* art gradients as drop-in classes for empty covers */
.art-1 { background: var(--art-grad-1); } .art-2 { background: var(--art-grad-2); }
.art-3 { background: var(--art-grad-3); } .art-4 { background: var(--art-grad-4); }
.art-5 { background: var(--art-grad-5); } .art-6 { background: var(--art-grad-6); }

/* badge set → pill equivalence (so markup drops in unchanged) */
.badge-accent { background: var(--violet-dim); color: var(--violet-2); }
.badge-ok     { background: rgba(34,197,94,.12);  color: var(--ok); }
.badge-warn   { background: var(--orange-dim);    color: var(--orange-2); }
.badge-muted  { background: var(--surface-3);     color: var(--text-2); }

/* card hover reads from the token above */
body.omni-shell .item:hover,
body.omni-shell .card-hover:hover { background: var(--card-hover); }

/* WCAG contrast: --text-3 raised to .56 for AA-normal on all surfaces. */
:root {
  --text-3: rgba(244,241,255,0.56);   /* was 0.48 — see above */
  /* --text-4 is the NON-TEXT tier: 3.02:1 at 0.375 alpha on the worst
     surface, raised from 0.28 (2.26:1). The intent has not changed — it is
     still for disabled glyphs, placeholder art marks and icon strokes, and
     it still must NOT carry body copy, labels or values; --text-3 is the
     muted-text token. What changed is the failure mode. At 2.26:1 the token
     was below every WCAG floor, so a lane that reached for it was left with
     something genuinely unreadable and nothing in the system caught it. At
     3.02:1 it clears the 1.4.11 / large-text floor, so the same misuse now
     degrades to "legible but wrong tier" instead of "invisible". */
}

/* Data display. Two rules an edit here must not break. (1) --viz-seq-1..5 is
   ONE hue with monotone lightness, ordered by magnitude — never a rainbow, and
   never a second hue dropped in at a midpoint; the anchor flips per theme, so
   on this dark surface more magnitude reads brighter. (2) --viz-empty is a
   SURFACE step, not a ramp step, so no-data never reads as a low value.
   Ratios are measured against the chart surface, --surface. */
:root {
  --viz-seq-1:    #503e8e;   /* lowest magnitude — 2.15:1, so a filled cell always reads as filled */
  --viz-seq-2:    #7565aa;
  --viz-seq-3:    #998bc7;
  --viz-seq-4:    #beb2e3;
  --viz-seq-5:    #e2d8ff;   /* highest — 13.77:1 */

  --viz-empty:    #1c1530;   /* no data: a surface step, not a hue — absence must not read as a low value */
  --viz-grid:     rgba(255,255,255,0.07);
  --viz-axis:     rgba(255,255,255,0.14);
  --viz-tick:     rgba(244,241,255,0.56);   /* 5.84:1 */
  --viz-emphasis: #b094ff;   /* the one highlighted series — 7.57:1 */
  --viz-deemph:   #6d6389;   /* context marks — 3.37:1, the floor for a meaningful graphical mark */
}

/* Light theme — opt in with <html data-theme="light">.
   Off by default. Accent values are darkened to hold WCAG AA on
   light surfaces; the dark-theme accents do not. */
[data-theme="light"] {
  --bg:         #fbfaff;
  --bg-2:       #f5f1fd;
  --surface:    #f2eefb;
  --surface-2:  #e7e0f5;
  --surface-3:  #dad0ee;
  --surface-hi: #cdc0e6;

  /* Ratios here are the WORST across the six light surfaces, not the best.
     --text-3 was quoted at 4.52:1 against --surface-2 only; on --surface-3
     and --surface-hi it actually measured 4.29:1 and 4.04:1, so the token
     failed AA on the two deepest light surfaces. 0.645 clears all six. */
  --text:   rgba(20,15,34,1);       /* 10.95:1 worst surface */
  --text-2: rgba(20,15,34,0.74);    /*  6.00:1 worst surface */
  --text-3: rgba(20,15,34,0.645);   /*  4.59:1 worst surface — was 0.60 (4.04:1, AA fail) */
  --text-4: rgba(20,15,34,0.495);   /*  3.04:1 — non-text tier, mirrors dark */

  --line:    rgba(20,15,34,0.10);   /* decorative divider — no floor */
  --line-2:  rgba(20,15,34,0.18);   /* decorative divider — no floor */
  --line-ctl:rgba(20,15,34,0.495);  /* control boundary — 3.04:1 worst surface */
  --line-hi: rgba(20,15,34,0.60);   /* hover / emphasis — 4.04:1, a step above --line-ctl */

  /* accents re-darkened — the dark-mode values fail on a light surface */
  /* The fill/ink split holds in light mode too, just without the squeeze:
     these accents are already dark enough that white ink passes on them
     (6.72:1 on --violet, 6.31:1 on --rose), so --rose-fill can be --rose's
     own value here. The token still has to EXIST in this theme — .pill-live
     paints from it unconditionally, and an undefined custom property would
     make the background declaration invalid and drop the pill to no fill
     with white text on the page behind it. */
  --violet:     #5b3fd6;   /* FILL — white ink 6.72:1; as a mark 3.93:1 worst surface */
  --violet-2:   #4a31b8;   /* INK  — 5.07:1 worst surface */
  --violet-dim: rgba(91,63,214,0.12);
  --violet-glow:rgba(91,63,214,0.28);
  --teal:       #115e59;   /* 5.91:1 */
  --teal-2:     #0f5150;   /* 7.08:1 */
  --teal-dim:   rgba(17,94,89,0.12);
  --orange:     #9a3a0a;   /* 5.49:1 */
  --orange-2:   #8a3208;   /* 6.45:1 */
  --orange-dim: rgba(154,58,10,0.12);
  --gold:       #7a4e00;   /* 5.61:1 */
  --rose:       #be1235;   /* INK — 4.92:1 on --surface-2 */
  --rose-fill:  #be1235;   /* FILL — white ink 6.31:1; no split needed at this lightness */
  --ok:         #11602d;   /* 5.99:1 */
  --warn:       #8a5300;   /* 4.94:1 */
  --fail:       #b91c1c;   /* 5.05:1 */

  --glass-bg:     rgba(251,250,255,0.82);
  --glass-shadow: 0 -8px 28px rgba(20,15,34,0.10);
  --card-hover:   rgba(20,15,34,0.035);
  --shadow-card:  0 1px 0 rgba(255,255,255,0.6) inset, 0 8px 24px rgba(20,15,34,0.10);
  --shadow-pop:   0 20px 60px rgba(20,15,34,0.18), 0 0 0 1px var(--line-2);

  /* data display — the ramp anchor flips: on a light surface more magnitude
     reads darker. Ratios here are against the chart surface, --surface. */
  --viz-seq-1:    #aa9be9;   /* 2.15:1 */
  --viz-seq-2:    #8a7bca;
  --viz-seq-3:    #6a5baa;
  --viz-seq-4:    #4a3b8b;
  --viz-seq-5:    #2a1b6b;   /* 12.54:1 */

  --viz-empty:    #e7e0f5;
  --viz-grid:     rgba(20,15,34,0.10);
  --viz-axis:     rgba(20,15,34,0.18);
  --viz-tick:     rgba(20,15,34,0.60);   /* 4.70:1 */
  --viz-emphasis: #5b3fd6;   /* 5.88:1 */
  --viz-deemph:   #8b839f;   /* 3.15:1 */
}
/* light mode kills the violet bloom — it reads as a smudge on white */
[data-theme="light"] body { background: var(--bg); }
/* the now-playing play button inverts */
[data-theme="light"] .np-play { background: var(--text); color: var(--bg); }

/* Light-mode element overrides for values components hard-code for the
   dark theme. Ratios documented against the light surfaces, as above. */

/* seek knob: white is invisible on the light track (1.47:1) — flip it to
   ink, with a page-tinted ring so it keeps an edge over the filled part
   of the bar too. Ink 12.70:1 vs track; ring 4.93:1 vs the fill. */
[data-theme="light"] .np-bar .knob {
  background: var(--text);
  box-shadow: 0 0 0 2px var(--bg), 0 1px 4px rgba(20,15,34,0.35);
}

/* preview banner: border + chip re-tint to the darkened accent family.
   Border weight matches the dark theme (1.77:1 vs the banner fill);
   chip text holds 4.93:1 on the chip tint. */
[data-theme="light"] .omni-sim-banner { border-color: rgba(154,58,10,0.38); }
[data-theme="light"] .omni-sim-banner::before { background: rgba(154,58,10,0.14); }

/* unread hover: the dark-theme tint composites to the same luminance as
   the unread base on light surfaces (1.02:1 step — no visible feedback);
   the darkened accent restores the step (1.11:1, vs 1.06:1 in dark). */
[data-theme="light"] .omni-notif[data-unread="1"]:hover { background: rgba(91,63,214,0.18); }

/* verified badge: the fill token darkens, so the near-black glyph falls
   to 2.45:1 — flip the glyph light. 7.27:1 on the darkened fill. */
[data-theme="light"] .verified { color: #f0fdfa; }

/* rose pill ink re-darkened — the pale ink reads 1.40:1 on the light
   tint. 5.23:1 on the deepest light surface. */
[data-theme="light"] .pill-rose { color: #a30d2f; }

/* Ink-follows-paint. A card may carry its own paint — a per-creator colour
   or image applied through the element's inline style. Inline paint cannot
   flip with the theme, so theme-flipped ink can land on it unreadably: the
   light theme turns the text family near-black, and near-black ink on a
   dark inline paint measures ~1:1 — invisible. Whenever a card paints
   itself, the ink and hairline custom properties must be pinned, on the
   card element, to the family that reads on THAT paint — identically in
   both themes, because the paint is identical in both themes.

   The [style*=] match is an interim heuristic: it detects that a card
   carries inline paint but cannot know the paint's luminance, so it
   assumes the paint is dark and pins the light-ink family. The only
   custom paint shipped today is dark, and light ink is also the safer
   default over an unknown photo background. A genuinely light custom
   paint is NOT cured by this heuristic — the page must measure its
   paint's luminance and stamp data-ink (hooks below), which overrides it.

   Mechanics this rule depends on:
   - Properties are declared on the card ELEMENT, so the theme blocks
     (which declare the same properties on the root) are outranked by
     inheritance, not by selector weight — the pin is theme-independent
     by structure.
   - color is re-anchored here because text with no colour rule of its
     own inherits a colour computed at <body> against the page theme; the
     pin must restart that inheritance at the painted boundary.
   - --ink / --ink-dim repeat the root-level alias mapping locally: the
     root alias resolves var(--text) AT THE ROOT, so without a local
     re-bind the legacy names would carry the theme's ink straight past
     the pin. Ratios below are measured on the currently shipped paint. */
body.omni-shell .card[style*="background-color"],
body.omni-shell .card[style*="background-image"] {
  --text:   #f4f1ff;                  /* 16.67:1 */
  --text-2: rgba(244,241,255,0.72);   /*  8.94:1 */
  --text-3: rgba(244,241,255,0.56);   /*  5.82:1 */
  --text-4: rgba(244,241,255,0.375);  /* non-text tier, as everywhere */
  --line:    rgba(255,255,255,0.07);
  --line-2:  rgba(255,255,255,0.14);
  --line-ctl:rgba(255,255,255,0.355);
  --line-hi: rgba(255,255,255,0.42);
  --ink:     var(--text);
  --ink-dim: var(--text-3);
  color: var(--text);
}

/* Paint-luminance contract — the durable shape the heuristic above defers
   to. A page that paints a card computes the paint's luminance once and
   stamps data-ink="light" (dark paint, light ink) or data-ink="dark"
   (light paint, dark ink) on the painted card. Both hooks pin the same
   property set as the heuristic and are theme-independent the same way.
   Ordering is load-bearing: each selector here weighs one type + two
   classes + one attribute — exactly what the heuristic weighs — so on a
   card matching both, these win by source order alone. Do not move them
   above the heuristic. A stylesheet cannot verify an arbitrary creator
   paint; only the page-side luminance measurement closes that loop, so
   the heuristic is the stopgap and the stamp is the contract. */
body.omni-shell .card[data-ink="light"] {
  --text:   #f4f1ff;
  --text-2: rgba(244,241,255,0.72);
  --text-3: rgba(244,241,255,0.56);
  --text-4: rgba(244,241,255,0.375);
  --line:    rgba(255,255,255,0.07);
  --line-2:  rgba(255,255,255,0.14);
  --line-ctl:rgba(255,255,255,0.355);
  --line-hi: rgba(255,255,255,0.42);
  --ink:     var(--text);
  --ink-dim: var(--text-3);
  color: var(--text);
}
/* Dark ink for a LIGHT paint: the light theme's measured text/hairline
   family — 10.95:1 full ink, 6.00:1 secondary, 4.59:1 tertiary against the
   DEEPEST light surface — so the pin holds WCAG AA for any paint at
   least as light as that surface range. A page must not stamp "dark"
   for a paint darker than that range. (The tertiary value tracks the light
   theme's corrected --text-3; the old 0.60 alpha measured 4.04:1 here and
   did not actually hold AA on the deepest paints this pin claims to cover.) */
body.omni-shell .card[data-ink="dark"] {
  --text:   rgba(20,15,34,1);
  --text-2: rgba(20,15,34,0.74);
  --text-3: rgba(20,15,34,0.645);
  --text-4: rgba(20,15,34,0.495);
  --line:    rgba(20,15,34,0.10);
  --line-2:  rgba(20,15,34,0.18);
  --line-ctl:rgba(20,15,34,0.495);
  --line-hi: rgba(20,15,34,0.60);
  --ink:     var(--text);
  --ink-dim: var(--text-3);
  color: var(--text);
}
