/* ════════════════════════════════════════════════════════════
 * Pyre — public viewer tip/support page (pyre.studio/<handle>).
 * Server-hydrated, NO app shell (public-site/, matrix principle 7).
 *
 * ⚠ The `:root` block below is GENERATED by scripts/gen-design-tokens.js from
 * shared/design-tokens.json — DO NOT HAND-EDIT anything between the
 * `design-tokens:start` / `design-tokens:end` markers. Change a value in
 * shared/design-tokens.json and re-run `node scripts/gen-design-tokens.js`
 * (`--check` fails CI on drift). This page is served standalone and cannot
 * @import public/omni-ui.css, so it carries its own generated mirror of the
 * exact same token block.
 *
 * Everything AFTER the end marker inside :root is hand-authored and preserved:
 * the font stacks, --touch-min/--safe-bottom, and the platform BRAND colours
 * (--twitch/--youtube/--kick/--tiktok are brand facts, never theme choices —
 * they must never be re-skinned with the palette).
 *
 * Nothing hand-authored may re-declare a generated property: the generated
 * block comes FIRST, so a later duplicate would silently win.
 * ════════════════════════════════════════════════════════════ */
:root {
/* design-tokens:start (generated by scripts/gen-design-tokens.js — DO NOT EDIT) */
  /* color */
  --bg:#141110;
  --surface:#1c1817;
  --surface-raised:#241f1d;
  --surface-inset:#191615;
  --surface-2:#2a2422;
  --surface-3:rgba(46,41,39,.55);
  --border:rgba(255,255,255,.14);
  --border-strong:rgba(255,255,255,.24);
  --border-accent:rgba(255,157,77,.45);
  --accent:#ff9d4d;
  --accent-soft:#ffb673;
  --accent-2:#ffc79a;
  --accent-wash:rgba(255,157,77,.14);
  --accent-glow:rgba(255,157,77,.28);
  --on-strong:#26140a;
  --inset-2:#171413;
  --text:#f7f3ef;
  --text-dim:#cec5bd;
  --text-faint:#9b918a;
  --ok:#45d0a8;
  --ok-soft:#6bd9b3;
  --warn:#ffd166;
  --danger:#f0505f;
  /* platform */
  --p-twitch:#9147ff;
  --p-youtube:#ff0000;
  --p-kick:#53fc18;
  --p-tiktok:#fe2c55;
  --p-discord:#5865f2;
  --p-x:#e7e9ea;
  --p-instagram:#e1306c;
  --p-facebook:#0866ff;
  /* space */
  --space-1:4px;
  --space-2:8px;
  --space-3:12px;
  --space-4:16px;
  --space-5:20px;
  --space-6:24px;
  --space-8:32px;
  --space-10:40px;
  /* radius */
  --r-sm:9px;
  --r:12px;
  --r-md:12px;
  --r-chip:7px;
  --r-lg:18px;
  --r-pill:999px;
  /* shadow */
  --shadow-card:0 1px 2px rgba(0,0,0,.25), 0 12px 32px -18px rgba(0,0,0,.65);
  --shadow-modal:0 24px 64px rgba(0,0,0,.7);
  --shadow-drawer:-8px 0 32px rgba(0,0,0,.55);
  --glow-accent:0 0 14px rgba(255,157,77,.45);
  /* motion */
  --dur-fast:120ms;
  --dur:180ms;
  --dur-slow:280ms;
  --ease:cubic-bezier(.2,.7,.3,1);
  /* breakpoint */
  --bp-mobile:560;
  --bp-tablet:980;
  /* design-tokens:end */
  /* platform brand colours — NOT theme tokens, never re-skinned.
     (The generated block above emits these under --p-* names; these are the
     names this stylesheet's own rules use.) */
  --twitch: #9147ff;
  --youtube: #ff0000;
  --kick: #53fc18;
  --tiktok: #fe2c55;
  /* The four the Public Profile's social row added (2026-08-15). `--x` is
     deliberately terse because the SOCIAL KIND is literally `x` and the two names
     have to line up (`--p-<kind>`); it is declared only inside this page's :root,
     alongside its three siblings, and nothing else on the page uses that name. */
  --discord: #5865f2;
  --x: #e7e9ea;
  --instagram: #e1306c;
  --facebook: #0866ff;
  /* fonts (family strings the JSON can't express; mirrors public/omni-ui.css) */
  --font-display: 'Chakra Petch', 'Segoe UI', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
  /* touch + safe-area (mirrors public/omni-ui.css S1 foundation) */
  --touch-min: 44px;
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  /* 🔴 THE ONE FAMILY WIDTH (owner, 2026-08-16: "the used area on 'Home' and
     'Chat' is much smaller than commands and the others, which looks weird").
     Home used to collapse to a 560px column whenever tipping was off — which is
     MOST creators — while the server-rendered pages stayed at 1040px, so
     clicking Commands doubled the column width under the visitor. Every
     container in the family now derives its width from THIS ONE VALUE, and
     `creator-shell-pages.test.js` fails if any of them re-declares its own.
     The wider value is deliberate: it is the airier one, and the live band's
     player column (the chat panel beside it is a fixed 340px) and the two-column
     card row are the parts that get the extra room. */
  --shell-w: 1120px;
  color-scheme: dark;
}

* { box-sizing: border-box; }
button, a, .amt, .pay, .slink { -webkit-tap-highlight-color: transparent; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: radial-gradient(120% 80% at 50% -10%, var(--surface-raised), var(--bg) 60%);
  min-height: 100dvh;
  /* 🔴 THE PAGE BODY NEVER SCROLLS SIDEWAYS (Web Interface Guidelines, and the
     owner's condition on the live band). The band's own tracks are already
     `minmax(0,1fr)` + `min-width:0` so nothing should reach here — this is the
     belt behind that, and it also catches a long unbroken bio or link label. */
  overflow-x: hidden;
  color: var(--text);
  font-family: var(--font-display);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
a { color: var(--accent-2); text-decoration: none; }

/* ══ THE EMBER ATMOSPHERE ═════════════════════════════════════════════════════
 * Owner, 2026-08-16: "It looks better, although still a bit dull." The ground
 * was a flat #141110 rectangle — correct tokens, no weather. Pyre's dark is
 * meant to be FIRE-LIT, so the page gets two fixed, pointer-events-none layers
 * behind everything:
 *
 *   ::before — a deep warm glow anchored top-left, a cooler ember falling off to
 *     the right, and a darkening at the very bottom so long pages do not end in
 *     a uniform slab. FIXED, so scrolling moves the content THROUGH the light
 *     instead of dragging the light along with it.
 *   ::after — a grain wash. A fractal-noise SVG as a data: URI, tiled small and
 *     held at ~5% opacity. This is the difference between "dark grey" and "a
 *     lit room": it
 *     breaks up the gradient banding that flat dark backgrounds show on cheap
 *     panels, and it costs one inline string.
 *
 * CSP-SAFE BY CONSTRUCTION: no external asset, no <script>, no url() pointing at
 * a host. The data: URI is an IMAGE, and core's helmet policy carries
 * `img-src 'self' https: data:` (server/config/security-headers.js).
 *
 * `z-index: -1` is what keeps both behind the page content: `body` establishes
 * no stacking context, so a negative-z child paints straight after the canvas
 * background and before every in-flow box. Nothing else on the page needs a
 * z-index because of it. */
body::before,
body::after {
  content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
}
body::before {
  background:
    radial-gradient(115% 85% at 8% -8%, rgba(249, 114, 13, .17) 0%, transparent 58%),
    radial-gradient(85% 65% at 92% 2%, rgba(240, 80, 95, .10) 0%, transparent 62%),
    radial-gradient(120% 60% at 50% 108%, rgba(0, 0, 0, .55) 0%, transparent 62%);
}
body::after {
  /* feTurbulence at a high base frequency = fine film grain, desaturated by the
     feColorMatrix so it stays GRAIN and never becomes colour speckle on the warm
     ground. The strength lives here, in one number: it must be felt, never seen.
     `stitchTiles` is what lets the 140px tile repeat without a visible seam. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
  opacity: .055;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--on-strong); padding: var(--space-3) var(--space-4); border-radius: var(--r-sm);
}
.skip-link:focus { left: var(--space-3); top: var(--space-3); }

.vpage {
  /* THE FAMILY WIDTH — one token, shared with `.cshell` (the server-rendered
     pages). The whole point of the shell is that clicking a tab does not change
     the page under you, and a jump in column width is exactly the kind of tell
     that makes two pages feel like two products. Never hardcode a width here. */
  max-width: var(--shell-w); margin: 0 auto;
  padding: var(--space-6) var(--space-4) var(--space-10);
  display: flex; flex-direction: column; gap: var(--space-4);
}
@media (max-width: 560px) {
  .vpage {
    padding: var(--space-4) var(--space-3)
             max(var(--space-8), env(safe-area-inset-bottom, 0px));
  }
}

/* ── cards ─────────────────────────────────────────────────── */
.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: var(--space-5); box-shadow: var(--shadow-card);
}
.card--raised { background: var(--surface-raised); }
.card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-4); }
.card-head h2 { margin: 0; font-size: 17px; font-weight: 700; }
.badge {
  font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: .04em;
  color: var(--accent-2); background: var(--accent-wash); border: 1px solid var(--border-accent);
  padding: 4px 10px; border-radius: var(--r-pill);
}

/* ══ THE HERO — one identity bar for EVERY creator surface ═══════════════════
 * Redesign 2026-08-16 (owner-approved mock): the banner is no longer a separate
 * letterbox above the page, it is the hero's own ground, faded into the page so
 * the identity row sits ON it. ONE hero wears every tab (the compact variant is
 * gone, 2026-08-16); scrolling condenses it — see `.cheader.is-condensed`.
 *
 * The SAME class names are used by public-site/creator-shell.js (server-rendered
 * subpages) and by tip.js (the client-rendered Home), which is what makes the
 * family look like one product instead of four pages. */
.chero {
  position: relative; isolation: isolate; min-width: 0;
  border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden;
  background:
    radial-gradient(90% 150% at 15% 0%, rgba(249, 114, 13, .20) 0%, transparent 55%),
    radial-gradient(70% 130% at 88% 15%, rgba(240, 80, 95, .16) 0%, transparent 55%),
    linear-gradient(165deg, var(--surface-raised) 0%, var(--bg) 80%);
}
/* The banner fades INTO the page rather than ending at a hard edge. */
.chero::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(to top, var(--bg) 0%, rgba(20, 17, 16, .35) 42%, transparent 72%);
}
/* (`.chero--compact` was DELETED 2026-08-16. Every non-Home surface used to wear
   a small-avatar, no-banner, no-bio variant, so moving between tabs swapped the
   identity block for a differently-sized one — half of the owner's "It still
   feels like a huge jump". There is now ONE hero on every tab and it CONDENSES
   ON SCROLL instead: `.cheader.is-condensed` below, driven by creator-shell.js's
   `mountHeader()` — which every surface in the family loads, so the shrink is a
   property of where you are on the page rather than of which tab you are on.) */
/* An <img>, NEVER a CSS url(): a creator-supplied string inside url() is an
   injection surface with no safeUrl() at the other end (tip.html's own note). */
.chero-banner { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; object-fit: cover; }
.chero-id {
  position: relative; z-index: 2; display: flex; align-items: flex-end; gap: var(--space-4);
  padding: 126px var(--space-5) var(--space-4);
  min-width: 0;
}
@media (max-width: 680px) {
  .chero-id { flex-direction: column; align-items: center; text-align: center; padding-top: 96px; }
}

.chero-av, .av-lg {
  width: 84px; height: 84px; border-radius: 50%; flex: none;
  background: linear-gradient(135deg, var(--accent), var(--accent-soft));
  display: grid; place-items: center;
  /* on the accent fill, text uses --on-strong (the ember accent is light —
     white would land under 2:1). Same rule as public/stream/shell.css. */
  font-family: var(--font-display); font-weight: 800; font-size: 32px; color: var(--on-strong);
  border: 4px solid var(--bg);
  position: relative; overflow: hidden;
}
.chero-av img, .av-lg img { width: 100%; height: 100%; object-fit: cover; }
/* LIVE gets the ring + the glow. Offline is the resting state of a channel, not
   an error, so it simply does not get them (never a red "offline" treatment). */
.chero.is-live .chero-av, .chero.is-live .av-lg {
  box-shadow: 0 0 0 2px var(--danger), 0 0 26px rgba(240, 80, 95, .45);
}

.chero-who { flex: 1 1 auto; min-width: 0; padding-bottom: 2px; }
.chero-name {
  margin: 0; font-size: 23px; font-weight: 700; letter-spacing: .01em;
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; min-width: 0;
}
.chero-name a { color: inherit; }
@media (max-width: 680px) { .chero-name { justify-content: center; } }
.chero .handle { color: var(--text-dim); font-family: var(--font-mono); font-size: 13px; margin-top: 2px; }
.chero-bio {
  color: var(--text-dim); font-size: 13px; margin: 3px 0 0; max-width: 52ch;
  white-space: pre-wrap; word-break: break-word;
}
@media (max-width: 680px) { .chero-bio { margin-inline: auto; } }
/* ── WHAT'S ON RIGHT NOW (2026-08-17) ──
   The live stream's title + category, directly under the name. ONE LINE, always:
   `white-space: nowrap` + `text-overflow: ellipsis` mean a 140-character title
   (or a title made of one unbroken word) can never grow the hero or wrap it into
   a paragraph — the value is streamer-typed and arrives without a layout budget.
   `min-width: 0` is what actually lets the ellipsis happen inside the flex
   column (`.chero-who`), whose default min-width would otherwise be `auto`. */
.chero-live {
  margin: 4px 0 0; min-width: 0; max-width: 60ch;
  color: var(--text); font-size: 13px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 680px) { .chero-live { margin-inline: auto; } }
/* The category is the quieter half and carries its OWN separator, so a stream
   with no category set renders the title alone with no dangling "·" — and the
   condensed rule below drops the separator by dropping this one element. */
.chero-live-cat { color: var(--text-dim); font-weight: 500; }
.chero-live-cat::before { content: ' · '; color: var(--text-dim); }

/* The creator's tagline sits between the handle and the bio: brighter than the
   bio (it is a title, not body copy) but never competing with the name. */
.vhero-title { color: var(--text); font-weight: 600; margin-top: var(--space-2); max-width: 46ch; word-break: break-word; }
@media (max-width: 680px) { .vhero-title { margin-inline: auto; } }

/* The right-hand stack: Support, then the social marks. */
.chero-cta { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-2); flex: none; padding-bottom: 2px; }
@media (max-width: 680px) { .chero-cta { align-items: center; } }
.tipbtn {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  font: inherit; font-size: 14px; font-weight: 800; padding: 11px 24px;
  border: 0; border-radius: var(--r-md); cursor: pointer;
  background: linear-gradient(180deg, var(--accent-soft), var(--accent));
  color: var(--on-strong); box-shadow: 0 4px 22px var(--accent-glow);
  transition: filter var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.tipbtn:hover { filter: brightness(1.06); transform: translateY(-1px); }

/* THE LIVE CHIP — the mock's pulsing red pill. `--danger` is the token for it:
   here it is not an error colour, it is the platform-universal "on air" red, and
   it appears ONLY when we actually know the creator is live (Twitch-only truth,
   plan F7 — an unknown state renders nothing at all rather than a lie). */
.livechip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 800; letter-spacing: .08em;
  padding: 4px 12px; border-radius: var(--r-pill);
  box-shadow: 0 0 18px rgba(240, 80, 95, .45);
}
.livechip::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: #fff; animation: cpulse 1.5s infinite; }
@keyframes cpulse { 50% { opacity: .35; } }
.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: var(--r-pill); border: 1px solid var(--border); color: var(--text-dim); }
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
/* (.pill--off removed 2026-08-16 — defined here and applied nowhere. The offline
   state is the BASE .pill, which is correct: "offline" is the resting state of a
   channel, not an error, and painting it --danger said otherwise. F8.8.) */

.tips-off-note { color: var(--text-dim); font-size: 13px; margin-top: var(--space-3); }
@media (max-width: 680px) { .tips-off-note { text-align: center; } }

.socials { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-4); padding: 0; list-style: none; }
@media (max-width: 680px) { .socials { justify-content: center; } }
.slink {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 12px; border-radius: var(--r-pill);
  background: var(--surface); border: 1px solid var(--border); color: var(--text); font-weight: 600; font-size: 12px;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast);
}
.slink:hover { border-color: var(--border-accent); background: var(--accent-wash); }
/* The brand mark. Geometry-only paths filled with `currentColor`, so a mark
   inherits the pill's text colour (and its hover) instead of shipping a baked-in
   brand hex that would fight the theme in one of the two colour schemes. */
.slink-mark { display: inline-flex; align-items: center; color: var(--text-dim); }
.slink:hover .slink-mark { color: var(--text); }
.slink-mark .smark { display: block; }

/* ══ THE LIVE BAND (plan Phase E) ══════════════════════════════════════════
 * The embedded player + the merged read-only chat, directly under the identity
 * bar. Absent from the DOM entirely unless the creator opted in, so nothing
 * here can affect a page that does not have one.
 *
 * 🔴 THE OVERFLOW CONTRACT (owner condition: "no overflow on PC"). A grid track
 * defaults to `min-content`, which for a 16:9 iframe and a chat panel means
 * "whatever the content demands" — i.e. the band would push the page wider than
 * the viewport at narrow desktop widths and the BODY would scroll sideways. So:
 *
 *   • the player track is `minmax(0, 1fr)` — the `0` is the whole point, it lets
 *     the track shrink below its content instead of forcing the row wide;
 *   • the chat column is a FIXED 340px at desktop and stacks UNDER the player at
 *     the breakpoint, exactly as the approved mock shows;
 *   • every box in the band carries `min-width: 0`, because a flex/grid item
 *     whose min-width is `auto` cannot shrink either;
 *   • the tab row WRAPS rather than overflowing, and each tab is `flex: none` so
 *     a long platform name never squeezes a mark to nothing;
 *   • the page body gets `overflow-x: hidden` as the belt behind all of it.
 *
 * public-site/tip.test.js pins each of these rules by name — the same
 * structural-CSS regression pin public/stream/sec-stream.test.js uses for the
 * Music card, which is where this class of bug was caught before. */
.liveband {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  align-items: stretch;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;                 /* the rounded corner clips the player, and
                                       nothing inside can paint past the frame */
  background: var(--surface);
  box-shadow: var(--shadow-card);
}
/* Player-only: one column, not a 340px hole. */
.liveband--solo { grid-template-columns: minmax(0, 1fr); }
@media (max-width: 900px) {
  /* The chat stacks UNDER the player (the mock's breakpoint). */
  .liveband { grid-template-columns: minmax(0, 1fr); }
}
.lb-player-col { display: flex; flex-direction: column; min-width: 0; }

/* ── the tab row ── */
.ptabs {
  display: flex; align-items: center; gap: 2px; flex-wrap: wrap; min-width: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.ptabs-spacer { flex: 1 1 auto; min-width: 0; }
.ptab {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  font-family: var(--font-display); font-size: 12px; font-weight: 600;
  padding: 6px 12px; border-radius: var(--r-sm);
  background: none; border: 1px solid transparent; color: var(--text-faint);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast);
}
.ptab:hover { border-color: var(--border); color: var(--text-dim); }
.ptab.on { background: var(--surface-raised); color: var(--text); border-color: var(--border); }
/* The brand mark takes the platform's own token colour. GEOMETRY ONLY — the
   marks come from the shared map (public-site/platform-icons.js), never a
   coloured dot and never an emoji. */
.ptab-mark-wrap { display: inline-flex; align-items: center; flex: none; }
.ptab-mark-wrap .ptab-mark { display: block; }
.ptab-mark-wrap .pm-twitch { color: var(--p-twitch); }
.ptab-mark-wrap .pm-youtube { color: var(--p-youtube); }
.ptab-mark-wrap .pm-kick { color: var(--p-kick); }
.ptab-mark-wrap .pm-tiktok { color: var(--p-tiktok); }
.ptab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Only a KNOWN-live platform gets this. Kick/YouTube report `live: null`
   (unknown, plan F7) and get no badge at all — never an "offline" one. */
.ptab-live {
  flex: none; font-family: var(--font-mono); font-size: 9px; font-weight: 700;
  letter-spacing: .06em; color: var(--danger);
}
.ptabs-watch { flex: none; font-size: 11px; color: var(--accent-2); white-space: nowrap; }

/* ── the player ── */
.lb-stage {
  position: relative; min-width: 0; background: #000;
  aspect-ratio: 16 / 9; display: grid; place-items: center;
}
.lb-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
.lb-offline { color: var(--text-faint); font-size: 13px; text-align: center; padding: var(--space-4); }

/* ── the chat panel ── */
.lb-chat {
  min-width: 0; min-height: 0; display: flex; flex-direction: column;
  border-left: 1px solid var(--border); background: var(--surface);
}
@media (max-width: 900px) {
  .lb-chat { border-left: 0; border-top: 1px solid var(--border); height: 340px; }
}
.lb-chat-frame { flex: 1 1 auto; width: 100%; min-width: 0; min-height: 320px; border: 0; display: block; }

/* ── columns ───────────────────────────────────────────────── */
.vcols { display: grid; gap: var(--space-4); grid-template-columns: 1.1fr .9fr; align-items: start; }
@media (max-width: 760px) { .vcols { grid-template-columns: 1fr; } }
.vstack { display: flex; flex-direction: column; gap: var(--space-4); }

/* ── tip form ──────────────────────────────────────────────── */
.field { margin-bottom: var(--space-4); }
.field label, .field .lbl { display: block; font-size: 12px; font-weight: 600; color: var(--text-dim); margin-bottom: 6px; }
.input, .textarea {
  width: 100%; background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: 11px 14px; color: var(--text); font-family: var(--font-display); font-size: 14px; outline: none;
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}
.input:focus, .textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
.textarea { resize: vertical; }

.amt-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); }
@media (max-width: 560px) {
  .amt-grid { grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); }
}
.amt {
  font-family: var(--font-mono); font-weight: 700; font-size: 15px; padding: 12px 0; border-radius: var(--r-sm);
  background: var(--surface-inset); border: 1px solid var(--border); color: var(--text); cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast);
}
.amt:hover { border-color: var(--border-accent); }
.amt.on { background: var(--accent-wash); border-color: var(--accent); color: var(--accent-2); box-shadow: var(--glow-accent); }

.pay-row { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.pay {
  display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; border-radius: var(--r-sm);
  background: var(--surface-inset); border: 1px solid var(--border); color: var(--text-dim);
  font-weight: 600; font-size: 12px; cursor: pointer;
}
.pay.on { border-color: var(--accent); color: var(--text); background: var(--accent-wash); }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer; border: 0;
  font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--text);
  padding: 12px 18px; border-radius: var(--r);
  transition: transform var(--dur-fast) var(--ease), filter var(--dur);
}
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn--primary { color: var(--on-strong); background: linear-gradient(135deg, var(--accent), var(--accent-soft)); box-shadow: var(--glow-accent); }
.btn--primary:not(:disabled):hover { transform: translateY(-1px); filter: brightness(1.08); }
.btn--lg { font-size: 16px; padding: 14px 20px; }
.btn-block { width: 100%; }

.helpbox { color: var(--text-dim); font-size: 12px; margin: var(--space-3) 0 0; }
.form-error { display: none; color: var(--danger); font-size: 13px; margin-top: var(--space-3); }
.form-error:not(:empty) { display: block; }

/* ── goals ─────────────────────────────────────────────────── */
.well { background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--space-4); }
.well + .well { margin-top: var(--space-3); }
.between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.between .t { font-weight: 600; }
.mono { font-family: var(--font-mono); }
.dim { color: var(--text-dim); }
.bar { height: 8px; border-radius: var(--r-pill); background: var(--surface-2); overflow: hidden; margin-top: 6px; }
.bar i { display: block; height: 100%; border-radius: var(--r-pill); background: linear-gradient(90deg, var(--accent), var(--accent-soft)); }

/* ── rows (recent tips / leaderboard / songs) ─────────────── */
.row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) 0; border-top: 1px solid var(--border); }
.row:first-of-type { border-top: 0; }
.row .grow { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.row .t { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .s { color: var(--text-dim); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.av { width: 34px; height: 34px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--accent-soft)); flex: none; }
.av.sm { width: 26px; height: 26px; }
.tip-end { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; flex: none; }
.tip-end .ago { font-family: var(--font-mono); font-size: 10px; color: var(--text-dim); }
.rank { width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center; font-family: var(--font-mono); font-weight: 700; font-size: 11px; background: var(--surface-inset); border: 1px solid var(--border); color: var(--text-dim); flex: none; }

.empty { color: var(--text-dim); font-size: 13px; padding: var(--space-3) 0; }

/* (the merch-strip rules moved out with the card on 2026-08-16 — the server has
   always sent `merchStrip: []`, so neither the markup nor these rules could ever
   render. They come back with the real surface, F8.5.) */

/* ── leaderboard range tabs + the emoji row ──────────────────────
   The tabs live in the card head, so the card keeps ONE heading and the
   range reads as a property of it rather than as a second control bar. */
.lbtabs { display: flex; gap: 4px; margin-left: auto; }
.lbtab {
  font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .04em;
  padding: 3px 8px; border-radius: var(--r-pill); border: 1px solid transparent;
  background: none; color: var(--text-dim); cursor: pointer;
}
.lbtab[aria-selected="true"] { color: var(--accent); border-color: var(--border-accent, var(--border)); background: var(--surface-inset); }
.emoji-row { display: flex; flex-wrap: wrap; gap: 4px; margin-top: var(--space-2); }
.emoji-btn {
  font-size: 16px; line-height: 1; padding: 4px 6px; border-radius: var(--r-sm);
  border: 1px solid var(--border); background: var(--surface); cursor: pointer;
}
.emoji-btn:hover { border-color: var(--border-accent, var(--border)); background: var(--accent-wash); }
/* Coarse pointers get a real 44px target (the page already has this block). */
@media (pointer: coarse) { .emoji-btn { min-width: 44px; min-height: 44px; } }

/* ── upcoming streams ───────────────────────────────────────────
   The strip sits between the hero and the tip column, so it stays inside the
   page rhythm rather than starting a new one. The reminder link is the same
   .slink pill the rest of the page uses, one size down. */
.sched { margin-top: var(--space-4); }
.sched .row { gap: var(--space-3); }
.slink.sm { padding: 5px 10px; font-size: 11px; flex: none; }

/* ── songs & media (the profile card) ───────────────────────────
   A sub-heading INSIDE a card — the songs card has two lists (now playing,
   up next) and needs to separate them without a second card. Deliberately
   quieter than .card-head h2: it labels a list, it does not open a section. */
.card .shead {
  font-family: var(--font-mono); font-size: 10px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint, var(--text-dim));
  margin-top: var(--space-3);
}
.card .shead:first-of-type { margin-top: 0; }
.card-head .mono.dim { font-size: 11px; }

/* ── voices (TTS samples) ──────────────────────────────────────
   THE CARD SHIPPED UNSTYLED (F8.3): voicesBlockHtml() has always emitted
   .voice-row / .voice-meta / .voice-sample and NONE of them existed here, so the
   block rendered as raw flow content with a browser-default <audio> under it —
   on a page whose whole job is to look finished. These follow the row language
   the recent-tips / leaderboard lists already use, so the card reads as part of
   the page rather than as an embed someone pasted in. */
.voice-row {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-3) 0; border-top: 1px solid var(--border);
}
.voice-row:first-of-type { border-top: 0; }
.voice-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 40%; }
.voice-meta .t { font-weight: 600; color: var(--text); word-break: break-word; }
.voice-meta .s { font-family: var(--font-mono); font-size: 11px; color: var(--text-dim); }
/* The native control, sized to the row and prevented from forcing the card wide.
   `height` keeps Chrome's and Firefox's very different default heights from
   changing the row rhythm. */
.voice-sample { flex: 1 1 220px; min-width: 0; max-width: 100%; height: 34px; }
@media (max-width: 560px) {
  .voice-row { gap: var(--space-2); }
  .voice-sample { flex-basis: 100%; }
}

/* ── footer ────────────────────────────────────────────────── */
.vfoot { text-align: center; color: var(--text-dim); font-size: 12px; padding-top: var(--space-4); }

.hide { display: none !important; }

/* ── loading / error full-page states ──────────────────────── */
.vstate { max-width: 520px; margin: 0 auto; padding: var(--space-10) var(--space-4); text-align: center; color: var(--text-dim); }
.vstate h1 { color: var(--text); font-size: 22px; margin: 0 0 var(--space-3); }
.spinner { width: 28px; height: 28px; margin: 0 auto var(--space-4); border: 3px solid var(--accent-wash); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }

/* ── mobile / touch refinements (additive, desktop byte-unchanged) ──
 * Touch-first sizing for tappable controls and the iOS no-zoom input fix.
 * Scoped to coarse pointers so the desktop layout is untouched. */
@media (hover: none) and (pointer: coarse) {
  /* >=44px touch targets — raise padding, never shrink the visual weight */
  .pay {
    min-height: var(--touch-min, 44px);
    padding-block: var(--space-3);
  }
  .slink {
    min-height: var(--touch-min, 44px);
    padding-block: var(--space-3);
  }
  /* iOS: a >=16px field font stops Safari from zooming on focus;
     padding keeps the visual size in line with the rest of the form */
  .input, .textarea {
    font-size: 16px;
    padding: var(--space-3) 14px;
  }
}

/* ── Profile-only layout (no tip hub) ────────────────────────────────────────
   The DEFAULT state for most creators: tipping off, so the tip hub is hidden.

   🔴 IT NO LONGER NARROWS THE PAGE (owner, 2026-08-16). This class used to set
   `max-width: 560px`, from back when the hero WAS the entire page and a wide
   column stranded it in a void. That is no longer true — the channel home now
   carries the banner hero, the sub-nav, the live band, the engagement strip and
   the card row, all of which fill the width — and because tipping-off is the
   common case, the override was what made Home render at half the width of
   Commands. The width comes from `.vpage` (i.e. `--shell-w`) like every other
   surface; this class only decides how the SHORT version of the page sits in
   the viewport: the footer sinks to the bottom instead of clinging to a hero
   that ends a third of the way down. Tokens only.

   (The old `.vhero` descendant rules went with it — that element was renamed
   `.chero` in the redesign, so they had been matching nothing.) */
.vpage--profile { min-height: 78vh; }
.vpage--profile .tips-off-note { text-align: center; }
.vpage--profile .vfoot { margin-top: auto; padding-top: var(--space-8); }

/* ── goal picker ("Where should it go?") ─────────────────────────────────────
   Radio-style wells inside the tip form: each tip goal, plus a General-support
   option that is selected by default. Same visual language as .well + .amt.on
   (accent border + accent wash when chosen) — tokens only. */
.goal-picks { display: flex; flex-direction: column; gap: var(--space-2); }
.gpick {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--space-3) var(--space-4); color: var(--text);
  font-family: var(--font-display); font-size: 14px;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast);
}
.gpick:hover { border-color: var(--border-accent); }
.gpick.on { border-color: var(--accent); background: var(--accent-wash); }
.gpick .t { font-weight: 600; }
.gpick .s { color: var(--text-dim); font-size: 12px; }
.gpick .bar { margin-top: 8px; }
.gpick .between { gap: var(--space-3); flex-wrap: wrap; }

/* ── "Make it monthly" toggle ──────────────────────────────────────────────── */
.toggle-row {
  display: flex; align-items: center; gap: var(--space-3); width: 100%; cursor: pointer;
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--space-3) var(--space-4); color: var(--text); text-align: left;
  font-family: var(--font-display); font-size: 14px;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast);
}
.toggle-row:hover { border-color: var(--border-accent); }
.toggle-row.on { border-color: var(--accent); background: var(--accent-wash); }
.tg-track {
  flex: none; width: 38px; height: 22px; border-radius: var(--r-pill);
  background: var(--surface-2); border: 1px solid var(--border); position: relative;
  transition: background var(--dur-fast), border-color var(--dur-fast);
}
.toggle-row.on .tg-track { background: var(--accent); border-color: var(--accent); }
.tg-knob {
  position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--text); transition: transform var(--dur-fast) var(--ease);
}
.toggle-row.on .tg-knob { background: var(--on-strong); transform: translateX(16px); }
.tg-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.tg-text .t { font-weight: 600; }
.tg-text .s { color: var(--text-dim); font-size: 12px; }
.pay.is-off { opacity: .45; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) { .tg-knob { transition: none; } }

/* ── trust line + payout-not-ready notice ──────────────────────────────────── */
.trust-line { color: var(--text-dim); font-size: 12px; margin: var(--space-3) 0 0; text-align: center; }
.notice {
  margin: 0 0 var(--space-3); padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface-inset); color: var(--text-dim); font-size: 13px;
}
#tip-form.is-locked .amt-grid, #tip-form.is-locked .goal-picks,
#tip-form.is-locked .pay-row, #tip-form.is-locked .input, #tip-form.is-locked .textarea {
  opacity: .5; pointer-events: none;
}

/* ── thank-you banner (post-checkout) ──────────────────────────────────────── */
.thanks {
  display: flex; align-items: flex-start; gap: var(--space-3);
  background: var(--accent-wash); border: 1px solid var(--border-accent); border-radius: var(--r-md);
  padding: var(--space-4);
}
.thanks-ico { font-size: 20px; line-height: 1.2; flex: none; }
.thanks-body { flex: 1; min-width: 0; }
.thanks-t { display: block; font-weight: 700; }
.thanks-msg { margin: 4px 0 0; color: var(--text-dim); white-space: pre-wrap; word-break: break-word; }
.thanks-x {
  flex: none; background: none; border: 0; cursor: pointer; color: var(--text-dim);
  font-size: 20px; line-height: 1; padding: 2px var(--space-2);
}
.thanks-x:hover { color: var(--text); }

@media (hover: none) and (pointer: coarse) {
  .gpick, .toggle-row { min-height: var(--touch-min, 44px); }
  .thanks-x { min-width: var(--touch-min, 44px); min-height: var(--touch-min, 44px); }
  /* The live band's platform tabs are real controls on a page people open on a
     phone — they get the same 44px target the rest of the page already has. */
  .ptab { min-height: var(--touch-min, 44px); padding-block: var(--space-2); }
}

/* ══════════════════════════════════════════════════════════════════════════════
 * THE CHANNEL SHELL — one page for every creator surface (redesign 2026-08-16)
 * ══════════════════════════════════════════════════════════════════════════════
 * Owner-approved mock ("channel home"): hero + a STICKY SUB-NAV, with
 * Commands / Song queue / Leaderboard as TABS inside the same shell rather than
 * four documents that look like four products. The deep links are unchanged and
 * still server-render real, crawlable pages — they just arrive wearing this.
 *
 * ONE STYLESHEET for the whole family. Until this file, `/s/<h>/commands`,
 * `/s/<h>/songs` and `/s/<h>/leaderboard` each shipped their own <style> block
 * with their own copy of the tokens — three skins drifting apart, and a visible
 * "flashbang" every time a visitor clicked through from the profile. The three
 * renderers now link THIS file (public-site/creator-shell.js STYLESHEET), and a
 * test asserts none of them ships a <style> of its own again.
 * The chat EMBED keeps its own inline skin on purpose: it is a widget designed
 * to be framed on somebody else's website, and a second request that a
 * third-party CSP may block is not a dependency a widget should have. */

.cshell { max-width: var(--shell-w); margin: 0 auto; padding: var(--space-6) var(--space-4) var(--space-10); min-width: 0; }
@media (max-width: 560px) { .cshell { padding: var(--space-4) var(--space-3) var(--space-8); } }

/* ══ THE STICKY HEADER BLOCK ══════════════════════════════════════════════════
 * Hero + sub-nav, sticking as ONE element (2026-08-16).
 *
 * WHY ONE ELEMENT AND NOT TWO: two sticky children need the lower one's `top:`
 * to equal the upper one's rendered height — a number CSS cannot read and that
 * changes with the creator's name, the banner and the viewport. And the gap
 * between them would be a transparent stripe with page content scrolling
 * through it. One sticky block has neither problem.
 *
 * CONDENSING. Stuck at full height the hero would eat the viewport, so tip.js
 * adds `.is-condensed` past a scroll threshold (with hysteresis, so a page that
 * reflows by a pixel at the boundary cannot flicker) and the hero collapses to
 * an identity ROW: banner faded out, avatar halved, bio and tagline gone. Every
 * tab behaves identically — that is the whole point. The transitions are
 * property-specific and short; `prefers-reduced-motion` drops them at the end of
 * this file, leaving the two end states intact. */
.cheader {
  min-width: 0;
  /* An OPAQUE ground while stuck — content scrolls behind this block, and the
     page's own atmosphere layers are fixed behind everything, so without it the
     hero would show the list sliding through it. */
  background: var(--bg);
  /* 🔴 …AND THAT GROUND MUST WEAR THE HERO'S OWN TOP RADIUS (owner, 2026-08-17,
     with a screenshot of the top-left corner: a dark SQUARE corner peeking out
     from under the lighter ROUNDED one). Two stacked layers, two different
     shapes: `.chero` is a rounded card (`--r-lg`) over this block's opaque
     `--bg`, which was painted square — so at each top corner the ~10px the
     rounding cut away showed the darker rectangle underneath. Nothing was
     misaligned; there were simply two radii where the eye expects one.
     Matching it here means ONE visible radius at the top of every surface. */
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  /* The bottom edge only exists once the header is actually holding position. */
  transition: box-shadow var(--dur) var(--ease);
}
/* 🔴 STICKY IS OPT-IN, AND THE OPT-IN IS "SOMETHING CAN CONDENSE ME". The class
   is added by `mountHeader()` in creator-shell.js — the same code that runs the
   scroll condense, on all four surfaces. Pinning the header in CSS instead would
   mean that on any page where that script failed to load, ~230px of identity bar
   was welded to the top of a reference list forever. Tying the two together
   makes the failure mode a header that simply scrolls away. */
.cheader.is-sticky { position: sticky; top: 0; z-index: 20; }
/* 🔴 CONDENSED = STUCK = SQUARE (2026-08-17). A rounded top corner is the
   language of a card sitting ON a page; once the header is holding the top of
   the viewport it is a BAR, and rounding it there only opens two notches for the
   list to scroll through. The hero below goes square in the same breath, so the
   "exactly one radius" rule holds in this state too. */
.cheader.is-condensed { box-shadow: 0 10px 24px -18px rgba(0, 0, 0, .9); border-radius: 0; }
/* The hero's own transitions. Only the properties that actually change — a
   blanket `transition: all` here would animate the banner's layout too. */
.cheader .chero { transition: border-color var(--dur) var(--ease); }
.cheader .chero-id {
  transition: padding var(--dur-slow) var(--ease);
}
.cheader .chero-av, .cheader .chero-banner, .cheader .chero-bio,
.cheader .vhero-title, .cheader .chero .handle, .cheader .socrow {
  transition: width var(--dur-slow) var(--ease), height var(--dur-slow) var(--ease),
              font-size var(--dur-slow) var(--ease), opacity var(--dur) var(--ease);
}
/* ── the condensed state: an identity ROW ── */
.cheader.is-condensed .chero-id { padding: var(--space-3) var(--space-4); align-items: center; }
.cheader.is-condensed .chero-av { width: 44px; height: 44px; font-size: 18px; border-width: 3px; }
.cheader.is-condensed .chero-name { font-size: 16px; }
.cheader.is-condensed .chero-banner { opacity: 0; }
/* The detail lines collapse rather than fade in place — a hidden element that
   still holds its height would leave the row twice as tall as it looks. */
.cheader.is-condensed .chero-bio,
.cheader.is-condensed .vhero-title,
.cheader.is-condensed .chero .handle,
.cheader.is-condensed .socrow,
.cheader.is-condensed .socials,
.cheader.is-condensed .tips-off-note {
  display: none;
}
/* …but the LIVE LINE SURVIVES the condense (2026-08-17) — pared back to the
   TITLE alone. It is the one detail line that is about right now rather than
   about the creator in general, so it is the one worth keeping in the sticky
   row a viewer reads while they scroll the page. Dropping the category element
   drops its `::before` separator with it, so nothing dangles. */
.cheader.is-condensed .chero-live { font-size: 12px; margin-top: 1px; max-width: 46ch; }
.cheader.is-condensed .chero-live-cat { display: none; }
.cheader.is-condensed .tipbtn { font-size: 12px; padding: 8px 16px; }
/* Hero and nav read as ONE block: the hero keeps its top corners and hands its
   bottom edge to the nav's border. */
.cheader .chero { border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom: 0; }
/* …and the hero with it (see `.cheader.is-condensed` above). */
.cheader.is-condensed .chero { border-radius: 0; }
@media (max-width: 680px) {
  .cheader.is-condensed .chero-id { flex-direction: row; text-align: left; padding-top: var(--space-3); }
  .cheader.is-condensed .chero-name { justify-content: flex-start; }
}

/* ── the sub-nav ──
   Tabs are ANCHORS with real hrefs (crawlable, and they work with JS disabled);
   tip.js upgrades EVERY one of them into an in-page pane swap. Tabs the creator
   has not enabled are ABSENT — never disabled — so every tab here is a
   destination that exists.
   It no longer sticks on its own: `.cheader` above does the sticking for the
   whole header block. */
.subnav {
  display: flex; gap: 2px; align-items: stretch;
  padding: var(--space-3) var(--space-2) 0;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(var(--bg) 86%, rgba(20, 17, 16, .86));
  backdrop-filter: blur(8px);
  /* A creator with every surface on has six tabs; on a phone they scroll
     sideways INSIDE the nav rather than widening the page (the body must never
     scroll sideways — the live band's overflow contract). */
  overflow-x: auto; scrollbar-width: none; min-width: 0;
}
.subnav::-webkit-scrollbar { display: none; }
.stab {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  font-size: 13px; font-weight: 600; padding: 9px 15px 11px;
  color: var(--text-faint);
  white-space: nowrap; cursor: pointer;
  /* NO bottom border. The active underline is the sliding `.stab::after` bar
     further down, and it has to sit INSIDE this box: `.subnav` scrolls
     horizontally, which per spec forces `overflow-y: auto` too, so anything
     hanging below the tab would be clipped (or grow a stray scrollbar). The
     extra 2px of bottom padding is the border's old height. */
  background: none; border: 0;
  font-family: var(--font-display);
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.stab:hover { color: var(--text-dim); }
/* The active tab's underline is the SLIDING bar further down this file
   (`.stab::after`), so this rule carries the colour change only. */
.stab.on { color: var(--text); }
.stab .cnt {
  font-size: 10px; font-family: var(--font-mono); background: var(--surface-2);
  border-radius: var(--r-pill); padding: 1px 7px; color: var(--text-faint);
}
.stab.on .cnt { color: var(--text-dim); }
.stab .livedot { width: 7px; height: 7px; border-radius: 50%; background: var(--danger); flex: none; animation: cpulse 1.5s infinite; }

/* ── panes ──
   The Home document carries three: the channel home itself, the chat tab and
   the schedule tab. Exactly one is visible; the others carry `.hide`. */
.cpane { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }

/* ── the engagement strip (goal + recent support) ── */
.engage {
  display: grid; grid-template-columns: 1.2fr .8fr; min-width: 0;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface); box-shadow: var(--shadow-card); overflow: hidden;
}
@media (max-width: 760px) { .engage { grid-template-columns: minmax(0, 1fr); } }
.engage-cell { padding: var(--space-4) var(--space-5); min-width: 0; }
.engage-cell + .engage-cell { border-left: 1px solid var(--border); }
@media (max-width: 760px) { .engage-cell + .engage-cell { border-left: 0; border-top: 1px solid var(--border); } }
.engage-h {
  font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-faint); font-weight: 700; margin: 0 0 var(--space-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.goalbar { height: 14px; border-radius: var(--r-pill); background: var(--surface-2); overflow: hidden; }
.goalbar i { display: block; height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-soft)); box-shadow: var(--glow-accent); }
.goalcap { display: flex; justify-content: space-between; gap: var(--space-3); font-size: 11.5px; color: var(--text-dim); margin-top: 6px; min-width: 0; }
.goalcap span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tick { font-size: 12.5px; color: var(--text-dim); padding: 3px 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tick b { color: var(--accent-soft); font-weight: 700; }

/* ── the card row under the strip ──
   Four mounts now (platforms · schedule · songs · commands), each of which
   renders only when it has something real. An EMPTY MOUNT IS DISPLAY:NONE: a
   grid item with no children is still a grid item, so without this rule the one
   card a bare creator does have would sit in column two with a hole beside it —
   which is precisely the "empty Home" this layer exists to remove. `:empty` is
   exact here because tip.js appends a node or nothing at all, and the mounts
   carry no whitespace or comments inside them. */
.chome-cards { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); align-items: start; min-width: 0; }
@media (max-width: 760px) { .chome-cards { grid-template-columns: minmax(0, 1fr); } }
.chome-cards > div { min-width: 0; }
.chome-cards > div:empty { display: none; }
.card-head a { font-size: 11px; color: var(--accent-2); font-weight: 600; white-space: nowrap; }

/* ── "Find me live on" — the zero-configuration card (owner ruling 2026-08-17)
   The connected platforms as brand chips. It is the one card that renders for a
   creator who has set up nothing at all, so it has to look finished on its own:
   the marks carry their own brand colour through `currentColor`, exactly like
   the live band's tabs, and nothing here is a coloured dot standing in for a
   logo. */
.plist { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; padding: 0; list-style: none; min-width: 0; }
.plink {
  display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  padding: 8px 12px; border-radius: var(--r-pill);
  background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text); font-size: 13px; font-weight: 600; text-decoration: none;
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur-fast), color var(--dur-fast);
}
/* The hover lift is turned off with the rest of the motion in the one
   reduced-motion block at the end of this file — never in a block of its own,
   or "all of it is opt-out" stops being checkable in one place. */
.plink:hover { transform: translateY(-2px); border-color: var(--border-accent); color: var(--accent-soft); }
.plink-mark { display: inline-flex; flex: none; }
.plink-mark svg { display: block; }
.plink-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The same treatment the player's own tab wears (`.ptab-live`) — one LIVE mark
   in the family, tokens only, and it appears ONLY where liveness was actually
   checked (Twitch, plan F7). */
.plink-live {
  flex: none; font-family: var(--font-mono); font-size: 9px; font-weight: 700;
  letter-spacing: .06em; color: var(--danger);
}

/* ── the social marks in the hero (icon-only, hover lift) ──
   The LABEL is still in the DOM (it is the link's accessible name) — visually
   hidden, never dropped, so a screen reader still hears "Twitch". */
.socrow { display: flex; flex-wrap: wrap; gap: 7px; margin: 0; padding: 0; list-style: none; }
.socrow .slink {
  width: 32px; height: 32px; padding: 0; justify-content: center; border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--text-dim); position: relative;
  transition: transform var(--dur-fast) var(--ease), color var(--dur-fast), border-color var(--dur-fast);
}
.socrow .slink:hover { transform: translateY(-2px); color: var(--accent-soft); }
.socrow .slink-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ── the live band, OFFLINE ──
   When we KNOW nobody is live (Twitch says so and there is no platform whose
   state we cannot know), the band collapses to a single row and the page leads
   with the engagement strip instead. It never says "offline" for a platform we
   did not check (plan F7). */
.liveband--off { grid-template-columns: minmax(0, 1fr); }
.lb-collapsed {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  flex-wrap: wrap; padding: var(--space-3) var(--space-4); min-width: 0;
}
.lb-collapsed .t { color: var(--text-dim); font-size: 13px; min-width: 0; }
.lb-open {
  flex: none; font-size: 12px; font-weight: 600; cursor: pointer;
  background: var(--surface-2); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 7px 12px; font-family: var(--font-display);
}
.lb-open:hover { border-color: var(--border-accent); }

/* (The CHAT tab's pane skin was removed 2026-08-16 with the tab itself — owner:
   "Chat shouldn't have its own page, only integrated in home with the stream
   preview". The chat's only skin is now `.lb-chat` in the live band above. The
   `/s/<h>/chat` embed document keeps its own self-contained one, on purpose.) */

/* ══ THE SUBPAGE SKINS ════════════════════════════════════════════════════════
 * Ported here from the three renderers' own <style> blocks, so the family has
 * ONE stylesheet. Everything is scoped under `.cshell` — these class names are
 * short and generic (`.track`, `.cmd`, `.chip`) and must never leak onto the
 * Home tab's markup. */

/* The plain container the 404 documents use. They carry NO shell: an unknown
   handle must not render an identity bar for a creator who may not exist. */
.page { max-width: 720px; margin: 0 auto; padding: var(--space-8) var(--space-4) var(--space-10); }

.cshell .subhead { display: flex; align-items: baseline; gap: var(--space-3); margin: var(--space-2) 0 var(--space-3); flex-wrap: wrap; }
.cshell .subhead h1 { margin: 0; font-size: 18px; font-weight: 700; }
.cshell .subhead .sub { color: var(--text-faint); font-size: 12.5px; }
.cshell section { margin: 0 0 var(--space-6); min-width: 0; }
.cshell .empty { text-align: center; color: var(--text-dim); padding: var(--space-8) 0; }
.cshell footer {
  margin-top: var(--space-8); padding-top: var(--space-4); border-top: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  flex-wrap: wrap; color: var(--text-faint); font-size: 12.5px;
}
.cshell footer .brand { display: flex; align-items: center; gap: var(--space-2); }
.cshell footer .flame { width: 18px; height: 18px; border-radius: 5px; background: linear-gradient(135deg, var(--accent), var(--accent-soft)); display: grid; place-items: center; font-size: 11px; }
.cshell footer a { color: var(--accent); font-weight: 600; }
.cshell .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ── commands ── */
.cshell .toolbar { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; margin-bottom: var(--space-4); min-width: 0; }
.cshell .search {
  display: flex; align-items: center; gap: var(--space-3); flex: 1 1 260px; min-width: 0;
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 9px 13px;
}
.cshell .search svg { flex: none; opacity: .5; }
.cshell .search input { flex: 1; background: none; border: 0; outline: 0; color: var(--text); font: inherit; font-size: 13.5px; min-width: 0; }
.cshell .search .kbd { font-family: var(--font-mono); font-size: 10px; color: var(--text-faint); border: 1px solid var(--border); border-radius: 5px; padding: 2px 6px; flex: none; }
.cshell .groups-nav { display: flex; gap: 6px; flex-wrap: wrap; min-width: 0; }
.cshell .gpill { font-size: 11px; font-weight: 600; color: var(--text-dim); padding: 6px 12px; border-radius: var(--r-pill); border: 1px solid var(--border); background: var(--surface-raised); cursor: pointer; }
.cshell .gpill.on { color: var(--accent-soft); border-color: var(--border-accent); background: var(--accent-wash); }
.cshell .gpill .n { color: var(--text-faint); font-family: var(--font-mono); font-size: 10.5px; margin-left: 4px; }
.cshell #sort { background: var(--surface-inset); color: var(--text-dim); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 7px 9px; font-family: var(--font-display); font-size: 12.5px; }
.cshell .ghead { display: flex; align-items: baseline; gap: var(--space-3); margin-bottom: 4px; flex-wrap: wrap; }
.cshell .ghead h2 { font-size: 14px; margin: 0; }
.cshell .ghead .desc { font-size: 12.5px; color: var(--text-faint); }
.cshell .glist { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; margin-top: var(--space-3); }
.cshell .cmd { display: grid; grid-template-columns: minmax(150px, 200px) minmax(0, 1fr) auto; gap: 14px; padding: 11px 16px; border-top: 1px solid var(--border); align-items: baseline; min-width: 0; }
.cshell .cmd:first-child { border-top: 0; }
.cshell .cmd:hover { background: var(--surface-raised); }
.cshell .cname { font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: var(--accent-soft); background: var(--surface-inset); border-radius: 6px; padding: 2px 8px; word-break: break-word; }
.cshell .cname .args { color: var(--text-faint); font-weight: 400; }
.cshell .cali { display: block; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-faint); margin-top: 3px; word-break: break-word; }
.cshell .cali-n { font-family: var(--font-mono); font-size: 10px; font-weight: 700; color: var(--text-faint); border: 1px solid var(--border); border-radius: 6px; padding: 1px 5px; margin-left: 6px; vertical-align: middle; }
.cshell .cdesc { color: var(--text-dim); font-size: 13px; min-width: 0; word-break: break-word; }
.cshell .chips { display: flex; gap: 6px; justify-content: flex-end; white-space: nowrap; }
.cshell .chip { font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .04em; padding: 2.5px 8px; border-radius: var(--r-chip); color: var(--text-faint); border: 1px solid var(--border); white-space: nowrap; }
.cshell .chip.cd { color: var(--text-dim); }
.cshell .chip.lvl-mod { color: var(--accent-2); border-color: var(--border-accent); }
.cshell .chip.lvl-sub { color: var(--accent); border-color: var(--border-accent); }
.cshell .chip.lvl-vip { color: var(--accent-soft); border-color: var(--border-accent); }
.cshell .chip.lvl-owner { color: var(--warn); border-color: var(--border-accent); }
.cshell .chip.off { color: var(--text-faint); background: var(--surface-inset); text-decoration: line-through; }
.cshell .cmd.disabled .cname, .cshell .cmd.disabled .cdesc { opacity: .45; }
@media (max-width: 560px) { .cshell .cmd { grid-template-columns: minmax(0, 1fr); gap: 4px; } .cshell .chips { justify-content: flex-start; } }

/* ── song queue ── */
.cshell .shead { font-size: 12px; color: var(--text-faint); margin: 0 0 var(--space-3); text-transform: uppercase; letter-spacing: .06em; }
.cshell .reqcard { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.cshell .reqcmd { font-family: var(--font-mono); font-size: 12.5px; color: var(--text); background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 6px 10px; }
.cshell .limits { font-family: var(--font-mono); font-size: 11px; color: var(--text-dim); }
.cshell .np-title { font-weight: 700; font-size: 1.05rem; word-break: break-word; }
.cshell .np-title a { color: inherit; }
.cshell .np-title a:hover { color: var(--accent-soft); }
.cshell .meta { color: var(--text-dim); font-size: 13px; margin-top: 4px; }
.cshell .list { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; }
.cshell .track { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; gap: 14px; padding: 11px 16px; border-top: 1px solid var(--border); align-items: baseline; min-width: 0; }
.cshell .track:first-child { border-top: 0; }
.cshell .track:hover { background: var(--surface-raised); }
.cshell .num { color: var(--text-faint); font-family: var(--font-mono); font-size: 12px; min-width: 1.6em; }
.cshell .tinfo { min-width: 0; }
.cshell .ttitle { display: block; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.cshell .ttitle a { color: inherit; }
.cshell .ttitle a:hover { color: var(--accent-soft); }
.cshell .tsub { display: block; color: var(--text-faint); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cshell .len { color: var(--text-dim); font-variant-numeric: tabular-nums; font-size: 13px; white-space: nowrap; }
@media (max-width: 560px) { .cshell .track { grid-template-columns: auto minmax(0, 1fr); } .cshell .chip { display: none; } }

/* ── leaderboard ── */
.cshell .rangetabs { display: flex; gap: var(--space-2); margin-bottom: var(--space-4); flex-wrap: wrap; }
.cshell .rangetab { font-size: 12.5px; font-weight: 600; padding: 6px 12px; border-radius: var(--r-pill); border: 1px solid var(--border); color: var(--text-dim); }
.cshell .rangetab.on { color: var(--accent); border-color: var(--border-accent); background: var(--accent-wash); }
.cshell table { width: 100%; border-collapse: collapse; }
.cshell th { text-align: left; font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); padding: 0 0 8px; }
.cshell th.n, .cshell td.n { text-align: right; }
.cshell td { padding: 11px 0; border-top: 1px solid var(--border); }
.cshell td.pos { font-family: var(--font-mono); color: var(--text-faint); width: 44px; }
.cshell tr.top td.pos { color: var(--accent); font-weight: 700; }
.cshell td.who { font-weight: 600; overflow-wrap: anywhere; }
.cshell td.n { font-family: var(--font-mono); color: var(--accent-2); white-space: nowrap; }

/* ══ TYPE RHYTHM ══════════════════════════════════════════════════════════════
 * ONE treatment for the label that opens a block. `.engage-h` already had it
 * (uppercase, tracked, small); the card heads were a second, unrelated voice at
 * 17px/700, so the page read as two designs stacked. They are the same size of
 * idea — "here is a list of things" — so they get the same eyebrow, one step
 * brighter than `.engage-h` because a card head is the card's only heading.
 *
 * NUMBERS ARE TABULAR everywhere they are read as a column or tick past: money,
 * counts, ranks, durations. Proportional digits make a goal caption jitter as it
 * counts up and a leaderboard's amounts fail to line up. */
.card-head h2,
.cshell .ghead h2 {
  font-size: 12.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-dim);
}
.engage-h { font-size: 11.5px; }
.mono, .goalcap, .tick, .badge, .amt, .rank, .tip-end,
.stab .cnt, .cshell .len, .cshell td.n, .cshell td.pos, .cshell .num, .cshell .gpill .n {
  font-variant-numeric: tabular-nums;
}

/* ══ MICRO-INTERACTIONS ═══════════════════════════════════════════════════════
 * Owner, 2026-08-16: "still a bit dull". Refined-dark per
 * docs/design/DESIGN-GUIDELINES.md — nothing bounces, nothing translates more
 * than 2px, and every one of them is CSS-only (no script, no library) and OFF
 * under `prefers-reduced-motion` (the block at the end of this file).
 *
 * The structure the owner approved is untouched: these are all states of boxes
 * that were already there. */

/* 1. The sub-nav's underline SLIDES in rather than blinking on. A pseudo-element
      rather than a border, so switching tabs animates instead of repainting —
      and it sits inside the tab's box (see `.stab`, which scrolls inside a
      clipping nav). */
.stab { position: relative; }
.stab::after {
  content: ''; position: absolute; left: 12px; right: 12px; bottom: 0; height: 2px;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--accent), var(--accent-soft));
  transform: scaleX(0); opacity: 0;
  transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.stab:hover::after { transform: scaleX(.5); opacity: .5; }
.stab.on::after, .stab.on:hover::after { transform: scaleX(1); opacity: 1; }

/* 2. Cards LIFT and their border warms on hover. The lift is only on the cards
      that are objects in a row (the two-column home cards); the tip form is a
      surface you work IN, not a thing you pick up, so it only warms. */
.card, .engage, .liveband, .well {
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.card:hover, .engage:hover { border-color: var(--border-strong); }
.chome-cards .card:hover {
  transform: translateY(-2px);
  border-color: var(--border-accent);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .28), 0 18px 40px -22px rgba(0, 0, 0, .8);
}

/* 3. The goal bar SHIMMERS — a slow sheen crossing the filled part, the one
      thing on the page that says "this is still moving". 7s and low-contrast:
      at a glance it is a gradient, only a lingering look catches it. */
.goalbar i, .bar i { position: relative; overflow: hidden; }
.goalbar i::after, .bar i::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, .28) 50%, transparent 70%);
  transform: translateX(-100%);
  animation: emberSheen 7s var(--ease) infinite;
}
@keyframes emberSheen { 0% { transform: translateX(-100%); } 55%, 100% { transform: translateX(100%); } }

/* 4. Ticker entries FADE IN, stepped, so the recent-support cell reads as a
      feed arriving rather than a block of text that was always there. `both`
      keeps the last frame — nothing re-animates on a repaint.
      (The cell's first child is its `.engage-h` heading, so the first ticker line
      is child 2 — hence the offsets below.) */
.tick { animation: tickIn var(--dur-slow) var(--ease) both; }
.tick:nth-child(3) { animation-delay: 60ms; }
.tick:nth-child(4) { animation-delay: 120ms; }
.tick:nth-child(5) { animation-delay: 180ms; }
.tick:nth-child(6) { animation-delay: 240ms; }
@keyframes tickIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* 5. The Support button BREATHES. Its glow, not its size — a pulsing primary
      action on a money page reads as pressure; a warming ember reads as alive.
      6s, and the resting shadow is exactly the one declared on `.tipbtn`. */
.tipbtn { animation: emberBreath 6s ease-in-out infinite; }
.tipbtn:hover { animation: none; }
@keyframes emberBreath {
  0%, 100% { box-shadow: 0 4px 22px var(--accent-glow); }
  50% { box-shadow: 0 4px 30px var(--accent-glow), 0 0 0 1px var(--border-accent); }
}

/* ══ THE SEAMLESS SWAP ════════════════════════════════════════════════════════
 * What a tab change looks like now that it is not a page load (2026-08-16).
 *
 * 1. THE SKELETON. The pane is filled instantly with a shimmer of the shape that
 *    is coming — never a spinner (which says "waiting" and nothing else) and
 *    never an empty box (which collapses the page height and throws the scroll
 *    position away). Code-authored markup only; no creator data reaches it.
 * 2. THE ENTER. A 150ms rise-and-fade on the pane itself, and ONLY on a body
 *    that genuinely just arrived over the network. A pane served from the
 *    session cache is replaced with no animation at all: the content is already
 *    known and correct, so fading it up from nothing is a blink bought for no
 *    information (owner, 2026-08-17: "weird flashing on the page"). Nor is
 *    `document.startViewTransition` used any more — with no
 *    `view-transition-name` it snapshots the WHOLE document and cross-fades the
 *    hero, the sticky nav and the fixed atmosphere to repaint one region.
 * Both are off under `prefers-reduced-motion` (the block below). */
.skel {
  display: block; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--surface) 0%, var(--surface-2) 40%, var(--surface) 80%);
  background-size: 300% 100%;
  animation: skelShimmer 1.4s linear infinite;
}
.skel-h { width: 180px; height: 20px; }
.skel-list {
  margin-top: var(--space-4); border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface); overflow: hidden;
}
.skel-row {
  display: grid; grid-template-columns: minmax(120px, 200px) minmax(0, 1fr); gap: 14px;
  padding: 13px 16px; border-top: 1px solid var(--border); align-items: center; min-width: 0;
}
.skel-row:first-child { border-top: 0; }
.skel-a { height: 13px; }
.skel-b { height: 13px; opacity: .6; }
/* Each row starts its shimmer slightly later, so the list reads as one wave
   rather than as six things flashing in lockstep. */
.skel-row:nth-child(2n) .skel { animation-delay: .12s; }
.skel-row:nth-child(3n) .skel { animation-delay: .24s; }
@keyframes skelShimmer { from { background-position: 100% 0; } to { background-position: -200% 0; } }

.pane-enter { animation: paneIn 150ms var(--ease) both; }
@keyframes paneIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* 3. THE LEAVE, for the ONE tab the swap layer cannot serve: Home clicked from a
 *    server-rendered subpage. That pane IS the profile document (live band, tip
 *    form, goals, voices), so it stays a real navigation — and this is what makes
 *    it read as a transition rather than a cut. The hero above it and the fixed
 *    ember atmosphere behind it are identical on both documents and are never
 *    touched, so only the pane content changes hands; the profile then plays
 *    `paneIn` on its own pane as it hydrates. Kept in step with
 *    tab-swap.js's LEAVE_MS — a longer keyframe would navigate mid-fade. */
.pane-leave { animation: paneOut 140ms var(--ease) both; }
@keyframes paneOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-4px); } }

/* THE SWAP TARGET ON THE PROFILE DOCUMENT wears `.cshell` as well (tip.html), so
   the subpage skins below — all scoped under it, on purpose — apply to a pane
   swapped in there. `.vpage` already supplies the column and its padding, so the
   pane must not add a second one. No `max-width` here: the family width belongs
   to the two containers that own it. */
.cpane.cshell { padding: 0; }

@media (prefers-reduced-motion: reduce) {
  .livechip::before, .stab .livedot { animation: none; }
  /* The swap: the pane still changes, it just does not move or shimmer. The
     skeleton keeps its shape (it is the layout, not the motion). */
  .skel { animation: none; }
  .pane-enter { animation: none; }
  /* …and the cross-document leave becomes an ordinary navigation (tab-swap.js
     skips the delay entirely under this setting, so nothing is left half-faded). */
  .pane-leave { animation: none; }
  /* …and the hero snaps between its two sizes instead of easing. */
  .cheader, .cheader .chero, .cheader .chero-id, .cheader .chero-av,
  .cheader .chero-banner { transition: none; }
  .tipbtn:hover { transform: none; }
  .socrow .slink:hover { transform: none; }
  /* The polish pass, off as a block: no sheen, no fade-in, no breathing, no
     lift. The STATES all survive (the active tab still shows its bar, a hovered
     card still warms) — only the movement goes. */
  .goalbar i::after, .bar i::after { animation: none; display: none; }
  .tick { animation: none; }
  .tipbtn { animation: none; }
  .stab::after { transition: none; }
  .chome-cards .card:hover { transform: none; }
  /* …and the zero-config platform chips (owner ruling 2026-08-17). */
  .plink:hover { transform: none; }
}
