@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap');

/* ---------- Base / type / radii / motion ---------- */
:root {
  --font-display: 'Space Grotesk', system-ui, -apple-system, sans-serif;
  --font-body:    'Geist', system-ui, -apple-system, sans-serif;
  --font-mono:    'Geist Mono', ui-monospace, monospace;

  /* Type scale (broadcast / dense) */
  --fs-overline: 11px;
  --fs-caption:  12px;
  --fs-meta:     13px;
  --fs-body:     14px;
  --fs-body-lg:  16px;
  --fs-h6:       18px;
  --fs-h5:       22px;
  --fs-h4:       28px;
  --fs-h3:       36px;
  --fs-h2:       48px;
  --fs-h1:       64px;
  --fs-display:  96px;

  --lh-tight: 1.05;
  --lh-snug:  1.2;
  --lh-base:  1.45;
  --lh-loose: 1.6;

  --tracking-display: -0.02em;
  --tracking-tight:   -0.01em;
  --tracking-normal:  0em;
  --tracking-wide:    0.08em;
  --tracking-mega:    0.18em;

  /* Spacing — 4-pt grid */
  --sp-0: 0;
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-7: 32px;
  --sp-8: 40px;
  --sp-9: 48px;
  --sp-10: 64px;
  --sp-11: 80px;
  --sp-12: 96px;

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

  /* Motion */
  --ease-out: cubic-bezier(.2,.7,.2,1);
  --ease-in-out: cubic-bezier(.6,.05,.3,.95);
  --dur-fast: 120ms;
  --dur-med: 220ms;
  --dur-slow: 420ms;

  /* Accent variants (toggled via [data-accent]) */
  --accent: #f59e0b;
  --accent-soft: rgba(245,158,11,.16);
  --accent-strong: #fbbf24;

  /* Universal semantic */
  --hot: #ef4444;          /* LIVE indicator */
  --hot-soft: rgba(239,68,68,.18);
  --success: #10b981;
  --warning: #f59e0b;
  --info: #38bdf8;
}

/* ---------- Membership ranks / solid actions ---------- */
/* Rank identity is stable across editions; surfaces and ink remain semantic.
   Solid photo ribbons and actions pair with on-solid in BOTH modes. */
:root {
  --membership-rookie: #9673c9;
  --membership-explorer: #bc7854;
  --membership-architect: #477edf;
  --membership-silver: #93a4bb;
  --membership-brigade: #bc9336;
  --membership-rookie-solid: #7253a4;
  --membership-explorer-solid: #984b2d;
  --membership-architect-solid: #205bbc;
  --membership-brigade-solid: #806008;
  /* Photo ribbons preserve the approved workbench gradients in both modes. */
  --membership-rookie-ribbon: linear-gradient(125deg, #5e548b, #7e6ab1);
  --membership-explorer-ribbon: linear-gradient(125deg, #8d452c, #ba6c45);
  --membership-architect-ribbon: linear-gradient(120deg, #174ca9, #236fcd 48%, #18749d);
  --membership-brigade-ribbon: linear-gradient(125deg, #91701a, #b78a18 58%, #825e0d);
  --membership-popular-ribbon: linear-gradient(115deg, #1232a4 0%, #175cce 32%, #087eab 55%, #1e4abd 78%, #1232a4 100%);
  --membership-action: #176b57;
  --membership-action-strong: #105141;
  --membership-on-solid: #ffffff;
  --control-touch: 44px;
}

/* ---------- DARK (default) ---------- */
:root,
[data-theme="dark"] {
  color-scheme: dark;

  --bg-base:     #060912;
  --bg-canvas:   #0a0f1c;
  --bg-surface:  #0f1626;
  --bg-elevated: #141d31;
  --bg-overlay:  #1a2540;
  --bg-inverse:  #f5f7fa;

  --border-subtle:  rgba(255,255,255,.06);
  --border-default: rgba(255,255,255,.1);
  --border-strong:  rgba(255,255,255,.18);

  --text-primary:   #f4f6fb;
  --text-secondary: #a5b0c4;
  --text-tertiary:  #6b7894;
  --text-inverse:   #0a0f1c;

  --shadow-sm: 0 1px 2px rgba(0,0,0,.4);
  --shadow-md: 0 6px 20px -8px rgba(0,0,0,.6);
  --shadow-lg: 0 20px 50px -15px rgba(0,0,0,.7);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 30px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(6,9,18,0) 0%, rgba(6,9,18,.6) 55%, rgba(6,9,18,.95) 100%);
  --grad-noise: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(245,158,11,.08), transparent);
}

/* ---------- LIGHT ---------- */
[data-theme="light"] {
  color-scheme: light;

  --bg-base:     #f3efe6;   /* warm newsprint */
  --bg-canvas:   #faf7f0;
  --bg-surface:  #ffffff;
  --bg-elevated: #ffffff;
  --bg-overlay:  #f5f1e8;
  --bg-inverse:  #0a0f1c;

  --border-subtle:  rgba(10,15,28,.06);
  --border-default: rgba(10,15,28,.12);
  --border-strong:  rgba(10,15,28,.24);

  --text-primary:   #0a0f1c;
  --text-secondary: #475569;
  --text-tertiary:  #94a3b8;
  --text-inverse:   #ffffff;

  --accent: #b8520a;
  --accent-soft: rgba(184,82,10,.12);
  --accent-strong: #d97706;

  --shadow-sm: 0 1px 2px rgba(20,15,5,.06);
  --shadow-md: 0 8px 24px -10px rgba(20,15,5,.18);
  --shadow-lg: 0 24px 60px -18px rgba(20,15,5,.25);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 24px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(250,247,240,.65) 55%, rgba(250,247,240,.98) 100%);
  --grad-noise: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(184,82,10,.07), transparent);
}

/* ---------- EVENT: US ELECTION (civic red/blue) ---------- */
[data-theme="election"] {
  color-scheme: dark;

  --bg-base:     #050918;
  --bg-canvas:   #0a1230;
  --bg-surface:  #0e1a3e;
  --bg-elevated: #122352;
  --bg-overlay:  #1a2e6b;
  --bg-inverse:  #f5f7fa;

  --border-subtle:  rgba(255,255,255,.07);
  --border-default: rgba(255,255,255,.14);
  --border-strong:  rgba(255,255,255,.28);

  --text-primary:   #f8fafc;
  --text-secondary: #b8c5e0;
  --text-tertiary:  #7a89b5;
  --text-inverse:   #050918;

  --accent: #ef4444;          /* GOP red */
  --accent-soft: rgba(239,68,68,.18);
  --accent-strong: #f87171;
  --accent-alt: #3b82f6;      /* Dem blue */
  --accent-alt-soft: rgba(59,130,246,.2);
  --accent-gold: #fbbf24;     /* star/medal */

  --hot: #ef4444;

  --shadow-md: 0 8px 28px -10px rgba(0,0,0,.7);
  --shadow-lg: 0 22px 60px -18px rgba(0,0,0,.85);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 32px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(5,9,24,0) 0%, rgba(5,9,24,.7) 55%, rgba(5,9,24,.98) 100%);
  /* faint stars-and-stripes wash */
  --grad-noise:
    radial-gradient(ellipse 60% 40% at 0% 0%, rgba(59,130,246,.18), transparent 60%),
    radial-gradient(ellipse 60% 40% at 100% 0%, rgba(239,68,68,.18), transparent 60%);
}

/* ---------- EVENT: WORLD CUP (turf green + gold) ---------- */
[data-theme="worldcup"] {
  color-scheme: dark;

  --bg-base:     #03150a;
  --bg-canvas:   #0a2615;
  --bg-surface:  #0d3019;
  --bg-elevated: #114022;
  --bg-overlay:  #185530;
  --bg-inverse:  #f5f7fa;

  --border-subtle:  rgba(255,255,255,.07);
  --border-default: rgba(255,255,255,.14);
  --border-strong:  rgba(251,191,36,.4);

  --text-primary:   #f7fbf4;
  --text-secondary: #b4d4b8;
  --text-tertiary:  #7ba089;
  --text-inverse:   #03150a;

  --accent: #fbbf24;          /* trophy gold */
  --accent-soft: rgba(251,191,36,.18);
  --accent-strong: #fcd34d;
  --accent-alt: #ffffff;      /* pitch lines */
  --accent-alt-soft: rgba(255,255,255,.12);

  --hot: #f97316;             /* orange whistle */
  --hot-soft: rgba(249,115,22,.18);

  --shadow-md: 0 8px 28px -10px rgba(0,0,0,.7);
  --shadow-lg: 0 22px 60px -18px rgba(0,0,0,.85);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 32px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(3,21,10,0) 0%, rgba(3,21,10,.7) 55%, rgba(3,21,10,.98) 100%);
  /* faint pitch stripes */
  --grad-noise:
    repeating-linear-gradient(180deg, rgba(255,255,255,.012) 0 80px, rgba(0,0,0,0) 80px 160px),
    radial-gradient(ellipse 80% 50% at 50% -20%, rgba(251,191,36,.1), transparent);
}

/* ---------- EVENT: EU (reflex blue + circle-of-stars gold) ---------- */
/* Flagship theme. Deep European navy, EU-gold accent, faint 12-star ring. */
[data-theme="eu"] {
  color-scheme: dark;

  --bg-base:     #050a24;   /* deep reflex-blue night */
  --bg-canvas:   #081132;
  --bg-surface:  #0c1a45;
  --bg-elevated: #102258;
  --bg-overlay:  #1a3079;
  --bg-inverse:  #f5f7fa;

  --border-subtle:  rgba(180,200,255,.08);
  --border-default: rgba(180,200,255,.15);
  --border-strong:  rgba(255,204,0,.35);

  --text-primary:   #f2f6ff;
  --text-secondary: #adbce4;
  --text-tertiary:  #6f80b4;
  --text-inverse:   #050a24;

  --accent: #ffcc00;          /* EU gold */
  --accent-soft: rgba(255,204,0,.16);
  --accent-strong: #ffd84d;
  --accent-alt: #4f8cff;      /* star-circle blue */
  --accent-alt-soft: rgba(79,140,255,.2);

  --hot: #ff5a5f;
  --hot-soft: rgba(255,90,95,.18);

  --shadow-md: 0 8px 28px -10px rgba(0,0,0,.75);
  --shadow-lg: 0 22px 60px -18px rgba(2,6,30,.9);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 34px -4px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(5,10,36,0) 0%, rgba(5,10,36,.72) 55%, rgba(5,10,36,.98) 100%);
  /* circle of stars (top), gold halo, EU-blue wash */
  --grad-noise:
    url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20240%20240%22%3E%3Cdefs%3E%3Cpath%20id=%22s%22%20d=%22M120.00,31.00%20L122.47,38.60%20L130.46,38.60%20L124.00,43.30%20L126.47,50.90%20L120.00,46.20%20L113.53,50.90%20L116.00,43.30%20L109.54,38.60%20L117.53,38.60%20Z%22%20fill=%22%23FFCC00%22%20fill-opacity=%22.08%22/%3E%3C/defs%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(0%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(30%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(60%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(90%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(120%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(150%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(180%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(210%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(240%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(270%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(300%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(330%20120%20120)%22/%3E%3C/svg%3E") no-repeat 50% -56px / 560px 560px,
    radial-gradient(ellipse 60% 42% at 50% -6%, rgba(255,204,0,.10), transparent 62%),
    radial-gradient(ellipse 95% 60% at 50% 2%, rgba(40,80,210,.22), transparent 72%);
}

/* ---------- EVENT: BULGARIA (modern tricolor · EU wink) ---------- */
/* The flag is WHITE-dominant: white (top) · green (middle) · red (bottom).
   So this is a LIGHT theme — clean white paper, green leading the UI,
   flag-red for LIVE/hot, EU-blue circle of stars as the wink. */
[data-theme="bulgaria"] {
  color-scheme: light;

  --bg-base:     #eef3f1;   /* soft cool paper */
  --bg-canvas:   #f8fbfa;
  --bg-surface:  #ffffff;
  --bg-elevated: #ffffff;
  --bg-overlay:  #e9f3ee;
  --bg-inverse:  #08231a;

  --border-subtle:  rgba(8,40,28,.07);
  --border-default: rgba(8,40,28,.13);
  --border-strong:  rgba(0,150,110,.45);

  --text-primary:   #0c1f17;   /* deep green-black ink */
  --text-secondary: #46605a;
  --text-tertiary:  #8a9c95;
  --text-inverse:   #ffffff;

  --accent: #00966e;          /* Bulgarian green (flag) */
  --accent-soft: rgba(0,150,110,.12);
  --accent-strong: #00a87b;
  --accent-alt: #d62612;      /* flag red */
  --accent-alt-soft: rgba(214,38,18,.12);
  --accent-gold: #ffcc00;     /* EU gold */

  --hot: #d62612;             /* LIVE = flag red */
  --hot-soft: rgba(214,38,18,.14);
  --success: #00966e;

  --shadow-sm: 0 1px 2px rgba(8,30,20,.06);
  --shadow-md: 0 8px 24px -10px rgba(8,40,28,.18);
  --shadow-lg: 0 24px 60px -18px rgba(8,40,28,.26);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 24px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(248,251,250,0) 0%, rgba(248,251,250,.65) 55%, rgba(248,251,250,.98) 100%);
  /* the flag, spatially: white top · green band · red foot · EU-blue stars top */
  --grad-noise:
    url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20240%20240%22%3E%3Cdefs%3E%3Cpath%20id=%22s%22%20d=%22M120.00,31.00%20L122.47,38.60%20L130.46,38.60%20L124.00,43.30%20L126.47,50.90%20L120.00,46.20%20L113.53,50.90%20L116.00,43.30%20L109.54,38.60%20L117.53,38.60%20Z%22%20fill=%22%23003399%22%20fill-opacity=%22.10%22/%3E%3C/defs%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(0%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(30%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(60%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(90%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(120%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(150%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(180%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(210%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(240%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(270%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(300%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(330%20120%20120)%22/%3E%3C/svg%3E") no-repeat 50% -70px / 360px 360px,
    linear-gradient(180deg,
      rgba(255,255,255,0)   0%,
      rgba(0,150,110,.055) 46%,
      rgba(0,150,110,.055) 58%,
      rgba(214,38,18,.07)  100%);
}
/* Light-skin tweaks for Bulgaria: keep image fades dark (white text stays legible) is default;
   give chips & overlay the light treatment so they read on white surfaces. */
[data-theme="bulgaria"] .chip { background: rgba(255,255,255,.85); color: var(--text-primary); }
[data-theme="bulgaria"] .nrb-overlay { background: rgba(8,30,20,.4); }

/* =========================================================
   THEME × MODE axis — design-sync port, 2026-07-16.
   Light and dark are PROPERTIES of each theme, not themes:
   `data-theme` picks the skin (base=no attr · eu · bulgaria ·
   election · worldcup), `data-mode` picks its light|dark
   rendering. WITHOUT data-mode every theme keeps its native
   scheme above (back-compat); legacy data-theme="light"/"dark"
   remain aliases for the base theme's two modes. Only the four
   non-native variants need blocks here — same-scheme combos
   resolve to the native blocks above. Authored as first drafts
   in the design system (design/system/shared/modes.css history);
   refine via the normal design-sync loop.
   ========================================================= */

/* ---------- BASE · LIGHT MODE (= the legacy light theme) ---------- */
[data-theme="base"][data-mode="light"],
[data-mode="light"]:not([data-theme]) {
  color-scheme: light;

  --bg-base:     #f3efe6;
  --bg-canvas:   #faf7f0;
  --bg-surface:  #ffffff;
  --bg-elevated: #ffffff;
  --bg-overlay:  #f5f1e8;
  --bg-inverse:  #0a0f1c;

  --border-subtle:  rgba(10,15,28,.06);
  --border-default: rgba(10,15,28,.12);
  --border-strong:  rgba(10,15,28,.24);

  --text-primary:   #0a0f1c;
  --text-secondary: #475569;
  --text-tertiary:  #94a3b8;
  --text-inverse:   #ffffff;

  --accent: #b8520a;
  --accent-soft: rgba(184,82,10,.12);
  --accent-strong: #d97706;

  --shadow-sm: 0 1px 2px rgba(20,15,5,.06);
  --shadow-md: 0 8px 24px -10px rgba(20,15,5,.18);
  --shadow-lg: 0 24px 60px -18px rgba(20,15,5,.25);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 24px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(250,247,240,.65) 55%, rgba(250,247,240,.98) 100%);
  --grad-noise: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(184,82,10,.07), transparent);
}

/* ---------- EU · LIGHT ("reflex day": blue-tinted paper, gold ink, reflex-blue alt) ---------- */
[data-theme="eu"][data-mode="light"] {
  color-scheme: light;

  --bg-base:     #eef1f9;
  --bg-canvas:   #f7f9fd;
  --bg-surface:  #ffffff;
  --bg-elevated: #ffffff;
  --bg-overlay:  #e7ecf8;
  --bg-inverse:  #050a24;

  --border-subtle:  rgba(5,16,60,.07);
  --border-default: rgba(5,16,60,.13);
  --border-strong:  rgba(154,123,0,.5);

  --text-primary:   #0b1533;
  --text-secondary: #45507a;
  --text-tertiary:  #8d97bd;
  --text-inverse:   #ffffff;

  --accent: #9a7b00;          /* EU gold at ink strength (AA on paper) */
  --accent-soft: rgba(200,166,0,.15);
  --accent-strong: #b8930a;
  --accent-alt: #003399;      /* official reflex blue */
  --accent-alt-soft: rgba(0,51,153,.12);

  --hot: #d3323c;
  --hot-soft: rgba(211,50,60,.13);

  --shadow-sm: 0 1px 2px rgba(10,20,60,.06);
  --shadow-md: 0 8px 24px -10px rgba(10,20,60,.16);
  --shadow-lg: 0 24px 60px -18px rgba(10,20,60,.22);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 24px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(247,249,253,.65) 55%, rgba(247,249,253,.98) 100%);
  /* circle of stars in reflex blue on paper + gold halo */
  --grad-noise:
    url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20240%20240%22%3E%3Cdefs%3E%3Cpath%20id=%22s%22%20d=%22M120.00,31.00%20L122.47,38.60%20L130.46,38.60%20L124.00,43.30%20L126.47,50.90%20L120.00,46.20%20L113.53,50.90%20L116.00,43.30%20L109.54,38.60%20L117.53,38.60%20Z%22%20fill=%22%23003399%22%20fill-opacity=%22.07%22/%3E%3C/defs%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(0%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(30%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(60%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(90%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(120%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(150%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(180%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(210%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(240%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(270%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(300%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(330%20120%20120)%22/%3E%3C/svg%3E") no-repeat 50% -56px / 560px 560px,
    radial-gradient(ellipse 60% 42% at 50% -6%, rgba(0,51,153,.08), transparent 62%),
    radial-gradient(ellipse 95% 60% at 50% 2%, rgba(255,204,0,.10), transparent 72%);
}

/* ---------- BULGARIA · DARK ("evening tricolor": green-black night, flag red LIVE) ---------- */
[data-theme="bulgaria"][data-mode="dark"] {
  color-scheme: dark;

  --bg-base:     #07130d;
  --bg-canvas:   #0a1a12;
  --bg-surface:  #0e2318;
  --bg-elevated: #133020;
  --bg-overlay:  #1a4230;
  --bg-inverse:  #f2f8f5;

  --border-subtle:  rgba(210,255,235,.07);
  --border-default: rgba(210,255,235,.13);
  --border-strong:  rgba(0,190,140,.4);

  --text-primary:   #eef7f2;
  --text-secondary: #a9c9ba;
  --text-tertiary:  #6d9483;
  --text-inverse:   #07130d;

  --accent: #00c08b;          /* flag green, night-bright */
  --accent-soft: rgba(0,192,139,.16);
  --accent-strong: #2adfa9;
  --accent-alt: #ff5a50;      /* flag red, night-bright */
  --accent-alt-soft: rgba(255,90,80,.16);
  --accent-gold: #ffcc00;     /* EU gold wink */

  --hot: #ff5a50;
  --hot-soft: rgba(255,90,80,.18);
  --success: #00c08b;

  --shadow-sm: 0 1px 2px rgba(0,0,0,.4);
  --shadow-md: 0 8px 28px -10px rgba(0,0,0,.7);
  --shadow-lg: 0 22px 60px -18px rgba(0,10,5,.85);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 30px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(7,19,13,0) 0%, rgba(7,19,13,.7) 55%, rgba(7,19,13,.98) 100%);
  /* the flag at night: faint white crown · green band · red foot + EU-gold stars */
  --grad-noise:
    url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20240%20240%22%3E%3Cdefs%3E%3Cpath%20id=%22s%22%20d=%22M120.00,31.00%20L122.47,38.60%20L130.46,38.60%20L124.00,43.30%20L126.47,50.90%20L120.00,46.20%20L113.53,50.90%20L116.00,43.30%20L109.54,38.60%20L117.53,38.60%20Z%22%20fill=%22%23FFCC00%22%20fill-opacity=%22.07%22/%3E%3C/defs%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(0%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(30%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(60%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(90%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(120%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(150%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(180%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(210%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(240%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(270%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(300%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(330%20120%20120)%22/%3E%3C/svg%3E") no-repeat 50% -70px / 360px 360px,
    linear-gradient(180deg,
      rgba(255,255,255,.035) 0%,
      rgba(0,150,110,.10)   46%,
      rgba(0,150,110,.10)   58%,
      rgba(214,38,18,.10)  100%);

  /* dark-scheme coverage-sentiment values (the cluster layer sets light ones
     on the bulgaria theme; carried here so the cluster port inherits them) */
  --sent-pos: #34d399;
  --sent-neu: #7c8aa5;
  --sent-neg: #f4625b;
}

/* ---------- ELECTION · LIGHT ("civic day": cool paper, GOP red ink, Dem blue) ---------- */
[data-theme="election"][data-mode="light"] {
  color-scheme: light;

  --bg-base:     #f1f3fa;
  --bg-canvas:   #f9fafd;
  --bg-surface:  #ffffff;
  --bg-elevated: #ffffff;
  --bg-overlay:  #e9edf8;
  --bg-inverse:  #050918;

  --border-subtle:  rgba(8,14,45,.07);
  --border-default: rgba(8,14,45,.13);
  --border-strong:  rgba(8,14,45,.26);

  --text-primary:   #0b1130;
  --text-secondary: #46507b;
  --text-tertiary:  #8f98bb;
  --text-inverse:   #ffffff;

  --accent: #c22b2b;
  --accent-soft: rgba(194,43,43,.12);
  --accent-strong: #de4747;
  --accent-alt: #1d4ed8;
  --accent-alt-soft: rgba(29,78,216,.12);
  --accent-gold: #b8860b;

  --hot: #c22b2b;
  --hot-soft: rgba(194,43,43,.12);

  --shadow-sm: 0 1px 2px rgba(10,15,45,.06);
  --shadow-md: 0 8px 24px -10px rgba(10,15,45,.16);
  --shadow-lg: 0 24px 60px -18px rgba(10,15,45,.22);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 24px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(249,250,253,.65) 55%, rgba(249,250,253,.98) 100%);
  --grad-noise:
    radial-gradient(ellipse 60% 40% at 0% 0%, rgba(29,78,216,.09), transparent 60%),
    radial-gradient(ellipse 60% 40% at 100% 0%, rgba(194,43,43,.09), transparent 60%);
}

/* ---------- WORLDCUP · LIGHT ("day match": pitch-tinted paper, trophy-gold ink) ---------- */
[data-theme="worldcup"][data-mode="light"] {
  color-scheme: light;

  --bg-base:     #eef5ef;
  --bg-canvas:   #f7fbf8;
  --bg-surface:  #ffffff;
  --bg-elevated: #ffffff;
  --bg-overlay:  #e5f0e7;
  --bg-inverse:  #03150a;

  --border-subtle:  rgba(6,30,16,.07);
  --border-default: rgba(6,30,16,.13);
  --border-strong:  rgba(160,125,0,.5);

  --text-primary:   #0b2013;
  --text-secondary: #3f5c4a;
  --text-tertiary:  #7fa08c;
  --text-inverse:   #ffffff;

  --accent: #a07d00;          /* trophy gold at ink strength */
  --accent-soft: rgba(180,145,0,.14);
  --accent-strong: #bf9a06;
  --accent-alt: #0c7a43;      /* pitch green ink */
  --accent-alt-soft: rgba(12,122,67,.12);

  --hot: #c2410c;
  --hot-soft: rgba(194,65,12,.13);

  --shadow-sm: 0 1px 2px rgba(8,30,15,.06);
  --shadow-md: 0 8px 24px -10px rgba(8,30,15,.16);
  --shadow-lg: 0 24px 60px -18px rgba(8,30,15,.22);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 24px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(247,251,248,.65) 55%, rgba(247,251,248,.98) 100%);
  --grad-noise:
    repeating-linear-gradient(180deg, rgba(12,80,40,.02) 0 80px, rgba(0,0,0,0) 80px 160px),
    radial-gradient(ellipse 80% 50% at 50% -20%, rgba(160,125,0,.07), transparent);
}

/* ---------- Accent overrides (Tweak) ---------- */
[data-accent="amber"]   { --accent:#f59e0b; --accent-soft:rgba(245,158,11,.16); --accent-strong:#fbbf24; }
[data-accent="cyan"]    { --accent:#22d3ee; --accent-soft:rgba(34,211,238,.16); --accent-strong:#67e8f9; }
[data-accent="violet"]  { --accent:#a78bfa; --accent-soft:rgba(167,139,250,.18); --accent-strong:#c4b5fd; }
[data-accent="red"]     { --accent:#ef4444; --accent-soft:rgba(239,68,68,.18); --accent-strong:#f87171; }
[data-accent="emerald"] { --accent:#34d399; --accent-soft:rgba(52,211,153,.16); --accent-strong:#6ee7b7; }
[data-accent="gold"]    { --accent:#ffcc00; --accent-soft:rgba(255,204,0,.18); --accent-strong:#ffd84d; }
[data-accent="green"]   { --accent:#00966e; --accent-soft:rgba(0,150,110,.14); --accent-strong:#00a87b; }

/* ---------- Density Tweak ---------- */
[data-density="cozy"]    { --sp-card: 16px; --r-card: 14px; --gap-grid: 14px; }
[data-density="default"] { --sp-card: 20px; --r-card: 18px; --gap-grid: 18px; }
[data-density="spacious"]{ --sp-card: 28px; --r-card: 24px; --gap-grid: 24px; }
:root { --sp-card: 20px; --r-card: 18px; --gap-grid: 18px; }

/* ---------- Brand logo roles (design: shared/brand-badge.css) ---------- */
/* THE SIX ROLES THE HEADER MARK PAINTS FROM. Ported with the design layer own
   selectors, which already name this codebase light/dark aliases alongside the
   design-side "base" skin, so one set of rows serves both spellings and future
   diffs stay mechanical. [data-theme="dark"] is added to the first row because
   that is this repo spelling of base-dark.

   NOT PORTED: the national-edition rows ([data-edition="bg|de|eu"]). Nothing in
   the shipped markup sets that attribute yet; they arrive with the BrandBadge
   component, and a selector that can never match is dead CSS. */
:root,[data-theme="base"],[data-theme="dark"]{--logo-ring:#d62612;--logo-field:#0b0b0d;--logo-ink:#ffffff;--logo-bar:#d62612;--logo-dot:#d62612;--logo-rule:#d62612}
[data-theme="base"][data-mode="light"],[data-theme="light"],[data-mode="light"]:not([data-theme]){--logo-ring:#d62612;--logo-field:#ffffff;--logo-ink:#0b0b0d;--logo-bar:#d62612;--logo-dot:#d62612;--logo-rule:#d62612}
[data-theme="eu"]{--logo-ring:#ffcc00;--logo-field:#050a24;--logo-ink:#f2f6ff;--logo-bar:#ffcc00;--logo-dot:#4f8cff;--logo-rule:#ffcc00}
[data-theme="eu"][data-mode="light"]{--logo-ring:#003399;--logo-field:#ffffff;--logo-ink:#0b1533;--logo-bar:#003399;--logo-dot:#c8a600;--logo-rule:#003399}
[data-theme="bulgaria"]{--logo-ring:#00966e;--logo-field:#ffffff;--logo-ink:#0c1f17;--logo-bar:#d62612;--logo-dot:#00966e;--logo-rule:#00966e}
[data-theme="bulgaria"][data-mode="dark"]{--logo-ring:#00c08b;--logo-field:#07130d;--logo-ink:#eef7f2;--logo-bar:#ff5a50;--logo-dot:#00c08b;--logo-rule:#00c08b}
[data-theme="election"]{--logo-ring:#ef4444;--logo-field:#050918;--logo-ink:#f8fafc;--logo-bar:#3b82f6;--logo-dot:#fbbf24;--logo-rule:#ef4444}
[data-theme="election"][data-mode="light"]{--logo-ring:#c22b2b;--logo-field:#ffffff;--logo-ink:#0b1130;--logo-bar:#1d4ed8;--logo-dot:#b8860b;--logo-rule:#c22b2b}
[data-theme="worldcup"]{--logo-ring:#fbbf24;--logo-field:#03150a;--logo-ink:#f7fbf4;--logo-bar:#ffffff;--logo-dot:#fbbf24;--logo-rule:#fbbf24}
[data-theme="worldcup"][data-mode="light"]{--logo-ring:#a07d00;--logo-field:#ffffff;--logo-ink:#0b2013;--logo-bar:#0c7a43;--logo-dot:#a07d00;--logo-rule:#0c7a43}

/* ---------- Header mark geometry ---------- */
:root {
  /* the plate size the standalone lockup derives from */
  --nrb-mark-size: 40px;
  --nrb-mark-size-header: 38px;
  --nrb-mark-size-compact: 34px;
  /* the masthead lockup cap height (plate dropped): standalone and compact */
  --nrb-mark-cap: 26px;
  --nrb-mark-cap-compact: 22px;
}


/* Scrollbar styling */
.scroll-pretty { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
.scroll-pretty::-webkit-scrollbar { width: 6px; height: 6px; }
.scroll-pretty::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 3px; }
.scroll-pretty::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }

/* ---------------- Header ---------------- */
.nrb-header {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in oklab, var(--bg-base) 84%, transparent);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  border-bottom: 1px solid var(--border-default);
}
.nrb-header-inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 24px;
  align-items: center;
  padding: 14px 24px;
  max-width: 1680px;
  margin: 0 auto;
}
/* == THE HEADER MARK (.nrb-mark) ==========================================
   The brand badge short form, shape="block", as a CSS-only lockup so plain
   header markup can carry it: the wordmark with its underbar. Geometry is the
   same 240x240 grid as the design BrandBadge, expressed as percentages of the
   plate box (container-query units for the type), so this mark and the React
   badge are the same drawing at any size.

   COLOUR COMES FROM THE SIX --logo-* ROLES, never from --accent or --text-*.
   The mark is the one element on a page that must stay THE BRAND in every
   skin, so it reads its own contract rather than the page palette.

   TWO FORMS, AND THE MASTHEAD ONE DROPS THE PLATE — no field, no ring, no
   radius, just the wordmark and the bar under it, because a plate sitting
   inside the masthead reads as a button. Freed of the square box the lockup
   sizes off its own cap height, so the same vertical space carries a much
   larger mark. The standalone plate form below is the base those rules
   override: keep both, or the cascade stops making sense. */
.nrb-mark {
  position: relative;
  display: inline-block;
  flex: none;
  width: var(--nrb-mark-size);
  height: var(--nrb-mark-size);
  border-radius: 15%;
  container-type: size;
  background: var(--logo-field);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--logo-ring) 22%, transparent);
  text-decoration: none;
  line-height: 0;
  transition: box-shadow var(--dur-fast) var(--ease-out);
}
.nrb-mark:hover { box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--logo-ring) 55%, transparent); }
.nrb-mark:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* the wordmark: 76/240 of the plate, baseline at 132/240, tracking -2/76em */
.nrb-mark span {
  position: absolute;
  left: 0;
  right: 0;
  top: 26.8cqh;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 31.7cqh;
  line-height: 1;
  letter-spacing: -.026em;
  text-align: center;
  color: var(--logo-ink);
}
/* the underbar: 104x12 at 68,150 on the 240 grid */
.nrb-mark::after {
  content: "";
  position: absolute;
  left: 28.3%;
  top: 62.5%;
  width: 43.3%;
  height: max(1px, 5cqh);
  background: var(--logo-bar);
}
/* border="double" — the outer ring, inset like the badge 4.5px stroke */
.nrb-mark[data-border="double"] { box-shadow: inset 0 0 0 max(1px, 1.9cqh) var(--logo-ring); }
.nrb-mark[data-border="double"]:hover { box-shadow: inset 0 0 0 max(1px, 1.9cqh) var(--logo-ring); }

/* the masthead form: plate dropped, sized off --nrb-mark-cap */
.nrb-header .nrb-mark {
  width: auto;
  height: auto;
  container-type: normal;
  border-radius: 0;
  background: none;
  box-shadow: none;
  padding-bottom: calc(var(--nrb-mark-cap) * .34);
  line-height: 1;
}
.nrb-header .nrb-mark:hover { box-shadow: none; }
.nrb-header .nrb-mark:hover span { color: var(--accent); }
.nrb-header .nrb-mark span {
  position: static;
  display: block;
  font-size: var(--nrb-mark-cap);
  letter-spacing: -.03em;
  text-align: left;
}
.nrb-header .nrb-mark::after {
  left: 0;
  top: auto;
  bottom: calc(var(--nrb-mark-cap) * .1);
  width: 100%;
  height: max(2px, calc(var(--nrb-mark-cap) * .115));
}
@media (max-width: 720px) {
  :root { --nrb-mark-cap: var(--nrb-mark-cap-compact); }
}

/* == NAV GLASS — the active category chip as a frosted plate ===============
   The active item gets a translucent tint, a 1px light edge and a real blur of
   the masthead behind it, and keeps the accent label. Light modes read the
   plate the other way round: a near-opaque white chip lifted off the
   newsprint, edged in ink rather than in light.

   THE FALLBACK IS NOT DECORATION. The mobile drawer and every other place this
   chip is used off the masthead has nothing behind it to blur, and a browser
   without backdrop-filter has nothing at all — in both cases the plate has to
   carry itself on tint and edge alone, or the ACTIVE chip is the one that
   disappears.

   [data-theme="light"] joins the mode row because it is this repo alias for
   base-light; the design layer names only [data-mode="light"], and without the
   alias the light skin would wear the dark glass. */
:root {
  --nrb-nav-glass-fill: rgba(255, 255, 255, .07);
  --nrb-nav-glass-edge: rgba(255, 255, 255, .22);
  --nrb-nav-glass-blur: 6px;
  --nrb-nav-glass-hover: rgba(255, 255, 255, .035);
}
[data-mode="light"], [data-theme="light"] {
  --nrb-nav-glass-fill: rgba(255, 255, 255, .72);
  --nrb-nav-glass-edge: rgba(10, 15, 28, .12);
  --nrb-nav-glass-hover: rgba(255, 255, 255, .45);
}
.nrb-nav { display: flex; gap: 8px; overflow-x: auto; padding: 0 4px; }
.nrb-nav::-webkit-scrollbar { height: 0; }
.nrb-nav-link {
  padding: 7px 12px;
  font-family: var(--font-body); font-size: var(--fs-meta); font-weight: 500;
  color: var(--text-secondary);
  border-radius: var(--r-md);
  white-space: nowrap;
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-nav-link:hover:not(.is-active) {
  color: var(--text-primary);
  background: var(--nrb-nav-glass-hover);
  box-shadow: inset 0 0 0 1px var(--border-subtle);
}
.nrb-nav-link.is-active {
  color: var(--accent);
  background: var(--nrb-nav-glass-fill);
  box-shadow: inset 0 0 0 1px var(--nrb-nav-glass-edge);
  -webkit-backdrop-filter: blur(var(--nrb-nav-glass-blur));
  backdrop-filter: blur(var(--nrb-nav-glass-blur));
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nrb-nav-link.is-active { background: color-mix(in oklab, var(--bg-elevated) 70%, transparent); }
}

.nrb-header-tools { display: flex; align-items: center; gap: 6px; grid-column: 3; justify-self: end; }
.nrb-icon-btn {
  width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-secondary);
  border-radius: var(--r-md);
  position: relative;
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-icon-btn:hover { color: var(--text-primary); background: var(--bg-elevated); }
/* == THE BAR GETS ONE FOCUS RING (Header Handoff section 07, gap 1) ==========
   Before this, .nrb-mark was the only thing in the masthead that defined
   :focus-visible; chips, icon buttons, Log in and Join all fell back to the UA
   outline, which on a dark navy bar is a thin black rectangle nobody can see.
   One ring, the mark own ring, stated once per control. */
.nrb-icon-btn:focus-visible,
.nrb-nav-link:focus-visible,
.nrb-login:focus-visible,
.nrb-join:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nrb-icon-badge {
  position: absolute; top: 6px; right: 6px;
  background: var(--hot); color: #fff;
  font-family: var(--font-mono); font-size: 9px; font-weight: 700;
  min-width: 14px; height: 14px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 3px;
}

.nrb-search { display: flex; align-items: center; }
.nrb-search.is-open .nrb-icon-btn { background: var(--bg-elevated); color: var(--accent); }

/* Language selector */
.nrb-lang { position: relative; }
.nrb-lang-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 36px; padding: 0 9px 0 10px;
  color: var(--text-secondary);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-lang-btn:hover { color: var(--text-primary); background: var(--bg-elevated); border-color: var(--border-strong); }
.nrb-lang.is-open .nrb-lang-btn { color: var(--accent); background: var(--bg-elevated); border-color: color-mix(in oklab, var(--accent) 45%, transparent); }
.nrb-lang-code {
  font-family: var(--font-mono); font-size: var(--fs-caption); font-weight: 600;
  letter-spacing: .06em;
}
.nrb-lang-btn svg:last-child { transition: transform var(--dur-fast) var(--ease-out); }
.nrb-lang.is-open .nrb-lang-btn svg:last-child { transform: rotate(180deg); }

.nrb-lang-menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  min-width: 178px;
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  padding: 6px;
  display: flex; flex-direction: column; gap: 2px;
  animation: nrb-lang-in var(--dur-fast) var(--ease-out);
}
@keyframes nrb-lang-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.nrb-lang-opt {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: var(--r-sm);
  color: var(--text-secondary); text-align: left;
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-lang-opt:hover { background: var(--bg-elevated); color: var(--text-primary); }
.nrb-lang-opt.is-active { color: var(--text-primary); }
.nrb-lang-opt.is-active svg { color: var(--accent); margin-left: auto; }
.nrb-lang-opt-code {
  font-size: 11px; font-weight: 700; letter-spacing: .06em;
  width: 26px; padding: 2px 0; text-align: center;
  border-radius: var(--r-xs);
  background: var(--bg-overlay); color: var(--text-secondary);
}
.nrb-lang-opt.is-active .nrb-lang-opt-code { background: var(--accent-soft); color: var(--accent); }
.nrb-lang-opt-label { font-family: var(--font-body); font-size: var(--fs-meta); }
.nrb-search-input {
  background: var(--bg-elevated);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  color: var(--text-primary);
  padding: 8px 12px;
  width: 260px;
  font: inherit;
  /* ⚑ 1.2, NOT the inherited 1.45. The handoff measured this: at body leading the
     input resolves to 38.3px against a 36px tool row, so OPENING SEARCH grew the
     masthead from 65px to 67.3px and shoved every sticky sub-bar down with it.
     A control inside a bar of fixed furniture may not change that bar height. */
  line-height: 1.2;
  outline: none;
  margin-left: 6px;
  transition: width var(--dur-med) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.nrb-search-input::placeholder { color: var(--text-tertiary); }
.nrb-search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
/* == REDUCED MOTION (Header Handoff section 06) =============================
   The handoff notes the header had no prefers-reduced-motion query at all. The
   width animation is the one genuinely large movement in the bar — 0 to 260px
   of horizontal travel next to the reader's eye — and the language popover
   keyframe slides as it fades. Both are suppressed; the colour transitions stay,
   since a 120ms tint is not motion. */
@media (prefers-reduced-motion: reduce) {
  .nrb-search-input { transition: border-color var(--dur-fast) var(--ease-out); }
  .nrb-lang-menu { animation: none; }
  .nrb-lang.is-open .nrb-lang-btn svg:last-child { transition: none; }
}
.nrb-user .avatar { width: 32px; height: 32px; border: 2px solid var(--border-default); }
.nrb-user { display: inline-flex; align-items: center; }

/* Account area (logged-out) */
.nrb-account { display: inline-flex; align-items: center; gap: 4px; margin-left: 2px; }
.nrb-login {
  display: inline-flex; align-items: center; justify-content: center;
  height: 36px; padding: 0 12px; white-space: nowrap;
  font-family: var(--font-body); font-weight: 500; font-size: var(--fs-meta);
  color: var(--text-secondary); border-radius: var(--r-md);
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-login:hover { color: var(--text-primary); background: var(--bg-elevated); }
.nrb-join {
  display: inline-flex; align-items: center; gap: 6px;
  height: 36px; padding: 0 15px;
  font-family: var(--font-body); font-weight: 600; font-size: var(--fs-meta);
  color: var(--text-inverse); background: var(--accent);
  border: 1px solid var(--accent); border-radius: var(--r-md);
  box-shadow: 0 0 0 0 var(--accent-soft);
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-join:hover { background: var(--accent-strong); border-color: var(--accent-strong); box-shadow: 0 0 18px -4px var(--accent-soft); }

/* Header — mobile (used by Pricing at narrow widths; Main Feed uses its own mobile bar) */
/* ---- language, as a SEGMENTED CONTROL (owner 2026-08-05) ----
   Every locale visible and one tap away, instead of a dropdown that hides four
   of five behind a click. Zero-JS by construction: it is a row of plain links
   with the current page as a non-link — nothing to open, nothing to close, so
   a baked page needs no script for it at all.
   The ACTIVE cell reuses the design system's own established "current language"
   treatment (.nrb-lang-opt.is-active .nrb-lang-opt-code — accent-soft plate,
   accent text) rather than a solid accent fill, which would put white-on-gold
   in light mode and fail contrast. */
/* == THE LANGUAGE SEGMENTED CONTROL (Header Handoff 2026-08-27, section 04.3) ==
   A padded TRACK with a PILL riding inside it, not a strip of cells divided by
   hairlines. The track is the recessed ground, the selected option is a raised
   surface lifted off it by a shadow — which is what makes it read as one control
   with a current position, rather than as several buttons that happen to touch.

   ⚑ CHILD-AGNOSTIC BY DESIGN. Three surfaces spell the option differently — the
   handoff says .nrb-langseg-opt, the SPA renders .nrb-langseg-btn, and a baked
   page emits bare a and span elements. Styling the direct child is what lets one
   rule serve all three; do not narrow it to a class without changing all three.

   ⚖️ IN LIGHT MODES THE TRACK HAS TO BE TINTED OR THE PILL VANISHES. The handoff
   flags this against itself: every light theme resolves --bg-elevated and
   --bg-surface to the same #ffffff, so a white pill on a white track separates by
   a 6%-alpha shadow alone. The track drops to --bg-overlay there, which is the
   one step of recess the palette already carries. */
.nrb-langseg {
  display: inline-flex; align-items: stretch;
  height: 36px; padding: 3px; gap: 2px;
  border: 1px solid var(--border-default); border-radius: var(--r-md);
  background: var(--bg-elevated);
}
/* ⚑ THE CONDITION IS REALLY "elevated == surface", WHICH CSS CANNOT ASK, so the
   selector enumerates the five palettes where it is true. Four arrive via
   data-mode; BULGARIA IS THE ONE THAT DOES NOT — it is natively light and
   carries no mode attribute at all, so a mode-only rule silently skips the
   flagship BG edition, which is the one place this matters most. Its :not()
   guard keeps the tint off bulgaria-dark, where the two tokens differ again. */
[data-mode="light"] .nrb-langseg,
[data-theme="light"] .nrb-langseg,
[data-theme="bulgaria"]:not([data-mode="dark"]) .nrb-langseg { background: var(--bg-overlay); }
.nrb-langseg > * {
  display: inline-flex; align-items: center; height: 100%; padding: 0 10px;
  border-radius: calc(var(--r-md) - 3px);
  font-family: var(--font-mono); font-size: var(--fs-caption); font-weight: 600;
  letter-spacing: .06em;
  color: var(--text-tertiary); text-decoration: none;
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-langseg > *:hover { color: var(--text-primary); }
.nrb-langseg > *:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nrb-langseg .is-active {
  background: var(--bg-surface);
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
}

@media (max-width: 720px) {
  .nrb-header-inner { padding: 11px 16px; gap: 12px; }
  .nrb-search-input { width: min(46vw, 200px); }
}
@media (max-width: 560px) {
  /* ⚠️ SCOPED TO THE MASTHEAD (2026-09-13). This said .nrb-lang flat, which was
     safe only while the dropdown lived nowhere else — the card's own note reads
     "below 560px shared/feed.css hides .nrb-lang FROM THE MASTHEAD; the edition
     moves to the shell's language row". The portal nav's column now wears the
     same component, and there it is the site's ONLY language control, with no
     second surface to fall back to. Unscoped, this one line took the edition
     switcher away from every phone reader of the baked site. */
  .nrb-header .nrb-lang { display: none; }
  .nrb-login { display: none; }
  .nrb-header-tools { gap: 2px; }
  .nrb-join { padding: 0 12px; }
  /* five two-letter cells have to share the bar with the wordmark and the
     mode toggle — tighten rather than hide: on a baked page this IS the
     language switch, there is no second surface to fall back to */
  .nrb-langseg > * { padding: 0 6px; font-size: 11px; letter-spacing: .03em; }
}


/* ---- baked-page adapter (generated; keeps the base sheet's element rules
   from leaking into the design chrome, and maps the zero-framework header
   widgets onto the design classes) ---- */
.nrb-header a:hover { text-decoration: none; }
.nrb-header .nrb-langs { display: flex; gap: 2px; }
/* the design system's button reset, scoped to the header — tokens.css's
   global reset is deliberately NOT in this chrome, so without this the
   toggle would wear the user-agent's native gray button styling */
.nrb-header button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
/* the language selector as a native <details> dropdown (no framework JS).
   [open] IS .is-open here: the card toggles a class from a script, a baked page
   is a file, so the browser's own disclosure state has to carry the card's own
   open treatment — accent ink on the elevated ground, an accent-tinted border,
   and the chevron turned over. Two spellings of the trigger are covered because
   two have existed: the summary wearing the button class itself (the column),
   and a button nested inside the summary (the masthead's first cut).
   ⚑ Higher specificity than the .is-open rules it mirrors, so a page that has
   BOTH a script and the attribute cannot end up half-open. */
details.nrb-lang > summary { list-style: none; cursor: pointer; }
details.nrb-lang > summary::-webkit-details-marker { display: none; }
details.nrb-lang { position: relative; }
details.nrb-lang[open] > summary .nrb-lang-btn,
details.nrb-lang[open] > summary.nrb-lang-btn {
  color: var(--accent);
  background: var(--bg-elevated);
  border-color: color-mix(in oklab, var(--accent) 45%, transparent);
}
details.nrb-lang[open] > summary .nrb-lang-btn svg:last-child,
details.nrb-lang[open] > summary.nrb-lang-btn svg:last-child { transform: rotate(180deg); }
/* the mode toggle shows the icon of the mode you would SWITCH TO's opposite:
   sun in effective-dark, moon in effective-light (attribute beats OS) */
.nrb-mode-toggle .ic-moon { display: none; }
.nrb-mode-toggle .ic-sun { display: inline; }
:root[data-mode="light"] .nrb-mode-toggle .ic-moon { display: inline; }
:root[data-mode="light"] .nrb-mode-toggle .ic-sun { display: none; }
@media (prefers-color-scheme: light) {
  :root:not([data-mode]) .nrb-mode-toggle .ic-moon { display: inline; }
  :root:not([data-mode]) .nrb-mode-toggle .ic-sun { display: none; }
}

/* the reader's OS preference picks the eu LIGHT variant (extracted from
   tokens.css [data-theme="eu"][data-mode="light"]) — UNLESS the header's mode
   toggle set an explicit data-mode (the attribute wins over the OS) */
@media (prefers-color-scheme: light) {
  :root:not([data-mode="dark"]) {
  color-scheme: light;

  --bg-base:     #eef1f9;
  --bg-canvas:   #f7f9fd;
  --bg-surface:  #ffffff;
  --bg-elevated: #ffffff;
  --bg-overlay:  #e7ecf8;
  --bg-inverse:  #050a24;

  --border-subtle:  rgba(5,16,60,.07);
  --border-default: rgba(5,16,60,.13);
  --border-strong:  rgba(154,123,0,.5);

  --text-primary:   #0b1533;
  --text-secondary: #45507a;
  --text-tertiary:  #8d97bd;
  --text-inverse:   #ffffff;

  --accent: #9a7b00;          /* EU gold at ink strength (AA on paper) */
  --accent-soft: rgba(200,166,0,.15);
  --accent-strong: #b8930a;
  --accent-alt: #003399;      /* official reflex blue */
  --accent-alt-soft: rgba(0,51,153,.12);

  --hot: #d3323c;
  --hot-soft: rgba(211,50,60,.13);

  --shadow-sm: 0 1px 2px rgba(10,20,60,.06);
  --shadow-md: 0 8px 24px -10px rgba(10,20,60,.16);
  --shadow-lg: 0 24px 60px -18px rgba(10,20,60,.22);
  --shadow-glow: 0 0 0 1px var(--accent), 0 0 24px -5px var(--accent-soft);

  --grad-hero: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(247,249,253,.65) 55%, rgba(247,249,253,.98) 100%);
  /* circle of stars in reflex blue on paper + gold halo */
  --grad-noise:
    url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20240%20240%22%3E%3Cdefs%3E%3Cpath%20id=%22s%22%20d=%22M120.00,31.00%20L122.47,38.60%20L130.46,38.60%20L124.00,43.30%20L126.47,50.90%20L120.00,46.20%20L113.53,50.90%20L116.00,43.30%20L109.54,38.60%20L117.53,38.60%20Z%22%20fill=%22%23003399%22%20fill-opacity=%22.07%22/%3E%3C/defs%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(0%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(30%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(60%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(90%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(120%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(150%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(180%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(210%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(240%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(270%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(300%20120%20120)%22/%3E%3Cuse%20href=%22%23s%22%20transform=%22rotate(330%20120%20120)%22/%3E%3C/svg%3E") no-repeat 50% -56px / 560px 560px,
    radial-gradient(ellipse 60% 42% at 50% -6%, rgba(0,51,153,.08), transparent 62%),
    radial-gradient(ellipse 95% 60% at 50% 2%, rgba(255,204,0,.10), transparent 72%);
  }
}
/* ══ src/styles/baked-only.css — CSS THAT ONLY THE BAKED SITE WEARS ══════════
   ⚠️ NOT IMPORTED BY main.tsx, AND THAT IS THE POINT. Every other sheet in this
   directory is loaded by the SPA and sliced into the baked pages. This one is
   the reverse: it is read ONLY by web/scripts/design-system-sync.mjs, which
   appends it verbatim to code/sitePublisher/designChrome.ts. Nothing here
   reaches the SPA bundle.

   WHY IT EXISTS: the baked site and the SPA are two surfaces over one design
   system, and they do genuinely diverge — the baker paints markup the React
   header never renders, and it serves six language editions where the SPA's
   font stack was decided for one. Those divergences used to be hand-typed
   INTO the generated designChrome.ts, where the next design:system:build run
   silently deleted them (measured 2026-08-26: 46 lines gone, three tests red).
   A generator cannot preserve what it does not know about, so the fix is to
   tell it — this file is that telling.

   ⚖️ A GENERATED FILE IS A PURE FUNCTION OF ITS SOURCES. If the baked pages
   need a rule, it belongs in a source, never in the output.

   HOW TO CHANGE ANYTHING BELOW: edit src/styles/baked-only.css (this text is
   copied into designChrome.ts, so if you are reading it there, you are in the
   output — go to the source), then run npm run design:system:build from web/.
   Pinned by tests/unit/sitePublisher/designChromeGeneration.test.ts, which
   regenerates the chrome in memory and fails if either section stops arriving.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1. THE DISPLAY FACE NEEDS CYRILLIC (baked-only override) ────────────────
   ⚠️ SPACE GROTESK HAS NO CYRILLIC. Measured against what Google actually
   serves: all 12 faces stop at Latin Extended, no U+04xx range anywhere. So
   on the BULGARIAN edition — the flagship one — every word set in the display
   face was silently falling through to system-ui, which is a different font
   on every machine and nobody's design decision.
   Geist is the answer already in the building: it covers Cyrillic, it is
   this site's own body face, and it is loaded on every page by the @import at
   the head of this sheet, so it costs no request. Latin editions are
   untouched — Space Grotesk still wins there — and Cyrillic ones get a
   designed grotesk instead of a lottery.

   ⚑ IT IS AN OVERRIDE, NOT AN EDIT, ON PURPOSE. tokens.css declares the SPA's
   own --font-display earlier in the generated sheet; this re-declaration wins
   by cascade order and leaves the two surfaces visibly, deliberately different.
   Widening the SPA's stack to match is a design decision for the owner, not a
   side effect of fixing the baker — until they make it, the divergence is
   stated here rather than hidden inside a generated token block.

   ⚖️ A FONT STACK IS A CLAIM ABOUT COVERAGE, AND SIX EDITIONS TEST IT. */
:root {
  --font-display: 'Space Grotesk', 'Geist', system-ui, -apple-system, sans-serif;
}

/* ── 2. THE SECTION NAV (20.1820) ────────────────────────────────────────────
   Baked-only because the SPA header renders .nrb-nav; .nrb-cats is markup that
   exists only in code/sitePublisher/pageTemplates.ts, so shipping these rules
   in feed.css would be dead weight in every SPA bundle.

   The header grid has always been auto | 1fr | auto with the tools pinned to
   column 3; the 1fr has rendered nothing since About and Archive moved to the
   footer on 2026-07-24. The sections fill it.

   ⚖️ **IT STARTS AT THE MARK — IT DOES NOT CENTRE** (Header Handoff 2026-08-27,
   section 01, and its section 08 Do-not list in as many words: *do not centre the
   middle column*). This REVERSES the original 20.1820 reading, which centred the
   row so it would not look like a tail on the logo. Centring made the nav float
   in the middle of an 856px elastic column, so the band between the mark and the
   first section read as a hole — which is what the owner pointed at. Left-aligned,
   the nav starts at a fixed offset from the mark no matter how many sections the
   registry loads, and the elastic space collects at the end where it belongs. */
.nrb-cats {
  grid-column: 2; justify-self: start;
  display: flex; align-items: center; gap: 8px;
  min-width: 0; max-width: 100%;
  /* a registry that grows past the band scrolls rather than wrapping the header
     into two rows — the header's height is furniture every page depends on */
  overflow-x: auto; scrollbar-width: none;
}
.nrb-cats::-webkit-scrollbar { display: none; }
/* ⚑ THE SECTION LINK IS THE SAME CHIP AS THE SPA NAV, RE-EXPRESSED.
   The handoff specifies one category nav; the SPA renders it as .nrb-nav-link
   buttons and a baked page renders it as .nrb-cats anchors, so the chip has to
   be stated twice or the baked half wears nothing. It wore nothing until now:
   the NAV GLASS shipped with 20.1880 landed only on .nrb-nav-link, which no
   baked page emits, so the tokens were defined and nobody read them. */
.nrb-cats a {
  flex: none;
  padding: 7px 11px; border-radius: var(--r-md);
  font-family: var(--font-body); font-size: 13.5px; font-weight: 500;
  line-height: 1; white-space: nowrap;
  color: var(--text-secondary); text-decoration: none;
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-cats a:hover:not([aria-current="page"]) {
  color: var(--text-primary);
  background: var(--nrb-nav-glass-hover);
  box-shadow: inset 0 0 0 1px var(--border-subtle);
}
/* ⚖️ THE SECTION YOU ARE IN IS STATED, NOT JUST TINTED. aria-current carries it
   for a reader who cannot see the weight change, and the weight carries it for
   one who cannot see colour — neither alone is the answer.

   ⚠️ The handoff section 08 says *do not give the active chip a second treatment
   on top of the plate*, and the weight is technically that second treatment. It
   is KEPT DELIBERATELY: the glass fill is white at 7% over navy, which in
   greyscale is a barely perceptible lightness step, so dropping the weight would
   trade a law this repo already wrote down for a signal that is nearly invisible
   without colour. Flagged for the owner rather than resolved silently. */
.nrb-cats a[aria-current="page"] {
  color: var(--accent);
  font-weight: 700;
  background: var(--nrb-nav-glass-fill);
  box-shadow: inset 0 0 0 1px var(--nrb-nav-glass-edge);
  -webkit-backdrop-filter: blur(var(--nrb-nav-glass-blur));
  backdrop-filter: blur(var(--nrb-nav-glass-blur));
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nrb-cats a[aria-current="page"] { background: color-mix(in oklab, var(--bg-elevated) 70%, transparent); }
}
/* handoff section 07, gap 1 — the bar had a focus ring on the mark alone */
.nrb-cats a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .nrb-cats a { transition: none; } }
/* Below the dock the band belongs to the wordmark and the tools; the sections
   are one tap away on the home page's own rail. */
@media (max-width: 1180px) { .nrb-cats { display: none; } }

/* ── 🧩 20.2210 D4 — RESERVED SLOTS HIDE WHILE EMPTY ─────────────────────────
   The baker writes anonymous boxes (data-nrb-slot="rail-foot", "main-foot")
   on every framed page so the next client feature has a home without a bake.
   Until the bundle fills one, it must cost the layout nothing: no gap, no
   height, no grid row. `:empty` is exact — a filled slot has children. */
[data-nrb-slot]:empty { display: none; }

/* ── 🧭 20.2300 — THE RESEARCH PAGES ─────────────────────────────────────────
   Three surfaces, one family: the HUB (its own page), the TRAIL and the CITED
   strip on a chapter's page, and the one TAG line a cited story gains.

   ⚖️ THE LOOK IS A BOUNDED OVERRIDE. `--exp-accent` and friends arrive as
   INLINE custom properties on `.exp-look`, written by the bake — never a second
   [data-theme] and never a second stylesheet (ONE EDITION, ONE SKIN). Every
   rule below falls back to the edition's own tokens, so an unpainted research
   is drawn in the site's colours and a painted one tints only itself. */
.exp-hub { display: block; }
.exp-head { margin: 0 0 18px; }
.exp-head h1 { margin: 6px 0 4px; }
.exp-head .dek { margin: 0; color: var(--text-muted); }
.exp-mark { font-size: 28px; line-height: 1; display: inline-block; }
.exp-over p { margin: 0 0 12px; }
.exp-must ul { margin: 0 0 18px; padding-left: 18px; }
.exp-must li { margin: 0 0 6px; }

/* THE TREE. Depth is indentation and nothing else: a chapter under a chapter is
   a TOPIC relationship, and the reader should feel that as nesting, not as a
   different kind of card. */
.exp-tree .chapters, .exp-tree .kids { list-style: none; margin: 0; padding: 0; }
.exp-tree .kids { margin-left: 18px; border-left: 1px solid var(--line); padding-left: 12px; }
.exp-tree .chapter { padding: 8px 0; border-bottom: 1px solid var(--line); }
.exp-tree .chapter .ttl { font-weight: 600; }
.exp-tree .chapter .ttl a { color: inherit; text-decoration: none; }
.exp-tree .chapter .ttl a:hover { color: var(--exp-accent, var(--accent)); text-decoration: underline; }
/* A chapter with no page in this edition yet: drawn, never linked — the tree is
   the SHAPE of the research, and a gap in it is information. */
.exp-tree .chapter .ttl .soon { color: var(--text-muted); }
.exp-tree .chapter .dek { display: block; color: var(--text-muted); font-size: 14px; }
.exp-tree .chapter .meta { display: inline-flex; gap: 10px; font-size: 12px; color: var(--text-muted); }
.exp-tree .chapter .cited::after { content: ' ◇'; }

/* THE RAIL of news the research cites — the site's OWN stories, linked. */
.exp-stories .strip { display: grid; gap: 10px; }
@media (min-width: 720px) { .exp-stories .strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* 🧭 THE COMPARATOR (20.2300 F) — the facts the research gathered. STATIC
   FIRST: this is a real table, readable with every script blocked; the slot box
   around it exists so an island can make it sortable later without a bake.
   It scrolls sideways on a phone rather than squeezing the columns to nothing,
   and the first column stays put so a row never loses the thing it describes. */
.exp-data { margin: 0 0 22px; }
.exp-data > [data-nrb-slot] { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.exp-data-table { border-collapse: collapse; width: 100%; font-size: 14px; }
.exp-data-table th, .exp-data-table td {
  border-bottom: 1px solid var(--line);
  padding: 7px 10px;
  text-align: left;
  vertical-align: top;
  white-space: nowrap;
}
.exp-data-table thead th {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-muted);
  border-bottom: 2px solid var(--exp-accent, var(--line));
}
.exp-data-table tbody th {
  position: sticky;
  left: 0;
  background: var(--bg);
  font-weight: 600;
}
.exp-data-table td.na { color: var(--text-muted); }
.exp-data-table a { color: var(--exp-accent, var(--accent)); text-decoration: none; }
.exp-data-table a:hover { text-decoration: underline; }
/* Not a comparison: the same facts, said plainly. */
.exp-data-list { list-style: none; margin: 0; padding: 0; font-size: 14px; }
.exp-data-list li { padding: 6px 0; border-bottom: 1px solid var(--line); }
.exp-data-list .ent { font-weight: 600; }
.exp-data-list .att { color: var(--text-muted); margin: 0 6px; }
.exp-data .more { margin: 8px 0 0; font-size: 12px; color: var(--text-muted); }

/* THE TRAIL and THE TAG — two single lines on a story page, and the only marks
   a research leaves there. Both are quiet by construction: a story's page is
   the story's, and its membership is a footnote, never a banner. */
.exp-trail, .exp-tag { margin: 12px 0; font-size: 14px; color: var(--text-muted); }
.exp-trail a, .exp-tag a { color: var(--exp-accent, var(--accent)); text-decoration: none; }
.exp-trail a:hover, .exp-tag a:hover { text-decoration: underline; }
.exp-cited { margin-top: 8px; }

/* ── 🌐 20.2315 — THE RESEARCH SPACE ─────────────────────────────────────────
   A published research at /@@{handle}/: its home and every chapter inside ONE
   frame that carries the research's navigation and nothing else — the site's
   name as the one door out, the research's own name, its editions, the table
   of contents, the page, one line of footer. The frame IS `.exp-look`, so a
   painted research tints its whole space; an unpainted one wears the edition's
   own colours. STATIC FIRST: the phone's contents are a <details>, no script. */
.rs-frame { min-height: 100vh; display: flex; flex-direction: column; background: var(--bg); color: var(--text); }
.rs-top { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 12px 20px; border-bottom: 2px solid var(--exp-accent, var(--line)); }
.rs-exit { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted, var(--text-muted)); text-decoration: none; }
.rs-exit:hover { color: var(--text); text-decoration: underline; }
.rs-name { display: inline-flex; align-items: center; gap: 8px; color: inherit; text-decoration: none; font-weight: 700; font-size: 18px; }
.rs-mark { font-size: 22px; line-height: 1; }
.rs-langs { margin-left: auto; display: inline-flex; gap: 10px; font-size: 12px; letter-spacing: .06em; }
.rs-langs a { color: var(--muted, var(--text-muted)); text-decoration: none; }
.rs-langs a:hover { color: var(--exp-accent, var(--accent)); }
.rs-langs [aria-current] { color: var(--text); font-weight: 700; }
.rs-body { flex: 1; display: grid; grid-template-columns: minmax(0, 1fr); gap: 28px; width: 100%; max-width: 1180px; margin: 0 auto; padding: 18px 20px 40px; box-sizing: border-box; }
.rs-main.shell { margin: 0; padding: 0 0 40px; width: 100%; min-width: 0; }
.rs-toc { display: none; }
.rs-toc-list { list-style: none; margin: 0; padding: 0; font-size: 14px; }
.rs-toc-list a { display: block; padding: 6px 10px; border-radius: 6px; color: var(--muted, var(--text-muted)); text-decoration: none; }
.rs-toc-list a:hover { color: var(--text); }
.rs-toc-list a[aria-current="page"] { color: var(--text); font-weight: 600; box-shadow: inset 3px 0 0 var(--exp-accent, var(--accent)); }
.rs-toc-list .rs-d0 a { font-weight: 700; color: var(--text); }
.rs-toc-list .rs-d2 a { padding-left: 22px; }
.rs-toc-list .rs-d3 a { padding-left: 34px; }
.rs-toc-list .rs-d4 a { padding-left: 46px; }
.rs-toc-m { border-bottom: 1px solid var(--line); padding: 8px 20px; }
.rs-toc-m > summary { cursor: pointer; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted, var(--text-muted)); }
.rs-toc-m .rs-toc-list { margin-top: 8px; }
.rs-turn { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 32px 0 0; padding-top: 16px; border-top: 1px solid var(--line); }
.rs-turn a { display: flex; flex-direction: column; gap: 4px; color: inherit; text-decoration: none; font-weight: 600; }
.rs-turn a span { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted, var(--text-muted)); font-weight: 400; }
.rs-turn a:hover { color: var(--exp-accent, var(--accent)); }
.rs-next { grid-column: 2; text-align: right; }
.rs-footer { border-top: 1px solid var(--line); padding: 14px 20px; font-size: 13px; color: var(--muted, var(--text-muted)); display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.rs-footer a { color: inherit; }
@media (min-width: 900px) {
  .rs-body { grid-template-columns: 240px minmax(0, 760px); justify-content: center; }
  .rs-toc { display: block; position: sticky; top: 16px; align-self: start; max-height: calc(100vh - 32px); overflow-y: auto; }
  .rs-toc-m { display: none; }
}

/* ── System pricing: shared by the baked fallback and hydrated view ─────────
   Rank colors belong to tokens.css. Surfaces/ink follow the edition's native
   or explicit mode; no prefers-color-scheme override competes with that axis.
   The subgrid aligns every section across a row, including annual savings.
   Breakpoints are layout geometry; all type, spacing and corners use DS roles. */
.pricing-shell {
  container: pricing / inline-size;
  position: relative; z-index: 1;
  max-width: 1680px; margin: 0 auto;
  padding: var(--sp-7) clamp(var(--sp-4), 3vw, var(--sp-8)) var(--sp-9);
}
/* Pricing owns the space normally reserved for both reader side columns. */
.pricing-layout {
  position: relative; z-index: 1; width: 100%; max-width: 1680px; margin: 0 auto;
  padding: 0 clamp(var(--sp-5), 3vw, var(--sp-8));
}
.pricing-layout > .pn-identity {
  position: relative; z-index: 2; display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-6); padding: var(--sp-6) 0;
}
.pn-identity .pn-mark { margin: 0; }
.pn-identity details.pn-language { position: relative; top: auto; right: auto; }
.pn-identity .nrb-lang-btn { min-height: var(--control-touch); }
.pricing-layout > .pricing-shell { max-width: none; padding: 0 0 var(--sp-9); }
.pricing-layout ~ .site-footer { max-width: 1680px; margin-left: auto; margin-right: auto; padding-left: clamp(var(--sp-5), 3vw, var(--sp-8)); padding-right: clamp(var(--sp-5), 3vw, var(--sp-8)); }
.pricing-layout ~ .site-footer > .shell { max-width: none; padding-left: 0; padding-right: 0; }
.pricing-page {
  font: var(--fs-body)/var(--lh-loose) var(--font-body);
  color: var(--text-primary);
}
.pricing-page *, .pricing-page *::before, .pricing-page *::after { box-sizing: border-box; }
.pricing-page h2 { color: var(--text-primary); text-transform: none; letter-spacing: var(--tracking-tight); }
.pricing-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.pricing-welcome {
  display: flex; gap: var(--sp-3); align-items: center;
  padding: var(--sp-5); margin-bottom: var(--sp-5);
  border: 1px solid color-mix(in srgb, var(--success) 35%, var(--border-default));
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--success) 7%, var(--bg-surface));
}
.pricing-welcome h2 { font: 700 var(--fs-h6)/var(--lh-snug) var(--font-display); margin: 0 0 var(--sp-1); }
.pricing-welcome p { margin: 0; color: var(--text-secondary); }
.pricing-welcome-icon {
  display: grid; place-items: center; flex: 0 0 var(--sp-9); height: var(--sp-9);
  border-radius: var(--r-md); color: var(--text-primary);
  background: color-mix(in srgb, var(--success) 16%, var(--bg-surface));
}
.pricing-grid { display: grid; grid-template-columns: repeat(5,minmax(0,1fr)); gap: var(--sp-6) var(--sp-4); align-items: stretch; }
.pricing-card {
  --pricing-pad: var(--sp-6);
  --tier: var(--membership-rookie); --tier-line: var(--membership-rookie);
  --tier-solid: var(--membership-rookie-solid); --tier-ribbon: var(--membership-rookie-ribbon);
  min-width: 0; display: flex; flex-direction: column; gap: var(--sp-5);
  padding: 0 var(--pricing-pad) var(--sp-6); overflow: hidden;
  border: 1px solid var(--border-default); border-radius: var(--r-xl);
  background: var(--bg-surface); box-shadow: var(--shadow-sm);
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.pricing-card--explorer { --tier: var(--membership-explorer); --tier-line: var(--membership-explorer); --tier-solid: var(--membership-explorer-solid); --tier-ribbon: var(--membership-explorer-ribbon); }
.pricing-card--architect { --tier: var(--membership-architect); --tier-line: var(--membership-silver); --tier-solid: var(--membership-architect-solid); --tier-ribbon: var(--membership-architect-ribbon); }
.pricing-card--brigade {
  --tier: var(--membership-brigade); --tier-line: var(--membership-brigade); --tier-solid: var(--membership-brigade-solid); --tier-ribbon: var(--membership-brigade-ribbon);
  border-color: color-mix(in srgb, var(--tier-line) 65%, var(--border-default));
}
.pricing-card.is-selected { border-color: var(--success); box-shadow: 0 0 0 2px color-mix(in srgb,var(--success) 35%,transparent); }
/* Overrides the article's reading-measure cap: the image reaches BOTH edges. */
.pricing-card > .pricing-photo {
  position: relative; width: calc(100% + 2 * var(--pricing-pad)); max-width: none;
  margin: 0 calc(-1 * var(--pricing-pad)); aspect-ratio: 16/9; overflow: hidden;
  background: var(--bg-elevated);
}
.pricing-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pricing-badge {
  position: absolute; left: var(--sp-3); top: var(--sp-3); max-width: calc(100% - 2 * var(--sp-3));
  border: 1px solid var(--tier-line); padding: var(--sp-1) var(--sp-2); border-radius: var(--r-sm);
  background: var(--bg-surface); color: var(--text-primary);
  font-size: var(--fs-overline); font-weight: 700; line-height: var(--lh-snug);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
}
.pricing-offer {
  position: absolute; right: 0; bottom: 0; z-index: 3;
  display: flex; flex-direction: column; gap: 6px;
  min-width: 210px; max-width: calc(100% - var(--sp-3));
  border-radius: var(--r-md) 0 0 0; padding: 9px 13px 10px 15px;
  background: var(--tier-ribbon); color: var(--membership-on-solid);
  box-shadow: 0 -2px 12px #101a2e38; text-align: left;
}
.pricing-offer-title { font: 700 15px/var(--lh-snug) var(--font-body); color: inherit; }
.pricing-offer-status { display: flex; flex-direction: column; gap: 6px; }
.pricing-offer-progress {
  display: block; width: 100%; height: 5px; border: 0; padding: 0;
  border-radius: var(--r-pill); overflow: hidden; appearance: none;
  background: color-mix(in srgb, var(--membership-on-solid) 22%, transparent);
}
.pricing-offer-progress::-webkit-progress-bar { background: transparent; }
.pricing-offer-progress::-webkit-progress-value { background: var(--membership-on-solid); border-radius: var(--r-pill); }
.pricing-offer-progress::-moz-progress-bar { background: var(--membership-on-solid); border-radius: var(--r-pill); }
.pricing-offer-count { font: 650 var(--fs-overline)/var(--lh-snug) var(--font-body); text-align: right; }
.pricing-offer--free { min-width: 132px; text-align: right; }
.pricing-offer--free .pricing-offer-title { font-size: 24px; font-weight: 780; line-height: 1; letter-spacing: -1px; }
.pricing-popular {
  position: absolute; right: 0; top: var(--sp-4); max-width: 65%;
  padding: 10px var(--sp-4); border-radius: var(--sp-2) 0 0 var(--sp-2);
  isolation: isolate; overflow: hidden; color: var(--membership-on-solid);
  background: var(--membership-popular-ribbon); background-size: 240% 180%;
  font: 700 var(--fs-meta)/1.3 var(--font-body); letter-spacing: .3px;
  border-top: 1px solid #8ce5ff66; border-bottom: 1px solid #60bfff66;
  box-shadow: 0 3px 16px #1377e44d, inset 0 1px 0 #ffffff33;
  animation: pricing-water-colour 9s ease-in-out infinite alternate;
}
.pricing-popular-label { position: relative; z-index: 2; text-shadow: 0 1px 3px #06276a, 0 0 7px #1232a4; }
.pricing-popular::before, .pricing-popular::after { content: ""; position: absolute; pointer-events: none; z-index: 0; }
.pricing-popular::before {
  inset: -90% -55%;
  background: radial-gradient(ellipse at 28% 35%, transparent 20%, #a0efff80 22%, transparent 26%), radial-gradient(ellipse at 72% 68%, transparent 21%, #bcefff70 24%, transparent 28%), radial-gradient(ellipse at 60% 20%, #a1f4ff66 0%, transparent 35%);
  filter: blur(2px); opacity: .65; animation: pricing-water-ripples 7s ease-in-out infinite alternate;
}
.pricing-popular::after {
  inset: -50% -100%;
  background: linear-gradient(110deg, transparent 35%, #adf3ff00 42%, #d9fcffa6 49%, #a1e8ff44 52%, transparent 59%), radial-gradient(ellipse at 49% 50%, #fff9 0%, transparent 12%);
  opacity: 0; animation: pricing-water-flare 8s ease-in-out infinite;
}
@keyframes pricing-water-colour { from { background-position: 0% 30%; } to { background-position: 100% 70%; } }
@keyframes pricing-water-ripples {
  0% { transform: translate(-7%, -6%) rotate(-12deg) scale(1); }
  50% { transform: translate(3%, 5%) rotate(2deg) scale(1.12); }
  100% { transform: translate(8%, -2%) rotate(12deg) scale(1.03); }
}
@keyframes pricing-water-flare {
  0%, 20% { transform: translateX(-40%); opacity: 0; }
  35% { opacity: .35; }
  55%, 100% { transform: translateX(40%); opacity: 0; }
}
@media (min-width: 1700px) {
  .pricing-offer { padding: 11px var(--sp-4) 12px 18px; }
  .pricing-offer--free .pricing-offer-title { font-size: var(--fs-h4); }
}
@media (prefers-reduced-motion: reduce), print {
  .pricing-popular, .pricing-popular::before, .pricing-popular::after { animation: none; }
  .pricing-popular::after { opacity: 0; }
}
.pricing-page .pricing-name {
  font: 700 var(--fs-h5)/var(--lh-snug) var(--font-display);
  letter-spacing: var(--tracking-tight); margin: var(--sp-2) 0 0; overflow-wrap: anywhere;
}
.pricing-billing { margin: 0; min-width: 0; }
.pricing-options {
  display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); align-items: stretch;
  padding: var(--sp-1); gap: var(--sp-1); background: var(--bg-canvas);
  border: 1px solid var(--border-default); border-radius: var(--r-lg);
}
.pricing-options--free { grid-template-columns: 1fr; height: calc(100% - var(--sp-9)); }
.pricing-option {
  appearance: none; background: none; border: 1px solid transparent; border-radius: var(--r-md);
  color: var(--text-secondary); font: inherit; text-align: left; min-width: 0;
  min-height: var(--control-touch); padding: var(--sp-3) var(--sp-2);
  display: flex; flex-direction: column; gap: var(--sp-2); cursor: pointer;
}
.pricing-option.is-active { border-color: var(--tier); background: color-mix(in srgb,var(--tier) 10%,var(--bg-surface)); color: var(--text-primary); }
.pricing-options--free .pricing-option { cursor: default; justify-content: space-between; }
.pricing-period { font-size: var(--fs-caption); font-weight: 600; line-height: var(--lh-base); min-height: calc(3 * var(--fs-caption) * var(--lh-base)); }
.pricing-period small { display: block; font: inherit; font-weight: 400; }
.pricing-standard { display: block; font-size: var(--fs-caption); font-weight: 500; line-height: var(--lh-base); }
.pricing-standard small, .pricing-current small { font-size: var(--fs-overline); white-space: nowrap; }
.pricing-current { display: grid; gap: 0; line-height: var(--lh-snug); margin-top: auto; }
.pricing-options--free .pricing-amount { padding-bottom: calc(var(--fs-overline) * var(--lh-snug)); }
.pricing-amount { font-size: var(--fs-h5); line-height: var(--lh-snug); font-weight: 700; letter-spacing: var(--tracking-tight); white-space: nowrap; font-variant-numeric: tabular-nums; }
.pricing-invoice { font-size: var(--fs-caption); line-height: var(--lh-base); color: var(--text-secondary); margin: var(--sp-2) 0 0; min-height: var(--sp-8); }
.pricing-highlight {
  border: 1px solid color-mix(in srgb,var(--tier-line) 65%,var(--border-default));
  border-radius: var(--r-lg); padding: var(--sp-5); color: var(--text-primary);
  background: linear-gradient(135deg,color-mix(in srgb,var(--tier-line) 15%,var(--bg-surface)),var(--bg-surface) 55%,color-mix(in srgb,var(--tier) 12%,var(--bg-surface)));
  display: flex; flex-direction: column; gap: var(--sp-4); min-height: 256px;
}
/* Keep one icon beside both text rows at every card width. The text may wrap
   naturally in each locale without moving the title below the icon column. */
.pricing-highlight-heading {
  display: grid; grid-template-columns: var(--sp-8) minmax(0, 1fr);
  grid-template-rows: auto auto; column-gap: var(--sp-3); row-gap: var(--sp-1);
  align-items: start;
}
.pricing-highlight-heading > .pricing-icon-tile { grid-column: 1; grid-row: 1 / 3; }
.pricing-highlight-heading > div { display: contents; }
.pricing-highlight-heading .pricing-kicker { grid-column: 2; grid-row: 1; min-width: 0; margin: 0; }
.pricing-highlight-heading h3 { grid-column: 2; grid-row: 2; min-width: 0; }
.pricing-icon-tile {
  width: var(--sp-8); height: var(--sp-8); flex-shrink: 0;
  display: grid; place-items: center; border-radius: var(--r-md);
  background: color-mix(in srgb,var(--tier-line) 25%,var(--bg-surface)); color: var(--text-primary);
}
.pricing-icon { display: block; flex-shrink: 0; }
.pricing-kicker { display: block; font-size: var(--fs-overline); font-weight: 600; line-height: var(--lh-base); letter-spacing: var(--tracking-wide); text-transform: uppercase; margin-bottom: var(--sp-1); }
.pricing-highlight h3 { font: 700 var(--fs-h6)/var(--lh-snug) var(--font-display); margin: 0; overflow-wrap: anywhere; hyphens: auto; }
.pricing-highlight p { font-size: var(--fs-body); line-height: var(--lh-loose); margin: 0; }
/* The note marker stays inline with the last word instead of occupying its
   own 24px flex box. Its invisible hit area still extends beyond the glyph. */
.pricing-footnote-link { position: relative; display: inline; font-size: .75em; vertical-align: super; line-height: 0; color: inherit; text-decoration: none; }
.pricing-footnote-link::after { content: ""; position: absolute; inset: calc(-1 * var(--sp-2)); }
.pricing-token-tag {
  align-self: flex-start; margin-top: auto; padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--border-default); border-radius: var(--r-pill);
  background: var(--bg-surface); color: var(--text-primary); font-size: var(--fs-caption); font-weight: 600; line-height: var(--lh-base);
}
/* Success action uses the DS button's type, spacing, corner and motion roles.
   A stable on-solid pair retains the accepted green in every edition/mode. */
.pricing-select {
  appearance: none; display: block; width: 100%; min-height: var(--control-touch);
  background: var(--membership-action); color: var(--membership-on-solid);
  border: 1px solid var(--membership-action); border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-4); font: 600 var(--fs-body)/var(--lh-base) var(--font-body);
  cursor: pointer; transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.pricing-select[aria-pressed=true] { background: var(--membership-action-strong); border-color: var(--membership-action-strong); }
.pricing-page button:focus-visible, .pricing-page a:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 2px; }
@media(hover:hover) {
  .pricing-select:hover { background: var(--membership-action-strong); border-color: var(--membership-action-strong); }
  .pricing-option:not(.is-active):hover { background: var(--bg-elevated); border-color: var(--border-strong); }
}
.pricing-thanks {
  background: color-mix(in srgb,var(--success) 8%,var(--bg-surface));
  border: 1px solid color-mix(in srgb,var(--success) 30%,var(--border-default));
  border-radius: var(--r-lg); padding: var(--sp-4); color: var(--text-primary); font-size: var(--fs-body);
}
.pricing-thanks h3 { font: 700 var(--fs-h6)/var(--lh-snug) var(--font-display); display: flex; gap: var(--sp-2); margin: 0 0 var(--sp-3); }
.pricing-thanks p { margin: var(--sp-1) 0; }
.pricing-thanks small { display: block; font-size: var(--fs-caption); }
.pricing-thanks del { color: var(--text-secondary); }
.pricing-thanks--empty { background: none; border: 0; padding: 0; }
.pricing-limits {
  container-type: inline-size; display: flex; flex-wrap: nowrap; gap: var(--sp-1); align-items: center;
  border-top: 1px solid var(--border-default); border-bottom: 1px solid var(--border-default);
  padding: var(--sp-3) 0; font-size: var(--fs-caption); color: var(--text-secondary);
}
.pricing-limits > span { white-space: nowrap; font-size: clamp(8px, 3.8cqw, var(--fs-caption)); }
.pricing-limits span + span { border-left: 1px solid var(--border-default); padding-left: var(--sp-1); }
.pricing-limits strong { font-size: 1.5em; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.pricing-access { font-size: var(--fs-body); font-weight: 600; color: var(--text-secondary); margin: 0; padding-bottom: var(--sp-2); }
.pricing-features { border-top: 1px solid var(--border-default); padding-top: var(--sp-5); min-width: 0; }
.pricing-features h3 { font: 600 var(--fs-overline)/var(--lh-base) var(--font-body); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-secondary); margin: 0 0 var(--sp-4); }
.pricing-features ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.pricing-features li { display: flex; align-items: flex-start; gap: var(--sp-3); color: var(--text-secondary); font-size: var(--fs-body); line-height: var(--lh-loose); overflow-wrap: anywhere; }
.pricing-features li .pricing-icon { width: var(--sp-4); height: var(--sp-5); margin-top: var(--sp-1); color: color-mix(in srgb,var(--tier-line) 70%,var(--text-primary)); }
.pricing-image-note, .pricing-availability { font-size: var(--fs-caption); color: var(--text-secondary); margin: var(--sp-4) 0; max-width: 100%; scroll-margin-top: var(--sp-12); }
.pricing-selection { min-height: 1px; }
.pricing-selection.is-visible { padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); background: var(--bg-surface); border: 1px solid var(--border-default); }
.pricing-selection p { margin: 0; }
@supports(grid-template-rows:subgrid) {
  .pricing-card { display: grid; grid-template-rows: subgrid; grid-row: span 8; row-gap: var(--sp-5); }
  .pricing-page--annual .pricing-card { grid-row: span 9; }
}
@container pricing (min-width:600px) and (max-width:1519px) {
  .pricing-card { --pricing-pad: var(--sp-4); }
  .pricing-highlight { padding: var(--sp-4); }
}
@container pricing (max-width:1199px) { .pricing-grid { grid-template-columns: repeat(2,minmax(0,1fr)); } }
@container pricing (max-width:599px) {
  /* Native swipe with both neighbours peeking; no page-level horizontal overflow. */
  .pricing-grid {
    display: flex; gap: var(--sp-3); overflow-x: auto; align-items: stretch;
    scroll-snap-type: x mandatory; overscroll-behavior-x: contain;
    padding: var(--sp-1) var(--sp-5) var(--sp-3); scroll-padding-inline: var(--sp-5);
  }
  .pricing-card {
    --pricing-pad: var(--sp-3); display: flex; flex: 0 0 calc(100% - var(--sp-8));
    gap: var(--sp-3); scroll-snap-align: center; scroll-snap-stop: always;
  }
  .pricing-welcome { display: none; }
  .pricing-card > .pricing-photo { height: 128px; aspect-ratio: auto; flex-shrink: 0; }
  .pricing-page .pricing-name { font-size: var(--fs-h6); margin-top: 0; }
  .pricing-option { padding: var(--sp-2); gap: var(--sp-1); }
  .pricing-period { min-height: 0; }
  .pricing-period small { display: none; }
  .pricing-amount { font-size: var(--fs-h6); }
  .pricing-invoice { min-height: 0; margin-top: var(--sp-1); }
  .pricing-options--free { height: auto; min-height: 0; }
  .pricing-options--free .pricing-option { min-height: 88px; }
  .pricing-billing:has(.pricing-options--free) .pricing-invoice { display: none; }
  .pricing-highlight { min-height: 0; padding: var(--sp-3); gap: 0; }
  .pricing-highlight-heading { align-items: center; row-gap: 0; }
  .pricing-highlight-heading h3 { grid-row: 1 / 3; font-size: var(--fs-body); }
  .pricing-highlight .pricing-kicker, .pricing-highlight > p, .pricing-token-tag { display: none; }
  .pricing-thanks--empty { display: none; }
  .pricing-offer { padding: var(--sp-2) var(--sp-3); }
  .pricing-offer-title { font-size: var(--fs-caption); }
  .pricing-offer-count { display: none; }
  .pricing-popular { top: var(--sp-8); }
}
@media(max-width:900px) {
  .pricing-layout > .pn-identity { padding: var(--sp-4) 0 var(--sp-5); }
}
@media(prefers-reduced-motion:reduce) { .pricing-card, .pricing-select { transition: none; } }
@media(forced-colors:active) {
  .pricing-option.is-active, .pricing-select[aria-pressed=true] { outline: 2px solid Highlight; outline-offset: -3px; }
  .pricing-card.is-selected { border-color: Highlight; }
}

/* Creator extends the membership palette with the existing success accent. */
.pricing-card--creator {
  --tier: var(--success); --tier-line: var(--success); --tier-solid: var(--membership-action);
  --tier-ribbon: linear-gradient(115deg,var(--membership-action-strong),var(--membership-action));
  border-color: color-mix(in srgb,var(--success) 65%,var(--border-default));
}
.pricing-creator-art {
  height: 100%; display: grid; place-items: center;
  color: var(--text-primary);
  background: radial-gradient(ellipse at 75% 25%,color-mix(in srgb,var(--success) 40%,transparent),transparent 65%),linear-gradient(135deg,var(--bg-elevated),var(--bg-surface));
}
.pricing-creator-art .pricing-icon { width: 88px; height: 88px; stroke-width: 1; }

:root {
  --bg: #ffffff; --card: #f6f7f9; --line: #e3e6ec;
  --text: #14161c; --muted: #5b6270; --faint: #8b91a0;
  --accent: #b8860b; --link: #1a5fb4;
  /* the hero's height at the OLD 760px measure — see .hero img */
  --hero-h: 423px;
  --ok: #1d7a4f; --bad: #b3261e; --warn: #9a6b00;
  /* THE DECK (owner 2026-08-14) — the two values that must flip with the mode.
     --ghost-face is how much of a player's portrait survives as the card's
     ground (a photo needs to sink FURTHER on white than it does on ink to read
     as a hint rather than a picture); --deck-shadow is what separates one
     fanned card from the one it overlaps. */
  --ghost-face: .30; --deck-shadow: rgba(20,22,28,.30);
  /* THE DISPLAY FACE (owner ask 2026-08-05 — port the workshop's PRODUCTION
     PREVIEW look onto the public page). A SYSTEM serif on purpose: it costs
     zero network requests, so the baked page keeps its no-third-party-request
     floor (20.0320 §5.4) while gaining the editorial register. Not a theme
     token — it is the same face in light and dark. */
  --serif: Georgia, 'Times New Roman', 'Droid Serif', serif;
}
/* dark values apply from the OS preference — UNLESS the header's mode toggle
   set an explicit data-mode (the attribute wins, mirroring the design chrome) */
@media (prefers-color-scheme: dark) {
  :root:not([data-mode="light"]) {
    --bg: #0b0e17; --card: #131727; --line: #232a40;
    --text: #e8eaf3; --muted: #9aa1b5; --faint: #6a7186;
    --accent: #d9a53f; --link: #7fa9e8;
    --ok: #53b184; --bad: #e06070; --warn: #d9a53f;
    --ghost-face: .42; --deck-shadow: rgba(0,0,0,.55);
  }
}
:root[data-mode="dark"] {
  --bg: #0b0e17; --card: #131727; --line: #232a40;
  --text: #e8eaf3; --muted: #9aa1b5; --faint: #6a7186;
  --accent: #d9a53f; --link: #7fa9e8;
  --ok: #53b184; --bad: #e06070; --warn: #d9a53f;
  --ghost-face: .42; --deck-shadow: rgba(0,0,0,.55);
}
* { box-sizing: border-box; }
/* ⚖️ THE FULL-BLEED GUARD, COMPLETED (2026-08-14) — MEASURED, not assumed.
   Two rails on this site break out of the 760px column with the classic
   width:100vw + margin-left:calc(50% - 50vw) idiom: the home page's category
   poster rail (categoryRailCss.ts) and the article's cast deck. 100vw counts
   the VERTICAL SCROLLBAR, so on a classic-scrollbar platform a bleeding rail is
   ~15px wider than the layout viewport and the PAGE gains a sideways scroll.
   categoryRailCss guards this with overflow-x:clip on BODY — which is not
   enough on its own: measured at 1600x900, the page still scrolled 8px
   sideways. Clipping at the ROOT is what actually closes it (8px -> 0).
   CLIP, never HIDDEN: clip does not create a scroll container, so it is legal
   beside a visible overflow-y and leaves sticky positioning and normal vertical
   scrolling untouched — hidden would silently make the page a scroll box. */
html { -webkit-text-size-adjust: 100%; overflow-x: clip; }
/* THE PAGE BACKGROUND (owner spec 2026-07-24 — DS handoff card
   components/handoff/eu-header-background.html, snapshot
   web/design/imports/2026-07-24-eu-header-background/): the baked page wears
   the SPA's EU treatment — the whole spec is TWO RULES. body fills with the
   theme's --bg-base (chrome token, flips with data-mode/OS scheme) and a
   fixed ::before paints --grad-noise (star ring anchored to the viewport
   top · halo · wash — reads as a crown behind the masthead, never scrolls).
   The header's translucency depends on body painting this — never move it
   onto a wrapper. */
body {
  margin: 0; background: var(--bg-base); color: var(--text);
  font-family: -apple-system, 'Segoe UI', 'Helvetica Neue', system-ui, Roboto, sans-serif;
  line-height: 1.6; font-size: 17px;
}
body::before {
  content: ''; position: fixed; inset: 0;
  background: var(--grad-noise);
  pointer-events: none; z-index: 0;
}
/* content sits above the wash layer (the spec's stacking rule) */
.shell, .site-footer { position: relative; z-index: 1; }
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; height: auto; display: block; }
/* ⚠️ THE SIDE PADDING IS A NAMED NUMBER because something else has to be able
   to give it back. Below 560px the chronicle's chapters bleed LEFT by exactly
   this much, and a bleed written as a second -18px is how the two drift the
   first time the page's margin is touched. The docked block at 1180px only
   re-shapes max-width and the grid, so this stays true at every width. */
.shell { max-width: 760px; margin: 0 auto;
  --nrb-page-pad: 18px;
  padding: 0 var(--nrb-page-pad) 64px; }
/* header chrome comes from the design system (.nrb-header — see designChrome.ts) */
/* (The 2026-07-24 mobile re-show for .nrb-lang is gone with the dropdown
   itself — the switcher is now the .nrb-langseg segmented control, which the
   SPA's 560px display-none rule never matched. The chrome
   tightens the segment padding on small screens instead of hiding it: on a
   baked page this IS the only language switch.) */
.hero { margin: 22px 0 6px; position: relative; }
/* ══ THE HERO WIDENS, IT DOES NOT GROW (owner 2026-08-19) ══════════════════
   With the story track raised to 1200px a 16:9 hero followed it to 675px
   tall — a picture that pushes the headline below the fold to say nothing new.
   The owner's rule: take the extra width by CROPPING, not by growing.

   aspect-ratio SETS the shape, max-height CAPS it, object-fit CROPS the
   difference. Below the cap the picture is a plain 16:9 and nothing is lost;
   above it the box goes on widening at a fixed height and the image is scaled
   to cover, so the top and bottom edges — the parts a news illustration puts
   least in — are what leaves.

   --hero-h is the height the hero HAD at the old 760px measure: 760 minus the
   status frame's 4px padding each side is 752, and 752 x 9/16 = 423. So the
   picture is exactly the size it always was on every width that used to
   exist, and only a column WIDER than the old maximum crops anything at all.

   object-position is the one knob here. 45% biases the keep-zone slightly
   above centre, where an illustration's subject usually sits; if a crop ever
   cuts a face, this is the line to change, not the height. */
.hero img {
  border-radius: 10px; width: 100%;
  aspect-ratio: 16 / 9; height: auto; max-height: var(--hero-h);
  object-fit: cover; object-position: 50% 45%;
}
/* The chapter art has the SAME problem and six chances to show it — the
   chronicle's images sit in the same column and were growing with it. Same
   rule, shorter cap: a chapter's picture is evidence for the paragraph beside
   it, not the story's front door. */
.move-art img {
  width: 100%; aspect-ratio: 16 / 9; height: auto;
  max-height: calc(var(--hero-h) * 0.72);
  object-fit: cover; object-position: 50% 45%;
}
/* THE STORY STATUS FRAME (design import 2026-07-24-cluster-a11y-proposals —
   the a11y floor: status = words + frame, colour is reinforcement only).
   Every story is DEVELOPING or COMPLETE: the frame wraps the hero <picture>,
   the localized verdict chip straddles its top edge as a solid plate (text
   never sits bare on the photo), and a developing story gets the footer bar
   with the next-research countdown below the image. */
.hero.status-developing picture, .hero.status-complete picture {
  display: block; }
/* ══ THE FRAME CLOSES AROUND THE WHOLE THING (design 2026-08-23) ═══════════
   The gold frame used to wrap the PICTURE alone, and the status row sat
   outside it with a 10px gutter — image and status read as two objects that
   happened to be stacked. The frame moves up onto the FIGURE, so it encloses
   the photo and the status row as ONE composition: no gutter, one 16px outer
   radius, and the row butting straight onto the picture with a shared 10px
   inner corner.

   ⚑ The 4px padding moves with it, which is what keeps --hero-h honest —
   the picture still loses exactly 8px of width to the frame, so the cap
   derived from the old 760px measure is unchanged. */
/* ══ THE FRAME LIGHTS IN THE STORY'S OWN COLOUR (owner 2026-08-24) ═════════
   The sweeping gold frame was the developing story's alone; the owner wanted
   the same treatment carrying the VERDICT — green for confirmed, red for
   debunked. So the sheen stops being a property of "developing" and becomes a
   property of having something definite to say:

     developing → --warn/--accent (unchanged)   confirmed → --ok
     debunked   → --bad                          disputed · unclear → NO sheen

   ⚑ DISPUTED AND UNCLEAR KEEP THE CALM RAIL. 24 and 4 live stories, and
   neither is a verdict — lighting them would say the case is closed when the
   whole point of those two words is that it is not. The frame lights only
   where the page has a position; everything else stays the quiet double rail.

   The colours ride ONE gradient through --frame-a/--frame-b, so a new lit
   state is two custom properties and never another copy of this rule. */
.hero.hero--lit {
  padding: 4px; border-radius: 16px;
  background: linear-gradient(115deg, var(--frame-a), var(--frame-b) 30%, var(--card) 50%, var(--frame-b) 70%, var(--frame-a));
  background-size: 240% 240%;
  animation: nrb-frame-sheen 8s ease-in-out infinite;
  box-shadow: 0 0 26px -10px var(--frame-b); }
.hero.lit-developing { --frame-a: var(--warn); --frame-b: var(--accent); }
.hero.lit-confirmed  { --frame-a: var(--ok);   --frame-b: var(--ok); }
.hero.lit-debunked   { --frame-a: var(--bad);  --frame-b: var(--bad); }
/* ⚑ THE CORNERS FOLLOW THE BAR, NOT THE STATUS. The picture opens its bottom
   corners only when something is there to close them; with the bar off the
   hero (ArticleView's DEVELOPING_BAR_ON_HERO) the frame closes on the image
   alone and all four corners round. Written as a :has() so the two states
   cannot drift: flipping that constant restores the flush seam with no CSS
   edit, and a square-bottomed picture with nothing beneath it is impossible to
   ship by accident. */
.hero.hero--lit img { border-radius: 10px; }
.hero.hero--lit:has(.hero-status-bar) img { border-radius: 10px 10px 0 0; }
.hero.hero--lit .hero-status-bar {
  margin: 0; border-top: 0; border-radius: 0 0 10px 10px; }
/* ⚠️ THE COMPLETE RAIL STAYS ON THE <picture>, NOT ON THE IMAGE. The design
   card draws its rail on a <div> with a background-image, where an inset
   shadow paints over the picture. Production's hero is a real <img>, whose
   bitmap is CONTENT and paints above any inset shadow of its own box — moved
   onto the image the rail would simply vanish. The wrapper is the only element
   in this subtree that has a background for the rail to sit on.
   ⚖️ A CARD BUILT ON A DIV CANNOT SPECIFY A RULE THAT ONLY WORKS ON A DIV. */
.hero.status-complete:not(.hero--lit) picture {
  padding: 4px; border-radius: 16px;
  background: var(--card);
  box-shadow: inset 0 0 0 1px var(--line), inset 0 0 0 3px var(--card), inset 0 0 0 4px var(--line); }
@keyframes nrb-frame-sheen { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
.hero-status-chip { position: absolute; top: -11px; left: 18px; z-index: 1;
  background: var(--bg); color: var(--muted); border: 1px solid var(--line); border-radius: 999px;
  padding: 4px 12px; font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
/* ══ THE PLATE NAMES THE CATEGORY (design + owner 2026-08-23) ══════════════
   It used to carry the verdict word, which made it the third thing on the
   page saying the same thing: the frame says developing, the footer bar says
   it in words, and the plate said it again. The plate now names the SECTION
   the story belongs to — and the kicker below the hero drops the category it
   was printing, so it is said once, on the frame, where the design puts it.
   The verdict of a CLOSED story moved to the stamp below. */
.hero-status-chip.category { color: var(--accent); border-color: var(--line); }
.hero-status-chip.debunked { color: var(--bad); border-color: var(--bad); }
.hero-status-chip.confirmed { color: var(--ok); border-color: var(--ok); }
.hero-status-chip.disputed, .hero-status-chip.developing { color: var(--warn); border-color: var(--warn); }
/* ══ THE VERDICT STAMP (design 2026-08-23) ═════════════════════════════════
   A closed story gets its verdict as an ink stamp in the hero's upper-right:
   a rectangular double rule around the verdict word alone, tilted, at half
   ink. It is DECORATION over an editorial fact that the page states in words
   elsewhere, so it takes no pointer and reads as text to a screen reader.

   ⚑ FOUR VERDICTS ARE LIVE, NOT TWO. The design colours "confirmed" and
   "debunked"; production also serves "disputed" (24 stories) and "unclear"
   (4). They take the tones those same words already wear on the chip — an
   uncoloured stamp would inherit whatever it landed on. */
.hero-status-stamp {
  position: absolute; top: 44px; right: 30px; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 8px 16px; border: 2px solid currentColor; border-radius: 8px;
  box-shadow: inset 0 0 0 2px transparent, inset 0 0 0 2px currentColor;
  font-family: var(--font-display);
  font-size: 17px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  white-space: nowrap; transform: rotate(22deg); pointer-events: none;
  opacity: .5; filter: drop-shadow(0 2px 10px rgba(0,0,0,.55)); }
.hero-status-stamp.confirmed { color: var(--ok); }
.hero-status-stamp.debunked { color: var(--bad); }
.hero-status-stamp.disputed { color: var(--warn); }
.hero-status-stamp.unclear { color: var(--muted); }
/* ══ THE ART CREDIT ════════════════════════════════════════════════════════
   Every hero here is drawn, so the credit is a constant — and it earns its
   place by being the one line on the page that says so. Icon LEADS the words
   (see aiGeneratedLabel): a phrase split around an icon cannot be
   translated without freezing English word order into five other languages. */
/* ⚑ ON THE PICTURE, NOT UNDER IT (owner 2026-08-23: "move this text over the
   image itself, i need to save space"). A closed story had grown a whole strip
   of vertical space to hold one small label; on the photograph the credit costs
   no row at all, sits with the thing it describes, and gives the closed hero
   back the design's own shape — frame, stamp, nothing underneath.

   The .hero-shot wrapper is the anchor and has to be: the FIGURE's bottom edge is the
   footer bar on a developing story, so a credit measured from there would float
   below the picture rather than on it.

   ⚖️ NEVER BARE TEXT ON A PHOTOGRAPH — the same rule the status chip follows.
   The art beneath is arbitrary and cannot be relied on for contrast, so the
   credit rides a solid dark plate with its own hairline. Those two colours are
   deliberately NOT tokens: they answer to the picture underneath, which is the
   same picture in both editions, and a plate that flipped with the page theme
   would be legible in one mode and gone in the other. */
.hero-shot { position: relative; display: block; }
.hero-status-ai { position: absolute; right: 12px; bottom: 12px; z-index: 1;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px 5px 9px; border-radius: 999px;
  background: rgba(6, 10, 24, .62); border: 1px solid rgba(255, 255, 255, .16);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  font-size: 11px; font-weight: 600; letter-spacing: .04em; color: #dfe4f2;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .5); }
.hero-status-ai svg { width: 14px; height: 14px; flex: none; fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; opacity: .85; }
/* ⚖️ THE PHONE DOES NOT GET THE SHORT VERSION. The first cut collapsed this to
   the mark alone below 560px — the words at font-size 0, the label left for
   screen readers. That is the same fault as tying the credit to the verdict,
   one platform down: most of these readers are on a phone, so hiding the words
   there hides them from nearly everyone who reads the page. The plate gets
   tighter instead — 10px, less padding, closer to the corner — and keeps
   saying what it is. */
@media (max-width: 560px) {
  .hero-status-ai { right: 7px; bottom: 7px; padding: 4px 8px 4px 7px; gap: 5px;
    font-size: 10px; letter-spacing: .02em; max-width: calc(100% - 14px); }
  .hero-status-ai svg { width: 12px; height: 12px; }
}
.hero-status-bar { display: flex; align-items: center; justify-content: space-between;
  gap: 8px 18px; flex-wrap: wrap; margin: 10px 2px 0; padding: 9px 14px;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  font-size: 14px; color: var(--muted); }
.hero-status-lead { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text); }
.hero-status-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--warn);
  box-shadow: 0 0 0 0 var(--warn); animation: nrb-dot-pulse 2.4s ease-out infinite; }
@keyframes nrb-dot-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--warn) 55%, transparent); }
  70% { box-shadow: 0 0 0 8px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
.hero-status-next { color: var(--faint); }
.hero-status-count { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--accent); }

/* The developing status while its bar is off the hero (ArticleView's
   DEVELOPING_BAR_ON_HERO): out of the picture, still in the page. */
.hero-status-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
}
.hero-status-bare { margin: 14px 0; display: flex; flex-direction: column; gap: 10px; }
/* ── 🗞 THE SHORT STORY PLATE (20.1860) ────────────────────────────────────
   The category plate's mirror: same box, same straddle of the frame's top
   edge, opposite corner. Together they read as one line of provenance — what
   section, and how long — rather than as a chip and a sticker.
   ⚑ It clears the verdict stamp, which is also upper-right but 44px down and
   rotated: the plate spans -11..15px, so the corner holds both.
   ⚑ NOT A BUTTON YET. It is the free end of the click-to-research path, and
   until that endpoint exists it must not look pressable — a control nobody can
   operate is the one thing this site refuses to bake. */
.hero-status-short { position: absolute; top: -11px; right: 18px; z-index: 1;
  background: var(--bg); color: var(--muted); border: 1px solid var(--line); border-radius: 999px;
  padding: 4px 12px; font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
.hero-status-bare .hero-status-short { position: static; align-self: flex-start; }
.hero-status-bare .hero-status-chip { position: static; align-self: flex-start; }
@media (prefers-reduced-motion: reduce) {
  .hero.hero--lit { animation: none; }
  .hero-status-dot { animation: none; }
}
/* ── THE EDITORIAL REGISTER ──────────────────────────────────────────────────
   Ported 2026-08-05 from the workshop's PRODUCTION PREVIEW — the operator's
   looking glass (workshopAdmin/pages/previewPage.ts → articleCss), which had
   quietly been the better-reading surface: serif display type, the section
   label as a small spaced accent EYEBROW rather than a second headline, and a
   dateline closed by a hairline. Only the TYPOGRAPHY crosses over; colour
   tokens, the EU page background and the hero status frame stay the baked
   site's own (they are separately owner-ratified specs). */
/* ══ THE MEASURE — ONE COLUMN EDGE, NOT ONE PER ELEMENT ═════════════════════
   (owner 2026-08-19: "the title needs to follow the resolution not to break …
   more things are not following the resolution")

   ⚖️ ch WAS THE WRONG UNIT HERE, and this is the correction. A ch is the
   element's OWN zero-width, so one shared max-width: 68ch resolved to a
   different physical width in every block that carried it. Measured in a
   1188px column: h1 477 · h2 435 · .meta 473 · .standing 567 · p 643 — SEVEN
   widths down one page, a right edge that stepped in and out at every heading,
   and a headline broken to four short lines beside 700px of empty column.

   ch is right for a READING MEASURE on one element. It is wrong the moment
   several elements have to agree where the column ends, because agreeing is a
   question about PHYSICAL width and ch answers a different one.

   So: one token, one edge. Everything in the article's flow takes --measure;
   the HERO opts out and keeps the full track, which is what gives the page its
   two honest widths — the picture and the column — instead of seven. */
article { --measure: 100%; }
/* ⚖️ THE THREAD HAS ONE X, AND EVERYTHING THAT DRAWS ON IT READS THIS. The
   chapter spine, the numbered ring's centre and the standing stop's border all
   stand here. Spelled once per user they drift — measured: a mobile override
   moved the standing's border to 12px while the chapters' spine stayed at 18,
   and the bead came to rest BESIDE the line instead of ending in it.
   ⚠️ Its own rule, deliberately: the line above is pinned character-for-character
   by shellGeometry.test.ts, and a shared token has no business making a
   neighbouring law's test rewrite itself. */
article { --thread-axis: 18px; }
/* ⚖️ THE WRAPPER IS THE ONLY WIDTH (owner 2026-08-20: "the max width of content
   wrapper and the content inside is full width is 860px"). --measure survives
   as ONE token so the blocks still agree on one edge, but that edge is now the
   wrapper's own — the story track, capped at 860 in topicTimelineCss. Every
   previous version of this line held a SECOND number (68ch, then 660, then
   min(100%, 760px)) inside a wrapper of a different width, and the difference
   between the two was the empty strip down the right of the column.

   The hero needs no exemption any more: at 100% it is already the full track,
   which is what the escape existed to give it. Kept as an explicit declaration
   because it is load-bearing the moment --measure is ever capped again.

   ⚠️ THE HYDRATION ROOT IS STILL NOT THE COLUMN. renderView wraps every baked
   article in a single [data-nrb-view] div; a bare child rule caps THAT ONE
   ELEMENT, which is how the page came out 660 wide by CONTAINMENT rather than
   by agreement, and why the old hero escape matched nothing at all (the hero
   is a grandchild). The root is excluded and the flow's own children carry
   the measure. */
article > *:not([data-nrb-view]),
article > [data-nrb-view] > *,
article section.move { max-width: var(--measure); }
article figure.hero { max-width: none; }

/* ══ THE HEADLINE IS DISPLAY TYPE (owner 2026-08-24) ═══════════════════════
   Lifted from the design system's own .article-hero-meta h1 rather than
   approximated: Space Grotesk through --font-display at 700, the same clamp,
   the same tracking and leading. text-wrap: balance comes with it — a
   headline this large rags badly at three words on the last line, and it is
   part of the rule being adopted, not an addition to it.

   ⚑ The --font-display token is DESIGN_CHROME_CSS's and Space Grotesk is already
   in its @import, so this costs no new font request. */
h1 { font-family: var(--font-display); font-weight: 700;
  /* ⚑ 42px, not the design's 56 (owner 2026-08-24). The ceiling is what moved,
     not the shape: the clamp still steps the headline down on a narrow column,
     it just stops climbing at 42. 4vw reaches 42 at a 1050px viewport, so every
     desktop width lands exactly on the number asked for. */
  font-size: clamp(32px, 4vw, 42px);
  letter-spacing: -0.02em; line-height: 1.05;
  margin: 20px 0 10px; text-wrap: balance; }
/* Section labels. EVERY bare h2 on a baked page is a label — the chronicle,
   the cast, the sources, the saga halves, "Latest" on a home, a profile's
   blocks — so the register is set here once. The one h2 that is a TITLE (a
   move's, inside .move) overrides back to the serif further down. */
h2 { font-size: 11.5px; letter-spacing: .22em; font-weight: 700; text-transform: uppercase;
  color: var(--accent); margin: 30px 0 10px; }
/* 🏷 THE SECTION EYEBROW (design handoff 2026-09-01 §1). The type was already
   this — mono, 11.5px, 700, .22em, uppercase from CSS, accent — and only the
   SPACING is stated here.

   📐 2026-09-11 — THE HEAD OWNS THE GAP TO THE BLOCK IT OPENS, AND THE GAP TO
   WHAT CAME BEFORE (owner: "the last posts offset from the next post is too
   much, make it same as the heroslider's header … increase the offset between
   the heroslider and the Latest stories x2").

   ⚑ 8px BELOW IS THE HERO KICKER'S OWN NUMBER, quoted deliberately: the two
   heads now open the same column and a reader scrolling past them should not
   be able to tell that one is a component and the other a bare h2.

   ⚠️ AND 8px ONLY ARRIVES IF THE NEXT BLOCK STOPS ARGUING. The gap was 24px
   because .cards carries margin: 24px 0 and the LARGER of two adjacent margins
   wins the collapse — the head's own 10px never decided anything. Hence the
   rule below; without it, editing this number does nothing at all.

   ⚑ 28px ABOVE IS THE OLD 14 DOUBLED. The 14 was an ALIGNMENT, not a rhythm —
   it put the first section head's bottom on the nav column's return line — and
   that datum moved on 2026-09-11 when the hero rail grew a kicker above itself
   and became the first line in this column. The number is free to be spacing
   again.

   ⚠️ SCOPED TO A DIRECT CHILD, AND THAT IS THE WHOLE CARE HERE. A wall's and a
   home page's section heads ARE direct children of .shell (both views return
   React Fragments, which emit no wrapper); an ARTICLE's headings — АКЦЕНТИ, КАК
   СЕ РАЗВИ, ИЗТОЧНИЦИ — sit inside <article>, where 30px of air between
   sections is the point. A descendant selector would have tightened every
   story on the site to fix an alignment on two page types.

   ⚑ THE SOURCE STRING STAYS SENTENCE CASE — the uppercase is CSS's, so a
   locale whose casing differs from a naive toUpperCase is never mangled. */
.shell > h2 { margin: 28px 0 8px; }
/* ⚖️ THE STORY GRID DOES NOT SET THE HEAD'S GAP. Scoped to a grid that FOLLOWS
   a section head, so .cards keeps its 24px everywhere it stands alone — and
   .catwall-stories, which sets its own 4px, loses here too, so a wall and a
   home page open their story lists at the same distance. */
.shell > h2 + .cards { margin-top: 0; }
.dek { font-size: 17px; color: var(--muted); font-style: italic; margin: 0 0 10px; }
.meta { font-size: 12.5px; color: var(--faint); display: flex; gap: 14px; flex-wrap: wrap; }
/* THE DATELINE — the meta row that follows the dek (published/updated) closes
   the head of the article with a rule. General sibling, not adjacent: the move
   sub-page slips its .range between the two, and the kicker/category .meta row
   sits ABOVE the dek, so this matches the dateline and only the dateline. */
.dek ~ .meta { border-bottom: 1px solid var(--line); padding-bottom: 14px; margin-bottom: 6px; }
.meta .cat { color: var(--accent); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; font-size: 12px; }
.disclosure { font-size: 13px; color: var(--faint); border-left: 3px solid var(--line); padding: 4px 10px; margin: 14px 0; }
/* (the .verdict badge + .verdict-note block was retired 2026-07-24 — status
   moved onto the hero frame above) */
.players { list-style: none; padding: 0; margin: 8px 0; }
.players li { padding: 8px 0; border-bottom: 1px solid var(--line); font-size: 15.5px; }
.players b { color: var(--text); }
/* (.goal is gone — owner 2026-08-14: a reader's card answers WHO and WHAT ROLE,
   and the third "wants →" line was what left the other two no room. The FIELD
   survives everywhere it means something: the doc, the state payload, the
   prompt composers, the operator's own board.) */
/* PROFILE PAGES (20.0760) — a linked actor in the PLAYERS block. */
.players a.playerlink { color: inherit; text-decoration: none; border-bottom: 1px solid var(--line); }
.players a.playerlink:hover { border-bottom-color: var(--accent); }
/* ── THE CAST IS A HAND OF CARDS (owner 2026-08-14) ──────────────────────────
   Ported wholesale from the workshop Focus board's DECK (the .as-deck .player
   rules in workshopAdmin/pages/workshopFocusPage.ts, owner 2026-07-30) — the
   operator and the reader now look at the SAME object. The cast is dealt as
   portrait 5:7 cards in one row, each overlapping its neighbour (by a TENTH
   here — see the deck arithmetic below for why the reader's hand is fanned
   wider than the operator's), the way you would hold them; the type is
   captioned at the BASE (a 5:7 portrait
   with a line of text at the top is a tall box, not a card); hover — or
   keyboard focus — lifts one clear of the fan to read it.

   THE PORTRAIT IS THE CARD'S GROUND, NOT A THUMBNAIL (owner 2026-08-14): the
   face fills the whole card behind the type and is masked to a GHOST — a hint
   of it along the top edge, fully gone by the middle — so the name and role
   never sit on a photograph and need no scrim to stay legible. Cards without a
   portrait get the monogram as that ghost instead (20.0760 §4a), so a
   half-carded cast still reads as one hand rather than a broken grid.

   The 20.0950 tier stays an ACCENT and never a word — and the fan makes the
   LEFT EDGE the one strip always visible, which is exactly where that accent
   already lived. The public keeps "no badge, nothing to translate".
   (Retired here: the 20.1020 grid of 250px landscape cards with a 52px face.) */
/* FULL-BLEED, CENTRED WHEN IT FITS (owner 2026-08-14) — the same treatment the
   home page's CATEGORY POSTER RAIL wears (20.1360, categoryRailCss.ts), reused
   verbatim rather than reinvented, so the two rails on this site behave alike:
   the row breaks out of the 760px column with the classic centred-100vw margin
   and takes the whole viewport, which is what buys a wide screen enough room to
   lay the whole hand out instead of handing the reader a scrollbar.
   The overflow-x:clip rule on body — already set by categoryRailCss, which
   every baked page loads — is what stops the 100vw breakout from giving the
   PAGE a sideways scrollbar on classic-scrollbar platforms. */
.players.playersrail { display: flex; flex-wrap: nowrap; align-items: flex-start; gap: 0;
  overflow-x: auto; width: 100vw; margin: 8px 0 0 calc(50% - 50vw);
  padding: 22px 0 18px; scrollbar-width: thin;
  /* The hand's arithmetic, stated ONCE — the card, and how much of it the next
     card covers. A TENTH (owner 2026-08-14, halved from the fifth that halved
     the workshop's third): the operator's card carries a name and a rank, a
     reader's carries a name, a role AND a goal, so every step away from the
     workshop's ratio bought back a piece of the sentence. 33 / 2 = 16.5, taken
     as 17 — the cards still shingle, but only the last word or so goes under.
     Everything below derives from these two. */
  --deck-card: 164px; --deck-lap: 17px; --deck-pad: 12px; }
/* CENTRED WHEN IT FITS, SCROLLABLE WHEN IT DOES NOT — the category rail's own
   trick: the edge pseudo-items carry auto margins, so free space splits equally
   around the hand; the moment the row overflows, the auto margins resolve to 0
   and the rail scrolls from its TRUE start. (A bare justify-content:center
   would strand the left-hand cards permanently unreachable.) Their flex-basis
   doubles as the edge gutter — and as the horizontal room a lifted card's
   scale needs at either end of the row. */
.players.playersrail::before, .players.playersrail::after { content: ''; flex: 0 0 18px; }
.players.playersrail::before { margin-left: auto; }
.players.playersrail::after { margin-right: auto; }
/* border-box so the arithmetic stays honest; aspect-ratio 5:7 makes it a
   portrait playing card. */
.players.playersrail li.playercard { position: relative; box-sizing: border-box; margin: 0;
  flex: 0 0 var(--deck-card); width: var(--deck-card); aspect-ratio: 5 / 7; overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 11px var(--deck-pad); border: 1px solid var(--line); border-left-width: 3px;
  border-radius: 10px;
  background: linear-gradient(160deg, var(--card) 0%, var(--bg) 62%);
  box-shadow: -10px 0 16px -5px var(--deck-shadow);
  transition: transform .18s ease;
  /* ██ A CARD COVERS THE CARD BEHIND IT WHOLE ██ (bug, owner 2026-08-14)
     The card is position:relative with z-index AUTO, so it never opened a
     stacking context of its own — which meant the caption inside it
     (.pbody, z-index: 1) was not layered against its OWN card but against the
     ROOT, above every card background in the rail. Six captions therefore
     floated over their neighbours and the fan showed all six at once, in one
     illegible overlap, while the card art underneath was correctly covered.
     isolate scopes each card's internals to the card, so painting falls back
     to DOM order and a card hides its neighbour's TEXT exactly as it already
     hid its neighbour's portrait. */
  isolation: isolate; }
.players.playersrail li.playercard + li.playercard { margin-left: calc(var(--deck-lap) * -1); }
/* The lift is also a SIZE change (owner 2026-08-14): a card you have singled
   out comes forward out of the hand, so it grows as it rises.
   ⚖️ THE RAIL MUST HAVE ROOM FOR THE GROWTH. overflow-x: auto forces overflow-y
   to auto too, so a scaled card is CLIPPED by the rail unless the padding pays
   for it. Scaling from the centre costs (--deck-card x .06)/2 = 4.9px on each
   side and (height x .06)/2 + 13px of lift = 20.1px on top. The 18px edge
   pseudo-items pay for the sides; 22px of padding-top pays for the top. Change
   the scale, the lift or --deck-card and you must re-do this arithmetic — the
   failure is silent, a quietly sheared card edge. */
.players.playersrail li.playercard:hover,
.players.playersrail li.playercard:focus-within { transform: translateY(-13px) scale(1.06); z-index: 5;
  box-shadow: -12px 6px 26px -6px var(--deck-shadow); }
/* ██ ONE CAPTION, END TO END, IN BOTH STATES ██ (owner ruling 2026-08-14)
   The caption is laid to the card's full width and NEVER re-laid — overlapped,
   lifted or last in the row, a card sets the same type in the same place. The
   neighbour simply covers part of it, and that is allowed: "no need to show all
   as visible" — a hand of cards is a thing you fan, and a fanned card showing
   part of its face is the whole idiom.
   The rejected alternative was fitting the type to the visible strip and
   widening it on lift. It kept every word on screen and cost the thing that
   matters more: the card CHANGED when you touched it, so the reader was reading
   a different card than the one they pointed at. Stability beats completeness
   here — the lift is there to reveal the card, never to re-set its words. */
/* TOUCH — there is no hover to lift a card with, so the hand is DEALT OUT: the
   same cards, no overlap, swipe to scroll. A fan you cannot peel apart is not a
   deck, it is a stack of hidden names. The caption needs no rule here — it was
   already laid end to end, so dealing the cards out simply uncovers it. */
@media (hover: none), (pointer: coarse) {
  .players.playersrail { gap: 10px; }
  .players.playersrail li.playercard + li.playercard { margin-left: 0; }
}
.playercard.p-main { border-left-color: var(--accent); }
.playercard.p-supporting { border-left-color: var(--muted); }
/* The ground layer. A filling inset + the mask = the ghost; pointer-events off
   so the name underneath it stays clickable.
   (No backticks anywhere below this point — the whole sheet is ONE template
   literal, and a backtick in a comment closes it.) */
.playercard .pface { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,1) 0%, rgba(0,0,0,.42) 24%, rgba(0,0,0,0) 50%);
  mask-image: linear-gradient(180deg, rgba(0,0,0,1) 0%, rgba(0,0,0,.42) 24%, rgba(0,0,0,0) 50%); }
.playercard .pface picture { display: block; width: 100%; height: 100%; }
/* object-position rides HIGH: only the top of the portrait survives the mask,
   and that is where a face is. */
.playercard .pface img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 20%;
  display: block; opacity: var(--ghost-face); }
.playercard .pface .mono { display: flex; align-items: flex-start; justify-content: center; height: 100%;
  padding-top: 5%; font-size: 82px; line-height: 1; color: var(--line); }
.playercard .pbody { position: relative; z-index: 1; min-width: 0;
  display: flex; flex-direction: column; gap: 3px; }
.playercard .pname { font-size: 12.5px; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
/* The card is small and the text is the caption — role and goal are CLAMPED
   rather than clipped, so a long role ends on a whole line instead of a
   half-eaten one at the top of the box. */
.playercard .prole { font-size: 11.5px; line-height: 1.35; color: var(--muted);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.playercard .pstat { align-self: flex-start; margin: 3px 0 0; padding: 0 8px; border: 1px solid var(--line);
  border-radius: 999px; font-size: 10.5px; line-height: 1.75; color: var(--muted); white-space: nowrap; }
/* ══ WHO'S WHO — THE CAST IN THE STORY COLUMN ═══════════════════════════════
   (owner 2026-08-20: "players the way we present them in the admin panel …
   in the middle column just above the State of play")

   ██ THE SIDE-COLUMN LIFT IS RETIRED ██ The 2026-08-19 ruling drew the cast
   as an absolutely-positioned column in the mirror track; the owner reversed
   it the next day — the cast reads IN FLOW, in the story column, exactly
   where it sits in the DOM (after the story, before where-things-stand),
   at every width. Nothing here positions it anymore: the block that lifted
   it (position:absolute + the --nrb-tl-tracks offset + the floating h2)
   is deleted, not disabled — below the docking width this was already the
   rendering, so narrow screens are byte-identical.

   The card CONTENT gains admin parity in THE PLACE-OF-USE ADJUSTMENTS below:
   the tier word and the wants→goal line the workshop deck shows. */
.cast { margin: 18px 0 0; }

/* ══ THE CAST CARD (design 2a + 2b) ════════════════════════════════════════
   COPIED VERBATIM from the design project's shared/cast-cards.css, whose own
   header names this file as the component's production home and requires the
   two copies stay identical:

     "The cast card's production home is code/sitePublisher/siteCss.ts (profile
      pages are zero-JS baked pages) ... This block is copied VERBATIM into
      siteCss.ts; keep it token-only so both copies stay identical."

   ⚠️ TOKEN-ONLY IS THE CONSTRAINT, not a style. This layer deliberately does
   NOT build on the .a-* atoms: those live in the SPA sheet and a baked nrb.bg
   page loads neither, so a card written against them looks right in the design
   preview and renders as unstyled markup on the live site. Shared tokens DO
   reach baked pages, via DESIGN_CHROME_CSS. Edit the design file first.

   ⚠️ 2a IS HERE BUT UNUSED TODAY. The article's who's-who renders 2b, and 2b's
   rules are overrides ON 2a — the art column, the body, the type, the name and
   the count all re-aim rules defined above them. Keeping only 2b would mean
   hand-flattening the cascade into a copy that is no longer verbatim, which is
   the one thing this block's contract forbids. 2a also ships as-is the moment
   the profile pages adopt it (20.1090 surfaces 2–4).
   Spec: z-docs/20.1090-player-card-component.ds.component.md */

/* ---- 2a cast card ---- */
.castcard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(184px, 1fr));
  gap: 14px;
}
.castcard {
  position: relative; display: block; margin: 0;
  aspect-ratio: 5 / 7; overflow: hidden;
  border-radius: var(--r-lg); border: 1px solid var(--border-default);
  background: var(--bg-elevated);
  transition: border-color .18s ease, transform .18s ease;
}
.castcard:hover { border-color: var(--border-strong); transform: translateY(-2px); }
.castcard-art { position: absolute; inset: 0; display: block; background-size: cover; background-position: center; }
.castcard-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.castcard-mono {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-size: 58px; font-weight: 700; line-height: 1;
  color: var(--text-tertiary); background: var(--bg-elevated);
}
/* THE SCRIM — 2a's stated exception to "text never on photo": every text pixel
   sits on a CONSTANT rgba(0,0,0,.90), so contrast stops depending on the
   picture. Worst case (white art) composites to rgb(26,26,26) = 17.4:1 against
   white; AAA is 7:1. If those numbers drift the exception is void. */
.castcard-body {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column; gap: 5px;
  padding: 12px 13px 13px; background: rgba(0, 0, 0, .90); color: #fff;
}
.castcard-body::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 100%; height: 76px;
  background: linear-gradient(to top, rgba(0, 0, 0, .90), rgba(0, 0, 0, 0));
  pointer-events: none;
}
.castcard-type {
  font-family: var(--font-mono); font-size: var(--fs-overline);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: rgba(255, 255, 255, .82);
}
.castcard-name { margin: 0; font-family: var(--font-display); font-size: 17px; line-height: 1.2; font-weight: 700; }
.castcard-name a { color: #fff; text-decoration: none; }
/* the single real link, expanded to the whole card */
.castcard-name a::after { content: ''; position: absolute; inset: 0; z-index: 1; }
.castcard:hover .castcard-name a { color: var(--accent); }
.castcard-name a:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; border-radius: 3px; }
@supports selector(:has(*)) {
  .castcard-name a:focus-visible { outline: none; }
  .castcard:has(.castcard-name a:focus-visible) { box-shadow: 0 0 0 3px var(--accent); border-color: transparent; }
}
.castcard-ice {
  margin: 0; font-size: var(--fs-caption); line-height: 1.4;
  color: rgba(255, 255, 255, .86);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
}
.castcard-count { font-size: var(--fs-caption); color: rgba(255, 255, 255, .66); }
/* the cohort deals its MEMBERS' cards rather than growing a face of its own */
.castcard-deck { position: absolute; inset: 0; }
.castcard-deckcard {
  position: absolute; top: 7%; left: 50%; width: 60%; margin-left: -30%;
  aspect-ratio: 5 / 7; overflow: hidden;
  border-radius: var(--r-md); border: 1px solid rgba(255, 255, 255, .22);
  background: var(--bg-elevated); transform-origin: 50% 92%;
  filter: drop-shadow(0 8px 18px rgba(0, 0, 0, .45));
  background-size: cover; background-position: center;
}
.castcard-deckcard img { width: 100%; height: 100%; object-fit: cover; display: block; }
.castcard-deckcard .castcard-mono { font-size: 30px; }
.castcard-deckcard:nth-child(1) { transform: rotate(0deg); z-index: 4; }
.castcard-deckcard:nth-child(2) { transform: rotate(-6deg); z-index: 3; }
.castcard-deckcard:nth-child(3) { transform: rotate(6deg); z-index: 2; }
.castcard-deckcard:nth-child(4) { transform: rotate(-12deg); z-index: 1; }
@media (prefers-contrast: more) {
  .castcard-body { background: #0b0e17; }
  .castcard-body::before { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .castcard, .castcard:hover { transition: none; transform: none; }
}

/* ---- 2c cast card, ACTOR-COLOUR (.castcard-ac) ----
   COPIED VERBATIM from the design handoff
   design_handoff_cast_card_actor_colour/cast-card-actor-colour.css, whose own
   header names the destination: "On integration, move this block verbatim into
   shared/cast-cards.css after the 2b block and delete this file."

   ██ IT REPLACES 2b ON THIS SURFACE, AND 2b GOES WITH IT ██ The article rail
   was the business card's only caller (grep: siteCss + one test), so leaving
   the variant and its ruled-list overrides behind would be a second cast card
   nothing renders — and the next reader could not tell which one is live.

   ⚖️ --player IS DATA, NOT A TOKEN. It is the actor's own primary colour from
   the cast record, and every other colour in the card is derived from it by
   color-mix. It is NOT authored here and must never be invented: the handoff
   is explicit that the no-colour path "must be exercised in every integration,
   since most actors have no colour on record", and the component answers it
   itself by falling back to the theme accent on the first line. Today that
   fallback is what EVERY card takes — brief.palette[].hex exists on the actor
   but playerCards carries only href/slug/img/darkImg, so no colour reaches the
   baker. Wiring it is a data pipe, not a stylesheet change. */
/* ██ THE SET OWNS THE LAYOUT ██ (design revision, block D). The card carries no
   width any more, so the grid that holds them does — two up, one up under 720.
   Open cards first, then folded. */
.castcard-ac-set {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  list-style: none; margin: 0; padding: 0;
}
@media (max-width: 720px) { .castcard-ac-set { grid-template-columns: 1fr; } }
.castcard-ac-set--one { grid-template-columns: 1fr; }

.castcard-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
  gap: 12px;
}

.castcard-ac {
  --player: var(--accent);                    /* fallback: actors with no colour on record */
  /* ⚖️ THE INK MIX IS UNCONDITIONAL (design revision, "the open-5 fix"). It was
     a light-only override block, which meant a DARK record colour — a party's
     navy, a club's bottle green — carried type RAW on a dark surface and
     vanished into it. One mix repairs that from both ends: it lifts a dark
     colour on a dark ground and darkens a bright one on paper. Measured across
     the theme x mode matrix in the exploration, name >= 4.5:1 and numeral
     >= 3:1 in every combination. Identification still comes from the FILLS and
     the hairline, which keep the pure record colour — only type is pulled. */
  --player-ink: color-mix(in oklab, var(--player) 52%, var(--text-primary));

  position: relative;
  display: flex;
  align-items: stretch;
  gap: 0;
  /* ⚠️ NO FIXED WIDTH. It was 330px, which fought every container it was put
     in; the -set grid owns the layout now and the card simply fills its cell. */
  min-width: 0;
  overflow: hidden;
  border-radius: var(--r-md);
  /* the fill: the player's colour over a HALF-TRANSPARENT surface, so the card
     reads on the canvas, over imagery, and in light mode from one rule */
  background: color-mix(in oklab, var(--player) 9%, color-mix(in oklab, var(--bg-surface) 40%, transparent));
  border: 1px solid color-mix(in oklab, var(--player) 30%, transparent);
  transition: background var(--dur-med) var(--ease-out),
              border-color var(--dur-med) var(--ease-out);
}
/* ██ TWO ZONES, TWO HOVERS — INDEPENDENT ██ (owner 2026-08-20: "hovering the
   number of stories also creates a hover effect for the entire card; I prefer
   the card to be separate areas and independent".)

   ⚖️ :hover ON A PARENT FIRES FOR EVERY DESCENDANT, so the moment the figure
   became its own link the card lit up underneath it — two highlights for one
   pointer, saying two different things about where a click would go. The card's
   hover now EXCLUDES the case where the figure is the thing being pointed at,
   which is what makes the two areas answer separately.

   Same for the focus ring: a keyboard user tabbing to the figure would
   otherwise ring the whole card while the figure draws its own — the figure
   carries its own focus-visible rule a few lines down. */
.castcard-ac:hover:not(:has(.castcard-ac-fig:hover)) {
  background: color-mix(in oklab, var(--player) 15%, color-mix(in oklab, var(--bg-surface) 55%, transparent));
  border-color: color-mix(in oklab, var(--player) 52%, transparent);
}
.castcard-ac:focus-within:not(:has(.castcard-ac-fig:focus-visible)) {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* the figure column — the stories count, TRAILING (owner 2026-08-20: "move this
   NUMBER + stories from most left to the most right part of the player's card").
   The hairline moves with it: it divides the figure from the body, so it belongs
   on whichever side the body is — leaving it on the right would draw a rule down
   the card's outer edge, doubling the border. */
.castcard-ac-fig {
  /* the wait chase is absolute INSIDE this column, so it anchors here */
  position: relative;
  flex: 0 0 74px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-1);
  line-height: 1;
  background: color-mix(in oklab, var(--player) 16%, transparent);
  border-left: 1px solid color-mix(in oklab, var(--player) 26%, transparent);
}
/* ██ THE FIGURE'S OWN HOVER ██ (owner 2026-08-20). As a link it must not wear
   link chrome — it is a panel, not a phrase — and its hover has to be legible
   as ITS OWN rather than the card's: the card lifts its whole fill on hover, so
   this deepens the figure's tint and warms the number instead. Two targets, two
   answers to "what am I about to click". */
.castcard-ac-fig { color: inherit; text-decoration: none; }
a.castcard-ac-fig { cursor: pointer; transition: background var(--dur-med) var(--ease-out); }
a.castcard-ac-fig:hover { background: color-mix(in oklab, var(--player) 34%, transparent); }
a.castcard-ac-fig:hover b { color: var(--player-ink); }
a.castcard-ac-fig:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.castcard-ac-fig b {
  font-family: var(--font-display);
  /* 24.5, down from 27: the numeral was competing with the name for first read
     when it is the SECOND thing a reader wants. */
  font-size: 24.5px; font-weight: 700;
  letter-spacing: var(--tracking-display);
  font-feature-settings: "tnum";
  color: var(--player-ink);
}
.castcard-ac-fig span {
  font-family: var(--font-mono);
  /* and the unit label comes UP — 9.5/500 in secondary ink, not 9 in tertiary.
     It is the word that says what the numeral COUNTS; at tertiary it read as
     disclaimer text under a headline number. */
  font-size: 9.5px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-secondary);
}

/* ══ THE PENDING COUNT ═══════════════════════════════════════════════════════
   ⚖️ THE COLUMN NEVER COLLAPSES AND NEVER PRINTS A ZERO. The count is fetched
   after mount, so on first paint there is nothing to print — and the honest
   answer is not "0" (a lie) nor a missing column (a layout jump on arrival),
   but a slot that is visibly WAITING. The numeral's space and the unit label
   both stay; a chase of dots runs the column's own perimeter, inset 7px so it
   never rides the border. One wait figure for every card, folded or open.

   ⚠️ NOT BAKED. With scripting off the count never arrives, and a perpetual
   spinner on a static page is a promise the page cannot keep (20.1170) — so
   the figure is absent from the baked bytes entirely and the pending state is
   something the client puts on at mount. */
.castcard-ac--pending .castcard-ac-fig b { opacity: 0; }
.castcard-ac--pending .castcard-ac-fig > span:not(.castcard-ac-wait) { opacity: .55; }
.castcard-ac-wait { display: none; }
@keyframes cc-snake { to { offset-distance: 100%; } }
@supports (offset-path: rect(0 1px 1px 0)) {
  .castcard-ac--pending .castcard-ac-wait,
  .castcard-ac--snake .castcard-ac-wait {
    display: block; position: absolute; top: 7px; right: 7px; bottom: 7px; left: 7px;
    pointer-events: none;
  }
  .castcard-ac-wait i {
    position: absolute; width: 4px; height: 4px; margin: -2px; border-radius: 50%;
    background: var(--player-ink);
    animation: cc-snake 2.6s linear infinite;
    offset-path: rect(0 100% 100% 0 round 4px); offset-distance: 0%; offset-rotate: 0deg;
  }
  .castcard-ac-wait i:nth-child(2) { animation-delay: -.13s; opacity: .72; }
  .castcard-ac-wait i:nth-child(3) { animation-delay: -.26s; opacity: .5; }
  .castcard-ac-wait i:nth-child(4) { animation-delay: -.39s; opacity: .32; }
  .castcard-ac-wait i:nth-child(5) { animation-delay: -.52s; opacity: .18; }
}
/* no offset-path: three dots pulse under the slot instead */
@supports not (offset-path: rect(0 1px 1px 0)) {
  .castcard-ac--pending .castcard-ac-wait {
    display: flex; align-items: center; justify-content: center; gap: 4px; height: 12px;
  }
  .castcard-ac-wait i {
    width: 4px; height: 4px; border-radius: 50%; background: var(--player-ink);
    opacity: .45; animation: cc-pulse 1.15s ease-in-out infinite;
  }
  .castcard-ac-wait i:nth-child(2) { animation-delay: .14s; }
  .castcard-ac-wait i:nth-child(3) { animation-delay: .28s; }
  .castcard-ac-wait i:nth-child(n+4) { display: none; }
}
@keyframes cc-pulse { 0%, 100% { opacity: .25 } 50% { opacity: 1 } }
/* arrival: the chase stops and the value is PULLED DOWN into the slot, once */
.castcard-ac--landed .castcard-ac-wait { display: none; }
.castcard-ac--landed .castcard-ac-fig b { opacity: 1; animation: cc-pull .95s cubic-bezier(.18, .72, .2, 1); }
@keyframes cc-pull {
  0%   { transform: translateY(-26px) scale(.8); opacity: 0 }
  40%  { opacity: 1 }
  74%  { transform: translateY(3px) scale(1.04) }
  100% { transform: translateY(0) scale(1); opacity: 1 }
}
@media (prefers-reduced-motion: reduce) {
  .castcard-ac-wait i { animation: none; opacity: .45; }
  .castcard-ac--landed .castcard-ac-fig b { animation: none; }
}

/* the body — name, then sentence, then type */
.castcard-ac-body {
  display: flex; flex-direction: column; gap: 6px;
  /* ⚠️ GROWS, and it has to now the figure TRAILS it. While the figure led, a
     content-sized body simply left slack on the right and nobody saw it; with
     the figure last, that same slack sits BETWEEN the text and the figure and
     pushes the count off the card's right edge — which is the one place the
     owner asked for it. min-width: 0 keeps a long name wrapping instead of
     forcing the flex line wider than the card. */
  flex: 1 1 auto;
  min-width: 0;
  padding: 14px 15px;
}
.castcard-ac-name {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-meta); font-weight: 600;
  letter-spacing: var(--tracking-normal);
  line-height: var(--lh-snug);
  text-wrap: balance;
}
.castcard-ac-name a { color: var(--player-ink); }
/* the "this opens elsewhere" mark — inline with the last word, never on its own
   line, and dimmer than the name so it reads as punctuation rather than a word */
.castcard-ac-out {
  display: inline-block; vertical-align: baseline;
  margin-left: 5px;
  opacity: .55;
  transition: opacity var(--dur-med) var(--ease-out);
}
.castcard-ac-name a:hover .castcard-ac-out { opacity: 1; }
.castcard-ac-name a:focus-visible { outline: none; }   /* the card carries the ring */
.castcard-ac-ice {
  margin: 0;
  font-family: var(--font-display);
  font-size: 15.5px; line-height: 1.35;
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
  text-wrap: pretty;
}
.castcard-ac-type {
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--text-tertiary);
}

/* ⚠️ THE LIGHT-ONLY INK BLOCK IS GONE. It lived here and did this same mix for
   [data-mode="light"] only; the card now carries it unconditionally, which is
   what repairs a dark record colour in dark mode. Deleting it rather than
   leaving it beside the new rule matters: it would still match, and the two
   would only agree by coincidence.

   ██ THE RANKED EDGE ██ A card that is not folded wears a heavier left border,
   so an open card reads as a different WEIGHT of thing from a folded one before
   any word is read. Drawn from --player-ink, not raw --player, so it survives a
   dark record colour on a dark ground — the same reason the type is mixed. */
.castcard-ac:not(.castcard-ac--fold):not(.castcard-ac--supporting):not(.castcard-ac--background) {
  border-left: 3px solid color-mix(in oklab, var(--player-ink) 70%, transparent);
}

/* ⚖️ BLUR IS OPT-IN NOW. It was unconditional, which cost every card a
   compositing layer to blur an OPAQUE story column — nothing was ever behind
   it. --glass is for the one surface that puts a card over a lead image. */
.castcard-ac--glass {
  backdrop-filter: blur(14px) saturate(1.3);
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
}

@media (prefers-reduced-motion: reduce) {
  .castcard-ac, .castcard-ac:hover { transition: none; }
}

/* ── THE PLACE-OF-USE ADJUSTMENTS ──────────────────────────────────────────
   Everything above is the design's, byte-for-byte. These are this SURFACE's,
   kept apart so the verbatim block stays diffable against the handoff.

   1. It is a <ul>, so it needs the list reset a <div> does not. The grid is
      the component's own auto-fill — in the story column that fans the cards
      two abreast, the same reading the workshop deck gives.
   2. The card authors width: 330px; here it fills its track instead, so a
      pair of cards shares the column edge-to-edge with no ragged right.
   3. THE TIER IS STRUCTURE, NOT A LABEL (owner 2026-08-20, third pass:
      "instead of having support main roles as labels — main players have
      left border highlighted + expanded; supported collapsed and secondary
      color (bluish) and on click only expand"). The badge words lasted one
      deploy; what the label said, the card's shape now says.
   4. ONE SWITCH, MANY TRIGGERS (owner, fourth pass: "each collapse …
      expand all support cards"): every folded card's <label> points at ONE
      hidden checkbox, so opening any of them opens the whole supporting
      cast. A <details> per card could not express this — each owns its own
      state, and its 'name' attribute does the opposite (it CLOSES the
      others) — so this is the house's script-free drawer idiom instead
      (topicTimelineCss's precedent): checkbox + labels + a :checked sibling rule.
      Zero JS, keyboard-reachable, nothing to hydrate (20.1170).
   5. THE MARKER GUTTER (owner, fourth pass: "star icon in front of the main
      players, so that they have same offset like the support player that
      have expand collapse icon in front"): EVERY name reserves the same
      22px gutter and draws its marker inside it — ★ for a principal, a
      rotating chevron for a folded card, nothing for an unranked one — so
      the titles line up down the column whatever the tier. The role line
      then indents PAST the title (34px) for hierarchy: marker · title ·
      description, each step further right.
   6. THE TITLE WEARS THE SECONDARY COLOUR (owner, to the screenshot): the
      OPEN card's NAME sets in the player ink — the actor's own colour where
      one exists, the theme's gold accent otherwise, mixed toward real ink in
      light mode by the block above. A FOLDED card's name is the quieter
      bluish --text-secondary instead: the fold IS the de-emphasis, and its
      title dresses accordingly. */
.cast .castcard-list { list-style: none; margin: 0; padding: 0; }
.cast .castcard-ac { width: 100%; }
/* ⚠️ DELIBERATELY NOT POSITIONED. It was, briefly, and that alone broke the
   card's click target: a positioned sibling with z-index:auto paints in TREE
   ORDER, so the body — which comes after the overlay — sat on top of it and
   swallowed every click that missed the title. MEASURED via elementFromPoint,
   twice. The overlay now names its own layer (z-index below) instead of
   relying on order, and nothing here may quietly re-enter that competition. */
/* ── the marker gutter: one offset, every tier ───────────────────────────── */
/* ⚖️ THE GUTTER IS SYMMETRIC, AND IT IS ARITHMETIC (owner 2026-08-20: "make
   sure that the space between the left edge and the icon is the same as the
   icon and the text"). The card's own left padding IS the first gap, so the
   second one must equal it: text = icon + gap. Both numbers live here once and
   every offset derives from them — a hand-typed 22px on one line and 34px on
   another is exactly how the two gaps drifted apart in the first place. */
.cast .castcard-ac { --cast-icon: 12px; --cast-gap: 15px; }
.cast .castcard-ac-name {
  position: relative;                      /* the marker anchors here — NOT a
                                              z-index: see the folded-card note
                                              below, an h3 lifted above the
                                              overlay eats the card's clicks */
  color: var(--player-ink);
  padding-left: calc(var(--cast-icon) + var(--cast-gap));
}
.cast .castcard-ac-name::before {
  position: absolute; left: 0;
  width: var(--cast-icon); text-align: center;
}
/* ⚖️ THE DESCRIPTION ALIGNS WITH THE TITLE, not past it (owner 2026-08-20,
   reversing the step-in of a few hours earlier: "align them again vertically").
   One left edge for the whole card's words; the HIERARCHY is carried by the
   marker gutter and the rule line beside them, not by a second indent — which
   only ever competed with the gutter for the same job. */
.cast .castcard-ac-ice {
  position: relative;
  padding-left: calc(var(--cast-icon) + var(--cast-gap));
}
/* ── THE RULE LINE (owner: "visual line same colour as the element above …
   that fades away going downward, for cognitive richness") ────────────────
   A hairline dropped from the title into the description's left margin, in
   the TITLE's own ink — gold under a principal, bluish under a folded card —
   fading out as it descends, the chronicle rail's own gesture at card scale.
   It carries the hierarchy the indent implies: the description hangs off the
   name rather than merely sitting under it.
   ⚖️ --castline is a per-CARD variable, not a colour repeated twice: the tier
   sets it once and the marker, the title and this line cannot disagree. */
.cast .castcard-ac { --castline: var(--player-ink); }
.cast .castcard-ac--supporting, .cast .castcard-ac--background { --castline: var(--text-secondary); }
.cast .castcard-ac-ice::before {
  content: '';
  /* ⚖️ IT HANGS FROM THE MARKER, NOT THE TITLE (owner 2026-08-20, to the
     screenshot: "it is under the left edge of the title, move it under the
     icon"). It drops from the marker's own centre — half the icon box, less
     half the hairline — so the ★ or the chevron reads as the thing it hangs
     from. Anywhere else it is a second left edge competing with the title's. */
  position: absolute; left: calc(var(--cast-icon) / 2 - .75px); top: .25em; bottom: 0;
  width: 1.5px; border-radius: 1px;
  background: linear-gradient(
    to bottom,
    color-mix(in oklab, var(--castline) 62%, transparent) 0%,
    color-mix(in oklab, var(--castline) 26%, transparent) 52%,
    transparent 100%);
}
/* ⚖️ WHERE A GOAL FOLLOWS, THE RULE RUNS ON PAST THE SENTENCE and reaches full
   transparency at the goal line — one gesture down the whole block instead of
   a stroke that stops short and leaves the last line orphaned beside nothing. */
.cast .castcard-ac-body:has(.castcard-ac-goal) .castcard-ac-ice::before { bottom: -1.7em; }
/* THE WANTS LINE (owner: "bring back what they want") — the third step of the
   same hierarchy, so it takes the description's indent rather than starting a
   fourth one. The line above fades INTO it: the description's rule stops at
   its own last line, and this sits in the space it fades toward, which is why
   the goal reads as the end of the card's thought rather than a new one. */
/* ██ THE GOAL LINE ██ renamed from -wants (design revision): it is the actor's
   stake in THIS story, and it occupies the footer slot -type used to hold.
   13px, not 11 — it is a SENTENCE, and at 11 mono it read as a system label
   rather than something written about this actor. --goal-size is the dial the
   exploration's slider tested live. */
.cast .castcard-ac-goal {
  margin: 6px 0 0; padding-left: calc(var(--cast-icon) + var(--cast-gap));
  font-family: var(--font-mono);
  font-size: var(--goal-size, 13px); line-height: 1.45;
  color: var(--text-tertiary);
  text-wrap: pretty;
}
.cast .castcard-ac-goal b { font-weight: 400; color: var(--accent); }
/* ── MAIN: the ★, the ink edge, always open ──────────────────────────────── */
.cast .castcard-ac--main { border-left: 3px solid var(--player-ink); }
.cast .castcard-ac--main .castcard-ac-name::before {
  content: '\2605';                        /* ★ — a glyph, not an emoji: it
                                             takes the actor's ink and the
                                             theme's weight, which a colour
                                             font would ignore */
  top: 0; font-size: 12px; line-height: inherit;
}
/* ── SUPPORTING / BACKGROUND: bluish, folded, chevron in the same gutter ─── */
.cast .castcard-ac--supporting .castcard-ac-name,
.cast .castcard-ac--background .castcard-ac-name { color: var(--text-secondary); }
.cast .castcard-ac--supporting .castcard-ac-name::before,
.cast .castcard-ac--background .castcard-ac-name::before {
  content: '';
  /* centred in the SAME icon box the ★ uses — a 6px square rotated 45° is
     ~8.5px across the diagonal, so it is offset by half the box less half
     that width, and the two markers share one optical centre */
  top: .38em; left: calc(var(--cast-icon) / 2 - 3px);
  width: 6px; height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);               /* ▸ closed */
  transition: transform var(--dur-med) var(--ease-out);
}
/* ██ THE TITLE MUST NOT EAT THE CARD'S CLICKS ██ MEASURED, not reasoned: an
   <h3> is a BLOCK, so lifting it above the toggle overlay (the obvious way to
   keep its link clickable) hands it the full width of the card — and a click
   anywhere on the empty half of the title row then hits dead markup instead of
   the toggle. elementFromPoint at the centre of a collapsed card returned
   H3, and the card did not open. So the title stays transparent to the pointer
   and only the ANCHOR takes itself back — the one part that must beat the
   overlay, and the only part that is not dead space. */
.cast .castcard-ac--supporting .castcard-ac-name,
.cast .castcard-ac--background .castcard-ac-name { pointer-events: none; }
.cast .castcard-ac--supporting .castcard-ac-name a,
.cast .castcard-ac--background .castcard-ac-name a,
.cast .castcard-ac--supporting a.castcard-ac-fig,
.cast .castcard-ac--background a.castcard-ac-fig {
  /* ⚠️ THE OVERLAY IS z-index: 2 AND COVERS THE WHOLE CARD, so anything that
     must stay clickable on a FOLDED card has to be lifted over it. This used to
     read "the one thing above the overlay"; the figure link (owner 2026-08-20)
     makes it two, and the folded card in the owner's own screenshot is exactly
     where that matters — the count sits inside the toggle's hit area. */
  pointer-events: auto; position: relative; z-index: 3;
}
/* ── THE SETTLE (owner: "smooth settle animation, that creates joy to
   observe") ───────────────────────────────────────────────────────────────
   The fold is a ONE-CELL GRID animating 0fr → 1fr. That is the whole trick:
   display:none → block cannot transition at all, and a max-height guess
   either clips a long role or spends half the easing on empty space — this
   tweens to the content's OWN height, with nothing to measure and no script.
   The row and the opacity run on slightly different clocks so the text
   arrives just after the box has made room for it, which is what reads as
   settling rather than sliding. */
.cast .castcard-ac-fold {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--dur-slow) var(--ease-out),
              opacity var(--dur-med) var(--ease-out);
}
/* the grid child MUST be able to shrink to nothing, or 0fr renders at its
   min-content height and the fold never closes */
.cast .castcard-ac-fold > * { min-height: 0; overflow: hidden; }
/* THE HIT AREA IS THE WHOLE CARD, in both states (owner: "clickable over the
   entire area not just the icon"). A first cut shrank it to the name row once
   open — a control that moves after you use it is a control you have to
   re-find. The cost is that the description is not comfortably selectable
   while the group is open; the owner's call, and the right one for a card
   whose text is a caption rather than a passage. */
.castcard-ac-toggle { position: absolute; inset: 0; z-index: 2; cursor: pointer; }
/* the switch itself: visually hidden but STILL FOCUSABLE, so the group is
   keyboard-operable without a line of script (a hidden-attribute input would take
   itself out of the tab order and strand the fold) */
.castcard-ac-switch {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
}
.castcard-ac-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
}
/* ── the open group ──────────────────────────────────────────────────────── */
.castcard-ac-switch:checked ~ .castcard-list .castcard-ac-fold {
  grid-template-rows: 1fr;
  opacity: 1;
  /* opening, the text waits for the room — closing (the rule above) it goes
     first, so the box never collapses over readable words */
  transition: grid-template-rows var(--dur-slow) var(--ease-out),
              opacity var(--dur-med) var(--ease-out) 90ms;
}
.castcard-ac-switch:checked ~ .castcard-list .castcard-ac--supporting .castcard-ac-name::before,
.castcard-ac-switch:checked ~ .castcard-list .castcard-ac--background .castcard-ac-name::before {
  transform: rotate(45deg);                /* ▾ open */
}
/* the input is hidden, so the focus ring has to land on what the reader can
   actually see — the hit areas themselves, every trigger lighting up together
   because they are all one control. Inset, so the ring draws inside the card
   rather than colliding with its neighbour. */
.castcard-ac-switch:focus-visible ~ .castcard-list .castcard-ac-toggle {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
  .cast .castcard-ac-name::before,
  .cast .castcard-ac-fold,
  .castcard-ac-switch:checked ~ .castcard-list .castcard-ac-fold { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
  .players.playersrail li.playercard { transition: none; }
}
/* ── THE CARD OPENS (owner 2026-08-14) ───────────────────────────────────────
   The clicked card, cloned to <body> at DOUBLE size with the clamps off, so the
   whole role and goal can be read. The hydrate bundle builds it; with no JS the
   deck simply keeps its hover-lift, which is why none of this is load-bearing.

   ██ IT RESTATES THE WHOLE BOX ON PURPOSE ██ every rule that shapes a card in
   the fan is scoped .players.playersrail li.playercard — and this panel is NOT
   in the rail (that is the entire point: the rail clips). So it inherits the
   inner pieces (.pface ghost, .pbody, tier accent) and nothing of the box, and
   must draw its own. Do not "de-duplicate" these against the rail's rules by
   loosening those selectors: unscoping them would drag the fan's own width,
   aspect-ratio and overlap onto this panel.

   The open transform is scale(1) rather than a scaled-up card, so the type is
   rendered at its real size and stays crisp — scale(.5) is merely the START,
   and it equals the card's footprint exactly, so the panel grows out of the
   card instead of appearing from nowhere. */
.playercard-zoom { position: absolute; z-index: 60; margin: 0; box-sizing: border-box;
  display: flex; flex-direction: column; justify-content: flex-end; overflow: hidden;
  padding: 20px 22px; border: 1px solid var(--line); border-left-width: 4px; border-radius: 14px;
  background: linear-gradient(160deg, var(--card) 0%, var(--bg) 62%);
  box-shadow: 0 26px 64px -14px var(--deck-shadow); isolation: isolate;
  transform: scale(.5); opacity: 0; transform-origin: 50% 50%;
  /* left/top are in the transition because SWITCHING reuses this element and
     only moves it — that slide from card to card is what makes ← / → and a
     thumb-swipe read as ONE carousel instead of six separate opens. A first
     open has no previous position to slide from, so the effect pins
     transition:none for that one frame. */
  transition: transform .24s cubic-bezier(.2, .7, .3, 1), opacity .16s ease,
              left .26s cubic-bezier(.2, .7, .3, 1), top .26s cubic-bezier(.2, .7, .3, 1);
  /* the panel owns horizontal drags (that is the switch gesture) and yields
     vertical ones, so the reader can still scroll the page through it */
  touch-action: pan-y; }
.playercard-zoom.is-open { transform: scale(1); opacity: 1; }
.playercard-zoom.is-closing { pointer-events: none; }
/* THE CLAMPS COME OFF — this panel exists to show the sentence the fanned card
   could only start. display:block is what actually releases -webkit-line-clamp;
   the clamp property alone is inert once the box is no longer a -webkit-box. */
.playercard-zoom .pbody { gap: 8px; }
.playercard-zoom .pname { display: block; font-size: 20px; line-height: 1.22; }
.playercard-zoom .prole { display: block; overflow: visible; font-size: 14.5px; line-height: 1.5; }
.playercard-zoom .pstat { font-size: 12px; }
@media (prefers-reduced-motion: reduce) {
  .playercard-zoom { transition: opacity .16s ease; transform: none; }
  .playercard-zoom.is-open { transform: none; }
}
/* ── THE SCRUBBER (owner 2026-08-14) — the carousel made VISIBLE ─────────────
   Under the open card on TOUCH only: a rectangle you can grab and drag across
   the cast. A swipe is a gesture you have to already know about; this is the
   same movement with a handle on it, and it doubles as the position readout
   ("4th of 6") that a swipe can never give you. A fine pointer has the arrow
   keys and gets no bar. Built by the hydrate bundle, so it is invisible to the
   zero-JS floor by construction.
   touch-action:none because the bar OWNS its drag — without it the browser
   would claim the horizontal pan for scrolling and the thumb would stutter. */
.deckbar { position: absolute; z-index: 60; display: flex; align-items: center;
  opacity: 0; transition: opacity .16s ease; touch-action: none; }
.deckbar.is-open { opacity: 1; }
.deckbar-track { position: absolute; left: 0; right: 0; height: 6px; border-radius: 999px;
  background: var(--line); }
/* The marker is a RECTANGLE, not a dot: it is a card, and its width is the
   share of the cast it stands for, so the bar reads as a map of the hand. */
.deckbar-thumb { position: absolute; top: 0; height: 100%; border-radius: 8px;
  background: var(--accent); box-shadow: 0 4px 12px -4px var(--deck-shadow);
  transition: left .18s cubic-bezier(.2, .7, .3, 1); }
/* Mid-drag the thumb must track the finger EXACTLY — an eased follow reads as
   lag when you are the one moving it. */
.deckbar.is-grabbed .deckbar-thumb { transition: none; }
@media (prefers-reduced-motion: reduce) { .deckbar-thumb { transition: none; } }
/* The card: a 5:7 visiting-card portrait. */
.profilecard { margin: 0 0 18px; }
.profilecard .onecard { display: inline-block; width: 190px; }
.cardface { display: block; position: relative; border-radius: 12px; overflow: hidden;
  border: 1px solid var(--line); background: var(--panel, transparent); aspect-ratio: 5 / 7; }
.cardface img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cardface .mono { display: flex; align-items: center; justify-content: center; height: 100%;
  font-size: 46px; color: var(--faint); }
.cardface .cardname { position: absolute; left: 0; right: 0; bottom: 0; padding: 6px 9px;
  font-size: 13px; color: #fff; background: rgba(0,0,0,.62); }
/* THE DECK — a co-billed group is drawn as its MEMBERS' cards, stacked and
   lightly tilted (owner ruling 2026-07-28), never as one combined portrait. */
.profiledeck { position: relative; height: 300px; margin: 0 0 18px; }
.profiledeck .deckcard { position: absolute; top: 8px; width: 168px; transform-origin: 50% 92%;
  filter: drop-shadow(0 10px 22px rgba(0,0,0,.35)); }
.profiledeck .deckcard:nth-child(1) { left: 0; }
.profiledeck .deckcard:nth-child(2) { left: 58px; }
.profiledeck .deckcard:nth-child(3) { left: 116px; }
.profiledeck .deckcard:nth-child(4) { left: 174px; }
.memberlist { list-style: none; padding: 0; margin: 4px 0 18px; display: flex; gap: 14px; flex-wrap: wrap; }
.profilestories .role { display: block; color: var(--muted); font-size: 14px; }
/* THE WALL (20.1020) — a story with art becomes a card: wide640 thumb beside
   the row the page always had. Thumbless stories keep the plain row, so a
   half-illustrated wall degrades to the old index instead of to holes. */
.profilestories:not(.rowcards) li.storycard { display: flex; gap: 13px; align-items: flex-start; }
.profilestories .sthumb { flex: 0 0 158px; border-radius: 9px; overflow: hidden; border: 1px solid var(--line); }
.profilestories .sthumb img { width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; display: block; }
.profilestories .sbody { min-width: 0; }
@media (max-width: 560px) { .profilestories .sthumb { flex-basis: 116px; } }
/* THE CHRONICLE AS A TIMELINE (ported from the production preview). Each
   chapter hangs off one hairline rail with an accent node at its date, so the
   diary reads as a SEQUENCE instead of as stacked sections — consecutive
   chapters share the rail because their borders meet. */
/* ══ THE CHRONICLE WEARS THE THREAD (owner 2026-08-19, the ornament swap) ═══
   The rail carried the furniture — numbered rings, a glowing spine, Δ rows and
   month labels — in a 360px column, while the chronicle it indexed was bare
   prose with a hairline down its left edge. The owner swapped them: ornament
   belongs where the reader is reading and there is width for it.

   ⚠️ THE GEOMETRY IS COUPLED, exactly as it was in the rail. The gutter (52px),
   the ring (36px) and --move-anchor (the ring's CENTRE from the section top)
   are one set of numbers; the spine is positioned from them and the Δ row and
   the month label are indented to match. Change one and recompute the rest —
   do not patch a broken thread with a second absolutely-positioned line. */
.move {
  --move-gutter: 52px;
  --move-anchor: 20px;      /* ring centre from the section's top edge */
  position: relative;
  margin: 0;
  padding: 0 0 22px var(--move-gutter);
  border-left: 0;           /* the hairline is a real element now, see .move-spine */
}
/* the numbered ring — the chapter's identity, drawn large. It is the one the
   rail gave up, so this is the only place a reader sees "04". */
.move-ring {
  position: absolute; z-index: 2; left: 0; top: 2px;
  width: 36px; height: 36px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; letter-spacing: .04em;
  color: var(--accent);
  border: 1.5px solid color-mix(in oklab, var(--accent) 50%, transparent);
  background:
    radial-gradient(circle at 50% 35%, var(--accent-soft), transparent 70%),
    var(--bg-elevated);
}
/* the spine, running from this ring down past the section's own bottom to meet
   the next one. Per-section rather than one line behind the list: the sections
   are siblings with no wrapper, and a single absolute line would have nothing
   to measure itself against. */
.move-spine {
  position: absolute; z-index: 1; pointer-events: none;
  left: var(--thread-axis, 18px); transform: translateX(-50%); width: 2px;
  top: var(--move-anchor); bottom: -22px;
  background: color-mix(in oklab, var(--accent) 42%, transparent);
  box-shadow: 0 0 7px -1px color-mix(in oklab, var(--accent) 30%, transparent);
  border-radius: 2px;
}
/* the LAST chapter's thread stops at its own ring — nothing follows it.
   ⚠️ NOT :last-of-type. The stop below is a <section> as well, so "last section"
   silently became the STOP the day it shipped and this rule stopped matching any
   chapter at all — the overhang then survived by DEFAULT rather than by
   permission, and the rule underneath that was supposed to grant it never fired.
   It looked right, which is the dangerous part. "Last chapter" is a statement
   about CHAPTERS, so it is written about chapters. */
.move:not(:has(~ .move)) .move-spine { display: none; }
/* …unless a standing stop follows it, in which case the thread carries on down
   to it. ⚠️ :has() is the whole mechanism, and its absence is the OLD
   behaviour — a thread that stops at the last chapter — not a broken one.
   ⚠️ AND IT STOPS AT ITS OWN FOOT, not 22px past it. The overhang exists so a
   chapter's spine reaches its NEIGHBOUR's ring; the stop is not a chapter and
   owns the bridge down to itself (.standing-stop::before), so an overhang here
   would draw the same 2px twice over — in two different colours, since the
   bridge is a gradient. */
.move:not(:has(~ .move)):has(~ .standing-stop) .move-spine {
  display: block; bottom: 0;
}
/* ⚠️ THE Δ DIVIDER IS RETIRED (owner 2026-08-19). It sat in the section's top
   margin — the same band the numbered ring occupies — and drew straight
   through it. Both facts it carried are already on the page: the dateline
   under every title says when a chapter happened, and the distance between
   two of them is a subtraction the reader can do. Neither was worth a row
   that collides with the thread. */
/* ⚠️ THE MONTH LABEL IS RETIRED (owner 2026-08-19), and so is the year that
   sat opposite it. The numbered ring says where a chapter is on the thread and
   the dateline under its title says when — a month heading over the top was a
   third answer to a question already answered twice. */
/* the one h2 that is a TITLE, not a label — back out of the eyebrow register */
.move h2 { font-family: var(--serif); font-size: 19px; font-weight: 700; letter-spacing: normal;
  text-transform: none; color: var(--text); margin: 2px 0 6px; }
.move h2 a { color: inherit; }
.move h2 a:hover { color: var(--accent); text-decoration: none; }
/* THE DATELINE, under its title (owner 2026-08-19 — it led the chapter until
   then). Two halves: the chapter's date range, and the same 'преди …' phrase
   the rail prints beside the same chapter, which is CLIENT-ONLY and simply
   absent on a page with no script. Both regular weight, like every other
   delta on the page. */
.range {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin: 0 0 10px; font-size: 11px; font-weight: 400;
  color: var(--faint); letter-spacing: .04em;
}
.range-ago { color: var(--accent); letter-spacing: .02em; }
/* Source lines under a chapter: an accent em-dash marker, not a second rail —
   the rail beside them already belongs to the chapter. */
.events { list-style: none; padding: 0; margin: 10px 0 0; }
.events li { position: relative; padding: 0 0 0 15px; margin: 5px 0; font-size: 13.5px; color: var(--muted); }
.events li::before { content: '\2014'; position: absolute; left: 0; color: var(--accent); }
.events .when { color: var(--faint); font-size: 11.5px; margin-right: 6px; }
/* the source arrow rides the marker's accent, not the body link blue */
.events a { color: var(--accent); }
/* ── THE CHAPTER'S EVIDENCE, REDUCED TO ITS SIZE (20.1700, 2026-08-19) ──────
   The dated event list became a paid surface; this count is what stands in its
   place. Quiet on purpose — it is a fact about the chapter, not a call to
   action, and it has nowhere to go yet (ArticleView's SOURCES_HREF is null
   until the plan page exists). When it does become a link the anchor inherits
   these rules and only gains the accent. */
.move-sources { margin: 10px 0 0; font-size: 12.5px; color: var(--faint); }
.move-sources a { color: var(--accent); text-decoration: none; }
.move-sources a:hover { text-decoration: underline; }
/* ── WHERE THINGS STAND — WHERE THE THREAD ENDS (owner 2026-08-22) ──────────
   The stop is not a block NEXT TO the thread, it is the thread's terminus: the
   spine comes down from the last chapter, beads on this container's TOP border
   and goes no further. Read top to bottom it is one line that arrives, stops
   on a dot, and hands the page over to the panel underneath it.

   ⚖️ THE MATERIAL IS THE CHRONICLE'S DAY WIDGET, NOT A NEW SURFACE. Border,
   fill and glow are .nrb-tl-head's own recipe lifted verbatim — 60% / 9% / 55%
   of --info — because the rail's head and this stop are the two ENDS of one
   chain, and two ends made of different materials read as two objects. The one
   substitution is the ground it mixes into: the rail sits inside a panel and
   mixes into --bg-surface, this sits on the page and mixes into --bg.

   ⚠️ IT USED TO STRADDLE THE AXIS — the container was inset by --thread-axis so
   its LEFT border stood on the thread, with the prose paid back by a padding
   subtraction. It worked, but it cost the panel its full width, made every
   horizontal number a piece of arithmetic, and put a hairline down the whole
   left side of a block whose job is to END a line, not to continue one. The
   line stops ON the box now, and the box is just a box. */
.standing-stop {
  position: relative;
  /* ⚠️ ONE NUMBER, TWO USERS: the gap above this stop is also exactly how far
     the bridging stub reaches back up to meet the last chapter's spine.
     Spelled twice they drift on the first tweak and the thread quietly breaks
     in the middle. */
  --standing-gap: 34px;
  --standing-axis: var(--thread-axis, 18px);   /* never its own number */
  /* ⚠️ THE STROKE IS PART OF THE ARITHMETIC, NOT DECORATION. An absolutely
     positioned child resolves top:0 against the PADDING box, which starts
     AFTER the border — so a bead at 0 floats one pixel INSIDE the panel and
     the thread reads as stopping just short of the line it is meant to land
     on. It costs a pixel on BOTH axes: the same offset puts the bead and the
     bridging stub at x=19 while the chapters' spine runs at x=18, so the line
     visibly steps sideways on its way into the dot. Subtracted in all four
     places — measured 18/18/18. */
  --standing-stroke: 1px;
  /* ⚠️ AND THE HORIZONTAL ONE IS A SEPARATE FACT. The subtraction above is
     paid because a side border pushes the content box in — but the phone
     shape below has NO side border, and paying it there shifts the bead, the
     bridging stub and the prose one pixel LEFT of the thread they are
     supposed to meet. The TOP border survives in both shapes, so the vertical
     stroke is untouched: two different facts, two names, and the phone block
     flips exactly one of them. At every other width this resolves to the same
     1px and nothing moves. */
  --standing-stroke-x: var(--standing-stroke);
  margin: var(--standing-gap) 0 0;
  /* ⚠️ and a THIRD time on the left padding: the border pushes the content box
     in by a pixel, so a flat 52px sets this prose one pixel right of the
     chapters' own text column. The two columns have to be the same column. */
  padding: 19px 22px 21px calc(var(--move-gutter, 52px) - var(--standing-stroke-x));
  border: var(--standing-stroke) solid color-mix(in oklab, var(--info) 60%, transparent);
  border-radius: var(--r-lg, 16px);
  background: color-mix(in oklab, var(--info) 9%, var(--bg));
  box-shadow: 0 0 24px -8px color-mix(in oklab, var(--info) 55%, transparent);
}
.standing-stop > h2 { margin-top: 0; }
/* The thread arrives from the last chapter and stops on this border. ⚠️ Drawn
   HERE rather than by extending that chapter's spine, because the gap between
   them belongs to neither section — this stub owns the bridge and therefore
   cannot leave one.
   ⚖️ IT CHANGES COLOUR ON THE WAY DOWN. It leaves as accent, which is what
   every chapter's spine is made of, and arrives as --info, which is what this
   panel is made of: the hand-over is drawn rather than asserted, so the eye
   accepts a blue dot at the end of a gold line. */
.standing-stop::before {
  content: ""; position: absolute; z-index: 1; pointer-events: none;
  left: calc(var(--standing-axis) - var(--standing-stroke-x));
  transform: translateX(-50%); width: 2px;
  top: calc(-1 * var(--standing-gap) - var(--standing-stroke));
  height: var(--standing-gap);
  background: linear-gradient(color-mix(in oklab, var(--accent) 42%, transparent),
                              color-mix(in oklab, var(--info) 70%, transparent));
  border-radius: 2px;
}
/* ⚖️ A BEAD, NOT A NUMBERED RING. A chapter is numbered because it is one of a
   sequence; the standing is not a chapter, and a number would claim it is. It
   sits ON the top border — half in, half out — which is what makes the line
   read as ending IN the panel rather than as pointing at it. */
.standing-bead {
  position: absolute; z-index: 2; top: calc(-1 * var(--standing-stroke));
  left: calc(var(--standing-axis) - var(--standing-stroke-x));
  transform: translate(-50%, -50%);
  width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid color-mix(in oklab, var(--info) 75%, transparent);
  background: var(--bg);
  box-shadow: 0 0 10px -1px color-mix(in oklab, var(--info) 55%, transparent);
}
/* The prose itself carries no chrome any more — the stop around it is the
   panel. It used to be 15px, which made the verdict quieter than the evidence
   above it; it sets like body copy now. */
.standing p { margin: 0 0 12px; }
.standing p:last-child { margin-bottom: 0; }
/* ══ THE THREAD LEAVES THE COLUMN ON A PHONE ══════════════════════════
   (owner 2026-08-22, design handoff design_handoff_mobile_thread_edge, promoted
   from variant 01 b of explorations/mobile-thread-gutter-10-ways.html)

   MEASURED at 390px: the column is 354px and the chronicle's gutter takes 52 of
   them — a 36px ring on an 18px axis — leaving the prose 302px, about THIRTY
   CHARACTERS a line at the 17px body size. Five Bulgarian words, no hyphenation
   to smooth the rag, against a reading measure everyone puts at 45–75. The
   standing panel paid twice, its own inset on top of that gutter: 283px, the
   narrowest column on the page, for the one block carrying the conclusion.

   ⚖️ THE DRAWING LEAVES THE COLUMN, THE DRAWING DOES NOT SHRINK. Not one of its
   dimensions moves — same gutter, same ring, same axis. The chapters simply
   start at the screen edge, so the gutter is paid out of the page's margin
   instead of out of the reading column, and the ring lands whole and flush.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 560px) {
  /* ⚠️ .move ONLY. The hero, the nutshell panel, the cast and the sources keep
     the column: the thread is what has business at the edge, and a page where
     everything bleeds has no margin left to bleed INTO. */
  /* ⚠️ AND IT HAS TO LEAVE THE MEASURE BEHIND, or the bleed is a SHIFT and not
     a widening. The max-width rule up top caps this block at 100% of the column, so a negative margin slides it left and the
     right edge comes back in by the same amount — measured, the prose stayed
     at 302px and the panel stopped 18px short of the screen. The hero already
     opts out of the cap the same way; this is the second thing that has to. */
  /* ⚠️ AND THE SELECTORS HAVE TO OUTRANK THE CAP, not merely follow it. The cap
     is written as article section.move (0,1,2) and as the hydration-root child
     rule (0,1,1); a bare .move here is 0,1,0 and loses, media query or not — media
     queries add no specificity. Measured with the bare form: every other number
     on this page was correct and only the two WIDTHS were not, which is the
     shape a specificity loss always takes. */
  article section.move {
    margin-left: calc(-1 * var(--nrb-page-pad));
    max-width: none;
  }

  /* ⚖️ A CARD THAT WIDE IS NOT A CARD. Kept as one, its side borders sit exactly
     on the screen edges and its 16px radius curves away from them, leaving a
     sliver of page at each corner that reads as a mistake. The band keeps the
     two borders that still describe something — where the conclusion starts and
     where it ends — and drops the two that were only ever describing the edge of
     a box. (Both were drawn side by side as 01 a / 01 b; the band won.)

     ⚖️ AND ITS PROSE ALIGNS TO THE LINE, NOT TO THE CHAPTERS. The standing is
     not another chapter, it is where the thread ENDS, so it lines up with the
     thread rather than with the things the thread was counting.

     ⚠️ WHICH IS WHY THE PADDING READS --standing-axis AND NOT 18px. The page
     margin happens to be 18px too, so the band's text also agrees with the hero
     and the sources — worth having, not worth depending on. If the two ever
     diverge, the thread wins.

     ⚠️ AND WHY --standing-stroke-x GOES TO ZERO: with no side border there is
     no border to compensate for, and the subtraction that is right everywhere
     else would put the bead and the bridge a pixel left of the spine. */
  article section.standing-stop {
    --standing-stroke-x: 0px;
    max-width: none;                 /* see .move above — the cap, not the margin */
    margin-left: calc(-1 * var(--nrb-page-pad));
    margin-right: calc(-1 * var(--nrb-page-pad));
    padding: 16px var(--standing-axis) 18px;
    border-left: 0;
    border-right: 0;
    border-radius: 0;
  }
}
/* one flat run: consecutive source GROUPS are separate <ul>s, so the default
   list margin was opening a gap that read as a missing heading */
.sources { list-style: none; padding: 0; margin: 6px 0 0; }
.sources li { font-size: 13px; color: var(--muted); padding: 4px 0; }
.sources .src-meta { color: var(--faint); font-size: 12px; margin-left: 6px; }
/* the display sizes step down on a phone — the clamp's own floor is 32px, which is still a wall at 390px */
@media (max-width: 560px) {
  h1 { font-size: 25px; }
  .dek { font-size: 15.5px; }
  .move h2 { font-size: 17px; }
}
/* The Updates section (20.0540 M3; styled + moved to the top 2026-07-24 —
   the live-blog block right under the verdict; the counted ribbon banner
   was retired the same day). */
/* SAGA LINKS (20.0700) — cross-Gambit navigation. "Previously" is a slim
   orientation line after the disclosure; "The story continues" is a list of
   follow-up links after State of Play. Accent border = "this is a road",
   matching the updates rail's visual grammar. */
/* .saga-prev — the retired one-line "Previously" form. KEPT deliberately: the
   chain rewrite (2026-07-31) mints a new sheet hash, so every page re-links
   this stylesheet on its next bake — including pages whose MARKUP has not been
   re-baked yet. Dropping the rule would leave those in an unstyled state for
   the length of the bake sweep. Remove after a full BAKE·FULL. */
.saga-prev { font-size: 14px; color: var(--muted); border-left: 3px solid var(--accent); padding: 4px 10px; margin: 10px 0; }
/* The RETIRED link-list form. KEPT for one bake sweep, same reason as
   '.saga-prev' above: this change mints a new sheet hash, so pages whose
   MARKUP has not been re-baked yet re-link the new sheet while still carrying
   the old list markup. Remove after a full BAKE·FULL. */
.saga-continues, .saga-previous { list-style: none; padding: 0; margin: 8px 0 4px; }
/* :not(.rowcards) — the freshly-baked list wears BOTH classes, and without
   this guard the retired rail would draw its accent border down the side of
   every new card. */
.saga-continues:not(.rowcards) li, .saga-previous:not(.rowcards) li {
  padding: 8px 0 8px 14px; border-left: 2px solid var(--accent); margin: 8px 0; font-size: 15.5px; }
.saga-date { color: var(--faint); font-size: 13px; }
/* ── THE STORY ROW CARDS — DS pattern 1b, landscape list row ────────────────────
   Ported 2026-08-05 from the design system project ('7bc2a2d4',
   components/cluster/cluster-cards.html + shared/cluster-cards.css, pull-
   verified identical to the repo bundle). Re-expressed under .rowcard
   rather than the DS's .a-card/.p1b because those atoms live in the SPA
   sheets a baked page never loads (the cast-cards precedent) — the geometry,
   the focus model and the token choices are the design's; only the selector
   namespace is ours. Sentiment + source-count are absent by contract, not by
   omission: a saga chapter has no field for either. */
/* 1b is a LIST of full-width rows, not a grid — the pattern's whole point is
   that the row is landscape, so the title gets one line instead of a clamped
   column. */
.rowcards { list-style: none; padding: 0; margin: 10px 0 4px;
  display: flex; flex-direction: column; gap: 10px; }
.rowcard { position: relative; display: flex; align-items: stretch; overflow: hidden;
  background: var(--bg-surface); border: 1px solid var(--border-default); border-radius: var(--r-card);
  transition: border-color var(--dur-med) var(--ease-out), box-shadow var(--dur-med) var(--ease-out),
    transform var(--dur-med) var(--ease-out); }
.rowcard:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); transform: translateY(-2px); }
/* 1b's thumbnail: a fixed 136px column that crops rather than letterboxes */
.rowcard .sc-thumb { flex: none; width: 136px; background: var(--bg-elevated); }
.rowcard .sc-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.rowcard .sc-body { display: flex; flex-direction: column; gap: 6px; padding: 13px 16px; min-width: 0; flex: 1; }
.rowcard .sc-title { margin: 0; font-size: 16px; line-height: 1.3; font-weight: 600; color: var(--text);
  letter-spacing: normal; text-transform: none;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
/* 1b's foot, STACKED (owner 2026-08-05): the date takes its own line under the
   title and the dek reads beneath it — a chapter's date labels the chapter, it
   is not a prefix to its summary. */
.rowcard .sc-foot { display: flex; flex-direction: column; gap: 3px; margin-top: auto; }
.rowcard .sc-time { font-family: var(--font-mono); font-size: 12px;
  color: var(--faint); white-space: nowrap; }
.rowcard .sc-dek { font-size: 13.5px; line-height: 1.4; color: var(--muted);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
/* THE FOCUS MODEL, verbatim from the DS: the title anchor is the ONE real
   link and its ::after covers the card, so the whole row is the click target
   with a single tab stop — and the ring lifts to the card. */
.rowcard .sc-link { color: inherit; text-decoration: none; }
.rowcard .sc-link::after { content: ''; position: absolute; inset: 0; z-index: 1; }
.rowcard:hover .sc-link { color: var(--accent); }
.rowcard:focus-within { box-shadow: 0 0 0 3px var(--accent); border-color: transparent; }
.rowcard .sc-link:focus-visible { outline: none; }
/* an artless chapter keeps the card, just without the column */
.rowcard.sc-noart .sc-body { padding-left: 15px; }
@media (max-width: 560px) {
  .rowcard .sc-thumb { width: 104px; }
  .rowcard .sc-title { font-size: 15px; }
}
@media (prefers-reduced-motion: reduce) {
  .rowcard { transition: none; }
  .rowcard:hover { transform: none; }
}
/* ── THE NUTSHELL PANEL (PRD 20.1740) ──────────────────────────────────────
   The summary as an ice-breaker: nutshell → highlights → baked SVG charts →
   the takeaway. Every colour is a token, so the panel follows light/dark with
   the rest of the page and no chart carries a hex value of its own.
   ⚑ Charts are width:100% over a fixed viewBox, so they reflow with the
   column and never force the body to scroll sideways.
   ⛔ NO BACKTICKS ANYWHERE IN THIS SHEET — it lives inside a TS template
   literal, so a backtick here ends the string and the whole file stops
   parsing. Cost one round on this very comment. */
/* THE CARD holds the NUTSHELL ALONE (owner 2026-08-20). Everything below it —
   highlights, charts, the takeaway — is ordinary article content and sets like
   the In Short section it was asked to match, so it carries no frame. */
.nut-panel {
  margin: 22px 0 26px; padding: 18px 18px 16px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 14px; border-left: 3px solid var(--accent);
}
.nut-h { margin: 0 0 8px; font-size: 15px; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); }
.nut-lede { margin: 0; font-size: 17px; line-height: 1.6; }
.nut-sub { margin: 16px 0 8px; font-size: 12.5px; letter-spacing: .09em; text-transform: uppercase; color: var(--faint); }
/* A figure is now a standalone article block, not a row inside the card. */
/* The trend's endpoint captions live OUTSIDE the stretched plot — SVG text
   inside a preserveAspectRatio:none viewBox is distorted with the geometry. */
/* THE BAR CHART IS A LIST, NOT A DRAWING — real text at a real size. */
/* The donut + its swatch legend sit side by side until the column is narrow. */
/* Wide tables scroll INSIDE their own box — the page body never scrolls. */
/* THE FINDING — present only when the data carries one. There is deliberately
   no empty state here: an artifact with no finding renders no callout at all. */
/* The takeaway is a normal heading + paragraph now; the only thing it keeps of
   its old callout is a slightly heavier weight, because it IS the conclusion. */
.nut-insight-p { font-weight: 500; }
@media (max-width: 560px) {
  .nut-panel { padding: 15px 14px 4px; }
}
.updates-summary { font-size: 15px; color: var(--muted); margin: 6px 0 0; }
.updates { list-style: none; padding: 0; margin: 8px 0 4px; }
.updates li { padding: 8px 0 8px 14px; border-left: 2px solid var(--accent); margin: 8px 0; font-size: 15.5px; }
.updates .when { display: block; color: var(--faint); font-size: 13px; margin-bottom: 2px; }
/* Home feed — "photo + caption plate" (SPA design pattern 1j, import
   2026-07-15-cluster-cards; adapted to this static sheet's tokens + zero-JS):
   a hero photo with a SOLID caption plate overlapping its lower edge, so text
   never sits on the image. */
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 22px; margin: 24px 0; }
.card { position: relative; background: var(--bg); border: 1px solid var(--line); border-radius: 14px; overflow: hidden; padding-bottom: 14px; }
.card-media { position: relative; display: block; }
.card-media img { aspect-ratio: 16 / 8.5; object-fit: cover; width: 100%; }
/* 🔷 THE FORGED COVER (20.1850) — a story with no photograph draws one, and it
   stands in exactly the picture's box so a bulletin card and a photographed one
   are the same object on the grid. display:block because an inline SVG sits
   on the text baseline and would leave a few pixels of card ground under it. */
.card-cover { display: block; aspect-ratio: 16 / 8.5; width: 100%; }
/* The caption plate overlaps the media's lower edge on every card, so the
   forge keeps its figure in the upper two thirds — see coverForge's LAYOUTS. */
.card-tag { position: absolute; top: 10px; left: 10px; background: var(--bg); color: var(--accent);
  border: 1px solid var(--line); border-radius: 6px; padding: 3px 8px;
  font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
/* 🗞 THE SHORT STORY BADGE (20.1860) — the section chip's mirror on the card's
   other top corner. Muted rather than accent: the section is what a reader is
   choosing by, and the length is what they are choosing between. Two gold chips
   on one picture would make neither of them mean anything. */
.card-short { position: absolute; top: 10px; right: 10px; background: var(--bg); color: var(--muted);
  border: 1px solid var(--line); border-radius: 6px; padding: 3px 8px;
  font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
.card .pad { position: relative; margin: 14px; background: var(--card); border: 1px solid var(--line);
  border-radius: 10px; padding: 13px 14px 14px; }
.card-media + .pad { margin: -34px 14px 0; box-shadow: 0 10px 26px -14px rgba(10, 14, 28, .32); }
.card h3 { margin: 0 0 7px; font-size: 17px; line-height: 1.28; }
.card h3 a { color: var(--text); }
.card h3 a:hover { color: var(--accent); text-decoration: none; }
/* the ARTICLE dek is italic (the editorial register); a CARD dek is a clamped
   two-line summary and stays upright — pattern 1j's own look, unchanged */
.card .dek { font-style: normal; font-size: 14px; color: var(--muted); margin: 0 0 9px; line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.card .date { font-size: 12.5px; color: var(--faint); }

/* ══ THE CLASSIC CARDS (design 092 / PRD 20.1900) ═════════════════════════════
   EVERY rule here is scoped under .cards--classic, which only the baker emits
   and only while NRB_FEATURE_CLASSIC_CARDS is on. With the flag off not one of
   them can match, which is what makes the rollback a flag flip plus a bake
   rather than a revert.

   The design builds on the DS patterns 1a and 1b, which this sheet never
   carried — the baked page has always been 1j. So the family is stated here
   rather than imported, in the tokens designChrome puts on every baked page.

   ⚖️ THE PHOTOGRAPH DECIDES THE PATTERN. A story that was photographed gets the
   crop (1a); one that was not gets the section's rail where the crop would be
   (1b) — not an empty frame, and not a drawn cover. That is the design's ruling
   that an unphotographed story should read as a DIFFERENT KIND of card.
   ══════════════════════════════════════════════════════════════════════════ */
.cards--classic { display: flex; flex-direction: column; gap: 12px; }
.cards--classic .a-card {
  position: relative; display: flex; flex-direction: column;
  background: var(--card); border: 1px solid var(--line); border-radius: 14px;
  overflow: hidden; transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease;
}
.cards--classic .a-card:hover { border-color: var(--accent); box-shadow: 0 10px 26px -14px rgba(10,14,28,.32); transform: translateY(-2px); }
.cards--classic .a-card:hover .a-h3 { color: var(--accent); }
/* ⚑ THE TITLE IS THE ONLY REAL LINK, and its ::after covers the whole card, so
   the card is ONE click target rather than a nest of them. The action row sits
   above it on z-index — a button inside a link would be a control the reader
   cannot press. */
.cards--classic .a-h3 { margin: 0; font-family: var(--font-display); font-size: 18px; line-height: 1.2; font-weight: 700; }
.cards--classic .a-h3 { display: block; -webkit-line-clamp: none; overflow: visible; }
.cards--classic .a-title-link { color: var(--text); text-decoration: none; }
.cards--classic .a-title-link:hover { text-decoration: none; }
.cards--classic .a-title-link::after { content: ""; position: absolute; inset: 0; z-index: 1; }
/* ⚑ NO CLAMP. The row sizes to its tallest card and stretches the rest to
   match, so a dek that runs long grows its card rather than being cut — the
   same bargain the headline already takes. The 4px right inset keeps the last
   word a hair short of the edge. */
.cards--classic .c-dek {
  margin: 0 4px 0 0; font-size: 13.5px; line-height: 1.45;
  color: var(--muted); font-style: normal;
}
.cards--classic .c-shot { position: relative; overflow: hidden; }
.cards--classic .p1a-media { display: block; }
/* ⚑ THE FRAME IS NOT CENTRED — it sits a third lower (design legend 3). A news
   photograph carries its subject above the middle far more often than below it,
   so a centred crop cuts heads and keeps floor. object-position, NOT
   background-position: this is a real img element, so the picture stays in the
   document, keeps its alt and its srcset, and is fetched as an image.

   ⚑ 25%, DOWN from the design's 67% (owner 2026-08-28, at the live page:
   "move the image even more downward" — meaning the PICTURE slides down inside
   the frame, not the frame down the picture). The two readings are opposite and
   my first attempt went the wrong way: HIGHER slides the frame toward the
   image's bottom and cuts heads off the top; LOWER lets the picture sit lower,
   which is what keeps a subject's head and its headroom in shot. Chosen by
   rendering 10 / 25 / 35 / 50 / 67 on a real card and looking, not by reasoning
   about which way the words pointed — they pointed both ways.
   (No backticks in this file, ever — it is a TS template literal.) */
.cards--classic .p1a-media img { display: block; width: 100%; aspect-ratio: 3 / 2; object-fit: cover; object-position: 50% 25%; }
/* ⚖️ THE BODY FILLS THE CARD, OR THE ACTION ROW CANNOT REACH THE BOTTOM.
   margin-top:auto on .c-acts only pushes within the box it is IN, and the
   body was content-height — so the row sat under the headline and its distance
   from the card's lower edge changed with every title length. Two cards in one
   row, equal height by the grid, had their hearts at different heights. flex: 1
   makes the body the full card, and the row's offset from the bottom becomes
   the body's own padding-bottom: one number, every card.
   ⚑ min-height: 0 beside it — a flex child's automatic minimum is its content,
   which would let a long headline push the box taller than the track. */
.cards--classic .p1a-body { display: flex; flex-direction: column; flex: 1; min-height: 0; padding: 16px 16px 18px 24px; gap: 10px; }
.cards--classic .p1b-body { display: flex; flex-direction: column; flex: 1; min-height: 0; padding: 14px 16px 18px 24px; gap: 9px; }
/* ⚑ THE SECTION NEEDS AIR UNDER IT. The pill and the headline were one flex gap
   apart, the same distance the headline keeps from the action row — so the card
   read as three evenly spaced things rather than a label above a story. */
.cards--classic .c-toprow { display: flex; align-items: center; gap: 8px; min-width: 0; margin-bottom: 5px; }

/* ⚑ ONE CATEGORY TREATMENT FOR BOTH KINDS: a pill with the section's figure in
   front, the colour carried by the BORDER and the GLYPH, never by the words.
   Over a photograph the tint is replaced by a solid ground — a translucent chip
   measures about 1.7:1 on an arbitrary picture, which is no contrast at all. */
.cards--classic .c-cat {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--text); background: color-mix(in oklab, var(--cat) 12%, var(--card));
  border: 1px solid color-mix(in oklab, var(--cat) 55%, var(--line));
  border-radius: 999px; padding: 3px 9px; white-space: nowrap;
  /* ⚑ A NOWRAP PILL WITH min-width:auto CANNOT SHRINK, so in a narrow cell it
     shoves the time label out of the row and past the card's edge. The pill may
     shrink; the LABEL inside it is what gives way, with an ellipsis. */
  min-width: 0;
}
.cards--classic .c-cat-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ⚖️ THE SECTION HOLDS ITS WORDS; THE DATE IS WHAT GIVES WAY. Measured in a
   third-width cell: the baked absolute date wants 162px, and with both allowed
   to shrink the pill's label collapsed to ZERO — a section chip showing only a
   glyph. The section is the navigational label and the date is also in the
   element's datetime either way, so the priority is not a close call. */
.cards--classic .c-cat { flex: none; }
.cards--classic .c-time { min-width: 0; }
.cards--classic .c-time-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ⚖️ TWO LABELS, ONE INSTANT, AND CSS PICKS — NEVER A SCRIPT (2026-08-29).
   Ellipsising the long form gave 'преди 4...', and a clipped label says LESS
   than nothing: four hours and four days read the same. Both forms are baked
   and the rule that sets the cell width also chooses the label, so the static
   page is already right and nothing moves when the bundle lands (20.1170).
   A measurement at hydration could not do this — it would be re-deciding a
   look the bake had committed to, one frame later, in front of the reader. */
.cards--classic .c-time-s { display: none; }
.cards--classic .c-cat svg { width: 12px; height: 12px; flex: none; fill: none; color: var(--cat); stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
[data-mode="light"] .cards--classic .c-cat svg,
[data-theme="light"] .cards--classic .c-cat svg,
[data-theme="bulgaria"]:not([data-mode="dark"]) .cards--classic .c-cat svg { color: color-mix(in oklab, var(--cat) 62%, #000); }
.cards--classic .c-cat--onimg { position: absolute; left: 10px; top: 10px; z-index: 2; background: var(--bg); }

/* ⚑ THE ELAPSED TIME sits opposite the pill. --fresh carries the recency band
   and colours the dial and the hairline; the digits stay neutral, because a
   coloured number on an arbitrary photograph is the same 1.7:1 trap. */
.cards--classic .c-time {
  margin-left: auto; display: inline-flex; align-items: center; height: 22px; gap: 4px;
  padding: 0 9px 0 2px; border: 1px solid color-mix(in oklab, var(--fresh, var(--line)) 42%, transparent);
  border-radius: 999px; background: color-mix(in oklab, var(--card) 76%, transparent);
  font-family: var(--font-mono); font-weight: 600; font-size: 11.5px; letter-spacing: .04em;
  color: var(--text); white-space: nowrap;
}
.cards--classic .c-time svg { width: 20px; height: 20px; flex: none; fill: none; stroke: var(--fresh, var(--faint)); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.cards--classic .c-time--onimg { position: absolute; right: 10px; top: 10px; z-index: 2; margin: 0; background: color-mix(in oklab, var(--bg) 92%, transparent); }

/* ⚑ THE RAIL MARKS THE SECTION ON EVERY CARD, not the absence of a photograph
   (design revision 2026-08-28, chosen as variant 01 of the rail exploration).
   On a photo card it has to clear the crop, hence the higher layer.
   ⚖️ LIGHT MODE TAKES A DARKER STEP, AND THE DESIGN MEASURED WHY: Икономика,
   Технологии and Други come to 2.1–2.9:1 against a light card, under the 3:1 a
   graphic object owes. The same step the freshness bands already take. */
.cards--classic .c-rail { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--cat); z-index: 1; }
.cards--classic .a-card.p1a .c-rail { z-index: 3; }
[data-mode="light"] .cards--classic .c-rail,
[data-theme="light"] .cards--classic .c-rail,
[data-theme="bulgaria"]:not([data-mode="dark"]) .cards--classic .c-rail { background: color-mix(in oklab, var(--cat) 62%, #000); }

/* ⚑ THE ACTION ROW IS BAKED HIDDEN AND REVEALED BY HYDRATION (20.0360's own
   bargain). Neither number can be known at bake time, and a baked "0 views" is
   not a neutral placeholder — it is a wrong number a reader cannot tell from a
   right one. z-index clears the title's covering ::after, or the buttons would
   be unpressable. */
/* ⚑ TWO VERBS, NO NUMBERS (owner 2026-08-28). Without the counts beside them
   the buttons are icon-only, so they need a real hit area of their own rather
   than the incidental one a number used to give them. */
.cards--classic .c-acts { display: flex; align-items: center; gap: 6px; margin-top: auto; }
/* Reserve the action row before hydration, without exposing inert controls. */
.cards--classic .c-acts[hidden] { visibility: hidden; pointer-events: none; }
.cards--classic .c-act {
  position: relative; z-index: 2; display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; margin-left: -6px;
  padding: 0; border: 0; border-radius: 999px; background: transparent; color: var(--muted);
  transition: color .16s ease, background .16s ease;
}
.cards--classic button.c-act:hover { background: color-mix(in oklab, var(--cat) 14%, transparent); }
.cards--classic .c-act svg { width: 17px; height: 17px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.cards--classic button.c-act { cursor: pointer; }
.cards--classic button.c-act:hover { color: var(--accent); }
.cards--classic .c-act--like:hover { color: var(--hot, #ff5a5f); }
.cards--classic .c-act--like[aria-pressed="true"] { color: var(--hot, #ff5a5f); }
.cards--classic .c-act--like[aria-pressed="true"] svg { fill: currentColor; }
.cards--classic .c-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cards--classic .a-title-link:focus-visible::after { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ── THE GRID (design §02, revised 2026-08-28: DYNAMIC HEIGHT) ───────────────
   ⚖️ NO ROW UNIT, NO SPANS, NO CLAMPS. The first cut fixed the row at 186px and
   spanned a photo card across two of them, which meant a long headline had to be
   cut to fit a number chosen in advance. Grid already sizes a row to its tallest
   item and stretches the rest to match, so cards sharing a row come out equal
   height WITHOUT anything being trimmed — the height follows the words instead
   of the words following the height.

   The track is SIX columns so both shapes divide it evenly: a photo story takes
   the full width, a short takes three (half). A six-track also leaves the
   three-up variant (span 2) reachable without re-cutting the grid.

   ⚖️ **TWO CARDS SIDE BY SIDE IS A DESKTOP SHAPE — 1024px, NOT 720** (owner
   2026-08-28: *"for mobile never two on same rows"*). A phone in PORTRAIT was
   never at risk; the column layout below the breakpoint already gives one card
   a row. The exposure was LANDSCAPE and tablets — an iPhone Pro Max turned
   sideways is 932px, so at a 720px threshold it paired. 1024 is above every
   phone in either orientation, which is what makes the rule true rather than
   usually true.

   ⚠️ Above 1100px the design still leaves the shape an OPEN QUESTION (§09 q3).
   Until it is answered this simply widens — no invented column. */
@media (min-width: 1024px) {
  .cards--classic {
    display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
    grid-auto-flow: row; gap: 16px; align-items: stretch;
  }
  .cards--classic .a-card { height: 100%; }
  .cards--classic .a-card.p1a { grid-column: 1 / -1; }
  /* ⚑ THREE ON A ROW. Six tracks divide by three, so a short takes two of them.
     At two-up a run of three came out 2 + 1 — two half cards and a full-width
     orphan beneath, which reads as a section break where none exists. */
  .cards--classic .a-card.is-short { grid-column: span 2; }
  /* a leftover PAIR goes to halves, not thirds: two thirds side by side would
     leave a third of the row empty, and a hole is worse than a wider card */
  .cards--classic .a-card.is-short.is-half { grid-column: span 3; }
  /* a lone leftover takes the whole width rather than sitting in a third */
  .cards--classic .a-card.is-short.is-solo { grid-column: 1 / -1; }
  /* ⚑ A THIRD OF THE ROW TAKES A SMALLER HEADLINE — measured, not fitted. The
     18px step that reads well at half width crowds at a third; the wider
     leftovers keep it, which is why this is scoped away from them. */
  .cards--classic .a-card.is-short:not(.is-solo):not(.is-half) .a-h3 { font-size: 16px; }
  .cards--classic .a-card.is-short:not(.is-solo):not(.is-half) .p1b-body { padding: 12px 12px 18px 20px; }
  /* THE THIRD-WIDTH CELL takes the short label. Measured: the pill leaves the
     time roughly 70px, the long date wants 162px and the short one about 35px.
     Hydration rewrites BOTH spans — the long to 'преди 4 часа', the short to a
     narrow unit — so whichever one is showing is the one that stays true. */
  .cards--classic .a-card.is-short:not(.is-solo):not(.is-half) .c-time-t { display: none; }
  .cards--classic .a-card.is-short:not(.is-solo):not(.is-half) .c-time-s { display: inline; }
  /* ⚑ THE PICTURE IS A CONSTANT BAND, AND ITS CROP SITS A THIRD LOW.
     The glass-panel overlay that stood here is REVERTED (design revision
     2026-08-28): the owner asked to lower the picture's centre, and the design
     answers that inside the band — object-position 50% 67% — rather than by
     putting the words on top of the photograph. Same goal, and it keeps the
     words on a ground of their own, which is the cheaper promise to keep.
     A longer headline grows the CARD; it never shrinks the crop. */
  .cards--classic .a-card.p1a .c-shot { flex: none; height: 240px; }
  .cards--classic .a-card.p1a .p1a-media,
  .cards--classic .a-card.p1a .p1a-media img { width: 100%; height: 100%; aspect-ratio: auto; }
  .cards--classic .a-card.is-short .p1b-body { padding: 12px 14px 18px 22px; gap: 6px; }
}
@media (prefers-reduced-motion: reduce) {
  .cards--classic .a-card, .cards--classic .c-act { transition: none; }
  .cards--classic .a-card:hover { transform: none; }
}
.tagline { color: var(--muted); font-size: 17px; margin: 6px 0 0; }
.archive-month { margin: 26px 0 6px; color: var(--muted); }
.archive-list { list-style: none; padding: 0; }
.archive-list li { padding: 7px 0; border-bottom: 1px solid var(--line); display: flex; gap: 14px; }
.archive-list .date { color: var(--faint); font-size: 13.5px; min-width: 110px; }
.site-footer { border-top: 1px solid var(--line); margin-top: 44px; }
.site-footer .shell { padding-top: 16px; padding-bottom: 26px; font-size: 13.5px; color: var(--faint);
  display: flex; gap: 16px; flex-wrap: wrap; }
.pulse-bar { display: flex; align-items: center; gap: 16px; margin: 26px 0 4px; padding-top: 14px;
  border-top: 1px solid var(--line); font-size: 14px; color: var(--muted); }
.pulse-bar[hidden] { display: none; }
.pulse-stat { display: inline-flex; align-items: center; gap: 6px; }
.pulse-label { color: var(--faint); }
.pulse-like { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; background: none;
  border: 1px solid var(--line); border-radius: 999px; padding: 5px 13px; color: var(--muted); font: inherit; }
.pulse-like:hover { border-color: var(--accent); }
.pulse-heart { color: var(--faint); line-height: 1; }
.pulse-like[aria-pressed="true"] { border-color: var(--bad); color: var(--bad); }
.pulse-like[aria-pressed="true"] .pulse-heart { color: var(--bad); }

/* ── THE /games/ RULES PAGE (20.1310 §9.4, P5) — zero-JS teaching boards ──
   gp-* dresses GamesRulesView's plain tables. The palette below is the
   board thumbnails' own pastel family, so the static face and the live
   board tell one visual story. */
.gp-lead { color: var(--muted); margin: 4px 0 22px; }
.gp-game { margin: 30px 0 34px; }
.gp-game h2 { margin: 0 0 6px; }
.gp-rule { font-size: 16px; margin: 0 0 14px; }
.gp-howto { font-size: 15px; margin: 16px 0 4px; }
.gp-rules { margin: 6px 0 0 0; padding-left: 22px; font-size: 15px; }
.gp-rules li { margin: 4px 0; }
.gp-board { border-collapse: collapse; border: 3px solid var(--fg, #0d1220); border-radius: 6px; }
.gp-board td { width: 34px; height: 34px; text-align: center; vertical-align: middle;
  font: 600 16px/1 var(--font-body, system-ui); border: 1px solid rgba(120,140,190,.35); }
/* Sudoku's static face: givens bold on paper white, seams heavier than
   hairlines — the same geometry the live module draws (neighbour test,
   never a modulo on the perimeter). */
.gp-board .gp-given { background: #fdfdf8; color: #0d1220; }
.gp-board .gp-empty { background: #fdfdf8; }
.gp-board .gp-rb { border-right: 3px solid #0d1220; }
.gp-board .gp-bb { border-bottom: 3px solid #0d1220; }
/* Oasis's static face: the lands in the thumb's pastel family, the answer's
   palms placed. */
.gp-board .gp-land-0 { background: #cfe8d8; }
.gp-board .gp-land-1 { background: #f6e3c1; }
.gp-board .gp-land-2 { background: #dcd6f2; }
.gp-board .gp-land-3 { background: #f3d1cf; }
.gp-board .gp-land-4 { background: #cfe3f2; }
.gp-board .gp-land-5 { background: #e6f0c9; }
.gp-board .gp-land-6 { background: #f2d9ec; }
.gp-board .gp-land-7 { background: #d8eee9; }


/* ══ ✅ THE VERDICT CHECK (owner 2026-10-01 — recipe in verdictCheckCss.ts) ══ */
.vcheck { --vc: var(--muted); }
.vcheck--confirmed { --vc: var(--ok); }
.vcheck--debunked  { --vc: var(--bad); }
.vcheck--disputed  { --vc: var(--warn); }
.vcheck--unclear   { --vc: var(--muted); }

/* the question — the HEADER MIRRORS THE FOOTER (owner 2026-10-01): the brand
   badge's BARE form (designChrome .nrb-mark, plate dropped) in an 86 px box
   whose divider lines up with the story fund's "since" box below · gap 15 ·
   the lead in GOLD MONO CAPS 12 px / .12em · the claim 18 px */
.vcheck-ask { display: flex; align-items: stretch; gap: 15px; padding: 16px 18px; background: var(--bg-surface); }
.vcheck-logo { flex: none; width: 86px; display: flex; align-items: center; justify-content: center;
  border-right: 1px solid var(--border-default); padding-right: 12px; margin-right: 2px; box-sizing: border-box; }
.vcheck-logo .nrb-mark { --nrb-mark-cap: 25px; width: auto; height: auto; container-type: normal; border-radius: 0;
  background: none; box-shadow: none; padding-bottom: calc(var(--nrb-mark-cap) * .34); line-height: 1; }
.vcheck-logo .nrb-mark:hover { box-shadow: none; }
.vcheck-logo .nrb-mark span { position: static; display: block; font-size: var(--nrb-mark-cap); letter-spacing: -.03em; text-align: left; }
.vcheck-logo .nrb-mark::after { left: 0; top: auto; bottom: calc(var(--nrb-mark-cap) * .1); width: 100%;
  height: max(2px, calc(var(--nrb-mark-cap) * .115)); }
/* 📱 ON A PHONE THE LOGO BOX GOES (owner 2026-10-02: "for mobile remove this
   logo section as it is breaking the layout") — at 375 px the 86 px box left the
   claim three words a line and the header taller than the picture. The words
   take the full width; the site's phone breakpoint (560 px). */
@media (max-width: 560px) { .vcheck-logo { display: none; } }
.vcheck-words { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
.vcheck-lead { margin: 0; color: var(--accent); font: 700 12px/1.35 var(--font-mono); letter-spacing: .12em; text-transform: uppercase; }
.vcheck-words > .vcheck-claim { margin: 8px 0 0; }
.vcheck-claim { display: flex; align-items: flex-start; gap: 8px;
  font: 600 18px/1.35 var(--font-display); color: var(--text-primary); }
/* the big quote opens the claim and never closes it: 43px, muted at 70%,
   nudged -2px — in each edition's own mark (bg/de „ · en/pt “ · fr/es «);
   the LOW „ sits on the baseline, so it alone is lifted by .38 of its size */
.vcheck-qm { flex: none; font: 800 43px/.72 var(--font-display); user-select: none; margin-top: -2px;
  color: color-mix(in srgb, var(--text-tertiary) 70%, transparent); }
.vcheck-qm--low { margin-top: calc(-2px - 43px * .38); }
.vcheck-qt { min-width: 0; }

/* the verdict tab — the category plate's sibling, in the verdict colour */
.vcheck-tab { display: inline-flex; align-items: center; gap: .45em; z-index: 3;
  padding: 4px 12px; border-radius: 8px; background: var(--vc); color: #050a24;
  font: 700 12px/1.6 -apple-system, "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
  letter-spacing: .05em; text-transform: uppercase; white-space: nowrap; }
.vcheck-tab-m { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%;
  background: #050a24; color: var(--vc); font: 900 10px/1 var(--font-body); }

/* the answer — the facts, tinted 10% in the verdict colour over a 2px seam
   (a container since 2026-10-01: the text, then the story fund's slot) */
.vcheck-facts-t { margin: 0; }
.vcheck-facts > [data-nrb-slot="story-fund"]:empty { display: none; }
.vcheck-facts--slot:has(> [data-nrb-slot="story-fund"]:empty) { display: none; }
.vcheck-facts { position: relative; margin: 0; padding: 12px 18px;
  background: color-mix(in srgb, var(--vc) 10%, var(--bg-surface)); border-top: 2px solid var(--vc);
  font: 400 15px/1.5 var(--font-body); color: var(--text-secondary); }

/* ── GAMBIT: all three inside the story's frame ── */
.hero.has-vcheck .vcheck-ask { border-radius: 10px 10px 0 0; }
.hero.has-vcheck img { border-radius: 0 !important; }
.hero.has-vcheck > .vcheck-facts { border-radius: 0 0 10px 10px; }
/* the folder tab: flush on the seam, rising into the picture's lower-left */
.hero.has-vcheck > .hero-shot > .vcheck-tab { position: absolute; left: 16px; bottom: 0; border-radius: 8px 8px 0 0; }
.hero.has-vcheck .hero-status-chip.category { z-index: 4; }
/* a check with no facts closes the frame on the picture */
.hero.has-vcheck:not(:has(> .vcheck-facts:not(.vcheck-facts--slot))) img { border-radius: 0 0 10px 10px !important; }
/* the unlit verdicts: the calm rail moves from the picture up to the figure */
.hero.has-vcheck:not(.hero--lit) { padding: 4px; border-radius: 16px; background: var(--card);
  box-shadow: inset 0 0 0 1px var(--line); }
.hero.status-complete.has-vcheck:not(.hero--lit) picture { padding: 0; background: none; box-shadow: none; border-radius: 0; }

/* ── BULLETIN: the same parts in a card, the tab a FOLDER TAB on its own row ── */
.vcheck-card { margin: 14px 0 18px; border-radius: 12px; overflow: hidden; border: 1px solid var(--border-default); }
.vcheck-card > .vcheck-tabrow { display: flex; padding: 0 16px; background: var(--bg-surface); }
.vcheck-card > .vcheck-tabrow > .vcheck-tab { border-radius: 8px 8px 0 0; }


/* ══ THE WIDGET SLOT ══════════════════════════════════════════════════════
   ONE surface between the header and the feed, holding exactly ONE widget —
   and a ROTATION SURFACE: more widgets will take turns in it, and none of them
   may disturb what is around it. The slot owns its OWN spacing; never add
   margin per-widget, or the gap under the header changes with whatever happens
   to be standing in it. */

/* ██ ONE HEIGHT, FOREVER, WITH OR WITHOUT JAVASCRIPT ██ (owner 2026-08-07)
   The baked page holds the TITLE widget alone and the slot is sized to it.
   Script then ADDS the game and swaps it into the same box. The two are never
   visible together — not stacked, not briefly, not with no script — so the
   slot's height is a constant and nothing above or below it can move, ever.

   That constant is the whole point: "on top" has to mean literally on top. An
   enhancement that arrives by pushing the feed down is not an enhancement, it
   is a layout bug with good intentions.

   Two earlier versions failed this and are worth remembering. One animated the
   height AND kept a min-height reserve; the reserve won at rest and the
   animation only ever fought it. The other baked both widgets STACKED and let
   the slot collapse when the game took over — readable with no script, and
   still 77px of travel through the feed on every page load. */

/* THE SWAP IS ONE GRID CELL, not two boxes taking turns being absolute.
   Both layers are placed at `1 / 1`, so the slot measures ONE widget at every
   instant of the swap, including the middle of it, and the departing one needs
   no inset arithmetic to line up with the arriving one. The absolute-position
   version had to mirror the slot's own padding in `top/left/right` — three
   numbers that were correct in the SPA and wrong inside `.shell`, because the
   two hosts pad the slot differently. */

/* THE HANDOVER'S TEMPO (owner 2026-08-07: slower).
   The design's numbers were 520/300/480/420+90ms. At that speed, with the
   puzzle resolving in ~100ms, the swap was over before the eye found it. These
   are the same CURVE, stretched — one variable so the whole handover stays in
   proportion rather than drifting into four unrelated numbers. */
.nrb-slot { --slot-swap: 900ms;
  position: relative; margin: 0 auto; padding: 18px 24px 0;
  max-width: 1680px; overflow: visible;   /* the 🎉 corner mark must not clip */
  display: grid; grid-template-columns: minmax(0, 1fr); }

/* On a BAKED page the slot lives inside `.shell`, which already supplies the
   column and its gutters — so it must add neither. (`.shell` does not exist in
   the SPA, so this rule is inert there: one sheet, two hosts, no fork.) */
.shell > .nrb-slot { padding: 18px 0 0; max-width: none; }

/* THE OCCUPANT SITS ON THE CARD EDGE.
   `--nrb-column` IS the card width (see feed.css), so the layer takes it
   directly — no padding, no inset, nothing to double-count. The grid grows to
   fit the same column plus its own gutters, so the two land on one line by
   construction rather than by two numbers agreeing. A baked page has no
   `--nrb-column`; the 725px fallback is the `.shell` column, so the two
   surfaces line up without either knowing about the other. */
/* EVERY layer sits in the one cell — the incoming one and the outgoing one
   both — so the slot is always exactly one widget tall. `--nrb-slot-h` is the
   TITLE widget's height, because the title is the widget the static page ships
   and therefore the one that sets the reserve. */
.nrb-slot-layer { grid-area: 1 / 1; box-sizing: border-box; margin: 0 auto; width: 100%;
  max-width: var(--nrb-column, 725px);
  height: var(--nrb-slot-h, 86px);
  display: flex; flex-direction: column; justify-content: center; }

/* THE WIDGET LEAVING. It keeps its box (same cell) and simply stops being part
   of the picture: up, back, blurred, gone. `pointer-events: none` because for
   ~900ms it is still a real element sitting on top of the one that replaced it,
   and a ghost that swallows the first click on the new widget is worse than no
   animation at all. */
.nrb-slot-layer--out { pointer-events: none;
  opacity: 0; transform: translateY(-14px) scale(.96); filter: blur(2px);
  transition: opacity calc(var(--slot-swap) * .58) var(--ease-out, ease-out),
              transform calc(var(--slot-swap) * .92) var(--ease-out, ease-out),
              filter calc(var(--slot-swap) * .58) var(--ease-out, ease-out); }

/* THE WIDGET ARRIVING — it trails the departure rather than crossing it, so
   the eye reads one handover instead of two things happening at once. */
.nrb-slot-layer.is-entering {
  animation: nrbSlotIn calc(var(--slot-swap) * .81) var(--ease-out, ease-out)
             calc(var(--slot-swap) * .17) both; }
@keyframes nrbSlotIn {
  from { opacity: 0; transform: translateY(12px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* ── occupant 1: the title. Never absent, so the slot has no empty state. ── */
.nrb-widget-title { padding: 6px 0 2px; }
.nrb-widget-title-h { margin: 0; font: 700 40px/1.05 var(--font-display, system-ui);
  letter-spacing: -.03em; color: var(--text-primary, #0d1220); }
.nrb-widget-title-d { margin: 6px 0 0; font: 400 15px/1.5 var(--font-body, system-ui);
  color: var(--text-secondary, #55607a); }
@media (max-width: 560px) {
  .nrb-widget-title-h { font-size: 30px; }
}

/* ── occupant 2: the game, in its ROW form ─────────────────────────────── */
.nrb-widget-play { display: flex; align-items: center; gap: 14px; width: 100%;
  position: relative; overflow: visible; text-align: left; cursor: pointer;
  padding: 12px 16px; border-radius: 14px;
  border: 1px solid var(--border-default, rgba(120,140,190,.22));
  background: var(--bg-surface, #fff);
  transition: border-color var(--dur-fast, .14s) var(--ease-out, ease-out),
              box-shadow var(--dur-fast, .14s) var(--ease-out, ease-out),
              transform var(--dur-fast, .14s) var(--ease-out, ease-out); }

/* HOVER — the accent belongs to the CHEVRON, not the border. The row is a
   "quiet list-row", per the design: on hover it firms up (--border-strong plus
   --shadow-sm) and the chevron is the only thing that colours and moves. An
   earlier pass put --accent on the border, which turned a restrained row into
   a highlighted banner and lost the distinction the design is making. */
.nrb-widget-play:hover { border-color: var(--border-strong, rgba(90,110,160,.42));
  box-shadow: var(--shadow-sm, 0 1px 2px rgba(16,24,48,.06), 0 4px 12px -6px rgba(16,24,48,.10)); }
.nrb-widget-play:hover .nrb-play-row-chev { transform: translateX(2px); color: var(--accent, #d9a53f); }
/* A press, not a bounce. */
.nrb-widget-play:active { transform: scale(.994); }
/* ── THE HIT TARGET IS AN INSET BUTTON (design .nrb-play-open, 2026-09-02) ──
   The row became a DIV the day it grew three level buttons: a button inside
   a button is not HTML. The whole row still opens the board — this button is
   inset:0 under everything — and the visible children sit above it with
   pointer-events:none so a click anywhere lands on it; the LEVELS re-enable
   pointer events, and stop propagation, so choosing one never opens the row. */
.nrb-play-open { position: absolute; inset: 0; z-index: 0; margin: 0; padding: 0; border: 0;
  background: none; border-radius: inherit; cursor: pointer; font: inherit; color: inherit;
  -webkit-tap-highlight-color: transparent; }
.nrb-play-open:focus-visible { outline: 3px solid var(--accent, #d9a53f); outline-offset: 3px; }
.nrb-widget-play > .nrb-play-thumb, .nrb-widget-play > .nrb-play-row-txt,
.nrb-widget-play > .nrb-play-row-chev, .nrb-widget-play > .nrb-play-row-act { position: relative; z-index: 1; pointer-events: none; }
.nrb-widget-play > .nrb-play-party { pointer-events: none; }
/* 🎚 THE LEVELS (design .nrb-play-levels / .nrb-play-level, owner 2026-09-02:
   "games need to have 3 levels of difficulty") — three small mono words with a
   dot between them, the played ones in --success with a tick. They wrap on a
   narrow card rather than shorten: they are the card's targets, not its meta. */
.nrb-play-levels { margin-top: 3px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; pointer-events: auto; }
.nrb-play-level { position: relative; display: inline-flex; align-items: center; gap: 4px;
  padding: 0; border: 0; background: none; cursor: pointer;
  font: 500 10.5px/1.3 var(--font-mono, monospace); letter-spacing: .05em;
  color: var(--text-primary, #0d1220);
  transition: color var(--dur-fast, .14s) var(--ease-out, ease-out); }
.nrb-play-level + .nrb-play-level::before { content: "·"; position: absolute; left: -8px; color: var(--border-strong, rgba(90,110,160,.42)); }
.nrb-play-level:hover { color: var(--accent, #d9a53f); }
.nrb-play-level.is-active { color: var(--success, #2f9e6f); font-weight: 600; }
.nrb-play-level.is-active:hover { color: var(--success, #2f9e6f); }
.nrb-play-tick { flex: none; }
.nrb-play-level:focus-visible { outline: 2px solid var(--accent, #d9a53f); outline-offset: 3px; border-radius: 3px; }

/* ── THE COMPACT CHASSIS (design shared/play.css `.nrb-play--compact`, pulled
   2026-09-01; owner: "we now have in our design a state called compact, lets
   use it all across the site").
   ⚖️ NO FRAME, NO SURFACE — the row sits inline in whatever holds it. Border,
   background and horizontal padding go to zero, the gap tightens 14 → 9, the
   vertical padding is 6px, and the board drops to 34px. The hover keeps only
   what a frameless row can show — the chevron's nudge — because a border that
   is not there cannot firm up and a shadow under nothing is a smudge.
   ⚑ EVERYTHING ELSE IS THE SAME ROW: the flight, the levels, the marks, the
   solved state. That is the whole point of a chassis rather than a second
   component, and why the class is ADDED to .nrb-widget-play rather than
   replacing it. */
.nrb-widget-play.nrb-play--compact { gap: 9px; padding: 6px 0;
  background: none; border: 0; border-radius: 0; box-shadow: none; }
.nrb-widget-play.nrb-play--compact:hover { border: 0; box-shadow: none; }
.nrb-widget-play.nrb-play--compact:active { transform: none; }
.nrb-widget-play.nrb-play--compact .nrb-play-row-t { font-size: 15px; }

/* ── THE INLINE MOTION (design handoff design_handoff_play_inline_row, pulled
   2026-09-02 — variant D3.12 "edge warm" out of the 300-item study
   explorations/play-inline-motion-300.html).
   ⚖️ AN INLINE ROW HAS NO FRAME TO LIGHT, SO ITS STATES ARE THE FIRST FRAMES
   OF THE OPENING FLIGHT: on hover the board grows 6% from its LEFT edge and
   its edge warms to the accent under --shadow-md, the text drifts 2px toward
   its full-screen position, the title takes the accent; a press pushes the
   same pose further (1.14, 4px) in 120ms and the flight launches from there;
   letting go springs back over 340ms with a 0.6% undershoot. Nothing new
   appears, nothing reflows — transform, box-shadow, filter and colour only.
   ⚠️ TWO TRANSITIONS ON PURPOSE: the base rule carries the 340ms spring, the
   hover rule its own 240ms ease-out. CSS reads the transition of the state
   being ENTERED, so hover-in is the ease-out and un-hover is the spring; one
   shared transition would lose the spring. Keep the split.
   ⚑ THE ROW WRAPS AN INSET BUTTON (the DS's anatomy, since the levels arrived
   2026-09-02), so :focus-within on the row is what plays the hover pose for
   a Tab stop — keyboard sees what the pointer sees. The
   chevron keeps the framed row's 120ms nudge (handoff decision 3: the quick
   "lead" element). Touch: no hover, the press IS the state, and the iOS tap
   flash is silenced so the pose is the only acknowledgement. */
.nrb-widget-play.nrb-play--compact { -webkit-tap-highlight-color: transparent; }
.nrb-widget-play.nrb-play--compact .nrb-play-row-t {
  transition: color 200ms var(--ease-out, cubic-bezier(.2,.7,.2,1)); }
.nrb-widget-play.nrb-play--compact .nrb-play-thumb {
  transform-origin: left center;
  transition: transform 340ms cubic-bezier(.34,1.56,.64,1),
              box-shadow 340ms cubic-bezier(.34,1.56,.64,1),
              filter 240ms var(--ease-out, cubic-bezier(.2,.7,.2,1)),
              opacity var(--dur-fast, 120ms) var(--ease-out, cubic-bezier(.2,.7,.2,1)); }
.nrb-widget-play.nrb-play--compact .nrb-play-row-txt {
  transition: transform 340ms cubic-bezier(.34,1.56,.64,1); }
.nrb-widget-play.nrb-play--compact:hover .nrb-play-thumb,
.nrb-widget-play.nrb-play--compact:focus-within .nrb-play-thumb {
  transform: scale(1.06);
  box-shadow: 0 0 0 1px var(--accent, #d9a53f), var(--shadow-md, 0 8px 24px -10px rgba(10,20,60,.16));
  filter: brightness(1.06);
  transition: transform 240ms var(--ease-out, cubic-bezier(.2,.7,.2,1)),
              box-shadow 240ms var(--ease-out, cubic-bezier(.2,.7,.2,1)),
              filter 240ms var(--ease-out, cubic-bezier(.2,.7,.2,1)),
              opacity var(--dur-fast, 120ms) var(--ease-out, cubic-bezier(.2,.7,.2,1)); }
.nrb-widget-play.nrb-play--compact:hover .nrb-play-row-txt,
.nrb-widget-play.nrb-play--compact:focus-within .nrb-play-row-txt {
  transform: translateX(2px); transition: transform 240ms var(--ease-out, cubic-bezier(.2,.7,.2,1)); }
.nrb-widget-play.nrb-play--compact:hover .nrb-play-row-t,
.nrb-widget-play.nrb-play--compact:focus-within .nrb-play-row-t { color: var(--accent, #d9a53f); }
/* press: continue along the flight path — no separate "pressed" look */
.nrb-widget-play.nrb-play--compact:active .nrb-play-thumb { transform: scale(1.14); transition-duration: 120ms; }
.nrb-widget-play.nrb-play--compact:active .nrb-play-row-txt { transform: translateX(4px); transition-duration: 120ms; }
/* the small board — the DS names it by attribute, so the port does too */
.nrb-play-thumb[data-play-thumb="sm"] { width: 34px; height: 34px; border-radius: 5px; }
/* a solved compact row keeps its leading mark as the one thing drawn around it */
.nrb-widget-play.nrb-play--compact.is-solved { padding-left: 10px; border-left-width: 4px; border-left-style: solid; }

/* The thumbnail is the design system's own asset — it already carries the
   rounded frame and the pastel region palette, so the row adds no padding or
   background of its own and cannot drift from the board's colours. */
.nrb-play-thumb { position: relative; flex: none; width: 46px; height: 46px;
  border-radius: 8px; overflow: hidden; line-height: 0; }
.nrb-play-thumb img { display: block; width: 100%; height: 100%; object-fit: cover;
  user-select: none; -webkit-user-drag: none; }

.nrb-play-row-txt { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.nrb-play-row-t { font: 600 16px/1.2 var(--font-display, system-ui); color: var(--text-primary, #0d1220); }
.nrb-play-row-m { font: 500 11.5px/1.3 var(--font-mono, monospace);
  color: var(--text-tertiary, #7c86a0); letter-spacing: .02em; }
.nrb-play-row-chev { flex: none; font-size: 22px; line-height: 1; color: var(--text-tertiary, #7c86a0);
  transition: transform var(--dur-fast, .14s) var(--ease-out, ease-out),
              color var(--dur-fast, .14s) var(--ease-out, ease-out); }
/* ── THE CASE IS THE STYLESHEET'S, NOT THE DATA'S (2026-08-07) ──────────────
   Every SHOUTING label on this surface used to be an upper-case literal in the
   component, which is how the whole overlay came to be Bulgarian on five
   editions. The words now come from the locale catalogue in sentence case —
   the only form that is correct for a German noun or a French accent in a
   sentence — and these rules do the shouting. */
.nrb-play-row-act { flex: none; font: 700 11px/1 var(--font-mono, monospace); letter-spacing: .08em;
  padding: 9px 13px; border-radius: 9px; white-space: nowrap; text-transform: uppercase;
  border: 1px solid var(--accent, #d9a53f); color: var(--accent, #d9a53f); }

/* (A `.nrb-play-noscript` rule lived here for a few hours on 2026-08-07, for a
   line telling a script-less reader that the puzzle needs JavaScript. It went
   with the row it explained: the static page no longer offers the game at all,
   so there is nothing left to apologise for.) */

/* SOLVED — a real 4px border, so it follows the corner radius rather than
   painting a straight bar across it. */
.nrb-widget-play.is-solved { border-left: 4px solid var(--play-win, #2f9e6f);
  background: linear-gradient(90deg, rgba(47,158,111,.10), rgba(47,158,111,0) 42%),
              var(--bg-surface, #fff); }
.nrb-play-thumb-tick { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; background: rgba(47,158,111,.72); color: #fff;
  font: 700 22px/1 var(--font-body, system-ui); }
/* A corner MARK, not a chip — bare glyph, surface-coloured drop shadow. */
.nrb-play-party { position: absolute; top: -9px; right: -7px; font-size: 19px; line-height: 1;
  filter: drop-shadow(0 1px 2px var(--bg-surface, #fff)); }

/* ── THE ROW OF CARDS (20.1310 §2.3, owner 2026-08-12) ──────────────────────
   Every visible game SIDE BY SIDE at every width — no stack, no carousel, no
   rotation among games. The reduction is a CONTAINER QUERY, never a JS
   branch: the slot is baked, and a JS reduction would give a script-off phone
   the full card and then move pixels on hydration. The ladder drops the LANE
   WORDS first (the meta line), then the chevron — the thumb and the name are
   the card's identity and survive every width. All cards share the slot's one
   height by construction: same flex row, same padding, same 46px thumb. */
/* ⚠️ COMPOUND, AND IT HAS TO BE. The element wears BOTH classes
   (nrb-widget-host nrb-play-row), and .nrb-widget-host further down this
   same sheet sets display:block. Equal specificity, so SOURCE ORDER decided
   it — the host is ~600 lines later, so it won, the row computed to block,
   and the two games stacked. It reached production looking like a missing
   layout when the layout was there and outranked. Naming both classes puts
   this at 0,2,0 and settles it wherever either rule moves to. */
/* ⚖️ THE GAP IS THE STORY GRID'S GAP (owner 2026-08-16: "properly align with
   the topics bellow"). Two equal cards at 22px sit their seam exactly where
   the two-column .cards grid sits its own, so the row reads as part of the
   page rather than a band laid over it. ⚠️ 22px is .cards' gap in siteCss.ts —
   if that moves, this moves. */
.nrb-widget-host.nrb-play-row { position: relative; display: flex; gap: 22px;
  align-items: stretch; container-type: inline-size; }
.nrb-play-row .nrb-widget-play { flex: 1 1 0; min-width: 0; }
/* A narrow card must SHORTEN its words, never grow a second line — a taller
   card is a taller slot, which the fixed-height law forbids. */
.nrb-play-row .nrb-play-row-t,
.nrb-play-row .nrb-play-row-m { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@container (max-width: 560px) {
  .nrb-play-row .nrb-play-row-m { display: none; }
  .nrb-play-row .nrb-widget-play { gap: 10px; padding: 10px 12px; }
  /* ⚠️ THE COMPACT CHASSIS KEEPS ITS OWN NUMBERS HERE. The reduction above is
     the FRAMED row tightening for a narrow card; it shares the compact rule's
     specificity and comes after it, so in a 286px column it quietly won —
     measured: gap 10 and padding 10/12 where the design says 9 and 6/0, with
     every other compact property landing. One more class on the selector is
     what settles it, and it lives in this block so nobody reads one without
     the other. */
  .nrb-play-row .nrb-widget-play.nrb-play--compact { gap: 9px; padding: 6px 0; }
}
@container (max-width: 400px) {
  /* the FRAMED card sheds its chevron; the compact card keeps it (owner
     2026-09-02: "in web right chevron that is also not there") */
  .nrb-play-row .nrb-widget-play:not(.nrb-play--compact) .nrb-play-row-chev,
  .nrb-play-row .nrb-play-row-act { display: none; }
}

/* ── ✨ THE DISTANT BURST (design explorations/play-icon-fireworks.html, owner
   2026-09-02: "for both games … distant burst animation … randomly every 35
   sec to 1 min"). Eighteen 2px sparks bloom from the board's centre, drift
   28–46px and are gone in 1.2s; transform + opacity only; the layer is
   zero-size and appended to the card by the client (play.tsx), positioned
   from the live thumb rect. ⚠️ Any ancestor with overflow:hidden cuts it —
   the card and the row are overflow:visible for the 🎉 mark already. */
.nrb-play-burst { position: absolute; width: 0; height: 0; pointer-events: none; z-index: 3; }
.nrb-play-burst i { position: absolute; left: 0; top: 0; width: 2px; height: 2px; border-radius: 50%;
  opacity: 0; background: var(--spark); box-shadow: 0 0 3px var(--spark);
  animation: nrbPlayBurst 1200ms cubic-bezier(.2,.7,.3,1) forwards; animation-delay: var(--d, 0s);
  will-change: transform, opacity; }
@keyframes nrbPlayBurst {
  0% { transform: translate(-50%, -50%) scale(.2); opacity: 0; }
  12% { opacity: 1; }
  70% { opacity: .45; }
  100% { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(.85); opacity: 0; }
}

/* The row's ONE link — the baked /games/ rules page (§9.4). A corner mark
   like the 🎉, so the slot's fixed height never moves for it. */
.nrb-play-rules-lnk { position: absolute; top: -10px; right: -8px; z-index: 1;
  font: 600 13px/1 var(--font-body, system-ui); text-decoration: none;
  color: var(--text-tertiary, #7c86a0); padding: 4px; border-radius: 50%; }
.nrb-play-rules-lnk:hover, .nrb-play-rules-lnk:focus-visible { color: var(--accent, #d9a53f); }

@media (prefers-reduced-motion: reduce) {
  /* The chevron may still colour; it must not travel, and the row must not press. */
  .nrb-widget-play:hover .nrb-play-row-chev { transform: none; }
  .nrb-widget-play:active { transform: none; }
  .nrb-play-burst { display: none; }
  /* The inline row's honest equivalent (handoff state 8): no scale, no drift;
     the ring, the 6% brightening and the title colour still cross-fade in
     120ms linear, so the row still acknowledges the pointer. */
  .nrb-widget-play.nrb-play--compact .nrb-play-thumb,
  .nrb-widget-play.nrb-play--compact .nrb-play-row-txt,
  .nrb-widget-play.nrb-play--compact .nrb-play-row-t { transition: color 120ms linear, box-shadow 120ms linear, filter 120ms linear, opacity 120ms linear; }
  .nrb-widget-play.nrb-play--compact:hover .nrb-play-thumb, .nrb-widget-play.nrb-play--compact:active .nrb-play-thumb,
  .nrb-widget-play.nrb-play--compact:focus-within .nrb-play-thumb,
  .nrb-widget-play.nrb-play--compact:hover .nrb-play-row-txt, .nrb-widget-play.nrb-play--compact:active .nrb-play-row-txt,
  .nrb-widget-play.nrb-play--compact:focus-within .nrb-play-row-txt { transform: none; }
  /* No rise, no blur — a plain cross-fade in the same cell, then gone. */
  .nrb-slot-layer--out { transform: none; filter: none; transition: opacity 200ms linear; }
  .nrb-slot-layer.is-entering { animation: nrbSlotFade 200ms linear both; }
  @keyframes nrbSlotFade { from { opacity: 0; } to { opacity: 1; } }
}

/* ══ THE STAGE ════════════════════════════════════════════════════════════
   Everything below here exists only once a reader has OPENED the board, so a
   no-JavaScript page never reaches it — but it ships in the same sheet,
   because a stylesheet that arrives after the click is a flash of unstyled
   overlay. */
/* iOS SAFARI: inset: 0 alone leaves a strip of page showing at the bottom.
   A fixed element there is laid out against a viewport whose height changes as
   the toolbars slide, and bottom: 0 resolves against the wrong one — the
   reader sees news cards under the scrim, and the footer row sits above real
   content instead of on the bed. 100dvh is the DYNAMIC viewport height, which
   is the one that tracks the toolbars; 100vh stays as the fallback for
   browsers without it (they were never wrong to begin with). bottom is
   dropped so the height is what decides, not two competing edges.

   MEASURED on iOS 18 WebKit (iPhone 16 Pro), both toolbar states, rather than
   reasoned about: toolbar shown 100dvh = 678 = the visible viewport; toolbar
   retracted 100dvh = 760 = the visible viewport. svh stays 678 and lvh stays
   760 throughout, so neither of those would have covered both states — dvh is
   the only one of the four that tracks.

   box-sizing is stated HERE and not inherited from the host's reset: with the
   default content-box this rule's own 18px padding is ADDED to the height, and
   the overlay overflows the viewport by exactly 36px — which is the reported
   bug again, from a different cause. Both hosts happen to set the reset today
   (siteCss.ts, tokens.css); a rule whose correctness depends on that is one
   host away from being wrong. The slot layer above states it for the same
   reason. */
/* ██ THE OVERLAY CARRIES ITS OWN ACCENT ██ (owner 2026-08-07)
   This surface is DARK in both modes — the scrim, the chrome, the panel and
   the celebration card are all mode-independent objects, exactly like the
   Ember trophy. But --accent is not: the design system flips it to #b8520a in
   LIGHT mode, a deep burnt orange chosen for dark text on a PALE page. Filling
   a CTA with that and printing near-black on it, on a panel that is dark
   anyway, produced a button that read as disabled (owner, on the light theme).
   So everything inside the overlay takes --play-accent, which is gold whatever
   the page around it is doing. A theme that wants to restyle the overlay now
   has ONE variable to set instead of having to know which of these rules
   happen to sit on dark. The ROW is deliberately excluded: it lives on the
   page and should follow the page. */
.nrb-play-overlay { --play-accent: #f0b429;
  position: fixed; top: 0; left: 0; right: 0; bottom: auto;
  height: 100vh; height: 100dvh; box-sizing: border-box;
  z-index: 90; display: flex;
  align-items: center; justify-content: center; padding: 18px; }
/* AN INERT BACKDROP, not a control (owner 2026-08-07). It was a full-screen
   close button; tapping it mid-game ended the round. It is a plain div now —
   the reset properties below are the harmless remains of that, and cursor:
   default was always saying what the element should have been doing. It still
   covers the viewport, so the news page behind receives no stray taps either. */
.nrb-play-scrim { position: absolute; inset: 0; border: 0; padding: 0;
  /* Darker bed — the board is the only lit object on the screen. */
  background: rgba(3, 5, 14, .90); backdrop-filter: blur(3px); cursor: default;
  animation: nrbScrimIn 520ms var(--ease-out, ease-out) both; }
@keyframes nrbScrimIn { from { opacity: 0; } to { opacity: 1; } }
.nrb-play-stage { position: relative; z-index: 1; max-width: 620px; width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 12px; }
.nrb-play-note { font: 500 14px/1.5 var(--font-body, system-ui); color: #e7eaf6; }

/* ── THE LANE PICKER (owner 2026-08-10) ────────────────────────────────────
   A day offers up to three boards — easy · medium · hard — and the round only
   opens after the reader has said which. Three big options on the dark bed,
   dressed in the tools' own language so nothing has to be learned twice; a
   played lane keeps its place (its scorecard is one tap away) but wears a
   dashed border and a quiet name. When only ONE lane is scheduled the picker
   never renders — the tap starts that lane directly, which is exactly the
   pre-lane behaviour. */
.nrb-play-pick { display: flex; flex-direction: column; gap: 12px;
  width: min(320px, 88vw); }
.nrb-play-pick-t { font: 700 12px/1 var(--font-mono, monospace); letter-spacing: .16em;
  text-transform: uppercase; color: #e7eaf6; text-align: center; margin: 0 0 6px; }
.nrb-play-pick-opt { display: flex; flex-direction: column; gap: 5px; text-align: left;
  padding: 14px 16px; border-radius: 10px; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, .28); background: rgba(255, 255, 255, .06); }
.nrb-play-pick-opt:hover { border-color: var(--play-accent, #f0b429);
  background: rgba(240, 180, 41, .10); }
.nrb-play-pick-name { font: 700 13px/1 var(--font-mono, monospace); letter-spacing: .12em;
  text-transform: uppercase; color: #e7eaf6; }
.nrb-play-pick-opt:hover .nrb-play-pick-name { color: var(--play-accent, #f0b429); }
.nrb-play-pick-meta { font: 500 12.5px/1.4 var(--font-body, system-ui);
  color: rgba(231, 234, 246, .62); }
.nrb-play-pick-opt.is-played { border-style: dashed; }
.nrb-play-pick-opt.is-played .nrb-play-pick-name { color: rgba(231, 234, 246, .62); }
/* THE BOARD'S OWN COLUMN. Its width is the BOARD's, because the board root is
   width:max-content and this stretches to its widest child — which is what
   makes the header row's right edge flush with the frame instead of flush with
   the 620px stage the board is centred inside.

   ⚠️ NO max-width HERE. It looks like the obvious safety and it is the bug:
   a 9x9 board is 366px and the stage inside a phone is 339, so capping the
   wrapper makes the BOARD overflow the wrapper — to the right only, poking 9px
   off the screen. Uncapped, the wrapper takes the board's own width and the
   stage's align-items:center overflows it EVENLY into the overlay's padding,
   which is what the bare board did before it had a wrapper at all. Measured
   both ways at 375px. */
.nrb-play-board { display: flex; flex-direction: column; gap: 12px; }
.nrb-play-top { display: flex; gap: 12px; align-items: center; width: 100%; }
/* The clock and its count stay together on the left and the reset takes the
   right end — the auto margin does it without a spacer element, and without a
   grid whose side tracks would have to be equal to keep anything centred. */
.nrb-play-top .nrb-play-reset { margin-left: auto; }
.nrb-play-clock { font: 700 22px/1 var(--font-mono, monospace); color: #e7eaf6; font-variant-numeric: tabular-nums; }
.nrb-play-count { font: 500 12px/1 var(--font-mono, monospace); color: #8e96b3; }
/* ── THE MESSAGE WELL — RESERVED, SO THE BOARD NEVER JIGGLES ───────────────
   (owner 2026-08-10: *"a red error container also appears below the board …
   it moves the board up and creates very unstable experience … reserve this
   space anyway for errors and hints so that it never jiggles"*)

   The stage is a centred column, so ANY growth below the board pushed the
   board itself up: a sentence arriving and leaving is the board hopping. The
   warning and the hint now live in one well of FIXED height — reserved
   whether or not it holds anything — so the board's position is a function of
   the board alone.

   ⚠️ A FIXED height, not a min-height: a min-height still grows, and growth is
   the entire bug. A rare third line scrolls INSIDE the well (overflow-y: auto)
   rather than moving the board — the text stays reachable, and the one thing
   that cannot happen is the layout shifting under a thumb.

   76px is measured, not chosen: the warning is the taller occupant at
   13.5px/1.5 with 8px padding and a border — two lines of it fit exactly, and
   two lines is what the longest dead-land sentence renders as in all five
   languages at this width. */
.nrb-play-msgs { width: 100%; max-width: 460px; height: 76px; box-sizing: border-box;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; overflow-y: auto; scrollbar-width: thin; }
.nrb-play-hint { max-width: 460px; text-align: center; font: 500 13.5px/1.5 var(--font-body, system-ui); color: #e7eaf6; }
/* ── THE DEAD-LAND WARNING (owner 2026-08-09) ──────────────────────────────
   A land sealed shut with desert breaks no rule at the moment it happens — the
   puzzle simply becomes unsolvable, and every deduction the player makes after
   that is built on a dead board. So it gets said in words as well as painted
   on the region.

   It sits ABOVE the hint line rather than replacing it: the hint explains the
   move you asked for, this explains a mistake you did not. Same red as the
   region's border and as a conflicting cell, because it is the same class of
   fact. It carries role=status in the markup, so a screen reader is told once
   and is not interrupted mid-sentence. */
.nrb-play-warn { max-width: 460px; text-align: center;
  font: 600 13.5px/1.5 var(--font-body, system-ui); color: #f0a2ad;
  padding: 8px 14px; border-radius: 8px;
  border: 1px solid rgba(224, 96, 112, .45); background: rgba(224, 96, 112, .12); }
/* THE PAIR STANDS APART (owner 2026-08-09: *"move the buttons a bit away from
   each other"*). 10px read as one segmented control rather than two separate
   verbs — and these two are not siblings in meaning: ПОДСКАЗКА spends nothing
   and ПОКАЖИ МИ spends a hint. The gap is the only thing on this surface
   saying they are different decisions.

   22px, and the ceiling is arithmetic, not taste: two 128px floors plus the
   gap must clear the narrowest board this row sits under. A 9x9 is 366px and
   the phone stage is 339 — 128+128+22 = 278 fits both with room, while 32
   would still fit and start to read as two unrelated islands. */
.nrb-play-tools { display: flex; gap: 22px; }
/* THE PAIR HOLDS ITS WIDTH. While the hint rests its label becomes a NUMBER,
   and a button that shrinks from PODSKAZKA to "4" — then again from 10 to 9 —
   moves the other button under the reader's thumb mid-game. Tabular figures do
   not save you here: the digit COUNT changes, not just the glyphs.
   A px floor, not ch: ch resolves against the font and MEASURED 79px where the
   Bulgarian label needs 104 — a unit that looks principled and is simply wrong
   is worse than a number somebody checked. 128 clears every tool label in all
   five locales; nowrap means a longer one grows the button instead of
   sprouting a second line. */
.nrb-play-tools button, .nrb-play-reset { font: 700 11px/1 var(--font-mono, monospace); letter-spacing: .1em; text-transform: uppercase;
  padding: 10px 16px; border-radius: 8px; cursor: pointer;
  min-width: 128px; white-space: nowrap;
  border: 1px solid rgba(255, 255, 255, .28); background: rgba(255, 255, 255, .06); color: #e7eaf6; }
.nrb-play-tools button:disabled { opacity: .35; cursor: default; }
/* ── ПОКАЖИ МИ IS LIVE, AND SAYS SO (owner 2026-08-09) ─────────────────────
   It spends a hint, so it is dead most of the round and awake for one press
   after each ПОДСКАЗКА. A button that only changes OPACITY between those two
   states is asking the reader to compare it against a memory of itself; the
   accent makes the window visible without their having to look for it.

   Outlined rather than filled: the armed reset is the filled gold object on
   this surface and it destroys a round. Two solid golds competing would make
   the loud one mean less. */
.nrb-play-tools button.is-live { border-color: var(--play-accent, #f0b429);
  color: var(--play-accent, #f0b429); background: rgba(240, 180, 41, .10); }
.nrb-play-tools button.is-live:hover { background: rgba(240, 180, 41, .18); }

/* ── THE RESET (owner 2026-08-09) ──────────────────────────────────────────
   *"i need it to be as the other buttons"* — so it IS one of them, sharing the
   rule above rather than restating it. It is not in the tools row because the
   owner placed it at the board's top right; it is in the board's own header,
   pinned right by the auto margin, where it covers no cell and needs no
   media query to stay on a phone.

   ARMED, it becomes the word: ИЗЧИСТИ turns into СИГУРНО? and only the second
   press wipes. The gold fill is the same object the primary CTA is — while it
   is asking, it IS the primary thing on the surface. The 128px floor it
   inherits is doing real work here: the two labels are different lengths, and
   a button that resizes under the thumb about to press it again is the exact
   accident this control exists to prevent. */
.nrb-play-reset { transition: background 140ms var(--ease-out, ease-out),
                              border-color 140ms var(--ease-out, ease-out),
                              color 140ms var(--ease-out, ease-out); }
.nrb-play-reset:hover { border-color: var(--play-accent, #f0b429); color: var(--play-accent, #f0b429); }
.nrb-play-reset.is-armed, .nrb-play-reset.is-armed:hover {
  border-color: var(--play-accent, #f0b429);
  background: var(--play-accent, #f0b429); color: #14192b; }

/* ── THE HINT'S COOLDOWN (owner 2026-08-07) ────────────────────────────────
   A hint is free to ask for and expensive to earn back, so it waits. The wait
   is drawn ON the button as a draining fill — a disabled control with no
   explanation reads as broken, and a number would need a per-second re-render
   and would displace the word that says what the button does.

   It is a BACKGROUND, not a pseudo-element: a background always paints behind
   the label with no stacking games, and background-size animates. The duration
   arrives as --cool from the component, so the 20 seconds live in ONE place
   (the TS constant) instead of being a number in two files.

   Not disabled under reduced-motion: this is a linear progress indicator, the
   one kind of movement that is information rather than decoration. Removing it
   would leave a dead button and no reason. */
.nrb-play-tools button.is-cooling {
  opacity: 1; cursor: default; color: #8e96b3;
  background-image: linear-gradient(rgba(240, 180, 41, .26), rgba(240, 180, 41, .26));
  background-repeat: no-repeat; background-position: left center;
  animation: nrbHintCool var(--cool, 20000ms) linear forwards; }
@keyframes nrbHintCool { from { background-size: 100% 100%; } to { background-size: 0% 100%; } }
/* The count is tabular so the button does not twitch as 10 becomes 9. */
.nrb-play-tools button .cool-n { font-variant-numeric: tabular-nums; color: #e7eaf6; }

/* The thumbnail keeps its BOX while lifted (opacity, never display:none), so
   the return flight has a real rect to land on. */
.nrb-play-thumb.is-lifted { opacity: 0; }

/* ══ THE OVERLAY CHROME ═══════════════════════════════════════════════════
   Title, rules and footer around the board. They TRAIL the board by 240ms on
   the way in and leave FIRST (200ms) on the way out, so the board leads the
   motion in both directions. */
.nrb-play-chrome, .nrb-play-ft {
  position: absolute; left: 0; right: 0; z-index: 2;
  display: flex; align-items: center; justify-content: space-between;
  padding: 20px 26px; pointer-events: none;
  animation: nrbChromeIn 460ms var(--ease-out, ease-out) 240ms both;
}
.nrb-play-chrome { top: 0; }
.nrb-play-ft { bottom: 0; }
.nrb-play-chrome > *, .nrb-play-ft > * { pointer-events: auto; }
.nrb-play-chrome.is-out, .nrb-play-ft.is-out { animation: nrbChromeOut 200ms linear both; }
@keyframes nrbChromeIn  { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes nrbChromeOut { to { opacity: 0; } }

.nrb-play-hd { display: flex; align-items: baseline; gap: 10px; }
.nrb-play-hd-t { font: 700 11px/1 var(--font-mono, monospace); letter-spacing: .16em; color: #e7eaf6;
  text-transform: uppercase; }
.nrb-play-hd-m { font: 500 11px/1 var(--font-mono, monospace); letter-spacing: .1em; color: #8e96b3; }
.nrb-play-hdacts { display: flex; align-items: center; gap: 8px; }
.nrb-play-x, .nrb-play-q { width: 34px; height: 34px; border-radius: 50%; cursor: pointer; flex: none;
  border: 1px solid rgba(255,255,255,.2); background: rgba(255,255,255,.06); color: #e7eaf6; font-size: 14px; }
.nrb-play-x:hover, .nrb-play-q:hover { border-color: rgba(255,255,255,.45); }
.nrb-play-q { font-weight: 700; }
.nrb-play-q[aria-expanded="true"] { border-color: var(--play-accent, #f0b429); color: var(--play-accent, #f0b429); }
.nrb-play-ft-l { font: 500 11px/1.5 var(--font-mono, monospace); letter-spacing: .06em; color: #8e96b3; }
.nrb-play-ft-x { background: none; border: 0; cursor: pointer; text-transform: uppercase;
  font: 700 11px/1 var(--font-mono, monospace); letter-spacing: .12em; color: #8e96b3; }
.nrb-play-ft-x:hover { color: #e7eaf6; }

@media (prefers-reduced-motion: reduce) {
  .nrb-play-chrome, .nrb-play-ft, .nrb-play-scrim { animation: none; }
}

/* ══ HOW TO PLAY ══════════════════════════════════════════════════════════
   The panel behind the ? in the header. It sits OVER the stage rather than
   replacing it: the board keeps its DOM and its engine instance, so opening
   the rules mid-game costs the player nothing and closing it puts them back
   exactly where they were.

   The teaching boards are rendered by the GAME ENGINE at a smaller cellPx, so
   the lands, the desert asterisk and the animal here are the ones the player
   is looking at behind the panel — a tutorial cannot come to illustrate a game
   we no longer ship. The red on the illegal step is the engine's own conflict
   check, not a drawing of one. */
.nrb-play-help { --play-accent: var(--play-accent, #f0b429);
  position: absolute; z-index: 3; left: 50%; top: 50%;
  transform: translate(-50%, -50%); width: min(420px, calc(100% - 24px));
  /* The steps slide sideways; without this the travel would bounce the panel
     into a horizontal scrollbar mid-animation. */
  overflow-x: hidden;
  /* It must never be taller than the viewport it is centred in — on a short
     phone in landscape the nav would otherwise sit off-screen with no way to
     reach it. */
  max-height: min(calc(100% - 24px), 640px); overflow-y: auto; overscroll-behavior: contain;
  /* BOTH EDGES, not just one. A classic scrollbar takes ~17px from the right,
     so a panel that scrolls centres its contents in what is LEFT — and the
     board sits visibly left of the panel it is supposed to be centred in. The
     bug only shows on a desktop that scrolls, which is why it survived a phone
     measurement that was correct to 1px. Reserving on both sides keeps the
     centre the centre whether it scrolls or not; browsers without it are
     exactly as they are today. */
  scrollbar-gutter: stable both-edges;
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px; border-radius: 16px; box-sizing: border-box;
  border: 1px solid rgba(255,255,255,.18); background: #0e1424;
  box-shadow: 0 30px 70px -30px rgba(0,0,0,.9);
  animation: nrbHelpIn 260ms var(--ease-out, ease-out) both; }
@keyframes nrbHelpIn { from { opacity: 0; transform: translate(-50%, calc(-50% + 10px)); }
                       to   { opacity: 1; transform: translate(-50%, -50%); } }

.nrb-play-help-hd { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.nrb-play-help-t { font: 700 12px/1 var(--font-mono, monospace); letter-spacing: .16em;
  text-transform: uppercase; color: #e7eaf6; }

/* The rules, as a real ordered list: it IS an enumeration, and a screen reader
   should say "3 items" rather than read three unrelated paragraphs. */
/* The hairline belongs to whichever block is SECOND — the rules moved under
   the walkthrough on 2026-08-07, so it moved with them. */
.nrb-play-rules { margin: 0; padding: 12px 0 0 22px; display: flex; flex-direction: column; gap: 9px;
  border-top: 1px solid rgba(255,255,255,.12); }
.nrb-play-rules li { font: 500 15px/1.5 var(--font-body, system-ui); color: #dfe5f5; }
.nrb-play-rules li::marker { color: var(--play-accent, #f0b429); font-weight: 700; }

.nrb-play-tut { display: flex; flex-direction: column; align-items: center; gap: 9px; }
.nrb-play-tut-board { display: flex; justify-content: center; }
.nrb-play-tut-cap { margin: 0; text-align: center; min-height: 2.9em;
  font: 500 14.5px/1.5 var(--font-body, system-ui); color: #eef1fa; }
/* Says out loud that this is not the puzzle in play. Small, but it is the
   difference between a tutorial and a spoiler. */
.nrb-play-tut-note { margin: 0; text-align: center;
  font: 500 11px/1.35 var(--font-mono, monospace); letter-spacing: .04em; color: #8e96b3; }

/* Decorative: the BUTTONS below are the navigation, so the dots must not also
   claim to be. Two ways to do the same thing is two things to get wrong. */
.nrb-play-dots { display: flex; gap: 6px; }
.nrb-play-dots i { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,.24); }
.nrb-play-dots i.on { background: var(--play-accent, #f0b429); }

.nrb-play-help-nav { display: flex; gap: 8px; width: 100%; }
.nrb-play-help-nav button { flex: 1; min-width: 0; min-height: 36px; padding: 7px 10px;
  border-radius: 10px; cursor: pointer; white-space: normal;
  font: 700 11.5px/1.2 var(--font-mono, monospace); letter-spacing: .1em; text-transform: uppercase;
  border: 1px solid rgba(255,255,255,.2); background: rgba(255,255,255,.06); color: #e7eaf6; }
.nrb-play-help-nav button:hover:not(:disabled) { border-color: rgba(255,255,255,.45); }
.nrb-play-help-nav button:disabled { opacity: .35; cursor: default; }
/* The one filled CTA on the panel. It prints near-black on the accent, so the
   accent has to stay a LIGHT gold — this is the rule the light theme broke. */
.nrb-play-help-nav button.go { border-color: var(--play-accent, #f0b429);
  background: var(--play-accent, #f0b429); color: #14192b; }
.nrb-play-help-nav button.go:hover:not(:disabled) { filter: brightness(1.08); }

/* ── THE STEP SLIDES (owner 2026-08-07) ────────────────────────────────────
   Out to the LEFT, in from the RIGHT — the page-turn direction, so the reader
   sees that something NEW arrived rather than wondering whether the caption
   changed at all. Back reverses it, because a transition that always travels
   the same way stops carrying any meaning.

   The board rides inside the slide: the engine's own DOM simply moves with its
   container, so this costs the tutorial no remount and no reflow of the board
   itself. Out is quicker than in (170 vs 240ms) — leaving should feel like
   getting out of the way, arriving should feel like arriving. */
/* The slide is a flex column of its own so wrapping the board + caption in it
   costs the panel no layout: it takes the tut column's gap and centring, and
   the two travel as one object rather than as two racing pieces. */
.nrb-play-slide { display: flex; flex-direction: column; align-items: center;
  gap: 9px; width: 100%; }
.nrb-play-slide.out-fwd  { animation: nrbStepOutL 170ms ease-in both; }
.nrb-play-slide.out-back { animation: nrbStepOutR 170ms ease-in both; }
.nrb-play-slide.in-fwd   { animation: nrbStepInR 240ms var(--ease-out, ease-out) both; }
.nrb-play-slide.in-back  { animation: nrbStepInL 240ms var(--ease-out, ease-out) both; }
/* A PERCENTAGE, not 30px. At a fixed 30 the step barely twitched — the reader
   saw the words change and had to decide for themselves whether anything had
   moved. Travelling most of its own width makes it unmistakably a page turn,
   and the panel clips it (overflow-x: hidden) so nothing escapes the card.
   Both halves travel LEFTWARD on Next: the old one leaves stage-left, the new
   one comes in from stage-right chasing it. Back mirrors that exactly. */
@keyframes nrbStepOutL { to   { opacity: 0; transform: translateX(-62%); } }
@keyframes nrbStepOutR { to   { opacity: 0; transform: translateX(62%); } }
@keyframes nrbStepInR  { from { opacity: 0; transform: translateX(62%); } to { opacity: 1; transform: none; } }
@keyframes nrbStepInL  { from { opacity: 0; transform: translateX(-62%); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .nrb-play-help { animation: none; }
  /* The step still CHANGES — it just does not travel. The fill mode on the
     rules above would otherwise leave a slide parked at opacity 0. */
  .nrb-play-slide.out-fwd, .nrb-play-slide.out-back,
  .nrb-play-slide.in-fwd, .nrb-play-slide.in-back { animation: none; }
}

/* ══ THE RESULT CARD ══════════════════════════════════════════════════════ */
.nrb-play-result {
  position: relative; overflow: hidden; isolation: isolate;
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 9px; width: 100%; max-width: 340px; padding: 15px 14px 14px;
  border-radius: 18px; border-width: 2px; border-style: solid;
  animation: nrbPlayWin 420ms var(--ease-out, ease-out) both;
}
@keyframes nrbPlayWin {
  from { opacity: 0; transform: translateY(16px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes nrbPlayPop {
  0%   { opacity: 0; transform: scale(.4) rotate(-14deg); }
  60%  { opacity: 1; transform: scale(1.18) rotate(4deg); }
  100% { opacity: 1; transform: scale(1) rotate(0); }
}

/* THE SKIN — colour only, so a second one is a single block later. */
.nrb-play-result--ember {
  /* The measured ramp. Override these, not the rules below. */
  --ember-1: #ffb703; --ember-2: #f96a06; --ember-3: #e83d0e;
  --ember-4: #d21f0a; --ember-5: #b21008;
  --ember-bloom: rgba(108, 14, 4, .3);
  --ember-glass: rgba(84, 16, 4, .3);
  --ember-edge: rgba(255, 255, 255, .38);
  border-color: rgba(255, 255, 255, .5);
  background: #e0380d;
  color: #fff3e8;
  box-shadow: 0 0 0 1px rgba(120, 25, 5, .2), 0 24px 60px -30px rgba(150, 35, 10, .6);
}

/* The bed. z-index:-1 under `isolation:isolate` so it cannot escape the card,
   and `overflow:hidden` clips it to the 24px radius. */
.nrb-play-art { position: absolute; inset: 0; z-index: -1; }
.nrb-play-art--ember {
  background:
    radial-gradient(84% 56% at 50% 26%, var(--ember-bloom), rgba(108, 14, 4, 0) 76%),
    linear-gradient(133deg, var(--ember-1) 0%, var(--ember-2) 16%, var(--ember-3) 40%,
                    var(--ember-4) 68%, var(--ember-5) 100%);
}

.nrb-play-crown { font-size: 22px; line-height: 1;
  filter: drop-shadow(0 2px 12px rgba(120, 25, 5, .55));
  animation: nrbPlayPop 620ms cubic-bezier(.22, 1.3, .36, 1) both; }

/* text-indent compensates for the trailing letter-space — without it the word
   sits visibly left of centre. Load-bearing, per the handoff. */
.nrb-play-verdict { font: 700 19px/1.15 var(--font-display, system-ui); text-transform: uppercase;
  letter-spacing: .2em; text-indent: .2em; color: #fff; text-shadow: 0 1px 0 rgba(150, 28, 6, .32); }
.nrb-play-time { font: 700 clamp(38px, 9vw, 52px)/1 var(--font-display, system-ui);
  letter-spacing: -.01em; font-variant-numeric: tabular-nums; margin: 1px 0 0; color: #fff;
  text-shadow: 0 2px 0 rgba(150, 28, 6, .3), 0 10px 22px rgba(120, 20, 5, .4); }
.nrb-play-par { font: 600 11.5px/1.4 var(--font-body, system-ui); letter-spacing: .02em;
  margin: 1px 0 0; color: rgba(255, 255, 255, .94); }

/* ── THE CARD TURNS BACK TO THE BOARD (owner 2026-08-10) ───────────────────
   *"clicking on some of the game stats or the time should flip the card to
   the game solution and again play the animation of celebration, but this
   time not flip the card (only manually from now on)"*

   So the time and the plates are CONTROLS: they carry the pointer, a focus
   ring and a title, because an element that does something must say so — the
   card is otherwise a poster and nothing on it invites a press. They are
   real buttons in the markup for the keyboard's sake; these rules only undo
   the button chrome so the card still reads as one printed object.

   The flip is MANUAL FOREVER after the first turn: the one automatic beat is
   the celebration hold at the win, and it fires once. */
.nrb-play-turn { display: contents; }
.nrb-play-time.nrb-play-turn, .nrb-play-plates.nrb-play-turn {
  display: block; border: 0; background: none; padding: 0; font: inherit; color: inherit;
  cursor: pointer; width: 100%; text-align: center; }
.nrb-play-plates.nrb-play-turn { display: flex; }
.nrb-play-turn:hover { filter: brightness(1.06); }
.nrb-play-turn:focus-visible { outline: 2px solid #fff; outline-offset: 3px; border-radius: 10px; }
/* The hint that the object turns — small, once, under the actions.
   ⚠️ IT IS A BUTTON (owner 2026-08-10). It was a <p>, and every signal on it
   said link: uppercase, letter-spaced, lit, sitting directly under a row of
   actions. It read as the third action and did nothing when pressed. A label
   that describes an action and sits where an action sits IS one. The chrome is
   stripped back off so the card still reads as a printed object rather than as
   a form. */
.nrb-play-turnnote { display: block; width: 100%; border: 0; background: none; padding: 4px 0 0;
  font: 600 10.5px/1.3 var(--font-mono, monospace); letter-spacing: .1em;
  text-transform: uppercase; color: rgba(255, 255, 255, .72); margin: 2px 0 0;
  cursor: pointer; text-align: center; }
.nrb-play-turnnote:hover { color: #fff; }
.nrb-play-turnnote:focus-visible { outline: 2px solid #fff; outline-offset: 2px; border-radius: 6px; }

/* ── COPIED, AND YOU CAN TELL (owner 2026-08-10) ───────────────────────────
   The label used to swap word-for-word inside the same white pill, which made
   a successful copy and a dead button look identical — and the button really
   could be dead, because the clipboard API is absent on plain http and the
   handler returned in silence. The copy has a fallback now, and the success
   has a POP and a RING: motion first, because the reader's eyes have already
   left the button by the time the label changes.

   It fires off the class, not off a hover or a transition, so it replays on
   every press rather than only the first. */
.nrb-play-act--primary.is-copied { animation: nrbCopied 1100ms var(--ease-out, ease-out) both; }
@keyframes nrbCopied {
  0%   { transform: scale(1);     box-shadow: 0 12px 26px -14px rgba(120, 25, 5, .9), 0 0 0 0 rgba(255, 255, 255, .85); }
  14%  { transform: scale(1.05);  box-shadow: 0 12px 26px -14px rgba(120, 25, 5, .9), 0 0 0 5px rgba(255, 255, 255, .38); }
  46%  { transform: scale(1);     box-shadow: 0 12px 26px -14px rgba(120, 25, 5, .9), 0 0 0 12px rgba(255, 255, 255, 0); }
  100% { transform: scale(1);     box-shadow: 0 12px 26px -14px rgba(120, 25, 5, .9); }
}
/* Off-screen, not display:none — a hidden live region is never announced. */
.nrb-play-sr { position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
/* THE SOLVED BOARD, shown when the card is turned over. It is the live engine
   again (locked), so the winning dance replays exactly as it did at the win —
   a screenshot would have been a different object. */
.nrb-play-review { display: flex; flex-direction: column; align-items: center; gap: 10px;
  cursor: pointer; }
.nrb-play-review-note { font: 600 10.5px/1.3 var(--font-mono, monospace); letter-spacing: .1em;
  text-transform: uppercase; color: #8e96b3; }

/* THE PLATE MATHS — three stat cells of exactly equal width, with the seal
   breaking the row. Each plate's flex-basis equals its own inner padding, so
   the remaining space splits 2:1 and all three cells land identical.
   min-width:0 is REQUIRED: flex items default to min-content, and the labels
   are nowrap, so without it the split degrades to roughly 3:2. */
.nrb-play-plates { display: flex; align-items: center; gap: 0; width: 100%; }
.nrb-play-plate { display: flex; min-width: 0; border: 1px solid var(--ember-edge);
  border-radius: 11px; background: var(--ember-glass);
  box-shadow: 0 6px 16px -14px rgba(90, 20, 5, .7); }
.nrb-play-plate--l { flex: 2 1 16px; padding-right: 16px; border-top-right-radius: 0; border-bottom-right-radius: 0; }
.nrb-play-plate--r { flex: 1 1 16px; padding-left: 16px; border-top-left-radius: 0; border-bottom-left-radius: 0; }

/* THE FIGURE STACKS: label over digit (owner 2026-08-07). Side by side, the
   label and the number competed for one line and the longest word in five
   languages set the cell width for all of them — Bulgarian PODSKAZKI beside a
   26px digit is why the row could not shrink. Stacked, the cell is as wide as
   its widest single word, so the same card holds every edition. */
.nrb-play-figure { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
  align-items: center; gap: 1px; padding: 7px 6px; justify-content: center; }
.nrb-play-figure + .nrb-play-figure { border-left: 1px solid rgba(255, 255, 255, .26); }
.nrb-play-figure-k { font: 600 9.5px/1.2 var(--font-body, system-ui); white-space: nowrap;
  letter-spacing: .02em; color: rgba(255, 255, 255, .92); }
.nrb-play-figure-v { font: 700 17px/1.1 var(--font-display, system-ui); font-variant-numeric: tabular-nums; color: #fff; }

/* filter, not box-shadow — box-shadow would draw the UNCLIPPED box's shadow. */
.nrb-play-seal { width: 62px; height: 58px; margin: 0 -9px; z-index: 1;
  display: flex; align-items: center; justify-content: center; flex: none;
  clip-path: polygon(25% 3%, 75% 3%, 100% 50%, 75% 97%, 25% 97%, 0 50%);
  background: linear-gradient(180deg, #fff9f0, #ffe4c9);
  color: #c8210a; font: 700 19px/1 var(--font-display, system-ui);
  filter: drop-shadow(0 7px 16px rgba(120, 20, 5, .55)); }

/* auto-fit, not repeat(4,1fr) — owner ruling #4: the earned set is variable. */
.nrb-play-tags { display: grid; grid-template-columns: repeat(auto-fit, minmax(86px, 1fr));
  gap: 5px; width: 100%; }
.nrb-play-tag { display: flex; align-items: center; justify-content: center; gap: 4px;
  padding: 7px 5px; border-radius: 9px; border: 1px solid rgba(255, 255, 255, .36);
  background: rgba(84, 16, 4, .28); box-shadow: 0 6px 14px -12px rgba(90, 20, 5, .7);
  font: 700 10px/1.15 var(--font-body, system-ui); text-align: center; color: #fff; }
/* Ember deliberately drops Gold's colour-coding: blue and red badge text does
   not survive the hot bed, so all four go white and the icons differentiate. */

.nrb-play-acts { display: flex; flex-direction: row; gap: 7px; width: 100%; }
/* WRAPS, and has no fixed height (2026-08-07). At this width a nowrap label
   overflowed the moment the card stopped being Bulgarian-only: "Back to the
   news" fits, "Zurück zu den Nachrichten" does not. Two short lines is the
   honest answer; a clipped verb is not. */
.nrb-play-act { flex: 1; min-width: 0; min-height: 36px; padding: 7px 10px; text-transform: uppercase;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  white-space: normal; text-wrap: balance; border-radius: 10px; cursor: pointer;
  font: 800 10.5px/1.2 var(--font-body, system-ui); letter-spacing: .02em; }
.nrb-play-act:focus-visible { outline: 3px solid #fff; outline-offset: 3px; }
/* The ONE light object on the card — it is the CTA and should read that way.
   No text-shadow: it muddied the glyph edges at this contrast. */
.nrb-play-act--primary { border: 1px solid rgba(255, 255, 255, .7);
  background: linear-gradient(180deg, #fff9f1, #ffe9d7); color: #bd1f07;
  box-shadow: 0 12px 26px -14px rgba(120, 25, 5, .9); }
.nrb-play-act--primary:hover { background: linear-gradient(180deg, #fff, #fff2e6); }
.nrb-play-act--ghost { border: 1px solid rgba(255, 255, 255, .5);
  background: rgba(84, 16, 4, .26); color: #fff; }
.nrb-play-act--ghost:hover { background: rgba(84, 16, 4, .36); }

@media (max-width: 430px) { .nrb-play-acts { flex-direction: column; } }
@media (prefers-reduced-motion: reduce) {
  /* The tick and the word still say it; the pop is the part that moves. */
  .nrb-play-act--primary.is-copied { animation: none; }
  .nrb-play-result, .nrb-play-crown { animation: none; }
}

/* The play widget's own host — a plain full-width wrapper. It must add NO
   layout of its own: the slot already positioned and sized it.
   ⚠️ It is NOT `.nrb-slot`: wearing that class nested a second slot inside the
   first, so the row inherited the slot's padding and centring twice and
   collapsed to its content width. A widget styles itself; the slot places it. */
/* ⚠️ display:block IS LAYOUT, whatever this comment used to claim — a div
   is block already, so it buys nothing, and being last in the sheet it beat
   the row's flex and stacked the games. It stays only because a host
   is not guaranteed to be a div; the row now outranks it by naming both
   classes. Anything added here must be checked against that rule. */
.nrb-widget-host { display: block; width: 100%; }


/* ══ THE CATEGORY POSTER RAIL (20.1360 · design 09B) ══════════════════════ */
/* FULL-BLEED (owner 2026-08-13): the rail spans the whole viewport while the
   page column stays 760px — the root breaks out of .shell with the classic
   centered-100vw margin. The section head keeps the SHELL's own alignment
   inside the bleed. body gets overflow-x:clip so the 100vw breakout can never
   hand the page a sideways scrollbar on classic-scrollbar platforms. */
body { overflow-x: clip; }
.cat-rail-root { width:100vw; margin-left:calc(50% - 50vw); }
.cat-sec { display:flex; align-items:baseline; justify-content:space-between; padding:14px 18px 8px; max-width:760px; margin:0 auto; }
.cat-sec-h { margin:0; font-family:var(--font-display); font-size:15px; font-weight:600; letter-spacing:-.01em; border:0; padding:0; }
.cat-sec-m { font-family:var(--font-mono); font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); }
.cat-rail { display:flex; gap:10px; overflow-x:auto; scroll-snap-type:x proximity; scroll-padding-inline:18px; padding:2px 0 16px; scrollbar-width:none; overscroll-behavior-x:contain; cursor:grab; }
.cat-rail::-webkit-scrollbar { display:none; }
.cat-rail.is-drag { cursor:grabbing; scroll-snap-type:none; }
.cat-rail > * { flex:0 0 auto; scroll-snap-align:start; }
.cat-rail-end { flex:0 0 4px; }
/* CENTERED WHEN IT FITS, SCROLLABLE WHEN IT DOESN'T: the edge pseudo-items
   carry auto margins — free space splits equally around the posters; once the
   row overflows, auto margins resolve to 0 and the rail scrolls from its true
   start (a bare justify-content:center would clip the left cards
   unreachable). Their flex-basis doubles as the edge gutter. */
.cat-rail::before, .cat-rail::after { content:''; flex:0 0 18px; }
.cat-rail::before { margin-left:auto; }
.cat-rail::after { margin-right:auto; }
/* the plate */
.cat-poster { position:relative; width:144px; aspect-ratio:5/7; border-radius:6px; overflow:hidden; background:var(--h); color:#0a0f1c; padding:12px; display:flex; flex-direction:column; justify-content:flex-end; isolation:isolate; text-decoration:none; }
.cat-poster:hover { text-decoration:none; }   /* print plates do not glow */
.cat-poster:focus-visible { outline:3px solid var(--accent); outline-offset:3px; }
/* the photo: top edge down, dissolved before the text (riso: grayscale, lift
   into the upper midtones, multiply onto the plate; ::after restores the hue) */
.cat-art { position:absolute; left:0; right:0; top:0; height:46%; display:block;
  mask-image:linear-gradient(180deg,#000 0 40%,rgba(0,0,0,.5) 74%,transparent 100%);
  -webkit-mask-image:linear-gradient(180deg,#000 0 40%,rgba(0,0,0,.5) 74%,transparent 100%); }
.cat-art img { width:100%; height:100%; object-fit:cover; display:block;
  filter:grayscale(1) brightness(1.75) contrast(.82); mix-blend-mode:multiply; opacity:.9; }
.cat-art::after { content:''; position:absolute; inset:0; background:var(--h); mix-blend-mode:color; opacity:.5; }
/* halftone: full over the photo, almost gone before the text stack */
.cat-dots { position:absolute; inset:0; z-index:1; pointer-events:none;
  background-image:radial-gradient(rgba(10,15,28,.55) 1px,transparent 1.2px); background-size:5px 5px;
  mask-image:linear-gradient(180deg,#000 0 32%,rgba(0,0,0,.5) 42%,rgba(0,0,0,.12) 47%,rgba(0,0,0,.06) 52%,rgba(0,0,0,.06) 100%);
  -webkit-mask-image:linear-gradient(180deg,#000 0 32%,rgba(0,0,0,.5) 42%,rgba(0,0,0,.12) 47%,rgba(0,0,0,.06) 52%,rgba(0,0,0,.06) 100%); }
/* the counter — resting state IS the finished poster; ink flips with the
   photography via the MODEL flag (--knock), never :has() */
.cat-num { position:absolute; top:0; right:8px; z-index:2; font-family:var(--font-display); font-variant-numeric:tabular-nums; font-size:56px; font-weight:700; letter-spacing:-.05em; color:rgba(10,15,28,.24); line-height:1; transition:opacity .32s var(--ease-out),transform .32s var(--ease-out); }
.cat-num--knock { color:color-mix(in oklab,var(--h) 5%,#fff); opacity:.9; }
.cat-num.is-armed { opacity:0; transform:translateY(-6px); }
/* the topper chip (D8) — a print annotation on its own ink plate, top-left,
   mirroring the counter; baked hidden+empty, filled by the beacon fetch */
.cat-topper { position:absolute; top:8px; left:8px; z-index:2; font-family:var(--font-mono); font-size:9.5px; letter-spacing:.02em; font-variant-numeric:tabular-nums; color:var(--h); background:rgba(10,15,28,.9); border-radius:4px; padding:2px 6px; max-width:calc(100% - 16px); overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
/* text stack — always on flat colour, never on the photo */
.cat-title { position:relative; margin:0; font-family:var(--font-display); font-size:18px; font-weight:700; letter-spacing:-.035em; line-height:1; text-transform:uppercase; color:#0a0f1c; border:0; padding:0; }
.cat-sub { position:relative; margin-top:6px; font-size:10.5px; line-height:1.3; color:rgba(10,15,28,.78); border-top:1.5px solid rgba(10,15,28,.5); padding-top:5px; min-height:32px; font-family:var(--font-body); }
/* the clamp lives on an INNER element: .cat-sub is a flex item and a flex
   item's display:-webkit-box is blockified, which silently kills the clamp */
.cat-sub-txt { display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; max-height:2.6em; }
.cat-sr { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0; }
@media (prefers-reduced-motion:reduce){ .cat-num { transition:none; } }
/* ── THE CATEGORY WALL's head (20.1360 S4b, RE-CUT 2026-08-31) ──────────────
   ⚖️ THE SECTION NAME SITS ON THE MASTHEAD'S LINE. Owner: "we need to be same
   height as the logo, so that they align, remove the pattern, and move the
   number to right ... add also the icon so that we can clearly see our
   category". It was a 113px poster: a 34px title over a halftone, with the
   count and the subtopic keys sharing a ruled line underneath. It is one 35px
   row now — glyph, label, and the count pushed to the right edge.

   ⚖️ THE WORDS ARE WHAT ALIGN, NOT THE BOXES (owner 2026-08-31). The band is
   59px and the mark is 35px, and they still read as one line because both sets
   of type land on 24-50px. The band simply carries its extra 24px ABOVE the
   type, where it runs off the top edge.
   ⚠️ THE PAIRING IS A LOOK, AND TWO FILES HOLD IT. Change .pn-mark's 26px/9px
   or .pn-fix's 24px top in portalNavCss and this band silently drifts out of
   line; categoryWall.test.ts pins every one of those numbers for that reason. */
.catwall-head {
  position:relative; box-sizing:border-box; overflow:hidden; isolation:isolate;
  /* ⚖️ THE TYPE SITS ON THE MARK'S LINE; THE BAND REACHES UP TO THE EDGE (owner
     2026-08-31: "align the category text to this already set logo ... then from
     there upward is this red band"). The MARK DOES NOT MOVE for this — the band
     is what stretches, which is why the column kept its 24px in portalNavCss.
     ⚑ THREE NUMBERS, ALL THE MASTHEAD'S OWN, and the arithmetic is the point:
       24  .pn-fix's top   — where the mark's type starts
     + 35  .pn-mark's box  — 26px of type over a 9px pad
     = 59  this band's height, measured from the page's top edge.
     Bottom-aligned with the mark's SAME 9px pad, the band's 26px type therefore
     occupies 24-50px exactly as the mark's does. Two boxes of different heights
     whose WORDS share a line — which is what was asked for, and is not the same
     as two boxes that merely start together. */
  display:flex; align-items:flex-end; gap:10px;
  min-height:59px;                     /* 24 (the mark's offset) + 35 (its box) */
  padding:0 14px 9px;                  /* 9 = .pn-mark's own padding-bottom */
  margin:0 0 18px;
  background:var(--h); color:#0a0f1c;
  /* ⚑ THE HERO SLIDER'S CORNER, not the story card's (owner 2026-08-31, asked
     for both and they are different numbers here: --r-card is 18px, the hero is
     14). The hero sits DIRECTLY under this band, so it is the shape the eye
     pairs it with. Written as a literal because --nrb-hero-r is declared ON the
     hero article, not on :root, and reaching for it here would resolve to
     nothing on a wall that has no lead.
     ⚑ BOTTOM CORNERS ONLY: "upward we go full bleeding edge". A rounded top on
     a band that runs off the top of the page would draw a corner where there is
     no corner — the shape has to say it was cut, not that it stopped. */
  border-radius:0 0 14px 14px;
}
/* ⚑ THE HALFTONE, STRONGEST WHERE THE BAND LEAVES THE PAGE (owner 2026-08-31:
   "we need the pattern to be from transparent bottom to visible top"). The
   poster's own screen — the same 5px grid in the same print ink — under a
   vertical fade that is FULL at the top edge and gone by the bottom.
   ⚖️ IT RESTORES THE POSTER'S OWN LAW BY A DIFFERENT ROUTE. The original mask
   existed to keep type off the pattern, and it faded downward because the words
   sat at the poster's foot. The words sit at this band's foot too — so the same
   direction does the same job, and the density now lands entirely in the 24px
   that runs off the top edge, where nothing has to be read on top of it.
   ⚑ A PSEUDO-ELEMENT, NOT A SPAN. It is decoration with no meaning, so it stays
   out of the markup entirely — which also means the shared view's output did not
   change and no render contract moves for it.
   ⚠️ ON A DARK CATEGORY INK THE DOTS NEARLY VANISH, and that is inherent to the
   ink rather than a bug here: this is a DARK screen on a plate the registry
   already gates to 4.5:1 against dark text. A light-dot variant was built and
   the owner chose this one. */
.catwall-head::before {
  content:''; position:absolute; inset:0; z-index:0; pointer-events:none;
  background-image:radial-gradient(rgba(10,15,28,.385) 1px, transparent 1.2px);
  background-size:5px 5px;
  -webkit-mask-image:linear-gradient(180deg,#000 0,rgba(0,0,0,.55) 45%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 0,rgba(0,0,0,.55) 45%,transparent 100%);
}
/* 🏷 The registry's glyph, in the band's own print ink through currentColor —
   the SAME mark the nav row carried, at the size the row draws it plus a little,
   because here it identifies the whole page rather than one line of a list. */
.catwall-ic { position:relative; z-index:1; flex:none; display:flex; width:20px; height:20px; }
.catwall-ic svg { width:100%; height:100%; fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
/* ⚠️ A WALL WITH NO PLATE (20.1980). Walls exist for every live category now,
   not only the five that carry an ink, and the band prints DARK text on the
   strength of the registry's 4.5:1 gate. A node whose branch has no plate gets
   NO BAND rather than an un-gated colour under that text — the row sits on the
   page instead, and the operator can grant it a plate in the Studio whenever
   they want the colour back. ⚑ It keeps the height either way: the band is the
   masthead's line, and a section without a colour is still on that line. */
.catwall-head--plain { background: none; box-shadow: none; padding-left: 0; }
.catwall-head--plain .catwall-title { color: var(--text); }
.catwall-head--plain .catwall-meta { color: var(--muted); }
.catwall-title {
  position:relative; z-index:1; margin:0; padding:0; border:0; min-width:0;
  font-family:var(--font-display); font-size:26px; font-weight:700;
  letter-spacing:-.03em; line-height:1; text-transform:uppercase; color:#0a0f1c;
  /* 26px is the masthead's own size, not a size that merely fits the band.
     ⚑ A long section name ELLIPSES rather than wrapping: a wrapped title would
     grow the row, and the row's height is the whole point of this re-cut.
     'Наука и здраве' is 14 characters in Bulgarian and longer in German. */
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* 🌍 THE SCOPE HALF OF THE TITLE (2026-09-03: "СВЯТ / БЪЛГАРИЯ"). Same size,
   lighter weight and a little translucent, so the section stays the thing the
   band is about and the scope reads as its qualifier — the way the <title>
   already says "Политика · България". Inherits the band's print ink, or the
   text colour on a plateless wall, without a rule of its own. */
.catwall-scope { font-weight:500; opacity:.72; }
/* ⇢ THE COUNT GOES RIGHT, and an auto margin is what sends it: the title keeps
   its natural width so the ellipsis above triggers on the title rather than on
   a flex-shrunk number. */
.catwall-meta {
  position:relative; z-index:1; flex:none; margin:0 0 0 auto; padding:0; border:0;
  /* ⚑ IT IS THE SECTION'S SIZE, SO IT READS LIKE ONE (owner 2026-08-31: bolder
     and bigger). At 12px regular it was a footnote to the name; the count is the
     other half of what this band says — what section, and how much of it. Still
     well under the 26px title, so the hierarchy holds. */
  font-family:var(--font-body); font-size:15px; font-weight:700; line-height:1;
  color:rgba(10,15,28,.82);
}
.catwall-stories { margin-top:4px; }
/* ── the band on a phone ─────────────────────────────────────────────────────
   ⚑ 30% OFF BOTH SIZES (owner 2026-08-31), at the SAME 900px where .pn-fix goes
   static and the mark leaves its fixed slot for a row at the top of the page.
   That is not a coincidence and it is why this block exists at all: the band's
   26px and its 59px height are BOTH borrowed from a mark that, below this width,
   is no longer sitting where they were measured against. The desktop alignment
   law simply does not apply here, so the band is free to be its own size — and
   at 375px a 26px section name ran out of room and ellipsised ("ТЕХНОЛОГИ...").
   ⚑ 26 × .7 = 18.2, taken as 18; 15 × .7 = 10.5, kept fractional because a
   count is a small number of digits and rounding it to 10 costs more than the
   half pixel does. */
@media (max-width: 900px) {
  .catwall-title { font-size:18px; }
  .catwall-meta { font-size:10.5px; }
}
/* ── THE BACK-TO-WALL PILL (20.1360 S4c) — client-minted, never baked.
   The category's plate as a floating escape hatch: ink ground, print ink
   text (every registry ink cleared 4.5:1 at the gate), bottom-centre, above
   the safe area. Exists only when hydrate.ts minted it from the visitor's
   own arrival — a JS-off reader has the browser's Back button instead. */
.catback { position:fixed; left:50%; transform:translateX(-50%); bottom:calc(14px + env(safe-area-inset-bottom, 0px)); z-index:60; display:flex; align-items:stretch; background:var(--h); color:#0a0f1c; border-radius:999px; box-shadow:0 4px 18px rgba(5,8,20,.45); overflow:hidden; }
.catback-link { display:block; padding:9px 6px 9px 16px; font-family:var(--font-display); font-size:13px; font-weight:700; letter-spacing:-.01em; text-transform:uppercase; color:#0a0f1c; text-decoration:none; white-space:nowrap; }
.catback-link:hover { color:#0a0f1c; text-decoration:none; }
.catback-x { border:0; background:transparent; color:rgba(10,15,28,.6); font:inherit; font-size:16px; line-height:1; padding:0 12px 0 6px; cursor:pointer; }
.catback-x:hover { color:#0a0f1c; }
.catback :focus-visible { outline:2px solid #0a0f1c; outline-offset:-4px; border-radius:999px; }
@media print { .catback { display:none; } }



:root {
  /* ██ THE SIDE COLUMNS YIELD BEFORE THE STORY DOES (owner 2026-08-19) ██
     Both tracks were locked at 360px, so between the docking width and about
     1500 the two of them plus their gaps ate 776px and the story took whatever
     was left — measured 588px at 1400, NARROWER than the 760 it used to get on
     a page with no rail at all. The columns are furniture; the story is the
     reason the page exists, so the furniture is what gives.
     clamp: 272 is the floor (below it a chapter card's two-line title starts
     breaking mid-word), 360 the ceiling the rail's geometry was measured at,
     23vw the taper between them. */
  --nrb-tl-w: clamp(272px, 23vw, 320px);
  /* The centring threshold uses the CEILING — it asks "is there room for the
     layout at full size", which is a question about the maximum, not about the
     width this particular viewport happens to give. */
  --nrb-tl-tracks: calc(320px * 2 + 860px + 56px);
  /* where the head pins, and therefore where the labels pin under it */
  --nrb-tl-pin: 78px;
  /* the hero's own top margin, paid by the rail so the head and the hero share
     a top edge. The PANEL pins at (pin − lead) so that after this padding the
     head lands exactly on the pin — at rest and pinned, the same 78px. */
  --nrb-tl-lead: 22px;
}
/* ══════════════════════════════════════════════════════════════════════════
   THE STORY CHRONICLE RAIL (20.1700) — design handoff `topic_timeline`,
   snapshot web/design/imports/2026-08-18-topic-timeline/
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 1. THE HOST ───────────────────────────────────────────────────────────
   The rail defaults to the DRAWER shape, and the wide media query promotes it
   to a docked column. That order matters: the narrow shape is the one a phone
   gets, and a phone should not have to parse and then undo a desktop grid. */
.nrb-tl {
  /* ██ THE DRAWER STARTS BELOW THE HEADER, NOT BEHIND IT ██ (measured
     2026-08-18). A `position: fixed` overlay is not free of its ancestors
     (20.1170's own trap): this panel lives inside `<main class="shell">`,
     which paints in its OWN stacking context (`position: relative; z-index: 1`
     in the base sheet), so its z-60 is sealed inside that context and the
     header's root-level z-50 wins. The first cut therefore slid the panel up
     under the masthead and guillotined its head plate.
     Raising `.shell`'s z-index would fix the paint order and break the sticky
     header over the article, so the honest fix is to start below it — which
     also leaves the hourglass on screen, still able to close what it opened.
     The fallback is the header's designed height; `hydrate.ts` replaces it
     with the MEASURED one, so a header that ever wraps is right anyway. */
  position: fixed; z-index: 60; top: var(--nrb-tl-top, 65px); bottom: 0; left: 0;
  width: 80vw; max-width: 420px;
  background: var(--bg);
  border-right: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  transform: translateX(-100%);
  /* closed ⇒ not focusable, so the drawer never swallows a tab stop while it
     is off-canvas; visibility flips back instantly on open, and only on close
     does it wait for the slide to finish */
  visibility: hidden;
  transition: transform .28s var(--ease-out), visibility 0s linear .28s;
}
.nrb-tl-inner { padding: var(--sp-card) 0 calc(var(--sp-card) * 2); }
#nrb-tl-toggle:checked ~ main .nrb-tl {
  transform: none; visibility: visible;
  transition: transform .28s var(--ease-out), visibility 0s;
}

/* the scrim — a LABEL, which is what makes "tap outside to close" work with no
   script at all. aria-hidden + no text: it is a gesture, not content. */
.nrb-tl-scrim {
  position: fixed; z-index: 59; inset: var(--nrb-tl-top, 65px) 0 0 0;
  background: color-mix(in oklab, #000 55%, transparent);
  opacity: 0; pointer-events: none;
  transition: opacity .28s var(--ease-out);
}
#nrb-tl-toggle:checked ~ main .nrb-tl-scrim { opacity: 1; pointer-events: auto; }

/* The header shrinks with the viewport (the chrome tightens its padding at
   720px: 14+36+14+1 = 65 becomes 11+36+11+1 = 59). The drawer's top edge has to
   follow, or a script-less phone gets a 6px sliver of the article showing above
   the panel. Measured at both widths, not derived. */
@media (max-width: 720px) { :root { --nrb-tl-top: 59px; } }

/* the checkbox itself: visually hidden but STILL FOCUSABLE, so the drawer is
   reachable by keyboard without a line of script (a `hidden` input would take
   the only script-free keyboard route away with it) */
.nrb-tl-toggle {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  border: 0; white-space: nowrap;
}
.nrb-tl-opener {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; flex: none;
  border-radius: var(--r-md); color: var(--muted); cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.nrb-tl-opener svg { width: 18px; height: 18px; }
.nrb-tl-opener:hover { color: var(--accent); background: var(--bg-elevated); }
/* the input is a body-level sibling of the header, so the focus ring has to
   travel by the general sibling combinator */
#nrb-tl-toggle:focus-visible ~ header .nrb-tl-opener {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

@media (min-width: 1180px) {
  /* DOCKED: the rail is column one and the story keeps its own 760px. */
  .shell--tl {
    /* ██ THE STORY IS CENTRED ON THE PAGE, AND THE RAIL DOES NOT MOVE IT ██
       (owner 2026-08-19). THREE columns, not two: the rail, the article, and a
       MIRROR of the rail's width that renders nothing. The empty third track is
       the whole mechanism — with equal side tracks the middle one is centred on
       the viewport by construction, so the story sits exactly where it would
       with no rail at all, and adding or removing the column cannot shift it.
       The shell drops its own max-width and centres the TRACKS instead, so the
       article keeps its designed 760px wherever there is room and shrinks only
       when there is not. */
    max-width: none;
    display: grid;
    grid-template-columns: var(--nrb-tl-w) minmax(0, 860px) var(--nrb-tl-w);
    justify-content: center;
    column-gap: 28px;
    /* STRETCH, deliberately (owner 2026-08-18). The rail's own content is far
       shorter than the article, so a start-aligned column ends where the last
       chapter does — and the sticky HEAD inside it would come unstuck a few
       hundred pixels down a story thousands of pixels long. Stretching the
       column to the row's height gives the head the whole article to travel. */
    align-items: stretch;
    /* The 13px the sticky rail is held below the header (78 − the header's 65)
       — paid by BOTH columns, or the thread would start a hair lower than the
       story it belongs to. Measured, not guessed: without it the rail rests at
       78 and the article at 65. */
    padding-top: 13px;
  }
  .nrb-tl {
    /* ██ THE THREAD STAYS ON SCREEN (owner 2026-08-19) ██
       Reversed twice, and the second reversal is the informative one. It first
       FLOWED with the story — as tall as it needed, running down the page — on
       the reasoning that a thread which moves with the piece reads as part of
       it. True, and it made the rail useless the moment you were reading: by
       chapter three the whole thread had scrolled away, so the reading
       position had nothing to highlight and the accordion folded chapters
       nobody could see. The owner's test is exact: click step 3, and steps 1
       through 5 must ALL still be visible.
       The accordion is what makes this affordable — a pinned panel only works
       if a long chronicle can shrink to fit, and folding what is behind the
       reader is precisely that. The two features are one feature.
       ⚖️ align-self:start is mandatory with sticky: the shell stretches its
       items, and a stretched box is the full row height, so there is nothing
       for the panel to stick INSIDE. */
    position: sticky;
    top: calc(var(--nrb-tl-pin) - var(--nrb-tl-lead));
    align-self: start;
    left: auto; right: auto; bottom: auto;
    width: auto; max-width: none;
    /* The floor under "all steps visible": if a chronicle is long enough to
       overflow even folded, the panel scrolls INSIDE itself rather than
       cutting chapters off. With the fold in play this is rare — the scrollbar
       is a safety net, not the design. */
    max-height: calc(100vh - var(--nrb-tl-pin) + var(--nrb-tl-lead) - 16px);
    overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain;
    background: none; border-right: 0; box-shadow: none;
    visibility: visible; transition: none;
    /* ── THE RAIL SITS IN ITS COLUMN ───────────────────────────────────────
       It used to be dragged to a flat 16px from the screen by a translateX,
       while the mirror track centred the story — the pin and the mirror
       disagreeing BY DESIGN, with the gap between them growing with the
       screen. That is the "drifted apart" the owner saw. The declaration that
       did it is deleted, exactly as the note beside it always said it should
       be: the rail now sits in its own 320px track, symmetric with the empty
       mirror on the other side, and the three tracks read as ONE centred
       group instead of two edges and a middle. */
  }
  /* The head sticks INSIDE this box, so this box has to be as tall as the
     column — otherwise the head comes unstuck a few hundred pixels down,
     wherever the rail's own content happens to end. Measured: without it the
     head released at ~350px of a 2154px article. */
  /* ── THE HEAD LINES UP WITH WHAT THE READER SEES ──────────────────────
     The two columns' BOXES already start together (both at 78px, measured),
     but the article's first visible element is the hero, and the hero carries
     a 22px top margin — so the thread began 22px above the story it indexes.
     Aligning boxes is not aligning content; this pays the hero's margin so the
     head and the hero share a top edge.
     (A hero-less page's first element carries a different margin, so it sits
     22px low instead — the trade is worth it: nearly every story has a hero.) */
  /* height:100% is GONE with the stretch it depended on — the panel is now as
     tall as its content, up to the max-height above. */
  .nrb-tl-inner { padding: var(--nrb-tl-lead) var(--sp-card) var(--sp-card); height: auto; }
  /* neither control has anything to do once the rail is always visible */
  .nrb-tl-opener, .nrb-tl-scrim { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .nrb-tl, .nrb-tl-scrim { transition: none; }
}

/* ── THE WAY BACK UP GLIDES ────────────────────────────────────────────────
   The head is an <a href="#top"> and the chapters are <a href="#move-N">, so
   the whole rail navigates by fragment — which means ONE declaration animates
   every one of them, with no handler and no script. Scoped with :has() to
   pages that actually carry a rail, rather than switched on globally from a
   component's own stylesheet; where :has() is unsupported the jump is instant,
   which is the behaviour we had.
   Inside no-preference, so a reader who asked for less motion keeps the jump —
   this is exactly the kind of long, whole-page travel that triggers people. */
@media (prefers-reduced-motion: no-preference) {
  html:has(.nrb-tl) { scroll-behavior: smooth; }
}

/* ── 2. THE LEAN INDEX ─────────────────────────────────────────────────────
   ██ THE ORNAMENT SWAP (owner 2026-08-19) ██ — the month labels, the Δ rows,
   the numbered rings and the segmented spine that used to live here all moved
   into the article's own chronicle, which has the width for them and is where
   the reader actually is. What the rail keeps is the look the chronicle had:
   a bead, a title, a count. Flat list, no buckets, no dividers. */
/* gap bridges card→card; the hairline's math depends on this value */
/* ██ WHO'S INVOLVED — THE RAIL'S ONE NON-CHAPTER STOP ██ (owner 2026-08-20:
   "below the header with the clock and above the real timeline events")

   ⚖️ IT WEARS THE CARD, NOT THE THREAD. A rail node carries a 18px gutter
   holding the bead the spine runs through — the visual claim "I am a moment on
   this timeline". The cast is not a moment, so it takes the same card shape and
   NO gutter, no bead, and sits above where the spine starts. The reader gets
   one glance that says "same rail, different kind of thing" without a label
   having to say it.

   Its own margin closes the gap to the list below: .nrb-tl-list carries the
   flex gap between chapters, and this sits outside that flow. */
/* ⚖️ THE THREAD RUNS THROUGH THIS ROW TOO (owner 2026-08-22). The cast is a
   stop on the same chronicle — it wears the node's shape for exactly that
   reason — but the thread used to begin at the first CHAPTER, so the line
   started in mid-air below a row that looked like it was already on it. The
   segment below reaches UP to the head's own bottom edge and DOWN into the
   list, so the head, the cast and the chapters are one continuous line.
   ⚠️ Its ends are the two MARGINS it has to cross, written as the same tokens
   that set them — the head's --sp-card above, this row's own 12px below. Spell
   either as a number and the line parts from the gap it is bridging. */
.nrb-tl-cast {
  position: relative;
  /* ⚖️ THE NODE'S OWN GRID, so the bead lands on the very same 18px track the
     chapters use and the title starts on their text column. A third track,
     auto-width, takes the count at the right — the side the rail's other
     figures live on. Laid out as flex it could only ever approximate those two
     edges; as the same grid it cannot miss them. */
  display: grid;
  grid-template-columns: 18px 1fr auto;
  column-gap: 10px;
  align-items: center;
  margin: 0 0 12px;
  padding: 11px 14px;
  border-radius: var(--r-lg, 16px);
  border: 1px solid var(--line);
  background: color-mix(in oklab, var(--bg-surface) 1%, transparent);
  color: var(--text);
  text-decoration: none;
  transition: border-color var(--dur-med) var(--ease-out),
              background var(--dur-med) var(--ease-out);
}
.nrb-tl-cast::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  /* the chapters' own axis — see .nrb-tl-spine, which reads the same sum */
  left: calc(14px + 9px); transform: translateX(-50%); width: 1px;
  top: calc(-1 * var(--sp-card)); bottom: -12px;
  background: color-mix(in oklab, var(--accent) 26%, transparent);
}
/* The figure sits ON the thread rather than beside it, the way a chapter's ring
   does — so it needs the ring's trick too: a ground of its own, or the line
   draws straight through the numeral. */
.nrb-tl-cast-fig, .nrb-tl-cast-title { position: relative; z-index: 1; }
/* the row is one line, so its gutter centres instead of hanging from the top */
.nrb-tl-cast .nrb-tl-gut { padding-top: 0; }
/* live: the same ground the chapters take, so being ON this stop looks the
   same as being on any other. */
.nrb-tl-cast.is-live {
  border-color: color-mix(in oklab, var(--accent) 55%, transparent);
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  box-shadow: 0 0 24px -8px color-mix(in oklab, var(--accent) 55%, transparent);
}
.nrb-tl-cast.is-live .nrb-tl-ring {
  border-color: var(--accent);
  background: radial-gradient(circle at 50% 35%, var(--accent-soft), transparent 70%), var(--bg-elevated);
}
.nrb-tl-cast:hover {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--line));
  background: color-mix(in oklab, var(--accent) 7%, transparent);
}
/* ⚠️ THE GLOBAL a:hover UNDERLINE REACHES IN HERE. The base rule that clears it
   is (0,1,0) and the hover rule that sets it is (0,1,1), so every
   row on this rail — the cast, the chapters and the standing stop — grew an
   underline the moment a pointer touched it. These are CARDS: the hover is the
   border and the ground, and a card does not underline itself. */
.nrb-tl-cast:hover, .nrb-tl-node:hover, .nrb-tl-cast:hover .nrb-tl-cast-title {
  text-decoration: none;
}
.nrb-tl-cast:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
/* The count reads as a FIGURE, the same tabular treatment the chapter chips and
   the clock use — a number the eye can compare, not a word. */
/* ⚖️ THE COUNT IS A LABEL ON THE RIGHT, NOT A NUMERAL ON THE LEFT (owner
   2026-08-22). Leading the row with a bare figure made the count the first
   thing read, when the row's subject is the WORDS — and it sat on the thread's
   axis, where the bead belongs. It reads as the rail's other right-hand meta
   now: mono, tabular, quiet, in a soft ground so it is a chip rather than
   loose digits. */
.nrb-tl-cast-fig {
  justify-self: end;
  min-width: 22px; text-align: center;
  padding: 2px 7px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; line-height: 1.4;
  font-feature-settings: "tnum";
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in oklab, var(--accent) 24%, transparent);
}
.nrb-tl-cast-title {
  min-width: 0;
  font-size: 13.5px; font-weight: 600; line-height: 1.25;
}
@media (prefers-reduced-motion: reduce) {
  .nrb-tl-cast, .nrb-tl-cast:hover { transition: none; }
}

/* ██ WHERE THINGS STAND — THE THREAD'S LAST STOP ██ (owner 2026-08-21)
   It wears .nrb-tl-node whole, which is the point: it is a stop on the same
   thread and should read as one. Two things it needs that the list was giving
   the chapters for free:

   1. THE GAP. The rows are spaced by the list's flex `gap`, and this sits
      OUTSIDE the list (that list is what the scroll-spy walks), so it pays for
      its own separation — the same 12px, or it lands flush against the last
      chapter and reads as part of it.
   2. ITS OWN --tl-gap. The spine's height is 100% PLUS that gap — an
      overhang that reaches down to the NEXT row and is what makes the beads
      read as one thread. --tl-gap is declared on .nrb-tl-list, so out here the
      calc would resolve against nothing and drop the whole declaration. It is
      0 because there IS no next row: the last chapter's own overhang bridges
      the 12px down to this stop, and the thread ends here. */
.nrb-tl-node--standing {
  margin-top: 12px;
  --tl-gap: 0px;
  /* the run's bottom clearance moves DOWN to whatever is last — see below */
  margin-bottom: var(--sp-card);
}
/* ⚠️ THE LIST'S BOTTOM PADDING WOULD STACK ON TOP OF THE GAP. Measured: the
   chapters sit 12px apart, but the stop landed 32px below the last one — the
   list's own bottom padding (--sp-card, 20px) plus this 12px margin.
   Two rows of the same thread at two different rhythms reads as a mistake, so
   the clearance is HANDED OVER rather than added to: when a standing stop
   follows, the list stops paying for the foot and the stop pays instead. */
.nrb-tl-list:has(+ .nrb-tl-node--standing) { padding-bottom: 0; }

/* ██ THE THREAD ENDS AT THE LAST STOP, WHICH IS NO LONGER THE LAST CHAPTER ██
   The spine is terminated by .nrb-tl-node:last-child, cut back to the bead so
   the thread stops on a dot instead of trailing into space. That selector means
   "last row of the LIST" — and with a stop now sitting below the list, the
   terminator was landing one row early: measured, the last chapter's spine ran
   29px where its neighbours ran 99, and the stop hung 71px below the end of the
   thread, visibly detached from the thing it belongs to.

   So the two rows trade jobs. The last chapter gets its FULL spine back (it has
   a neighbour now, and the run must reach it), and the stop takes the
   terminator. --tl-anchor is declared on .nrb-tl-list, which is not an ancestor
   of the stop, so the stop restates it — 25px, the measured bead centre of an
   open row. */
.nrb-tl-list:has(+ .nrb-tl-node--standing) .nrb-tl-node:last-child .nrb-tl-spine {
  height: calc(100% + var(--tl-gap));
}
.nrb-tl-node--standing { --tl-anchor: 25px; }
.nrb-tl-node--standing .nrb-tl-spine { height: var(--tl-anchor); }

.nrb-tl-list {
  /* The horizontal inset lives on .nrb-tl-inner now (it has to, so the head
     can be flush with it) - this only keeps the run clear of the panel's foot. */
  padding: 0 0 var(--sp-card);
  display: flex; flex-direction: column; gap: 12px;
  --tl-gap: 12px;      /* must equal the flex gap above */
  --tl-anchor: 25px;   /* MEASURED bead centre — see the folded note below */
}

/* chapter card — translucent so the bright spine reads through it.
   An <a> here, not the DS's <button>: see TopicTimelineView's header. */
.nrb-tl-node {
  position: relative; z-index: 0;
  display: grid;
  /* 18px, not 52: the gutter held a 44px ring over a date chip. The ring is a
     10px bead now and the chip moved out of the gutter (see .nrb-tl-time). */
  grid-template-columns: 18px 1fr;
  column-gap: 10px;
  align-items: start;
  width: 100%; min-height: 44px; text-align: left;
  padding: 13px 14px;
  border-radius: var(--r-lg, 16px);
  border: 1px solid var(--line);
  background: color-mix(in oklab, var(--bg-surface) 1%, transparent);
  color: inherit; text-decoration: none;
  /* box-shadow is IN the list on purpose: the scroll-spy's .is-live carries a
     glow, and a glow that snaps on at every chapter boundary reads as a flicker
     down the whole column while the reader scrolls. 260ms is slower than
     --dur-fast because this transition is driven by scrolling rather than by a
     pointer — it has to survive passing THROUGH a chapter without strobing. */
  /* PADDING IS IN THE LIST, and it has to be: the fold shrinks the card by
     rolling its meta row away AND closing 13px of padding to 7px. Animate the
     first without the second and the row loses its contents smoothly, then
     jumps the last 12px — which is what "not smooth" looked like. */
  transition:
    border-color 260ms var(--ease-out), background 260ms var(--ease-out),
    box-shadow 260ms var(--ease-out), opacity var(--dur-fast) var(--ease-out),
    padding 260ms var(--ease-out);
}
/* Same as the head: the base sheet's a:hover underline propagates into the
   title, the source count, the ring number and the chip. A chapter card is a
   card, not a prose link — it says it is interactive with its border and its
   ring, never with a rule under four unrelated pieces of text. */
a.nrb-tl-node,
a.nrb-tl-node:hover,
a.nrb-tl-node:focus,
a.nrb-tl-node:focus-visible,
a.nrb-tl-node:active { text-decoration: none; }
.nrb-tl-node:hover { border-color: color-mix(in oklab, var(--accent) 45%, var(--line)); background: var(--bg-surface); }
a.nrb-tl-node:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The rows are <a href="#move-N"> again (owner 2026-08-19, second ruling: the
   chronicle is back in the body). The hover rules above stay CLASS-scoped
   rather than a-scoped so a future hrefless state still feels touchable. */
.nrb-tl-node { cursor: pointer; }

/* ── THE HAIRLINE (owner 2026-08-19: "kept as a faint hairline") ───────────
   The rail's accent spine — 2px, glowing — went to the chronicle with the rest
   of the ornament. What stays is the quietest line that still reads as ONE
   thread rather than a stack of rows: 1px, no glow, a third of the accent. */
.nrb-tl-spine {
  position: absolute; z-index: 1;
  left: calc(14px + 9px);  /* card padding-left + half the 18px gutter */
  top: 0; height: calc(100% + var(--tl-gap));
  width: 1px; transform: translateX(-50%);
  background: color-mix(in oklab, var(--accent) 26%, transparent);
  pointer-events: none;
}
.nrb-tl-node:first-child .nrb-tl-spine { top: var(--tl-anchor); height: calc(100% - var(--tl-anchor) + var(--tl-gap)); }
.nrb-tl-node:last-child  .nrb-tl-spine { height: var(--tl-anchor); }

/* gutter: ring bead stacked over a time chip — both opaque, so the
   spine breaks cleanly around them like beads on a thread */
.nrb-tl-gut { display: flex; justify-content: center; padding-top: 6px; }
/* ── THE BEAD ──────────────────────────────────────────────────────────────
   A 44px numbered ring became a 10px dot when the numbers went to the
   chronicle. It stays z-2 and OPAQUE for the same reason it always was: the
   hairline threads behind the gutter and the bead is what breaks it. */
.nrb-tl-ring {
  position: relative; z-index: 2;
  width: 10px; height: 10px; flex: none; border-radius: 50%;
  border: 1.5px solid color-mix(in oklab, var(--accent) 55%, transparent);
  background: var(--bg-elevated);
  transition: all var(--dur-fast) var(--ease-out);
}
.nrb-tl-node:hover .nrb-tl-ring,
.nrb-tl-cast:hover .nrb-tl-ring { border-color: var(--accent); background: var(--accent); }

/* ══ DONE (owner 2026-08-19) ═══════════════════════════════════════════════
   A chapter the reader has been past wears a green tick, and it KEEPS it —
   .is-done comes from a high-water mark that only falls, not from the
   reading position, so scrolling back up unfolds the row without un-marking
   it. The two states are independent on purpose: folded is where you ARE,
   done is where you have BEEN.

   The bead grows from 10px to 18px to have room for the tick. It grows on the
   same curve as the fold, so a chapter closing and a chapter being marked read
   as one motion rather than two things happening at a chapter. */
/* ⚖️ THE SAME RECIPE AS THE BACK-TO-TOP BUTTON (owner 2026-08-19, pointing at
   it: stroke colour is great, the icon is green, the background is somewhat
   almost transparent green). That control is color:--info + a 45% border
   + a 14% ground; this is that, in --success. The first cut was a SOLID green
   fill with the tick knocked out in the page colour — a filled badge, loud
   beside a 10px bead, and nothing else on the page is drawn that way. */
/* ⚠️ AND THE CAST ROW WEARS THEM TOO. These were scoped to .nrb-tl-node, so
   a row that is a stop on the same thread could not show that it had been
   read — the tick is the state, and the state is not a chapter's alone. */
.nrb-tl-node.is-done .nrb-tl-ring,
.nrb-tl-cast.is-done .nrb-tl-ring {
  width: 18px; height: 18px;
  color: var(--success);
  border-color: color-mix(in oklab, var(--success) 45%, transparent);
  background: color-mix(in oklab, var(--success) 14%, transparent);
}
/* ⚠️ AND THE THREAD FOLLOWS THE BIGGER RING. A done row that is still OPEN
   wears the 18px ring in a 10px bead's place, which pushes the bead centre
   from 25 to 29 — measured. Without this the spine under every read-but-open
   chapter stops 4px short. The folded case re-declares it again below (23) and
   wins by coming later in the sheet, which is the only reason these two can
   both be written as flat single-class rules. */
.nrb-tl-node.is-done { --tl-anchor: 29px; }
/* The tick, drawn rather than typed: two borders on a rotated box, in
   currentColor so it is the same green as the ring that holds it. A glyph
   would inherit the sheet's font stack and land differently in every fallback;
   an SVG would mean markup for something the state already implies. */
.nrb-tl-node.is-done .nrb-tl-ring::after,
.nrb-tl-cast.is-done .nrb-tl-ring::after {
  content: "";
  position: absolute; left: 50%; top: 45%;
  width: 4px; height: 8px;
  border: solid currentColor;
  border-width: 0 2px 2px 0;
  transform: translate(-50%, -50%) rotate(45deg);
}
.nrb-tl-node.is-done:hover .nrb-tl-ring {
  border-color: var(--success);
  background: color-mix(in oklab, var(--success) 22%, transparent);
}

/* eu theme: solid white glyphs inside the rings (dark mode only — in the
   eu light mode the pale rings need accent-ink glyphs back) */
[data-theme="eu"] .nrb-tl-ring,
[data-theme="eu"] .nrb-tl-node.is-live .nrb-tl-ring { color: #fff; }
[data-theme="eu"][data-mode="light"] .nrb-tl-ring,
[data-theme="eu"][data-mode="light"] .nrb-tl-node.is-live .nrb-tl-ring { color: var(--accent); }
@media (prefers-color-scheme: light) {
  [data-theme="eu"]:not([data-mode="dark"]) .nrb-tl-ring,
  [data-theme="eu"]:not([data-mode="dark"]) .nrb-tl-node.is-live .nrb-tl-ring { color: var(--accent); }
}
/* The chip rides the META ROW now (the gutter is 18px and holds only the
   bead), so it no longer has to break the thread and no longer needs its own
   opaque plate — it is one word in a quiet line. */
/* REGULAR (owner 2026-08-19): every delta on the page is regular weight — the
   rail's chip here and the chronicle's own Δ row in siteCss. It is a
   measurement, not an emphasis. */
.nrb-tl-time {
  font-size: var(--fs-caption); font-weight: 400; letter-spacing: .02em;
  color: var(--accent);
  white-space: nowrap;
  white-space: nowrap;
}
.nrb-tl-body { min-width: 0; display: flex; flex-direction: column; gap: 6px; padding-top: 3px; }
/* MEDIUM, not semibold (owner 2026-08-19): six of these stacked in a 360px
   column at 600 read as a list of headlines competing with the story beside
   them. 500 keeps the title first in the row's hierarchy without shouting. */
.nrb-tl-title { font-size: var(--fs-meta); font-weight: 500; color: var(--text); line-height: 1.3; letter-spacing: -.01em; text-wrap: pretty; }
.nrb-tl-meta {
  font-size: var(--fs-caption); color: var(--faint);
  display: flex; align-items: baseline; gap: 8px;
  /* NO text-transform: Bulgarian words keep their case (handoff §4). */
  text-transform: none; line-height: 1.5;
}
.nrb-tl-count::before { content: "·"; margin-right: 8px; color: var(--line); }

/* the chapter the reader is on — exactly one per column */
.nrb-tl-node.is-live {
  border-color: color-mix(in oklab, var(--accent) 60%, transparent);
  background: color-mix(in oklab, var(--accent) 9%, var(--bg-surface));
  box-shadow: 0 0 24px -8px color-mix(in oklab, var(--accent) 55%, transparent);
}
.nrb-tl-node.is-live .nrb-tl-ring {
  border-color: var(--accent);
  background:
    radial-gradient(circle at 50% 35%, color-mix(in oklab, var(--accent) 55%, transparent), transparent 72%),
    var(--bg-elevated);
  color: var(--text);
  box-shadow: 0 0 16px -2px color-mix(in oklab, var(--accent) 55%, transparent);
}
/* ══ THE ACCORDION (owner 2026-08-19) ══════════════════════════════════════
   Above the reading position the thread FOLDS: a chapter the reader has gone
   past keeps its number and its title on one clamped line and gives up its
   date chip and its source count. Below the reading position — the story's
   earlier chapters, still ahead of them — everything stays open.

   ⚖️ WHY UPWARD. The rail runs newest-first and the body now does too, so
   scrolling down the article walks DOWN the column. Folding what is above is
   folding what is behind you; the road ahead stays legible. A collapsed row is
   still a full anchor — the point is a shortcut list, not a discard pile.

   ⚠️ THIS IS A SCRIPT-ONLY STATE. .is-after is set by the scroll-spy from
   the reading position, which a baked page cannot know, so a script-free rail
   is fully open — correct, and 20.1170's floor. It also means NOTHING folds at
   hydration on a page opened at the top: the live chapter is then the topmost
   one and there is nothing above it. Landing deep via #move-N does fold on
   mount, inside the rail only — the article's own box never moves. */

/* the ghost stays: .72 rather than the DS's .6, because the handoff's §7
   requires the ghosted state to still pass contrast and .6 does not here */
.nrb-tl-node.is-after { opacity: .72; }
.nrb-tl-node.is-after:hover, .nrb-tl-node.is-after:focus-within { opacity: 1; }

/* ⚠️ THE SPINE IS LAYOUT-COUPLED (see this file's header). --tl-anchor is the
   BEAD'S CENTRE from the card's top, and it is MEASURED rather than derived.
   Derived, the two came out 24 and 16; measured they are 25 and 23, because
   the gutter's own 6px padding and the ring's border both land in the sum and
   both were missed. The folded figure was 7px out — a visible break in the
   thread under the last folded card. A folded row is always a DONE row (folded
   means above the reader, which is exactly what earns the tick), so it is the
   18px ring that is measured there, not the 10px bead. The variable is
   re-declared ON THE CARD, so every spine calc() that reads it follows without
   being rewritten; that is the whole reason it is a custom property. */
.nrb-tl-node.is-after {
  --tl-anchor: 23px;
  padding-top: 7px; padding-bottom: 7px;
  min-height: 0;
}
.nrb-tl-node.is-after .nrb-tl-gut { gap: 0; }
.nrb-tl-node.is-after .nrb-tl-body { gap: 0; padding-top: 2px; }
/* ONE LINE, TRUNCATED — the title is the whole payload of a folded row, so it
   is the one thing that may not disappear. Clamped rather than nowrap so a
   long Bulgarian headline ends in an ellipsis instead of overflowing a 360px
   column. */
.nrb-tl-node.is-after .nrb-tl-title {
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* ██ THE DELTA SURVIVES THE FOLD, UNDER THE TITLE (owner 2026-08-19) ██
   It first rolled away with the row; then it was pinned to the right END OF
   THE TITLE'S LINE, which cost no height but read as a badge stuck to a
   headline. It now sits BELOW the title where it sits on an open card — the
   same place, one line up. A folded row is two short lines rather than one,
   which is the trade the owner asked for.

   ⚖️ NOTHING TO OVERRIDE. The meta is a normal flex child of the body again,
   so this is the ABSENCE of a rule rather than a rule — the only thing the
   fold still does to it is clamp the title above it to a single line. */
.nrb-tl-node.is-after .nrb-tl-meta { max-height: none; opacity: .8; }
/* the OPEN state's side of the same transitions — declared here so both ends
   exist and the card animates in BOTH directions (folding as the reader passes
   a chapter, unfolding when they scroll back up) */
.nrb-tl-time, .nrb-tl-meta { max-height: 4em; }
/* ══ THE FOLD OPENS LIKE A CUP (owner 2026-08-19) ══════════════════════════
   The first cut was 240ms on the sheet's default ease-out — quick, linear-
   feeling, and it read as a row snapping shut. Half again as long (360ms) on a
   curve that leaves slowly, travels, and SETTLES rather than arriving:
   cubic-bezier(.34,.68,.24,1) is flatter at both ends than --ease-out.

   ⚖️ The petals and the cup move on DIFFERENT clocks, which is what stops it
   looking like one box resizing. Closing: the contents fade first (200ms) and
   the height follows them down, so the row empties and then closes. Opening:
   the height leads and the contents arrive 120ms into it, so the cup opens
   and then fills. Same two declarations, read in opposite directions — the
   delay lives on the OPEN state, so it applies only on the way back. */
/* ══ AND IT CLOSES IN ORDER (owner 2026-08-22) ══════════════════════════════
   "it needs to be one after another, so that step 1 always first, step 2 etc …
   then the collapse needs to be super smooth and not so fast."

   Two changes, and they are separate ideas.

   ⚖️ THE STAGGER IS WHAT MAKES IT A SEQUENCE INSTEAD OF AN EVENT. Rows used to
   change together, so a settle after a rail click or a scrub folded four of
   them in one movement and the eye read a single flinch. Each row now starts a
   beat after the one above it — small enough that the first is already moving
   before the reader registers a wait, large enough that the order is legible.
   ⚠️ The delay is keyed to POSITION IN THE LIST, not to the reading direction:
   nth-child(1) is the top row on screen, which is the one the owner means by
   "step 1", whichever way the thread happens to run.

   ⚖️ AND THE CLOSE IS THE SLOW HALF. Opening and closing are not the same
   gesture: opening restores something the reader asked for and should feel
   prompt; closing takes something away, and taking something away quickly is
   how a row reads as snapping shut. 360 → 560ms on the close, with the open
   left where it was. Same two declarations, read in opposite directions.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {
  /* the beat between rows — one number, read by every clock below */
  .nrb-tl-list { --tl-beat: 38ms; }
  .nrb-tl-list .nrb-tl-node { --tl-step: 0ms; }
  .nrb-tl-list .nrb-tl-node:nth-child(2)  { --tl-step: calc(var(--tl-beat) * 1); }
  .nrb-tl-list .nrb-tl-node:nth-child(3)  { --tl-step: calc(var(--tl-beat) * 2); }
  .nrb-tl-list .nrb-tl-node:nth-child(4)  { --tl-step: calc(var(--tl-beat) * 3); }
  .nrb-tl-list .nrb-tl-node:nth-child(5)  { --tl-step: calc(var(--tl-beat) * 4); }
  .nrb-tl-list .nrb-tl-node:nth-child(6)  { --tl-step: calc(var(--tl-beat) * 5); }
  .nrb-tl-list .nrb-tl-node:nth-child(7)  { --tl-step: calc(var(--tl-beat) * 6); }
  .nrb-tl-list .nrb-tl-node:nth-child(8)  { --tl-step: calc(var(--tl-beat) * 7); }
  /* ⚠️ THE LADDER STOPS HERE ON PURPOSE. Past eight rows the tail would wait
     longer than the movement itself; every row from the ninth down joins on the
     eighth's beat, so a long chronicle still reads as a sequence and still
     finishes inside a second. */
  .nrb-tl-list .nrb-tl-node:nth-child(n+9) { --tl-step: calc(var(--tl-beat) * 8); }

  .nrb-tl-node,
  .nrb-tl-gut, .nrb-tl-body, .nrb-tl-ring {
    transition-duration: 360ms;
    transition-timing-function: cubic-bezier(.34,.68,.24,1);
    transition-delay: var(--tl-step, 0ms);
  }
  .nrb-tl-gut, .nrb-tl-body { transition-property: gap, padding; }
  /* OPEN: the cup is already moving when the contents arrive. */
  .nrb-tl-time, .nrb-tl-meta {
    transition:
      max-height 360ms cubic-bezier(.34,.68,.24,1) var(--tl-step, 0ms),
      padding 360ms cubic-bezier(.34,.68,.24,1) var(--tl-step, 0ms),
      opacity 240ms cubic-bezier(.34,.68,.24,1) calc(var(--tl-step, 0ms) + 120ms);
  }
  /* CLOSING: slower, and the contents still leave before the height follows. */
  .nrb-tl-node.is-after,
  .nrb-tl-node.is-after .nrb-tl-gut,
  .nrb-tl-node.is-after .nrb-tl-body,
  .nrb-tl-node.is-after .nrb-tl-ring {
    transition-duration: 560ms;
    transition-timing-function: cubic-bezier(.28,.64,.18,1);
  }
  .nrb-tl-node.is-after .nrb-tl-time,
  .nrb-tl-node.is-after .nrb-tl-meta {
    transition:
      max-height 560ms cubic-bezier(.28,.64,.18,1) var(--tl-step, 0ms),
      padding 560ms cubic-bezier(.28,.64,.18,1) var(--tl-step, 0ms),
      opacity 300ms cubic-bezier(.28,.64,.18,1) var(--tl-step, 0ms);
  }
}





/* ── THE HEAD STICKS, AND IS THE WAY BACK UP ───────────────────────────────
   It rides down the whole article (the column stretches for exactly this), and
   once it is stuck it grows an up-arrow in its middle cell. The control itself
   is an <a href="#top"> and works with no script; only the ARROW waits for the
   stuck state, because a static page has no scroll position to describe. */
/* The base sheet underlines every anchor on hover (siteCss a:hover). The head
   is a link now, and text-decoration propagates to its descendants — so the
   day, the date and the clock all picked up a rule written for prose links.
   Killed in every interaction state, not just at rest. */
.nrb-tl-head,
.nrb-tl-head:hover,
.nrb-tl-head:focus,
.nrb-tl-head:focus-visible,
.nrb-tl-head:active { color: inherit; text-decoration: none; }
@media (min-width: 1180px) {
  /* ⚠️ TOP 0, NOT THE PIN. The head used to stick against the VIEWPORT, back
     when the rail was a column flowing down the page. The panel is the
     scrollport now, so both the head and the labels pin against IT — and the
     panel is what holds the 78px. An offset measured against the wrong box is
     the classic sticky bug: it looks right until the box moves. */
  .nrb-tl-head { position: sticky; top: 0; z-index: 3; }

  /* ⚠️ THE BACKDROP IS GONE (owner 2026-08-19, who selected it on the page:
     "there seems to be some dark rectangle at the top of this timeline").
     It was, and it had stopped doing anything long before that.

     It was built when the rail FLOWED down the page: the head stuck against
     the VIEWPORT, nothing clipped, and a chapter scrolling up really did
     appear beside and above it. Then the rail became a PINNED PANEL with
     overflow-y: auto — and a scroll container clips its own content, which
     is the same job. The plate has been decoration ever since.

     Measured before deleting: with the panel scrolled to the bottom the head
     sits flush at the panel's top edge (both 78px), so the strip the plate
     covered is ZERO PIXELS tall, and hit-testing it returns nothing painted
     with the plate or without it. What the plate DID do was paint a flat
     --bg-base rectangle over a page whose dark theme is a gradient — a visible
     dark block above the card.

     ⚖️ Third patch this week to outlive its cause (the month labels' pin, the
     Δ row, this). When the thing a workaround worked around is replaced,
     the workaround does not announce itself. */

  /* THE SECTIONS STICK. A month label pins directly beneath the head and stays
     there for the length of its own group, so the thread always says which
     month the reader is in — and, being opaque and the full width of the
     column, it is also what the cards disappear UNDER. The handover is free:
     each label's containing block is its own group, so the next month's label
     pushes into the pin exactly as the previous group ends.

     ⚠️ THE STICKY LABEL IS GONE WITH THE LABELS (2026-08-19). It used to pin
     here and cover the head's bottom margin, which is why the backdrop stopped
     at the head's border rather than reaching past it. With no label to bury,
     the backdrop takes that job back — see its bottom below. */
}
.nrb-tl-head.is-stuck { cursor: pointer; }
.nrb-tl-head.is-stuck .nrb-tl-head-top { visibility: visible; }
.nrb-tl-head.is-stuck:hover {
  border-color: color-mix(in oklab, var(--info) 85%, transparent);
  box-shadow: 0 0 28px -6px color-mix(in oklab, var(--info) 70%, transparent);
}
.nrb-tl-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.nrb-tl-head-top {
  /* ⚖️ HIDDEN, NOT ABSENT. It holds its 30px track from the first paint, so the
     moment it becomes visible nothing around it has to move. */
  visibility: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  justify-self: center;
  width: 30px; height: 30px; border-radius: 50%;
  color: var(--info);
  border: 1px solid color-mix(in oklab, var(--info) 45%, transparent);
  background: color-mix(in oklab, var(--info) 14%, transparent);
}
.nrb-tl-head.is-stuck:hover .nrb-tl-head-top { color: var(--text); border-color: var(--info); }
/* ⚖️ THE LIVE MARKER IS THE ARROW NOW. The widget carried two of them — a
   pulsing dot beside the clock and this circle — and the dot was the one that
   cost layout. One marker, on the control that is already a circle, and the
   ring it throws is drawn OUTSIDE its own box (box-shadow, not size) so the
   pulse cannot push anything either. */
@media (prefers-reduced-motion: no-preference) {
  .nrb-tl-head.is-stuck .nrb-tl-head-top { animation: nrb-tl-pulse 2.4s infinite; }
}
@media (prefers-reduced-motion: no-preference) {
  .nrb-tl-head-top { transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
}

/* the head — the "now" anchor. Its RESERVE is fixed by the day+date cell, the
   only cell a script-less page ships: the clock arrives INSIDE this height, so
   nothing below the rail can move when script lands (20.1170's second test). */
/* ██ THE DESIGN'S HEAD, RESTORED (owner 2026-08-19) ██
   The handoff's rule is padding + radius + border + tinted ground + glow, and
   NOTHING else — no height, no margin. I added both and a backdrop to go with
   them, all inside the DOCKED media query, which is why the drawer kept the
   real design and only the web version drifted. The owner spotted it from the
   two side by side.

   Each addition had a reason and each reason is now gone:
   - height:88px existed so the sticky MONTH LABELS could pin at a known
     offset beneath the head. The labels were retired hours later; the height
     outlived them.
   - margin:0 20px 20px gave the card side gaps — and those gaps are what a
     scrolling chapter leaked through, which is what the backdrop was then
     built to cover. The gap creates the fault the plate hides.
   The inset moved to .nrb-tl-inner, where it belongs: the CONTAINER holds
   the column's padding and the card is flush inside it, so there are no side
   margins left to leak through and no plate needed over them. */
.nrb-tl-head {
  /* ⚖️ THE ARROW'S TRACK IS RESERVED, NOT DISCOVERED (owner 2026-08-22: "when
     the arrow go up appears should not move"). It used to live in a 1fr middle
     cell, so the moment it appeared that column's min-content jumped to 30px
     and the two auto columns beside it were squeezed — which is why the clock
     lost its last digit at the narrowest docked width and nowhere else. A fixed
     track costs an empty 30px on a page that never scrolls, and buys a widget
     that cannot move. ⚠️ The no-script page has no arrow at all and reserves it
     too; that is the point, not an oversight. */
  /* ⚖️ THE CLOCK AND THE ARROW ARE FIXED; THE WORDS YIELD. minmax(0, 1fr) lets
     the day/date column shrink to nothing, so the two cells that must never be
     clipped never are — whatever the weekday is called. ⚠️ Pixel-tuning this
     column is a trap: it was sized against Bulgarian's "понеделник", and
     Portuguese ships "quarta-feira". A layout tuned to the longest word you
     happened to look at is a layout that breaks in the edition you did not. */
  display: grid; grid-template-columns: minmax(0, 1fr) 30px auto; align-items: center; gap: 10px;
  margin: 0 0 var(--sp-card);
  /* ⚠️ 18px, NOT --sp-card. MEASURED at the rail's 272px clamp floor: the head
     had 190px of content box for 217.6px of content and overflowed by 27.6 —
     it has never fitted at the narrowest docked width, and the arrow's arrival
     only made the shortfall visible by adding 30px to it. Every number in this
     widget is now sized so the sum fits there, because a fixture that is
     stable at one width and clipped at another is not a fixture. */
  padding: 18px;
  /* the progress bar rides the bottom edge, absolutely — see below */
  position: relative;
  overflow: hidden;
  border-radius: var(--r-lg);
  border: 1px solid color-mix(in oklab, var(--info) 60%, transparent);
  background: color-mix(in oklab, var(--info) 9%, var(--bg-surface));
  box-shadow: 0 0 24px -8px color-mix(in oklab, var(--info) 55%, transparent);
}
/* the yielding column: it takes what is left and truncates rather than push */
.nrb-tl-head-when { min-width: 0; }
.nrb-tl-head-day {
  font-size: 15px; font-weight: 700; color: var(--accent);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* nowrap AND tabular: the date is the widest thing in its column, and a
   proportional date changes width as the digits do. */
.nrb-tl-head-date { font-size: 11.5px; color: var(--muted); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
.nrb-tl-head-clock {
  display: flex; align-items: center; justify-content: flex-end;
  padding: 2px 0 2px 10px; border-left: 1px solid var(--line);
}
/* ⚖️ TABULAR FIGURES, AND THIS IS THE OTHER HALF OF "STABLE". A proportional
   1 is narrower than a 7, so 11:11 and 17:27 are not the same width — the cell
   was re-measuring itself every minute, all day, in the tightest column on the
   page. tnum makes every digit one width, so the clock occupies the same space
   at every hour it will ever show.
   ⚠️ AND IT IS 18px, NOT 22. At the narrowest docked width the rail is at its
   272px floor, and 22px of clock plus a reserved arrow does not fit beside the
   day and the date — this is the size at which nothing has to give. */
.nrb-tl-head-time {
  font-size: 17px; font-weight: 600; letter-spacing: .01em; color: var(--text);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum";
}

/* ██ THE READING PROGRESS, ALONG THE HEAD'S BOTTOM EDGE ██ (owner 2026-08-20:
   "at the bottom of it can you make progress to the place of the reading").

   ⚖️ ABSOLUTE, SO IT COSTS THE HEAD NO LAYOUT. The head is a three-column grid
   with measured cells; a fourth FLOW child would open an implicit row and grow
   the head the instant the script arrived. That is movement at hydration, which
   is the one thing 20.1170 forbids outright — the same reason the clock cell has
   its height reserved rather than reserving it by existing.

   The head is the "now" object: it says where the STORY stands (its latest
   development, its live clock), and this one line says where the READER stands
   in it. Blue-to-gold is not decoration either — the sheet already spends blue
   on live/now and gold on the reading position, so the fill travels from the
   story's colour to the reader's. */
.nrb-tl-head-progress {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: color-mix(in oklab, var(--info) 20%, transparent);
}
.nrb-tl-head-progress-fill {
  display: block; height: 100%; width: 0;
  border-radius: 0 2px 2px 0;
  background: linear-gradient(90deg, var(--info), var(--accent));
  /* Short: the bar answers a scroll, and a bar that lags its own scroll reads
     as a broken bar rather than a smooth one. */
  transition: width .18s var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .nrb-tl-head-progress-fill { transition: none; }
}

/* ⚠️ THE NOW DOT IS RETIRED (owner 2026-08-22) and its rules go with it — the
   pulse moved to the back-to-top circle above. The pulse keyframes stay because
   that circle uses them; the ring-pulse pair had no other caller and is gone.
   Blue is still live/now and gold is still the reading position: the separation
   the handoff asks for survives the marker that used to carry it. */
@keyframes nrb-tl-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--info) 50%, transparent) } 70% { box-shadow: 0 0 0 9px transparent } 100% { box-shadow: 0 0 0 0 transparent } }
@media (prefers-reduced-motion: reduce) {
  .nrb-tl-head-top { animation: none; }
}

/* ── THE ARRIVAL (owner 2026-08-19) ────────────────────────────────────────
   The confetti that lived here is retired: it existed because a chapter had
   nothing to open while the chronicle was paid, and the chapters are back. A
   click now goes somewhere, so the job changed from "make the dead click fun"
   to "confirm the arrival" — which is a smaller, quieter effect and a real one.

   ⚖️ ZERO JAVASCRIPT, by construction. This is :target, so the welcome fires
   on the fragment the browser is already handling — the same mechanism that
   made the head's back-to-top work script-free (20.1170). The scroll-spy's
   .is-live is the OTHER half and needs a script; these two must not be
   confused, because only one of them is allowed to be a fact on a baked page. */

/* THE LANDING PAD. Without this the chapter's own title arrives UNDER the
   sticky masthead — the anchor is honoured and the reader sees the second
   paragraph. 65px of header + air; kept in one place because the rail's
   --nrb-tl-top pays the same debt for the drawer. */
article section.move,
/* ⚠️ AND THE STANDING STOP, which the rail links to by name. Without it that
   jump lands the panel's own heading under the masthead — one selector, one
   number, because two rules with 84 in each is how they drift. The :target
   welcome below deliberately does NOT extend here: its keyframes end on a
   transparent background with fill:both, which would strip this panel's own
   --info fill and glow the moment the pulse finished. A chapter has no
   background to lose; this one does. */
article section.standing-stop { scroll-margin-top: 84px; }

@keyframes nrb-move-welcome {
  0%   {
    background: color-mix(in oklab, var(--accent) 15%, transparent);
    box-shadow:
      0 0 0 12px color-mix(in oklab, var(--accent) 15%, transparent),
      0 0 44px -6px color-mix(in oklab, var(--accent) 50%, transparent);
  }
  /* the settle: most of the wash is gone by a third of the way in, and the
     rest drains slowly — a pulse that decays rather than a flash that ends */
  35%  {
    background: color-mix(in oklab, var(--accent) 9%, transparent);
    box-shadow:
      0 0 0 12px color-mix(in oklab, var(--accent) 9%, transparent),
      0 0 30px -6px color-mix(in oklab, var(--accent) 28%, transparent);
  }
  100% {
    background: transparent;
    box-shadow: 0 0 0 12px transparent, 0 0 0 -6px transparent;
  }
}
@media (prefers-reduced-motion: no-preference) {
  article section.move:target {
    border-radius: var(--r-lg, 16px);
    /* duration, easing, DELAY, count, fill. The delay is the no-JS floor: even
       with no script the jump is a smooth glide (scroll-behavior is CSS), so a
       pulse that starts on click has half burned out before the reader
       arrives — measured, and the owner's word for it was "too early". 220ms
       covers a short glide. It cannot cover a long one: with no script there
       is no way to know how far the jump was, and a fixed wait long enough for
       the worst case makes every short jump feel dead. The script closes that
       gap below. */
    animation: nrb-move-welcome 2400ms cubic-bezier(.22,.85,.3,1) 220ms 1 both;
  }
  /* ⚖️ THE SCRIPT'S HALF, and it is a SUPPRESSOR rather than a trigger. While
     a rail click is still gliding the animation is switched OFF; REMOVING the
     attribute on arrival is what starts it, because an element re-acquiring an
     animation runs it from zero. No second class, no imperative restart, and
     if the script never loads the floor above still fires. */
  html[data-nrb-arriving] article section.move:target { animation: none; }
}
/* Asked for no motion ⇒ the arrival is still CONFIRMED, just held still: a flat
   tint that stays while the fragment matches. Dropping the effect entirely
   would leave exactly the readers who most need the cue without one. */
@media (prefers-reduced-motion: reduce) {
  article section.move:target {
    border-radius: var(--r-lg, 16px);
    background: color-mix(in oklab, var(--accent) 8%, transparent);
    box-shadow: 0 0 0 12px color-mix(in oklab, var(--accent) 8%, transparent);
  }
}


/* ══ THE RAIL, MIRRORED TO THE RIGHT ═══════════════════════════════════════
   A trial flip. Written as an OVERRIDE of the left-hand default rather than as
   a rewrite of it, so reverting is one word in the module above and no rules
   have to be re-derived. */
/* The DRAWER half first, then the docked half — order is load-bearing. Both
   set `transform`, and the drawer's off-canvas translateX(100%) is NOT inside a
   media query (the drawer is the default shape). Written after the docked rule
   it won at desktop too and shoved the rail a full column past the viewport —
   measured: right edge 1773 on a 1431 screen. */
.nrb-tl {
  left: auto; right: 0;
  border-right: 0; border-left: 1px solid var(--line);
  transform: translateX(100%);
}
@media (min-width: 1180px) {
  /* The rail is FIRST in the DOM (it is a nav, and reading order should reach
     it before the story), so both columns need explicit placement — otherwise
     auto-placement drops the article into track 1 the moment the rail leaves. */
  /* ⚠️ BOTH need an explicit ROW, not just a column. Grid auto-placement is
     SPARSE: placing the rail (DOM-first) at column 3 leaves the cursor past
     column 2 of row 1, so the article — which asks for column 2 — cannot go
     backwards and lands in row 2, i.e. UNDERNEATH the whole rail. Measured:
     the story started ~1200px down the page. Naming row 1 on both pins them
     side by side. */
  .nrb-tl { grid-column: 3; grid-row: 1; }
  .shell--tl > article { grid-column: 2; grid-row: 1; }
  /* ██ THE EMPTY TRACK STOPS BEING EMPTY (20.1810) ██ The mirror column exists
     to centre the story and has rendered nothing since the dock shipped. The
     colophon takes it — the section, the actors, and the stamp — and the two
     rails now face each other across the story.
     ⚖️ ONE HOME PER READER. The article's own meta lines carry the publication
     and the dates everywhere the colophon does not exist; here they hand over,
     so nothing is said twice at any width. Below this breakpoint the colophon
     is display:none and the meta lines are back. */
  .nrb-colo { grid-column: 1; grid-row: 1; min-width: 0; }
  /* ⚠️ BOTH SHAPES, BECAUSE EVERY REAL PAGE IS THE SECOND ONE. With the bundle
     on, the baker wraps the article's children in the hydration root, so the
     meta lines are article > [data-nrb-view] > .meta — a grandchild. The
     one-level selector this shipped with matched only the script-off page, so
     on every hydrated page the colophon and the meta line BOTH rendered and
     the dateline was said twice. Same pattern as the measure rule further up
     the sheet: name the direct child and the wrapped one. */
  .shell--tl > article > .meta,
  .shell--tl > article > [data-nrb-view] > .meta { display: none; }
  /* The mirrored DRAWER moved its hairline to the left edge, and that rule is
     not media-scoped (the drawer is the default shape) — so at desktop it drew
     a full-height rule down the gap between the story and the thread. The
     left-hand default zeroed border-right here; the mirror has to zero the
     other side. */
  .nrb-tl { border-left: 0; }
  /* ⚠️ THE PIN IS GONE ON THIS SIDE TOO (owner 2026-08-20) — but the
     DECLARATION STAYS, because it was doing two jobs and only one of them is
     retired. This was the MIRROR of the left-hand pin, and because SIDE is
     'right' it was the one actually shipping: deleting only the left-hand twin
     left the rail dragged to 16px from the screen while the who's-who had
     already moved to the content group (measured at 1700: margins 72 / 16).

     ⚖️ A MIRRORED RULE IS TWO RULES, AND ONLY ONE OF THEM RUNS.

     ⚠️ AND IT IS ALSO THE DESKTOP RESET. The drawer's off-canvas
     translateX(100%) is NOT media-scoped (the drawer is the default shape) and
     is written ABOVE this block, so whatever stands here is the only thing
     overriding it at desktop. Removing the declaration outright rather than
     neutralising it handed the rail a full-column shove — exactly the failure
     the note at the top of this block already described, reproduced: measured
     transform +320px, right margin -248. Hence an explicit none. */
  .nrb-tl { transform: none; }
}

/* ── 3. THE TWO DS UTILITIES, SCOPED ───────────────────────────────────────
   `.mono` and `.eyebrow` are tokens.css globals in the SPA. A baked sheet has
   no business minting global utility classes, so they are scoped to the rail. */
.nrb-tl .mono { font-family: var(--font-mono); font-feature-settings: "tnum"; }
.nrb-tl .eyebrow {
  font-family: var(--font-mono); font-size: var(--fs-overline);
  letter-spacing: var(--tracking-mega); text-transform: uppercase;
}



/* ── IN-ARTICLE FIGURE (design handoff, .nrbfig-*) ────────────────────────────
   Four encodings, one shell. Tokens only; no raw hex; no opacity on a container.
   MOTION: none. Figures do not animate, do not count up, and do not transition
   on hover — they are read, not operated.
   ────────────────────────────────────────────────────────────────────────── */

/* ---- shell: identical in every encoding ----
   ⚑ LIQUID GLASS (owner 2026-08-22) — a DEPARTURE from the handoff, which
   specifies a flat --bg-surface. The card is now a translucent pane that frosts
   whatever it sits on, with a glossy top edge.

   ⚖️ EVERY VALUE IS STILL A TOKEN MIXED WITH TRANSPARENT — no raw hex, no
   opacity on the container. Opacity on the box would fade the DATA with the
   glass; mixing the background alone leaves the drawing at full strength, which
   is the whole point.

   ⚠️ THE FROST NEEDS SOMETHING BEHIND IT. On the article this is the canvas and
   its star texture; drop the card on a flat surface and the blur has nothing to
   do, so it degrades to a plain translucent pane rather than to something
   broken. An engine without backdrop-filter gets the same. */
.nrbfig { position: relative; isolation: isolate;
  background: color-mix(in oklab, var(--bg-surface) 62%, transparent);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  border: 1px solid color-mix(in oklab, var(--text-primary) 13%, transparent);
  border-radius: var(--r-md, 12px); padding: 22px 24px 20px; display: grid; gap: 16px;
  margin: 22px 0;
  /* The gloss: a lit top edge, a settled bottom one, and the lift that stops a
     translucent pane from reading as a hole in the page. */
  box-shadow:
    inset 0 1px 0 color-mix(in oklab, var(--text-primary) 16%, transparent),
    inset 0 -1px 0 color-mix(in oklab, var(--bg-canvas) 55%, transparent),
    0 14px 34px -18px rgba(0, 0, 0, .55); }
/* the sheen — one soft diagonal wash, low enough that it never competes with a
   value printed on top of it */
.nrbfig::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; z-index: -1;
  background: linear-gradient(147deg,
    color-mix(in oklab, var(--text-primary) 7%, transparent) 0%,
    transparent 38%,
    transparent 72%,
    color-mix(in oklab, var(--bg-canvas) 30%, transparent) 100%); }
.nrbfig-hd { display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
  padding-bottom: 12px; border-bottom: 1px solid var(--border-default); }
.nrbfig-title { margin: 0; font-family: var(--font-display); font-size: 15px; font-weight: 600; letter-spacing: -.015em; }
.nrbfig-unit { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-tertiary); }
.nrbfig-note { margin: 0; display: flex; gap: 10px; align-items: baseline; font-size: 12.5px;
  line-height: 1.55; color: var(--text-secondary); text-wrap: pretty; }
.nrbfig-note b { flex: none; font-weight: 400; font-family: var(--font-mono); font-size: 8.5px;
  letter-spacing: .18em; text-transform: uppercase; color: var(--accent);
  border: 1px solid color-mix(in oklab, var(--accent) 34%, transparent); border-radius: 3px; padding: 3px 6px; }

/* ---- 02 · columns on a gridded baseline ---- */
.nrbfig-cols { display: grid; gap: 10px; }
.nrbfig-cols-plot { --plot-h: 214px; position: relative; display: grid; grid-auto-flow: column;
  grid-auto-columns: 1fr; align-items: end; gap: 34px; height: var(--plot-h); padding: 0 20px;
  border-bottom: 1px solid var(--border-strong); }
/* ⚠️ THE LABEL STOPS KNOCKING A HOLE IN THE LINE. It used to sit ON the
   gridline wearing an opaque --bg-surface patch; against a translucent card
   that patch reads as a brighter rectangle floating over the frost. So the LINE
   starts after the label instead, and the label sits in the gutter it left. No
   overpainting, and it works at any transparency. */
.nrbfig-gl { position: absolute; left: 26px; right: 0; top: calc(100% - var(--at,0) * var(--plot-h) / 100);
  border-top: 1px solid var(--border-subtle); }
.nrbfig-gl span { position: absolute; left: -26px; top: 0; transform: translateY(-50%);
  font-family: var(--font-mono); font-size: 9px; color: var(--text-tertiary); }
.nrbfig-col { position: relative; z-index: 1; display: grid; justify-items: center; align-content: end; gap: 8px; }
.nrbfig-col b { font-weight: 400; font-family: var(--font-mono); font-size: 15px; color: var(--accent); }
.nrbfig-col i { display: block; width: 100%; max-width: 64px; height: calc(var(--h,0) * var(--plot-h) / 100);
  background: linear-gradient(to top, color-mix(in oklab, var(--accent) 55%, transparent), var(--accent));
  border-radius: 3px 3px 0 0; }
.nrbfig-cols-names { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 34px;
  padding: 0 20px; font-size: 12px; color: var(--text-secondary); text-align: center; }

/* ---- 05 · one 100% stack ---- */
.nrbfig-stack { display: grid; gap: 12px; }
.nrbfig-stack-bar { display: flex; height: 56px; border-radius: 6px; overflow: hidden; }
/* THE SHARE IS THE WIDTH — never flex: a flex basis absorbs the label padding
   and skews the split away from the percentages printed inside it. */
.nrbfig-seg { flex: none; box-sizing: border-box; width: calc(var(--w,50) * 1%); min-width: 0;
  display: grid; align-content: center; gap: 3px; padding: 0 14px; background: var(--accent); color: var(--text-inverse); }
.nrbfig-seg--mid { background: color-mix(in oklab, var(--accent) 62%, var(--bg-elevated)); color: var(--text-primary); }
.nrbfig-seg--minor { background: color-mix(in oklab, var(--accent) 34%, var(--bg-elevated)); color: var(--text-primary); }
.nrbfig-seg b { font-weight: 400; font-family: var(--font-mono); font-size: 14px; }
.nrbfig-seg span { font-size: 10.5px; opacity: .8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nrbfig-stack-foot { display: flex; justify-content: space-between; gap: 12px;
  font-family: var(--font-mono); font-size: 12.5px; color: var(--text-secondary); }
.nrbfig-stack-foot b { font-weight: 400; color: var(--text-primary); }

/* ---- 06 · proportional areas ---- */
.nrbfig-areas { display: grid; grid-template-columns: auto auto 1fr; align-items: end; gap: 26px; padding: 4px 0 0; }
.nrbfig-sq { position: relative; width: calc(var(--side,100) * 1px); height: calc(var(--side,100) * 1px);
  display: grid; align-content: end; padding: 10px; border-radius: 4px;
  background: color-mix(in oklab, var(--accent) 82%, transparent); color: var(--text-inverse); }
.nrbfig-sq--minor { background: color-mix(in oklab, var(--accent) 34%, var(--bg-elevated)); color: var(--text-primary); }
.nrbfig-sq em { position: absolute; top: 8px; right: 10px; font-style: normal; font-family: var(--font-display);
  font-weight: 700; font-size: 22px; line-height: 1; letter-spacing: -.02em; font-variant-numeric: tabular-nums; opacity: .95; }
.nrbfig-sq b { font-weight: 400; font-family: var(--font-mono); font-size: 16px; line-height: 1; }
.nrbfig-sq span { font-size: 10px; opacity: .85; }
.nrbfig-areas-legend { margin: 0; align-self: end; padding-bottom: 6px; font-size: 12px;
  line-height: 1.55; color: var(--text-tertiary); }

/* ---- 06d · ring ---- */
.nrbfig-ring { display: grid; grid-template-columns: auto 1fr; gap: 26px; align-items: center; }
.nrbfig-ring svg text { font-family: var(--font-mono); }
.nrbfig-key { display: grid; gap: 10px; }
.nrbfig-key div { display: grid; grid-template-columns: 12px 1fr auto; gap: 10px; align-items: center;
  font-size: 12.5px; color: var(--text-secondary); }
.nrbfig-key i { height: 12px; border-radius: 2px; background: var(--accent); }
.nrbfig-key i.is-minor { background: color-mix(in oklab, var(--accent) 30%, var(--bg-elevated)); }
.nrbfig-key b { font-weight: 400; font-family: var(--font-mono); font-size: 13px; color: var(--text-primary); }

/* ---- the envelope, as rows under the finding (owner 2026-08-22) ----
   SEE · KEY · WHY moved inside the card and became rows of the highlights list.
   They compose it through its own two attributes — data-tone="neutral" (the
   accent is already spoken for here, by the drawing and by the note's chip) and
   data-density="compact" (a caption, not a headline) — so this styles the list
   rather than forking it. */
.nrbfig-env { margin-top: 2px; }
/* the one row that is not a plain sentence: a KEY's term leads it */
.nrbfig-env b { font-weight: 600; color: var(--text-secondary); }

/* ---- the table artifact, and the >5-value fallback ----
   NOT A FIFTH DRAWING. The panel has always had a TABLE artifact (20.1740), and
   past five values no drawing in the set can carry a figure honestly — so the
   numbers are printed instead of a shape being invented for them. It rides the
   same shell, so a page never shows two different table looks. */
.nrbfig-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.nrbfig-table { border-collapse: collapse; width: 100%; font-size: 12.5px; }
.nrbfig-table th, .nrbfig-table td { text-align: left; white-space: nowrap;
  padding: 6px 14px 6px 0; border-bottom: 1px solid var(--border-subtle); }
.nrbfig-table tbody tr:last-child th, .nrbfig-table tbody tr:last-child td { border-bottom: 0; }
.nrbfig-table thead th { font-family: var(--font-mono); font-weight: 400; font-size: 9px;
  letter-spacing: .14em; text-transform: uppercase; color: var(--text-tertiary); }
.nrbfig-table tbody th { font-weight: 400; color: var(--text-secondary); white-space: normal; }
.nrbfig-table td { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--text-primary); }

/* ---- narrow columns: the two area encodings stack ---- */
@media (max-width: 560px) {
  .nrbfig { padding: 18px 16px 16px; }
  .nrbfig-areas { grid-template-columns: auto auto; row-gap: 16px; }
  .nrbfig-areas-legend { grid-column: 1 / -1; padding-bottom: 0; }
  .nrbfig-ring { grid-template-columns: 1fr; justify-items: start; }
  .nrbfig-cols-plot { --plot-h: 170px; gap: 18px; }
  .nrbfig-cols-names { gap: 18px; }
}



/* ── THE HIGHLIGHTS LIST (design handoff, .nrbhl-*) ───────────────────────────
   The nutshell panel's АКЦЕНТИ. Tokens only; no fixed hex. Motion: none — no
   hover, no reveal, no stagger.

   Two spacings own the rhythm and live on the list, not the row:
     --nrbhl-gapx  bar → text   (20px)
     --nrbhl-pady  text → rule  (12px)
   ────────────────────────────────────────────────────────────────────────── */
.nrbhl { list-style: none; margin: 0; padding: 0; display: grid; gap: 0;
  --nrbhl-gapx: 20px; --nrbhl-pady: 12px; }
/* ⚖️ THE DISPLAY FACE AT READING SIZE — the one place in the article body that
   does this, and what makes the panel read as a summary rather than as prose. */
.nrbhl li { position: relative;
  padding: var(--nrbhl-pady) 0 var(--nrbhl-pady) var(--nrbhl-gapx);
  font-family: var(--font-display); font-weight: 400; font-size: 15.5px;
  line-height: 1.5; letter-spacing: -.012em; color: var(--text-primary); text-wrap: pretty; }
/* A rule BETWEEN rows only — never above the first or below the last. */
.nrbhl li + li { border-top: 1px solid var(--border-subtle); }
.nrbhl li:first-child { padding-top: 0; }
.nrbhl li:last-child { padding-bottom: 0; }
/* The bar is inset to the PADDING, so it measures the text block rather than
   the row box — and the end rows lose that inset so the list sits flush. */
.nrbhl li::before { content: ""; position: absolute; left: 0;
  top: var(--nrbhl-pady); bottom: var(--nrbhl-pady);
  width: 4px; border-radius: 3px; background: var(--accent); }
.nrbhl li:first-child::before { top: 0; }
.nrbhl li:last-child::before { bottom: 0; }
.nrbhl li:nth-child(2)::before { background: var(--info); }
.nrbhl li:nth-child(3)::before { background: var(--success); }
.nrbhl li:nth-child(4)::before { background: var(--warning); }
.nrbhl li:nth-child(5)::before { background: var(--hot); }
.nrbhl li:nth-child(n+6)::before { background: var(--text-tertiary); }

/* the kicker above the list */
.nrbhl-h { margin: 0 0 14px; font-family: var(--font-display); font-size: 11px;
  font-weight: 600; letter-spacing: .18em; text-transform: uppercase; color: var(--accent); }

/* one neutral bar for every row — for sections where the accent is busy elsewhere */
.nrbhl[data-tone="neutral"] li::before { background: var(--text-tertiary); }

/* tighter reading of the same list — mobile panels, sidebars */
.nrbhl[data-density="compact"] { --nrbhl-gapx: 16px; --nrbhl-pady: 10px; }
.nrbhl[data-density="compact"] li { font-size: 14.5px; }

@media (max-width: 560px) { .nrbhl { --nrbhl-gapx: 16px; --nrbhl-pady: 10px; } }



/* ══ PAGE MINIMAP · .nrb-mm-* ═══════════════════════════════════════════════
   Design system: shared/page-minimap.css · spec card
   components/page-minimap/page-minimap.html. Carried verbatim; the tokens are
   the site's own, so there is nothing to translate.
   ═════════════════════════════════════════════════════════════════════════ */
.nrb-mm{position:fixed;left:0;right:0;bottom:0;z-index:40;display:none;padding:0 var(--mm-gutter,12px) calc(var(--mm-pad-b,10px) + env(safe-area-inset-bottom,0px));pointer-events:none;
  --mm-h:18px;               /* graphic height of a block                     */
  --mm-gap:2px;              /* px cut off each block's right edge            */
  --mm-hit:44px;             /* touch strip, grown upwards only               */
  --mm-ink:var(--text,#14161c);
  --mm-accent:var(--accent,#b8860b);
  --mm-paper:var(--bg,#fff);
  --mm-unread:color-mix(in oklab,var(--mm-ink) 10%,transparent);
  --mm-read:color-mix(in oklab,var(--mm-accent) 32%,transparent);
  --mm-live:color-mix(in oklab,var(--mm-accent) 60%,transparent);
  --mm-lede:color-mix(in oklab,var(--mm-ink) 6%,transparent);
  --mm-win-line:color-mix(in oklab,var(--mm-ink) 55%,transparent);
  --mm-win-fill:color-mix(in oklab,var(--mm-ink) 5%,transparent)}

/* Phone only — the docked page keeps its right-hand chronicle column. */
@media (max-width:560px){.nrb-mm{display:block}}
/* Docs, previews and any surface that has no chronicle column at all. */
.nrb-mm--always{display:block}

/* The reach. The graphic is 18px; the rest is invisible thumb room that MUST
   NOT extend below the dock — on a gesture-navigation phone the bottom 20px
   belong to the OS, which wins every time and takes the first press of the
   session with it. */
.nrb-mm-hit{position:absolute;left:0;right:0;bottom:0;height:var(--mm-hit);pointer-events:auto;touch-action:none;-webkit-tap-highlight-color:transparent}

.nrb-mm-track{position:relative;height:var(--mm-h);pointer-events:none}

/* One block per chapter + one for the lede. Absolute, in document space. */
.nrb-mm-blk{position:absolute;top:0;bottom:0;padding:0;border:0;background:var(--mm-unread);border-radius:2px;pointer-events:auto;font:inherit;color:inherit;-webkit-tap-highlight-color:transparent;transition:background .16s linear}
.nrb-mm-blk::after{content:"";position:absolute;left:0;right:0;bottom:0;height:var(--mm-hit)}  /* the block's own 44px column */
.nrb-mm-blk.is-read{background:var(--mm-read)}
.nrb-mm-blk.is-live{background:var(--mm-live)}
/* ⚠️ NOT TAPPABLE, deliberately: a press on the lede falls through to the track
   and behaves as "centre me here" rather than "jump to the hero", which nobody
   wants. It is context, not a destination. */
.nrb-mm-blk--lede{background:var(--mm-lede);pointer-events:none}
.nrb-mm-blk:focus-visible{outline:2px solid var(--mm-accent);outline-offset:2px}

/* The viewport window: 3px proud of the blocks top and bottom, so it reads as
   glass laid OVER them rather than as one more block. */
.nrb-mm-win{position:absolute;top:-3px;bottom:-3px;border:1px solid var(--mm-win-line);border-radius:3px;background:var(--mm-win-fill);pointer-events:none}

/* Dimmed at rest, full while the reader is moving or touching. ⚖️ Idle dimming
   rather than hide-on-scroll: a map that is missing while you are moving is
   missing exactly when you need it. */
.nrb-mm-track,.nrb-mm-win{opacity:.62;transition:opacity .22s}
.nrb-mm.is-active .nrb-mm-track,.nrb-mm.is-active .nrb-mm-win{opacity:1}
.nrb-mm.is-drag .nrb-mm-win{border-color:var(--mm-accent);background:color-mix(in oklab,var(--mm-accent) 12%,transparent)}

/* Peek: the chapter under the finger. Drag only, ABOVE the strip — the finger
   sits below it, so nothing the reader needs to see is under their own thumb.
   Proportionally translated, so it never leaves the screen and never needs
   clamping arithmetic. */
.nrb-mm-peek{position:absolute;bottom:calc(100% + 10px);display:none;max-width:238px;padding:8px 11px;border-radius:10px;background:var(--mm-ink);color:var(--mm-paper);box-shadow:0 10px 30px -14px rgba(0,0,0,.55);pointer-events:none}
.nrb-mm.is-drag .nrb-mm-peek{display:block}
.nrb-mm-peek b{display:block;font-family:var(--font-mono,ui-monospace,Menlo,monospace);font-size:9.5px;letter-spacing:.13em;color:color-mix(in oklab,var(--mm-accent) 70%,var(--mm-paper))}
.nrb-mm-peek span{display:block;font-family:var(--serif,Georgia,serif);font-size:13.5px;line-height:1.28;margin-top:3px}
.nrb-mm-peek i{display:block;font-style:normal;font-family:var(--font-mono,ui-monospace,Menlo,monospace);font-size:9.5px;letter-spacing:.1em;opacity:.66;margin-top:4px}

/* The article has to clear the dock, or its last line hides under the strip. */
@media (max-width:560px){.nrb-mm-scope{padding-bottom:64px}}
.nrb-mm-scope--always{padding-bottom:64px}

@media (prefers-reduced-motion:reduce){.nrb-mm-blk,.nrb-mm-track,.nrb-mm-win{transition:none}}



/* ── the column ──────────────────────────────────────────────────────────── */
/* ⚠️ VISIBILITY LIVES HERE, NOT IN THE DOCK'S QUERY. COLOPHON_CSS is
   concatenated AFTER the timeline sheet, and both selectors are one class
   deep — so a display:block written over there loses to this display:none on
   source order alone, silently, and the column never appears. It owns its own
   switch and asks the timeline sheet for the breakpoint, so the two can never
   disagree about where the third track begins. */
.nrb-colo { display: none; }
@media (min-width: 1180px) { .nrb-colo { display: block; } }
.colo-inner {
  position: sticky; top: 84px;
  display: flex; flex-direction: column; gap: 22px;
  /* 22px is the hero's own top margin: the column's first line starts level
     with the picture's top edge rather than 16px above it, so the two tracks
     begin on the same line instead of nearly doing. */
  padding: 22px 0 2px 20px;
  /* The spine leads the text (owner 2026-08-24, left aligned). It sits on the
     reading edge, so every line — pill, stamp, actor — starts from the same
     vertical rule, which is the only thing holding three unrelated facts
     together as one column. */
  border-left: 1px solid var(--line);
  text-align: left;
}

/* ── the section ─────────────────────────────────────────────────────────── */
.colo-cat {
  --colo-ink: var(--accent);
  position: relative;
  display: inline-flex; align-items: center;
  align-self: flex-start;
  padding: 7px 2px 7px 0;
  font-family: var(--font-display); font-size: 12.5px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--colo-ink); text-decoration: none;
  transition: opacity .16s ease;
}
.colo-cat--plain { cursor: default; }
.colo-cat:hover .colo-cat-label, .colo-cat:focus-visible .colo-cat-label { text-decoration: underline; text-underline-offset: 4px; }
/* 🏷 THE SECTION'S MARK RIDES THE SPINE (owner 2026-09-01: "the category part
   is replaced with the one we have now"). It was a 9px bead, which said "a
   section" and never which one; it is now the glyph the nav row carries and the
   wall band repeats — one drawing, three surfaces, the registry's own
   assignment.

   ⚑ OUT OF FLOW, ON PURPOSE, and that is inherited from the bead. As a flex
   child the mark dragged the pill's text left of every other line in the
   column — the one thing this column's shared left rule exists to prevent.
   Positioned, it rides the spine and the label starts where the stamp and the
   cast start.

   ⚑ CENTRED ON THE RULE, NOT BESIDE IT, and the half pixel is the rule's own.
   The spine is a 1px BORDER outside 20px of padding, so its middle sits 20.5px
   left of the content edge and an 18px mark starts at -(20.5 + 9). Measured at
   -29 first: mark centre 1087.0 against a spine centre of 1086.5. Invisible,
   and wrong for a reason worth writing down — leaving a border's width out of a
   centring sum is how the next one lands somewhere nobody can explain.

   ⚠️ THE HALO IS WHAT KEEPS THE RULE OUT OF THE DRAWING. A stroked glyph is
   mostly holes; without a ground the hairline runs straight through the middle
   of it. The bead solved this with a 4px box-shadow — a ring around a filled
   circle. An icon needs the ground UNDER it, hence a painted background and a
   radius, not a shadow. */
.colo-ic {
  position: absolute; left: -29.5px; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; display: flex;
  color: var(--colo-ink);
  background: var(--bg); border-radius: 50%;
  box-shadow: 0 0 0 3px var(--bg);
}
.colo-ic svg {
  width: 100%; height: 100%;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ── who is in it ────────────────────────────────────────────────────────── */
.colo-h {
  margin: 0 0 2px; font-family: var(--font-mono);
  font-size: 10px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase;
  color: var(--faint);
}
.colo-cast { display: flex; flex-direction: column; gap: 4px; }
/* ⚖️ A LIST OF LINKS, NOT A LIST OF CARDS. Every row here goes somewhere — the
   role line left with the second cast block it was copying — so the rows tighten
   into something that reads as navigation and answers on hover across its whole
   width, the way a menu does. */
.colo-actor {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 9px;
  margin: 0 -8px; padding: 6px 8px; border-radius: 8px;
  text-decoration: none; color: inherit;
  transition: background .16s ease;
}
.colo-face {
  grid-column: 1; grid-row: 1;
  width: 26px; height: 26px; border-radius: 50%; font-size: 10.5px;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-weight: 700; letter-spacing: .02em;
  color: var(--muted);
  background: var(--card); border: 1px solid var(--line);
  transition: color .16s ease, border-color .16s ease, background .16s ease;
}
.colo-name {
  grid-column: 2; grid-row: 1; min-width: 0;
  font-family: var(--font-display); font-size: 13.5px; font-weight: 600;
  line-height: 1.3; color: var(--text); letter-spacing: -.01em;
}
/* every row is an anchor now, so the whole row is the target */
.colo-out { grid-column: 3; grid-row: 1; justify-self: end; align-self: center;
  width: 13px; height: 13px; opacity: 0; transition: opacity .16s ease; }
.colo-out svg { width: 100%; height: 100%; fill: none; stroke: var(--accent);
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.colo-actor:hover, .colo-actor:focus-visible { background: var(--card); }
.colo-actor:hover .colo-face, .colo-actor:focus-visible .colo-face {
  color: var(--accent); border-color: color-mix(in oklab, var(--accent) 55%, transparent);
}
.colo-actor:hover .colo-name, .colo-actor:focus-visible .colo-name { color: var(--accent); }
.colo-actor:hover .colo-out, .colo-actor:focus-visible .colo-out { opacity: 1; }
/* A main player who has not earned the wall yet (one live story) — the same
   furniture, none of the affordances: no hover answer, no accent, a default
   cursor. The colo-cat--plain posture, applied to a person. */
.colo-actor--plain { cursor: default; }
.colo-actor--plain:hover, .colo-actor--plain:focus-visible { background: transparent; }
.colo-actor--plain:hover .colo-face, .colo-actor--plain:focus-visible .colo-face {
  color: var(--muted); border-color: var(--line);
}
.colo-actor--plain:hover .colo-name, .colo-actor--plain:focus-visible .colo-name { color: var(--text); }

/* ── when, and by whom ───────────────────────────────────────────────────── */
.colo-foot {
  display: flex; flex-direction: column; gap: 6px;
  padding-top: 15px; border-top: 1px solid var(--line);
}
/* ⚑ THE RULE FOLLOWS THE ORDER. The stamp used to close the column and wore
   the only divider; with the cast last, each block needs its own opening rule
   or the two run together as one undifferentiated list. */
.colo-cast { padding-top: 15px; border-top: 1px solid var(--line); }
.colo-when { display: flex; flex-direction: column; gap: 1px; }
.colo-when-l { font-size: 10px; letter-spacing: .13em; text-transform: uppercase; color: var(--faint); }
.colo-when-v { font-size: 12px; color: var(--muted); }
.colo-pub { margin-top: 5px; font-size: 11.5px; line-height: 1.4; color: var(--faint); }

/* ── the reader's three verbs (20.1950) ──────────────────────────────────────
   Icons and numbers, no words. It closes the column, so it wears the same
   opening rule every block above it does. */
.colo-actwrap { padding-top: 15px; border-top: 1px solid var(--line); }
.colo-acts { display: flex; align-items: center; gap: 6px; }
.colo-act {
  display: inline-flex; align-items: center; gap: 6px; height: 32px;
  padding: 0 10px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--card); color: var(--muted);
  font-family: var(--font-mono); font-size: 12px; font-weight: 600; letter-spacing: .03em;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.colo-act svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
/* An empty count must not collapse the pill into a jumping shape while the
   beacon is still in flight, so the slot holds its own width from the start. */
.colo-act-n { min-width: 1ch; text-align: left; font-variant-numeric: tabular-nums; }
.colo-act-n:empty { min-width: 0; }

button.colo-act { cursor: pointer; }
button.colo-act:hover, button.colo-act:focus-visible { color: var(--text); border-color: var(--muted); }
.colo-act--like[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
.colo-act--like[aria-pressed="true"] svg { fill: currentColor; }
.colo-act--share { padding: 0 9px; }

/* ⚖️ THE EYE IS DIM UNTIL THE COUNT EARNS ITS INK. Below the floor, and before
   the beacon answers at all, it makes no claim — and those two states look the
   same because neither is a claim. It is a READING, never a control: no
   pointer, no hover, nothing to press. */
.colo-act--views { cursor: default; }

@media (prefers-reduced-motion: reduce) {
  .colo-cat, .colo-face, .colo-out, .colo-name, .colo-act { transition: none; }
}



/* ── SECTION HEADER (design card SH-01…SH-08) ─────────────────────────────── */
/* The line that opens a block. BASELINE-aligned, not centred: a kicker and the
   count beside it are different sizes, and baselines are what make two
   different sizes look like one line. */
.nrb-sech {
  display: flex; align-items: baseline; gap: 12px;
  min-height: 32px; margin: 0; color: var(--text);
}
/* ⚑ THE 32px IS THE ACTION'S TOUCH TARGET, not the line's height. It is the
   card's, and it is what stops a row of headers changing height depending on
   whether each one happens to carry a link. BASELINE alignment then leaves a
   header with nothing on the right sitting at the TOP of that box with 18px of
   dead space under it — so an instance that will never carry an action says
   min-height: 0 and gets its line back. All three of today's do; the rule is
   written in their own sheets, beside the margins it interacts with. */
.nrb-sech * { box-sizing: border-box; }
/* The title never wraps and never pushes the rule off the line. */
.nrb-sech-t {
  margin: 0; flex: none; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* SH-01 THE FEED LABEL — mono and quiet. It names a LIST: the tree of rubrics,
   a column of rows. Quiet because the list under it is the content. */
.nrb-sech[data-kind="label"] .nrb-sech-t {
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
}

/* SH-02 THE KICKER — the display face in the accent. It names a SURFACE: the
   most-read rail, the games shelf. Louder than a label on purpose; the thing
   below it is a place, not a list. */
.nrb-sech[data-kind="kicker"] .nrb-sech-t {
  font-family: var(--font-display); font-size: 11px; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; color: var(--accent);
}

/* SH-04…06 THE BLOCK TITLE — a heading that is read as a heading. */
.nrb-sech[data-kind="title"] .nrb-sech-t {
  font-family: var(--font-display); font-size: 18px; font-weight: 600;
  letter-spacing: -.02em; line-height: 1.2; color: var(--text);
}
.nrb-sech[data-kind="title"][data-size="lg"] .nrb-sech-t { font-size: 24px; }

/* SH-07/08 THE DAY DIVIDER — the one kind that carries its own rule above it,
   because it separates rather than opens. */
.nrb-sech[data-kind="day"] {
  flex-wrap: wrap; align-items: flex-end; gap: 9px; min-height: 0;
  padding-top: 12px; border-top: 1px solid var(--line);
}
.nrb-sech[data-kind="day"] .nrb-sech-t {
  font-family: var(--font-mono); font-size: 30px; font-weight: 400;
  line-height: .78; letter-spacing: 0; text-transform: uppercase; color: var(--accent);
}
.nrb-sech-date {
  font-family: var(--font-display); font-weight: 700; font-size: 17px;
  line-height: 1; letter-spacing: -.01em; color: var(--text);
}
.nrb-sech[data-kind="day"] .nrb-sech-count { order: 3; margin-left: auto; line-height: 1.3; }
.nrb-sech[data-kind="day"] .nrb-sech-rule { display: none; }
/* the phone's day divider stacks the date under the weekday */
.nrb-sech[data-kind="day"][data-size="sm"] .nrb-sech-t { font-size: 19px; }
.nrb-sech[data-kind="day"][data-size="sm"] .nrb-sech-date {
  font-size: 15px; order: 3; flex: 1 0 100%; margin-top: 2px;
}
.nrb-sech[data-kind="day"][data-size="sm"] .nrb-sech-count { order: 2; align-self: baseline; }

/* ── the four slots every kind may carry ──────────────────────────────────── */
/* The dot takes a category's ink through one custom property, so a new rubric
   needs no rule of its own. */
.nrb-sech-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none; align-self: center;
  background: var(--sech-accent, var(--accent));
}
.nrb-sech-count {
  font-family: var(--font-mono); font-size: 11px; color: var(--faint);
  font-variant-numeric: tabular-nums; flex: none;
}
/* The hairline is always in the markup and only sometimes visible — see the
   view's note: a slot that appears and disappears moves everything right of it. */
.nrb-sech-rule {
  flex: 1; height: 1px; min-width: 16px; align-self: center; background: var(--line);
}
.nrb-sech[data-rule="none"] .nrb-sech-rule { background: transparent; }
.nrb-sech-meta { flex: none; font-size: var(--fs-meta); color: var(--faint); }
.nrb-sech-action {
  flex: none; display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 0 4px; border-radius: 8px;
  font-size: 12.5px; color: var(--muted); text-decoration: none;
}
.nrb-sech-action:hover { color: var(--accent); background: var(--line); }
.nrb-sech-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nrb-sech-arrow { width: 14px; height: 14px; }



/* ── the surface ─────────────────────────────────────────────────────────── */
/* The slider lives INSIDE the 760px column, not bled to the viewport like the
   poster rail above it: it is a card, and the game row directly below it is
   the same width. A lead that is wider than the thing under it reads as two
   unrelated pages stacked. */
.nrb-hero-root { margin: 16px 0 4px; }
/* 🏷 THE RAIL'S NAME, ABOVE THE RAIL, AT EVERY WIDTH (owner 2026-09-11: "on the
   same level of the 'all categories' but above the hero-slider we need the
   kicker label we see when in mobile version"). It was the phone's alone from
   2026-08-31; the desktop's copy of the word lived in the nav row at the card's
   FOOT, which is a caption on the controls rather than a name on the surface.

   ⚖️ ONE SURFACE, ONE NAME — so the nav row's eyebrow goes dark the moment this
   one lights up (the rule right below). That law is the reason the phone label
   was written in the first place; moving it up must not break it.

   ⚑ IT IS THE KICKER (design card SH-02) and the tree's return line is the FEED
   LABEL (SH-01), so the two columns open in the same voice at the same height.
   The zero top margin is the alignment: .nrb-hero-root's own 16px is what puts
   this line's box on the tree's, and a margin here would push it off. */
.nrb-hero-head { margin: 0 0 8px; min-height: 0; }
/* ⚠️ THE FOOT'S EYEBROW IS THE ONE THAT GOES. The dots and the arrows keep the
   row (they are controls, and they have nowhere else to be); only the WORD
   leaves, because it is now printed over the card instead of under it. */
.nrb-hero-split-eyebrow { display: none; }
.nrb-hero--split {
  --nrb-hero-r: 14px;
  /* ⛰ THE PARALLAX DRIFT's four inputs (design, fourth pull 2026-08-26). Each
     breakpoint states its own framing instead of the keyframes owning one, so
     the phone can drift on a different axis without a second animation name
     having to know why. */
  --nrb-hero-zoom-from: 1.3;
  --nrb-hero-zoom-to: 1.42;
  --nrb-hero-pan-from: 11%;
  --nrb-hero-pan-to: 15%;
  position: relative;
  /* ⚑ THE ARTICLE NO LONGER SPLITS — the CARD does (20.1830 revision). It is a
     rail of whole stories now, so the two-column composition belongs to each
     story, and the article is just the track.
     ⚠️ ONE ROW, AND THE NAV RIDES ON TOP OF IT. Giving the nav a grid row of
     its own pushed it BELOW the card, where it painted on the article's own
     ground — which under a lit verdict is the status gradient, so the rail grew
     a green band with the dots stranded on it. The nav has always sat OVER the
     picture; it is positioned, not placed. */
  display: grid; grid-template-rows: 1fr;
  min-height: 264px;
  margin: 0; overflow: hidden;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--nrb-hero-r);
  /* THE WHOLE CARD IS THE SWIPE SURFACE, and vertical scroll stays the
     browser's. Declaring it here rather than guarding it in the handler is what
     makes a downward drag feel like the page rather than like a rail deciding
     not to move — the browser never hands us the gesture at all. */
  touch-action: pan-y;
}

/* ── the story status frame ──────────────────────────────────────────────── */
/* ⚖️ THE SAME FRAME THE ARTICLE WEARS, and it changes with the rotation. The
   colours, the sweep and its keyframes are the ARTICLE'S — reused, not copied
   (siteCss .hero--lit), so the two surfaces cannot drift on what a verdict
   looks like. The state is decided once, by render/ArticleView.litFrameState.
   ⚑ THE FRAME CLOSES AROUND THE WHOLE SLIDER as one composition, and the inset
   below is what makes it VISIBLE AT ALL. An absolutely positioned child
   resolves inset:0 against its ancestor's PADDING BOX — which is the outer
   edge of the padding, not the inner one — so the art and the veil covered all
   4px of the frame and the sheen painted underneath a photograph. Measured
   before the fix: the art sat 1px inside the card (the border) instead of 5.
   The progress rail is inset with them for the same reason: a frame with a bite
   out of its top edge is not a frame. */
/* ⚖️ THE HAIRLINE VERDICT (owner 2026-08-29: "lets go for hairline verdict",
   after seeing 4px, 1px and 0px live). The band is gone; what marks a live
   verdict now is the card's OWN 1px border, which is transparent so the
   gradient shows straight through it. ⚑ 0px PADDING AND A 1px FRAME ARE THE
   SAME PICTURE — the frame always had two contributors, the padding and that
   border, and only the loud one is being removed.

   ⚑ ONE THICKNESS, ONE PLACE. The 4 used to be written six times across four
   rules, so trying another value meant six coupled edits and any missed one
   drew a frame with a bite out of it. Change this token and the padding, the
   two insets, their radii and the rail all follow. */
.nrb-hero--split { --nrb-hero-frame: 0px; }
.nrb-hero--split[data-status] { padding: var(--nrb-hero-frame); border-color: transparent; }
.nrb-hero--split[data-status] .nrb-hero-split-art,
.nrb-hero--split[data-status] .nrb-hero-split-veil {
  inset: var(--nrb-hero-frame);
  border-radius: calc(var(--nrb-hero-r) - var(--nrb-hero-frame)); overflow: hidden;
}
.nrb-hero--split[data-status] .nrb-hero-split-progress {
  top: var(--nrb-hero-frame); left: var(--nrb-hero-frame); right: var(--nrb-hero-frame);
  border-radius: calc(var(--nrb-hero-r) - var(--nrb-hero-frame)) calc(var(--nrb-hero-r) - var(--nrb-hero-frame)) 0 0;
}
/* ⚠️ THE ART IS WHAT A CROSSFADE LANDS ON, so it has to be opaque. Found by
   holding a slide at 50% and looking: nothing solid sat behind the slides —
   the art layer transparent, the card's background-COLOR transparent, and the
   frame gradient the only backdrop. Every rotation painted a half-faded
   photograph over a green wash. It was invisible while a 4px band of that same
   gradient was on screen anyway; with the band gone it is a flash from
   nowhere. This line is the fix, and it is worth keeping at any thickness. */
.nrb-hero--split[data-status] .nrb-hero-split-art { background: var(--card); }
.nrb-hero--split[data-status="developing"],
.nrb-hero--split[data-status="confirmed"],
.nrb-hero--split[data-status="debunked"] {
  background: linear-gradient(115deg, var(--frame-a), var(--frame-b) 30%, var(--card) 50%, var(--frame-b) 70%, var(--frame-a));
  background-size: 240% 240%;
  animation: nrb-frame-sheen 8s ease-in-out infinite;
  box-shadow: 0 0 26px -10px var(--frame-b);
}
.nrb-hero--split[data-status="developing"] { --frame-a: var(--warn); --frame-b: var(--accent); }
.nrb-hero--split[data-status="confirmed"]  { --frame-a: var(--ok);   --frame-b: var(--ok); }
.nrb-hero--split[data-status="debunked"]   { --frame-a: var(--bad);  --frame-b: var(--bad); }
/* The calm double rail — a closed case with nothing left to announce. Also
   where disputed and unclear land, deliberately. */
/* ⚑ A CLOSED CASE WEARS THE SITE'S ORDINARY EDGE. The calm DOUBLE rail was
   drawn with inset shadows inside the 4px band; with no band the art covers
   them and they paint nothing at all. So complete stops borrowing the status
   border and simply keeps the hairline every other card on the page has — a
   settled story marked by having nothing to announce, which is what the double
   rail was saying the long way round. */
.nrb-hero--split[data-status="complete"] {
  background: var(--card); border-color: var(--line); box-shadow: none;
}

/* ── the progress rail ───────────────────────────────────────────────────── */
/* A hairline across the top edge that fills over the dwell, so a reader can see
   the next story coming instead of being surprised by it. ⚑ ABSOLUTE, so its
   arrival costs the surface no height — the same rule the nav row follows. */
.nrb-hero-split-progress {
  position: absolute; top: 0; left: 0; right: 0; z-index: 3;
  height: 3px; overflow: hidden;
  background: color-mix(in oklab, var(--text) 12%, transparent);
}
.nrb-hero-split-progress i {
  display: block; height: 100%; width: 100%;
  background: var(--accent); transform-origin: left center;
  /* The rail fills over the whole dwell but FADES IN as it goes: invisible for
     the first moments, rising to 80% by the last third and holding there. It
     reports time without ever competing with the headline — which is the only
     reason a progress bar is allowed on a reading surface at all. */
  animation: nrb-hero-split-fill linear forwards, nrb-hero-split-reveal linear forwards;
  animation-duration: var(--nrb-hero-dwell, 16.5s);
}
@keyframes nrb-hero-split-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes nrb-hero-split-reveal { 0%, 14% { opacity: 0; } 70%, 100% { opacity: .8; } }

/* ── the track, and the cards on it ──────────────────────────────────────── */
/* ██ ONE CELL, EVERY CARD ██ — the height law (20.1170), moved up one level.
   It used to be the BODIES that shared a cell, so the row stood as tall as the
   tallest headline and never moved through a rotation. Now that a card carries
   its own words, it is the CARDS that share it, for the identical reason and by
   the identical mechanism.

   ⚑ visibility, NOT display. A hidden card must still be MEASURED, or the cell
   collapses to whichever story happens to be showing and the surface changes
   height every time the rail turns. It also takes the inactive card out of the
   tab order and the accessibility tree, which display:none would do too — and
   the measurement, which it would not. */
.nrb-hero-split-track { grid-row: 1; display: grid; min-width: 0; }
.nrb-hero-split-card {
  grid-area: 1 / 1; position: relative; min-width: 0; overflow: hidden;
  border-radius: calc(var(--nrb-hero-r) - var(--nrb-hero-frame));
  /* the split composition is the CARD's, not the article's */
  display: grid; grid-template-columns: minmax(0, 66.666%) minmax(0, 33.334%);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 1400ms cubic-bezier(.37, 0, .29, 1), visibility 0s linear 1400ms;
}
.nrb-hero-split-card.is-active {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity 1400ms cubic-bezier(.37, 0, .29, 1), visibility 0s;
}

/* ── the art ─────────────────────────────────────────────────────────────── */
/* Full bleed: the picture has no edge anywhere, and is pushed right inside its
   own box so the subject clears the text column. */
.nrb-hero-split-art { position: absolute; inset: 0; }
.nrb-hero-split-slide {
  position: absolute; inset: 0;
  /* ⚑ THE CROSSFADE MOVED TO THE CARD. There is exactly one slide per card now,
     so the slide has nothing left to fade against; it stays opaque and the card
     above it does the dissolve. The element itself remains because the drift
     animation and the hydration clock both aim at it. */
  opacity: 1; pointer-events: none;
  /* 1.4s, from 620ms then 1.1s: at five slides the dissolve is the surface's
     whole motion vocabulary, and a slower one reads as a dissolve rather than
     as a cut. It also has to outlast nothing — the drift underneath runs for
     twice the dwell, so the two never race. */
  transition: opacity 1400ms cubic-bezier(.37, 0, .29, 1);
}
.nrb-hero-split-slide.is-active { pointer-events: auto; }
.nrb-hero-split-img { position: absolute; inset: 0; display: block; background: var(--card); }
/* ⚑ A PICTURE, NOT A BACKGROUND (a departure from the card, and a forced one).
   The design draws the art with background-image, which cannot carry a dark
   sibling through prefers-color-scheme and is invisible to loading=lazy — two
   things every other image on this site already depends on. The transform the
   card puts on the box goes on the image instead; the result is identical. */
/* 🔷 THE FORGED COVER stands where the picture would (20.1850). It is an SVG
   with a viewBox and slice, so it fills the same box at any aspect without the
   transform the photograph wears — a generated composition has no subject to
   push clear of the text column, and pushing it would only crop the figure. */
.nrb-hero-split-img.nrb-cover { width: 100%; height: 100%; }
.nrb-hero-split-img img {
  width: 100%; height: 100%; object-fit: cover; object-position: center;
  transform-origin: center;
}
/* ── the parallax drift ──────────────────────────────────────────────────── */
/* Only the story on screen moves: over its dwell the picture creeps in and
   slides a little further right, so the art is never quite still while the type
   stays fixed. TWICE the dwell, so the story changes long before the drift
   completes — the motion reads as barely-there rather than as a zoom.
   ⚑ DECLARED ON EVERY SLIDE AND PAUSED; only the active one runs. A paused
   animation keeps painting its own transform, so an outgoing picture FREEZES
   where it was instead of snapping back to the base zoom mid-fade — removing an
   animation restores the underlying value instantly and nothing can transition
   that. hydrate.ts rewinds the incoming slide's clock to 0 as it activates.
   ⚑ BOTH KINDS OF ART DRIFT. A photograph carries the transform on its img; a
   forged cover IS the element (an inline svg), so it names itself here. */
.nrb-hero-split-img img,
svg.nrb-hero-split-img {
  animation: nrb-hero-split-drift calc(var(--nrb-hero-dwell, 16.5s) * 2) linear forwards;
  animation-play-state: paused;
  will-change: transform;
}
.nrb-hero-split-slide.is-active .nrb-hero-split-img img,
.nrb-hero-split-slide.is-active svg.nrb-hero-split-img { animation-play-state: running; }
@keyframes nrb-hero-split-drift {
  from { transform: scale(var(--nrb-hero-zoom-from)) translateX(var(--nrb-hero-pan-from)); }
  to { transform: scale(var(--nrb-hero-zoom-to)) translateX(var(--nrb-hero-pan-to)); }
}
/* The veil starts at the art's left edge, so the whole gradient falls ON the
   image — none of it lands on the flat surface, which would read as a muddy
   band instead of a dissolve. The fill is the card's own ground, so the
   dissolve lands on exactly the tone the text sits on, in both modes. */
.nrb-hero-split-veil {
  position: absolute; inset: 0; pointer-events: none;
  background: var(--card);
  -webkit-mask-image: radial-gradient(168% 256% at -30% 50%, #000 0%, #000 40%, rgba(0,0,0,.55) 62%, transparent 88%);
  mask-image: radial-gradient(168% 256% at -30% 50%, #000 0%, #000 40%, rgba(0,0,0,.55) 62%, transparent 88%);
}

/* ── the legibility halo ─────────────────────────────────────────────────── */
/* A 1px outline in the veil's OWN colour, so a word that lands where the veil
   has thinned still separates from the picture. Zero offset, one pixel: it
   reads as the ground hugging the glyphs, never as elevation.
   ⚑ THE VARIABLE LIVES ON THE CARD, NOT ON THE BODY (the design puts it on the
   body). This port lifted the nav row OUT of the bodies so one row serves every
   slide — so a variable declared on a body would never reach the eyebrow, which
   is the one line on a phone that sits lowest over the art. */
.nrb-hero--split { --nrb-hero-halo: var(--card); }
.nrb-hero-split-tag,
.nrb-hero-split-body h2,
.nrb-hero-split-lede,
.nrb-hero-split-meta,
.nrb-hero-split-eyebrow {
  text-shadow:
    1px 0 0 var(--nrb-hero-halo), -1px 0 0 var(--nrb-hero-halo),
    0 1px 0 var(--nrb-hero-halo), 0 -1px 0 var(--nrb-hero-halo),
    1px 1px 0 var(--nrb-hero-halo), -1px 1px 0 var(--nrb-hero-halo),
    1px -1px 0 var(--nrb-hero-halo), -1px -1px 0 var(--nrb-hero-halo);
}

/* ── the words ───────────────────────────────────────────────────────────── */
/* ██ ONE CELL, EVERY BODY ██ — the height law (20.1170 test 2: scripting ON,
   nothing may move). All five bodies occupy grid cell 1/1, so the row is as
   tall as the TALLEST of them and stays that height through every rotation and
   through the hydration handover. Stacking them in flow instead would make the
   surface change height with each slide, which is the same defect as an
   enhancement that pushes the page down — just on a timer. */
.nrb-hero-split-stack {
  position: relative; z-index: 1; grid-column: 1;
  /* one body now; the nav row left for the article and is positioned over the
     card, so the space it used to occupy as a grid row is reserved here. */
  display: grid; grid-template-rows: 1fr;
  padding: 20px var(--sp-card) 60px;
}
.nrb-hero-split-body {
  grid-row: 1; grid-column: 1; min-width: 0;
  display: flex; flex-direction: column; justify-content: center; gap: 10px;
}
/* visibility, not display: it takes the inactive slide out of the tab order and
   the accessibility tree (five stacked headlines is five lead stories) while
   still letting the cell measure every body, which is what keeps the height
   constant. */
.nrb-hero-split-body.is-active {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: opacity 420ms var(--ease-out), visibility 0s;
}
/* 🗞 The section and the length, as one row (20.1860). The badge cannot take
   this surface's top-right corner — the state chip holds it at every width —
   so the pair sits together where the section already was. */
.nrb-hero-split-tags { display: flex; align-items: center; gap: 6px; align-self: start; flex-wrap: wrap; }
.nrb-hero-split-tag--short { color: var(--muted); }
.nrb-hero-split-tag {
  align-self: start;
  background: var(--bg); color: var(--accent);
  border: 1px solid var(--line); border-radius: 6px; padding: 3px 8px;
  font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
}
/* Overrides the page's bare-h2 label rule (11.5px, uppercase, accent) — every
   other h2 on a baked page IS a section label; this one is a headline. */
.nrb-hero-split-body h2 {
  margin: 0; padding: 0; text-transform: none; color: var(--text);
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(21px, 2.3vw, 27px);
  line-height: 1.08; letter-spacing: -0.02em;
  max-width: 22ch; text-wrap: pretty;
}
.nrb-hero-split-body h2 a { color: inherit; text-decoration: none; }
.nrb-hero-split-body h2 a:hover { text-decoration: underline; text-underline-offset: 3px; }
.nrb-hero-split-lede {
  margin: 0; font-size: 14px; line-height: 1.5; color: var(--muted);
  max-width: 46ch; text-wrap: pretty;
  /* Two lines, hard: the dek is written for an article page and runs long, and
     a lede that wraps to five lines pushes the CTA out of a fixed-height cell. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* ⚑ WITHHELD, NOT DELETED (design, third pull: the state chip and the CTA carry
   this row now). One property un-withholds it, and the markup stays so the
   machine-readable stamp does not leave the page with the line that showed it. */
.nrb-hero-split-meta {
  display: none; align-items: center; gap: 9px; flex-wrap: wrap;
  font-family: var(--font-mono); font-size: 11.5px; color: var(--faint);
}
.nrb-hero-split-sep { opacity: .5; }
.nrb-hero-split-avatar {
  width: 24px; height: 24px; border-radius: 50%; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg); border: 1px solid var(--line);
  font-size: 8px; font-weight: 700; letter-spacing: .04em; color: var(--muted);
}

/* ── the status line ─────────────────────────────────────────────────────── */
/* The hero's echo of the article's own status frame, and deliberately the same
   grammar: words carry the meaning, colour only reinforces. */
/* ⚑ THE CHIP IS THE CARD'S, NOT THE BODY'S (design, third pull). It pins to the
   card's top-right corner at EVERY width now — on a desktop the body is only
   two thirds of the card, so a chip inside it would land in the middle of the
   picture rather than at its corner. One per slide, all in the same spot; only
   the active one is visible, and the swap is the same class flip the slides use. */
.nrb-hero-split-status {
  position: absolute; top: var(--sp-card); right: var(--sp-card); z-index: 3;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 11px 5px 9px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg);
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em;
  color: var(--muted);
  opacity: 0; visibility: hidden;
  transition: opacity 420ms var(--ease-out), visibility 0s linear 420ms;
}
.nrb-hero-split-status.is-active {
  opacity: 1; visibility: visible;
  transition: opacity 420ms var(--ease-out), visibility 0s;
}
.nrb-hero-split-status b { font-weight: 600; color: var(--text); letter-spacing: .02em; }
.nrb-hero-split-status[data-status="developing"] { border-color: color-mix(in oklab, var(--accent) 55%, transparent); }
.nrb-hero-split-status[data-status="developing"] b { color: var(--accent); }
.nrb-hero-split-status[data-status="complete"] { box-shadow: inset 0 0 0 3px var(--bg), inset 0 0 0 4px var(--line); }
.nrb-hero-split-status-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--accent); box-shadow: 0 0 0 0 var(--accent);
  animation: nrb-hero-split-pulse 2.4s var(--ease-out) infinite;
}
.nrb-hero-split-status[data-status="complete"] .nrb-hero-split-status-dot {
  background: var(--faint); animation: none; box-shadow: none;
}
@keyframes nrb-hero-split-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--accent) 55%, transparent); }
  70% { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* ── the call to action ──────────────────────────────────────────────────── */
.nrb-hero-split-cta {
  align-self: start; margin-top: 1px;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 14px; border-radius: 8px;
  background: var(--accent); color: #0a0f1c;
  font-size: 12.5px; font-weight: 700; letter-spacing: .02em; text-decoration: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.nrb-hero-split-cta:hover { opacity: .88; text-decoration: none; }
.nrb-hero-split-cta svg {
  width: 14px; height: 14px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
}

/* ── the controls ────────────────────────────────────────────────────────── */
/* ██ THE ROW IS BAKED, THE CONTROLS ARE NOT ██ (20.1170). A baked page ships
   the eyebrow alone — it is a label, and a label is content — inside a row
   whose height is already the height of an arrow button. Script drops the dots
   and the arrows INTO that reserved 30px, so the surface below never moves at
   the handover. min-height is doing the load-bearing work here; deleting it
   turns this into the stacked-masthead bug of 2026-08-07. */
/* ⚑ THE ARTICLE'S SECOND ROW, not a card's. One row serves the whole rail —
   inside a card it would be repeated once per story and read the eyebrow that
   many times to a screen reader. It sits in the article's own gutter, which is
   why the padding it used to inherit from the stack is stated here. */
.nrb-hero-split-nav {
  position: absolute; left: 0; bottom: 20px; z-index: 2;
  display: flex; align-items: center; gap: 10px;
  min-height: 30px; padding: 0 var(--sp-card);
}
.nrb-hero-split-eyebrow {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text); margin-right: 2px;
}
.nrb-hero-split-dots { display: flex; align-items: center; gap: 6px; margin-right: auto; }
.nrb-hero-split-dot {
  width: 7px; height: 7px; padding: 0; border-radius: 50%;
  background: var(--line); border: 0; cursor: pointer; appearance: none;
  transition: background var(--dur-fast) var(--ease-out), width var(--dur-fast) var(--ease-out);
}
.nrb-hero-split-dot:hover { background: var(--faint); }
.nrb-hero-split-dot.is-active { width: 18px; border-radius: 4px; background: var(--accent); }
.nrb-hero-split-arrow {
  width: 30px; height: 30px; flex: none; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--line); border-radius: 8px;
  color: var(--muted); cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.nrb-hero-split-arrow:hover { color: var(--text); border-color: var(--faint); }
.nrb-hero-split-arrow svg {
  width: 16px; height: 16px;
  fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
}

/* ── the phone ───────────────────────────────────────────────────────────── */
/* ██ THE COLUMNS COLLAPSE, THE TREATMENT DOES NOT ██ (design, second pull,
   2026-08-26). The first cut cropped the art to a 16/9 band on top and put the
   words underneath on the flat card — two stacked boxes, and the veil, which is
   the whole idea of this component, was switched off. The art now keeps the
   WHOLE card and the veil simply TURNS: the same dissolve, the same tokens, one
   axis rotated, so the picture fades upward into the text instead of sideways.
   The breakpoint moved 640 -> 720 with it. */
@media (max-width: 720px) {
  .nrb-hero--split { grid-template-columns: 1fr; min-height: 420px; }

  /* ══ THE RAIL OF WHOLE CARDS (20.1830 revision, owner 2026-08-30) ═════════
     "now all cards are in the slider container -> whereas i prefer they are all
     in the same row as horizontal scroll set (like the categories) and then we
     can swipe and select, but initially the rotation is automatic … we show the
     card bigger as they are now."

     ⚖️ THE CONTAINER GOES AND THE CARDS BECOME THE SURFACE. The hero used to be
     one bordered box with a rail hidden inside it; below 720px there is no box
     at all — just a row of complete stories scrolling edge to edge, exactly the
     shape the category rail above it already has. Each card carries its own
     picture AND its own headline, so a neighbour peeks as a story rather than
     as an anonymous strip of photograph.

     ⚖️ A SCROLL CONTAINER, NOT A TRANSFORM. Native snap gives a thumb momentum,
     rubber-banding and interruption mid-flick, and it works with the script
     off — STATIC FIRST, JS ON TOP. The bundle adds only the automatic rotation
     and the tap-to-centre. */
  .nrb-hero--split {
    --nrb-hero-peek: 22px;
    --nrb-hero-gap: 10px;
    /* the box itself: gone. No frame, no ground, no clipping — the rail has to
       be able to show its neighbours OUTSIDE what used to be the card. */
    background: none; border: 0; border-radius: 0; padding: 0;
    box-shadow: none; overflow: visible; min-height: 0;
  }
  /* ⚑ AND THE FRAME WITH IT. The status frame painted the CONTAINER's edge, and
     there is no container; the verdict is not lost, because every card carries
     its own state chip in its corner — which is where a reader looks anyway
     once the stories are side by side. */
  .nrb-hero--split[data-status] { background: none; box-shadow: none; }

  /* ⚠️ THE TRACK IS A DIRECT CHILD OF AN <article>, so siteCss's reading-measure
     rule (article > :not([data-nrb-view]) { max-width: var(--measure) }) matches
     it at (0,1,1) and would cap the rail at the reading measure. That defeat is
     silent — it drops the one declaration and leaves the rest of the block
     working — and it cost three wrong fixes the last time it happened here. */
  .nrb-hero--split .nrb-hero-split-track {
    max-width: none;
    display: flex; gap: var(--nrb-hero-gap);
    /* the pad is the WHOLE edge (peek + gap), or the FIRST card can never
       centre itself: scrollLeft cannot go below zero. */
    padding-inline: calc(var(--nrb-hero-peek) + var(--nrb-hero-gap));
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none; -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
    /* the article declares pan-y so the page keeps vertical gestures; the rail
       needs the horizontal one back or a swipe does nothing. */
    touch-action: pan-x pan-y;
  }
  .nrb-hero--split .nrb-hero-split-track::-webkit-scrollbar { width: 0; height: 0; }
  .nrb-hero-split-card {
    /* exactly the content box, which the padding above has already reduced by
       the edge — subtracting the peek again here would take it twice. */
    flex: 0 0 100%;
    scroll-snap-align: center;
    min-height: 420px;
    border-radius: var(--nrb-hero-r);
    /* every card is on screen now, at full strength, so the crossfade has
       nothing left to say. Nothing marks the centre but its position — the
       words a reader is looking at are on the card in front of them. */
    opacity: 1; visibility: visible; pointer-events: auto;
    grid-template-columns: 1fr;
  }
  /* ⚑ NOTHING ANIMATES ON THE RAIL. The cross-fade is the desktop's; here the
     motion is the scroll itself, and a card that also faded as it passed the
     centre is a second animation arguing with the thumb.
     ⚠️ BOTH SELECTORS, because .nrb-hero-split-card.is-active is (0,2,0) and
     a bare class is (0,1,0): the ACTIVE card kept the desktop's 1.4s opacity
     transition and went on fading under the swipe. Measured, not guessed — the
     computed value still read "opacity 1.4s" with the rule apparently in
     force. */
  .nrb-hero-split-card,
  .nrb-hero-split-card.is-active {
    transition: none;
  }
  /* ⚖️ EVERY CARD AT FULL STRENGTH (owner 2026-08-30: "lets all be normal cards
     fully visible and we just swipe left right … no progress no nothing"). The
     dimming came from the peek rail, where a neighbour was an anonymous strip
     of photograph and needed pushing back. A neighbour is a whole story now —
     dimming it says the rail has an opinion about which one you should read,
     and the rail's job is only to hold them. Nothing marks the centre but its
     position; the words a reader is looking at are on the card in front of
     them. */
  .nrb-hero-split-card:not(.is-active) { cursor: pointer; }

  /* ⚠️ EVERY CARD WEARS ITS OWN CHIP, and its own picture at its own size —
     the two defects the owner caught together ("we have image but once the
     transition stops it gets smaller and more labels appears").

     BOTH came from rules gated on .is-active, written when exactly one story
     was ever on screen. On a rail where all of them are:

     · THE STATE CHIP sat at opacity 0 until its card became active, so a chip
       FADED IN as the rail settled — a label arriving out of nowhere on a card
       the reader was already looking at.
     · THE DRIFT is worse, because only the ACTIVE slide was given the phone
       keyframes; every other card kept the DESKTOP drift, paused at its own
       first frame — scale 1.3 and panned 39px. Measured: 1.3 on the waiting
       cards against 1.0 on the active one, so the picture visibly SHRANK the
       moment its card arrived.

     ⚖️ SO THE DRIFT STOPS HERE ALTOGETHER. On this rail the motion is the
     scroll; a picture that also zooms under a swipe is a second animation
     arguing with the thumb, and the same reasoning already took the dimming,
     the progress bar and the cross-fade off the phone. With no animation the
     art simply covers its card, which is what object-fit was always doing. */
  .nrb-hero-split-status { opacity: 1; }

  /* ⚑ THE PHONE GIVES THE NAME MORE AIR. Same line, same voice as the desktop's
     — only the space above it changes, because on a phone the header sits
     directly over it with none of the frame's own gutter in between. */
  .nrb-hero-head { margin: 22px 0 8px; }

  /* ── what the rail makes redundant (owner 2026-08-29) ────────────────────
     "of course read more and most readed and the progress bellow can be
     removed." The dots were six 7px targets standing in for a gesture nobody
     could make; the neighbouring cards are the control now, and unlike a dot
     they say which story they lead to. ⚑ HIDDEN, NOT DELETED — all three still
     draw above 720px, and the headline is a link at every width, so the CTA is
     the only affordance withdrawn and never the only one in. */
  .nrb-hero--split .nrb-hero-split-cta,
  .nrb-hero--split .nrb-hero-split-nav,
  /* ⚑ AND THE PROGRESS RAIL GOES TOO. It reported how long until the next story
     arrived — a promise the rail stops making the moment a thumb owns it, and
     the auto-rotation now only opens the surface rather than running it. A bar
     counting down to something that has stopped happening is worse than none. */
  .nrb-hero--split .nrb-hero-split-progress { display: none; }
  /* the art stays absolute and full-bleed — it is the card's ground now, and
     a CENTRED picture only breathes: there is no side column to clear, so the
     phone drifts on scale alone rather than panning a subject out of frame */
  /* ⚑ THE DRIFT IS OFF ON THIS RAIL, and the override has to come AFTER the
     rule above it: both are (0,3,0), so order is the only thing separating
     them. Left earlier in the block it lost the animation-NAME and survived
     only because it had already zeroed the duration — true, but true by
     accident, which is not a thing to leave in a stylesheet. */
  .nrb-hero-split-img img,
  .nrb-hero-split-slide.is-active .nrb-hero-split-img img,
  svg.nrb-hero-split-img,
  .nrb-hero-split-slide.is-active svg.nrb-hero-split-img {
    animation: none; transform: none;
  }
  .nrb-hero-split-veil {
    -webkit-mask-image: radial-gradient(150% 112% at 50% 116%, #000 0%, #000 44%, rgba(0,0,0,.55) 66%, transparent 92%);
    mask-image: radial-gradient(150% 112% at 50% 116%, #000 0%, #000 44%, rgba(0,0,0,.55) 66%, transparent 92%);
  }
  /* the words hug the bottom, under a deep top pad that leaves the picture its
     own space above them */
  .nrb-hero-split-stack { padding: 96px var(--sp-card) 18px; }
  .nrb-hero-split-body { justify-content: flex-end; gap: 10px; }
  .nrb-hero-split-body h2 {
    font-size: calc(var(--fs-h3) * .74);
    min-height: calc(var(--fs-h3) * .74 * 1.08 * 2);
    max-width: none;
  }
  .nrb-hero-split-lede { font-size: var(--fs-meta); max-width: none; }
  /* ── the top edge: the section at one corner, the state at the other ──── */
  /* Both leave the flow and sit ON the art, so each needs a ground of its own —
     the chip rule this site has followed since the hero frame: NEVER BARE TEXT
     ON A PHOTOGRAPH. */
  .nrb-hero-split-tags {
    position: absolute; top: var(--sp-card); left: var(--sp-card); z-index: 2;
  }
  .nrb-hero-split-tag { background: var(--bg); border-color: var(--faint); text-shadow: none; }
  /* already pinned at every width — the phone only nudges it clear of the
     section chip's taller box and tightens the type */
  .nrb-hero-split-status {
    top: calc(var(--sp-card) + 6px);
    border-color: var(--faint);
    font-size: 10px; padding: 4px 9px 4px 8px;
  }
  /* ⚑ TWO ABSOLUTES ON ONE EDGE WILL MEET (this port's departure, and it is the
     six-edition law doing the arguing). The design draws one Bulgarian pair
     that happens to fit; German says 'Geschichte in Entwicklung' where Bulgarian
     says 'Историята се развива', and at some width the state slides under the
     section. Neither may grow past its own half, and the label that runs out of
     room ellipses instead of colliding. Costs nothing whenever there is room. */
  .nrb-hero-split-tags { max-width: 44%; }
  .nrb-hero-split-tag { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .nrb-hero-split-status { max-width: 52%; }
  .nrb-hero-split-status b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* ⚑ NO ORPHANED SEPARATOR — kept against the day the byline row is
     un-withheld. At one column the byline and the date no longer fit on a line,
     so the row wrapped and left the middle dot hanging alone at the end of the
     first line. The DATE takes its own line and the dot goes; the monogram
     stays beside the name it belongs to, which a plain column direction would
     have broken apart. */
  .nrb-hero-split-meta { gap: 8px 9px; }
  .nrb-hero-split-meta time { flex: 0 0 100%; }
  .nrb-hero-split-sep { display: none; }
  /* a thumb has the whole card's width to press */
  .nrb-hero-split-cta { align-self: stretch; justify-content: center; }
  .nrb-hero-split-nav { gap: 8px; }
  /* THE RAIL IS SWIPEABLE ON A PHONE and the arrows are desktop affordances —
     the design's ruling. hydrate.ts binds the swipe, so the two halves of
     that ruling ship together: taking the arrows away without giving the thumb
     the gesture would leave the dots as the only way through the rail. */
  .nrb-hero-split-arrow { display: none; }
}

@keyframes nrb-hero-split-drift-phone { from { transform: scale(1); } to { transform: scale(1.1); } }

@media (prefers-reduced-motion: reduce) {
  /* ⚑ AND THE DRIFT. It is the quietest motion on the surface and the longest —
     33 seconds of a picture that will not hold still is exactly what someone
     who asked for less motion asked to be spared. */
  .nrb-hero-split-img img, svg.nrb-hero-split-img,
  .nrb-hero-split-slide.is-active .nrb-hero-split-img img,
  .nrb-hero-split-slide.is-active svg.nrb-hero-split-img { animation: none; }
  .nrb-hero-split-slide, .nrb-hero-split-body, .nrb-hero-split-status,
  .nrb-hero-split-dot, .nrb-hero-split-cta, .nrb-hero-split-arrow { transition: none; }
  .nrb-hero-split-status-dot { animation: none; }
  /* ⚑ AND THE SHEEN, which is the largest motion on the surface — an 8s
     infinite sweep across the whole card. It was the one this block forgot. */
  .nrb-hero--split[data-status] { animation: none; }
}
/* The rail still SHOWS where the dwell has got to; it just stops animating
   there — a reader who asked for less motion still deserves the answer. */
@media (prefers-reduced-motion: reduce) {
  .nrb-hero-split-progress i { animation: none; transform: scaleX(1); }
}



/* ── the column ──────────────────────────────────────────────────────────── */
/* ⚖️ FIXED, NOT STICKY (owner 2026-08-30: "this left and the right columns they
   never scroll").
   Sticky was the first answer and it was wrong for one measurable reason: it
   only holds while its CONTAINER is still passing under the viewport, and a
   column given the screen's height fills that container exactly — zero travel,
   so it rides the page. Measured before the fix: the column's top went from 0
   to -61 on a 961px document and stayed there.

   ⚑ TWO ELEMENTS, BOTH LOAD-BEARING. .pn stays IN FLOW and carries nothing but
   the WIDTH, which is what keeps the frame three columns wide and the measure
   where it belongs; .pn-fix is fixed to the viewport, so the tree and the dock
   are never part of what scrolls. Fixing .pn itself instead takes it out of the
   flex row and the rail collapses onto the column — measured, not guessed. */
/* ⚖️ THE COLUMN'S WIDTH IS ONE VARIABLE, READ BY FOUR THINGS (owner
   2026-08-31: "1448 -> measure = 780px the rest give to the columns · 1600+ the
   column should be 320px not 246").
   The in-flow placeholder, the FIXED box that overlays it, the rail on the
   other side, and the footer's inset all have to state the same number — and
   the footer is a SIBLING of the frame, not a descendant, which is why this
   lives on :root and not on .pn-shell.
   ⚑ TWO BANDS, NOT AN INTERPOLATION. 286 is what 1448 leaves once the measure
   takes its 780; 320 is the owner's number for a wide screen. Between them the
   frame is capped and centred, so the step happens where nothing is being
   squeezed. */
:root { --pn-col: 286px; }
@media (min-width: 1600px) { :root { --pn-col: 320px; } }

.pn { width: var(--pn-col); flex: none; }
.pn-fix {
  /* ⚖️ THE MARK DOES NOT MOVE (owner 2026-08-31: "the logo the current position
     is right, do not move it"). A wall's section band reaches UP to the mark
     instead — see .catwall-head, which bleeds off the top edge and lands its
     text on this mark's own line. The alignment is the band's job, not this
     column's; moving the column would have dragged the home page's chrome along
     with it to fix a wall. */
  position: fixed; top: 24px; width: var(--pn-col);
  display: flex; flex-direction: column; min-height: 0;
  /* ⚑ THE SCREEN'S HEIGHT, so the dock has a bottom to sit at (owner, circling
     it: "this needs to be down"). Free of charge now that the column is out of
     flow — as an in-flow box this same height was what inflated the page and
     broke the very pinning it was meant to serve. */
  height: calc(100vh - 48px);
}

/* the mark — the way home, and the top of the column */
.pn-mark { display: block; position: relative; width: max-content; margin: 0 0 22px; padding-bottom: 9px; line-height: 1; text-decoration: none; }
.pn-mark span { display: block; font-size: 26px; font-weight: 800; letter-spacing: -.03em; color: var(--logo-ink, var(--text)); }
.pn-mark::after { content: ""; position: absolute; left: 0; bottom: 2px; width: 100%; height: 3px; background: var(--logo-bar, var(--accent)); }
.pn-mark:hover span { color: var(--accent); }
/* ⚠️ THE MARK DRAWS ITS OWN UNDERBAR AND WANTS NO SECOND ONE (owner 2026-09-01:
   "remove the hover underline on the logo text"). The base sheet's global
   a:hover adds a text-decoration, and it WINS: a:hover is one class-worth of
   specificity ahead of .pn-mark's plain text-decoration:none, so on hover the
   browser drew a text underline straight through the brand's own 3px bar. The
   rest-state rule was never enough on its own. */
a.pn-mark:hover { text-decoration: none; }
.pn-mark:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ── the line above the tree ──────────────────────────────────────────────── */
/* 🏷 THE RETURN LINK IS AN EYEBROW, NOT A BUTTON (design handoff 2026-09-01 §2:
   "the former button-like treatment is gone").
   ⚑ IT NOW MATCHES THE SECTION HEADS' SIZE — 11.5px, one step quieter in
   colour, so the two read as one family rather than as a control above a list.
   ⚑ AND THE NEGATIVE TOP MARGIN IS AN ALIGNMENT, NOT A NUDGE: -4px is what puts
   this line's bottom on the first section eyebrow's bottom across the frame.
   The two are a pair — change one and the columns stop meeting. */
/* 🏷 BOTH FORMS, ONE BOX (2026-09-11). The home page's line is now the design
   card's FEED LABEL — a header component — while a rubric page's is still a back
   link. Two different elements standing in one place, so the BOX is stated here
   for both and the TYPE only for the anchor: the header owns its own type and
   must not be fought, and the tree below must not jump between page types.

   ⚑ min-height: 0 CANCELS THE COMPONENT'S 32px, which is reserved for an
   action this line will never carry (see sectionHeaderCss.ts). Leaving it on put
   the label 7px below the centre column's kicker instead of beside it, because
   the component aligns to the BASELINE and so parks a lone title at the top of
   that box. With it off, the -4px is the same alignment it always was: this
   line's bottom on the first section eyebrow's bottom, across the frame. */
.pn-back {
  display: flex; align-items: baseline; margin: -4px 0 14px; min-height: 0;
}
a.pn-back {
  gap: 7px; text-decoration: none;
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
}
a.pn-back:hover { color: var(--text); }
/* ⚑ THE ONLY ACCENT THIS LINE TAKES IS THE FOCUS RING (the handoff says so in
   as many words). It had none: the column's rows carry their own, and this
   anchor was inheriting whatever the page's default outline happened to be. */
a.pn-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* a text chevron, not an icon */
.pn-back-c { font-size: 13px; line-height: 1; }
/* ⚑ THE HOME PAGE'S LINE IS NOT A LINK — you are already at all sections. It
   keeps the chevron's width as padding so the two states sit on one baseline
   and the column does not shift height between them. */
.pn-back--static { padding-left: 19px; cursor: default; }

/* ── the panel, and the disclosure that opens it on a phone ──────────────── */
/* ⚑ DISPLAY:CONTENTS ON DESKTOP, so the back line, the rows and the footnote lay
   out as direct children of the column exactly as they did before the panel
   wrapped them — the dock's auto margin still sees them as siblings, and the
   rows keep the scroll that lets a tall tree give before the dock does. */
.pn-panel { display: contents; }
/* ⚖️ INERT ABOVE THE PHONE. Neither the input nor its labels reach the
   accessibility tree here: display:none removes them outright, so a desktop
   reader is never offered a control for a panel that is already open. */
.pn-toggle, .pn-launch, .pn-scrim, .pn-phead { display: none; }

/* ── the rows ────────────────────────────────────────────────────────────── */
/* ⚑ The rows are the part that gives: a tall tree on a short screen scrolls
   here rather than pushing the dock off the bottom of the column.
   ⚑ THE NEGATIVE MARGIN IS WHAT LETS A ROW'S BOX BREATHE. The selected and
   hovered states are filled boxes, and a box flush with the text reads as a
   crop; the row pads itself back by the same 8px, so the ink starts where it
   always did and only the box is wider. */
.pn-rows { display: flex; flex-direction: column; gap: 1px; min-height: 0; overflow-y: auto; scrollbar-width: thin; margin: 0 -8px; }
.pn-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 8px; border-radius: 9px; text-decoration: none;
  border: 1px solid transparent;
  font-size: 14.5px; font-weight: 600; color: var(--text);
  transition: background .13s ease, color .13s ease, border-color .13s ease;
  /* 🌳 THE INDENT SAYS "THESE ARE INSIDE THAT" (owner 2026-08-31, the whole
     branch at once). ⚑ CAPPED AT THREE STOREYS: the registry may grow deeper
     than the design drew, and a 286-320px column cannot spend 16px a level
     forever — but every node is still DRAWN, because a node nobody can reach
     from here is worse than one that shares an indent with its parent. */
  margin-left: calc(min(var(--d, 0), 3) * 16px);
}
a.pn-row:hover {
  background: color-mix(in oklab, var(--cat, var(--text)) 13%, transparent);
  border-color: color-mix(in oklab, var(--cat, var(--text)) 32%, transparent);
  text-decoration: none;
}
a.pn-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* ⚠️ A ROW WITH NO WALL. It is a span, never an anchor — see PortalNavView. The
   muted ink is the only thing that says so visually; the markup says it to a
   screen reader by not being a link at all. */
.pn-row--flat { color: var(--muted); cursor: default; }
/* ⚑ THE PAGE YOU ARE ON. Also a span, for the same reason the current edition
   and the current scope are: a link to here is a promise nothing will happen. */
.pn-row--now {
  background: color-mix(in oklab, var(--cat, var(--text)) 20%, transparent);
  border-color: color-mix(in oklab, var(--cat, var(--text)) 55%, transparent);
  cursor: default;
}

.pn-ic { flex: none; display: flex; color: var(--cat, var(--muted)); }
.pn-ic svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.pn-l { flex: 0 1 auto; min-width: 4ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ⚑ THE LEADER IS THE COUNT'S OWN UNDERLINE, in the section's ink — it ties a
   number to a row across the gap the ellipsis leaves, which is the one job a
   dotted rule does better than alignment alone. */
.pn-n {
  flex: 1; min-width: 0; text-align: right; padding-bottom: 2px;
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  border-bottom: 1px dotted color-mix(in oklab, var(--cat, var(--line)) 55%, transparent);
}
/* the fresh count is the one lit thing in the column, so it has to earn it:
   it appears only when there IS something new (see PortalNavView) */
.pn-n i { font-style: normal; font-weight: 700; color: var(--link); }

.pn-foot {
  margin: 14px 0 0; padding-left: 8px;
  font-family: var(--font-mono); font-size: 9px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--faint); line-height: 1.6;
}

/* ── the games close the tree (design handoff 2026-09-01 §4) ─────────────── */
/* ⚖️ THEY LEFT THE RAIL FOR THE COLUMN. The 2026-08-30 answer put them beside
   the stories, out of the measure; the handoff puts them at the FOOT OF THE
   TREE instead, on every framed page — "one instance, one place", so a reader
   meets the games where they meet the sections rather than in a different
   corner per page type. The rail keeps its width (it is what holds the frame's
   geometry) and, for now, nothing else.

   ⚑ IT IS THE LAST BLOCK INSIDE .pn-panel, which is what carries it into the
   phone's drawer: below 900px the panel floats and the games travel with it. */
.pn-games { margin: 16px 0 0; padding-top: 12px; border-top: 1px solid var(--line); }
/* ⚑ A SECTION HEAD, NOT A CAPTION. It was 9px and read as one; it now carries
   the same 11.5px as the return line and the section eyebrows, which is what
   makes those three one family. The 8px inset aligns it with the rubric rows'
   TEXT rather than with their icons. */
/* ⚑ THE TYPE IS THE HEADER COMPONENT'S NOW (design card SH-02, the KICKER) and
   only the PLACEMENT is left here: the 8px inset aligns the word with the rubric
   rows' TEXT rather than with their icons. */
.pn-games-h { margin: 0 0 6px; padding-left: 8px; min-height: 0; }
/* one sentence, allowed to wrap — nothing below it depends on its height */
.pn-games-d { margin: 0 8px 10px; font-size: 12.5px; line-height: 1.45; color: var(--faint); }
.pn-games-list { display: flex; flex-direction: column; gap: 8px; }

/* ⚠️ THE SLOT'S RESET FOLLOWS IT AGAIN. The play sheet cancels the slot's own
   gutter and its 1680px cap with a CHILD selector on .shell, because on a baked
   page the shell supplies the column; in the tree that selector stops matching
   and back come a 24px inset and a max-width inside a 286px box. This is the
   third home the slot has had and the third time this reset has had to travel
   with it — see .pn-rail's copy below, kept because the rail is still a place a
   slot could be put back into. */
/* ⚠️ AND IT NEEDS A DEFINITE WIDTH, which the rail never had to give it. The
   slot is a GRID whose single column is minmax(0, 1fr); as a flex item with no
   width it shrinks to fit, the 1fr resolves against nothing, and the whole
   thing collapses — measured at 0px wide, centred, with the game's name in the
   DOM and none of it on screen. width:100% is what makes the 1fr mean the
   column. */
.pn-games-list > .nrb-slot { padding: 0; max-width: none; width: 100%; }
/* the rows stack in a column and the reserve is the content's, exactly as in
   the rail: there is nothing beneath them to push */
.pn-games-list .nrb-slot-layer { height: auto; min-height: var(--nrb-slot-h, 86px); }
.pn-games-list .nrb-widget-host.nrb-play-row { flex-direction: column; gap: 8px; }
.pn-games-list .nrb-widget-title-h { font-size: 23px; }
.pn-games-list .nrb-widget-title-d { font-size: 13px; }
.pn-games-list .nrb-play-rules-lnk { right: 0; }
/* Games share the same compact, visible cards below the hero at every width. */
.pn-games-main { display: block; margin: 26px 0 18px; }
.pn-games-main .pn-games { margin: 0; padding-top: 0; border-top: 0; }
.pn-games-main .pn-games-h { margin: 0 0 2px; }
.pn-games-main .pn-games-d { margin: 0 8px 8px; }
/* A compact row needs no 86px reserve. Keep a short gap above, more air below. */
.pn-games-main .nrb-slot-layer { min-height: 0; padding-bottom: 12px; }
.pn-games-main .nrb-widget-host.nrb-play-row { flex-direction: row; gap: 12px; }
.pn-games-main .nrb-play-row .nrb-widget-play { flex: 1 1 0; min-width: 0; }
.pn-games-main .nrb-play-levels, .pn-games-main .nrb-play-row-chev { display: none; }
.pn-games-main .nrb-widget-play.nrb-play--compact {
  padding: 7px 10px; border-radius: 12px;
  background: color-mix(in oklab, var(--bg-surface, #fff) 55%, transparent);
}
/* The SPA docks temporal content on the left; the right rail is personal. */
.pn-fix > .nrb-nowbar-mount { flex: none; margin-bottom: 18px; }

/* ── the dock ────────────────────────────────────────────────────────────── */
/* It closes the column the way the colophon's verbs close theirs: a rule, then
   the furniture that belongs to the whole site rather than to this page. */
.pn-dock {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  /* auto, not a fixed margin — this is what pins it to the bottom */
  margin-top: auto; padding-top: 11px; border-top: 1px solid var(--line);
}
/* 🕰⚡ THE COLUMN'S FOOT (owner 2026-09-02, three rulings): the reader's verbs
   ABOVE the dateline, "at the bottom, but above all others", with NO rule over
   them; the stamp under them behind its own rule; the dock's rule then separates
   the stamp from the editions. The foot takes the auto margin the dock had, so
   the three sit at the bottom together. */
.pn-colfoot { margin-top: auto; padding: 0 0 11px; border-top: 0; }
.pn-colfoot + .pn-dock { margin-top: 0; }
.pn-colfoot .colo-actwrap { margin: 0; padding: 0 0 12px; border-top: 0; }
.pn-colfoot .colo-foot { margin: 0; padding: 12px 0 0; border-top: 1px solid var(--line); }
/* 🌍 THE HAND-ROLLED CHIP ROW IS GONE (2026-09-13) — .pn-lang / .pn-lang-a /
   .pn-lang-on were this column's own drawing of a control the design system
   already owns. The dock now renders the design's .nrb-lang, so the look comes
   from DESIGN_CHROME_CSS and only the PLACEMENT is left here (see the dock
   block further down). .pn-lang-on had in fact never drawn anything: the view
   filtered the current edition out of the list rather than marking it. */
/* ⚠️ THE BUTTON NEEDS ITS OWN GROUND HERE. .nrb-icon-btn sets a size and a
   colour but no background — inside the masthead something else painted it, and
   in the column the browser's default button grey showed through as a solid
   block. A control that inherits its look from where it used to live is not
   styled, it is lucky. */
.pn-mode {
  flex: none; width: 30px; height: 30px; padding: 0; cursor: pointer;
  background: transparent; border: 1px solid var(--line); border-radius: 8px;
  color: var(--muted);
}
.pn-mode:hover { color: var(--text); border-color: var(--muted); background: color-mix(in oklab, var(--text) 6%, transparent); }
.pn-mode:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pn-mode svg { width: 15px; height: 15px; display: block; }

/* ── the phone ───────────────────────────────────────────────────────────── */
/* ⚖️ THE PANEL IS OPENED BY A CHECKBOX, NOT BY A SCRIPT.
   The design's phone answer is a floating panel, and the first cut of this
   column refused it for one reason: a panel that needs JavaScript leaves a
   reader with no bundle holding NOTHING, and this IS the navigation (20.1170).
   A hidden checkbox and a label open it with no script at all — the same device
   the design's own exploration uses for its switcher — so the panel and STATIC
   FIRST were never actually in tension. The chip strip that stood here instead
   was the compromise; it is gone.

   ⚑ THE MARK AND THE EDITIONS STAY IN THE TOP ROW where the header used to be.
   Only the TREE floats. */
@media (max-width: 900px) {
  /* ⚑ THE COLUMN REJOINS THE FLOW DOWN HERE. There is no side column on a
     phone — the mark and the editions are a row where the header used to be —
     so the fixed box that makes the column stand still on web would only cover
     the top of the story. */
  .pn { width: auto; flex: auto; }
  .pn-fix {
    position: static; width: auto; height: auto;
    padding: 14px var(--sp-card) 10px; border-bottom: 1px solid var(--line);
    display: grid; grid-template-columns: auto 1fr; align-items: center; column-gap: 12px;
  }
  .pn-mark { margin: 0; grid-column: 1; grid-row: 1; }
  .pn-dock { grid-column: 2; grid-row: 1; justify-content: flex-end; margin: 0; padding: 0; border: 0; }

}

/* Collapsible category navigation is shared by desktop and mobile. */
  /* ── the launcher ─────────────────────────────────────────────────────── */
  /* ⚑ IT CARRIES THE SECTION'S NAME on a wall (see PortalNavView), so a reader
     can see where they are while the panel is shut — the design's own reason. */
  .pn-launch {
    display: flex; align-items: center; gap: 7px; cursor: pointer;
    position: fixed; right: 14px; bottom: 16px; z-index: 41;
    max-width: calc(100vw - 28px);
    padding: 9px 13px; border-radius: 999px;
    border: 1px solid var(--line); color: var(--text);
    /* ⚑ 15.75px = 10.5 + 50% (owner 2026-09-08: the two floating controls at the
       foot read too small on a phone). Kept EXACT rather than rounded to the
       sheet's usual .5 step — the ask was a proportion, and a quarter-pixel is
       invisible where being faithful to it is not. The 14px icons beside it are
       unchanged: they were LARGER than the label before and now sit just under
       it, which is the ordinary relationship rather than a new one. */
    font-family: var(--font-mono); font-size: 15.75px; letter-spacing: .09em; text-transform: uppercase;
    background: color-mix(in oklab, var(--bg) 82%, transparent);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
    box-shadow: 0 2px 6px rgba(16, 24, 48, .06), 0 10px 30px -12px rgba(16, 24, 48, .22);
  }
  .pn-launch-l { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .pn-launch-i, .pn-launch-c { flex: none; width: 14px; height: 14px; display: block; color: var(--muted); }
  .pn-launch-i { color: var(--accent); }
  .pn-launch-c { transition: transform .16s ease; }
  /* ⚠️ THE PHONE'S CHRONICLE DOCK IS FIXED TO THIS CORNER TOO, and it got here
     first. .nrb-mm is an 18px strip across the foot of an ARTICLE at 560px and
     under (pageMinimapCss) — a surface the column never met until the frame
     reached story pages, so the launcher landed straight on it.
     ⚑ THE BODY CLASS IS THE ONLY HONEST SIGNAL. hydrate.ts adds .nrb-mm-scope
     exactly when the strip is mounted — the script ran AND this story has
     chapters — so a width query here would lift the button on every phone page
     that has no dock to clear. */
  body.nrb-mm-scope .pn-launch { bottom: 62px; }
  body.nrb-mm-scope .pn-toggle:checked ~ .pn-panel { bottom: 108px; }
  .pn-toggle:checked ~ .pn-launch .pn-launch-c { transform: rotate(180deg); }
  /* the input is the state, never a picture — but it must stay FOCUSABLE, so it
     is hidden by size and not by display, and the label lights up with it */
  .pn-toggle { display: block; position: fixed; width: 1px; height: 1px; opacity: 0; margin: 0; pointer-events: none; }
  .pn-toggle:focus-visible ~ .pn-launch { outline: 2px solid var(--accent); outline-offset: 2px; }

  /* ⚑ TAP AWAY TO CLOSE — a second label for the same input, so it costs no
     script either. Inert while shut. */
  .pn-scrim { display: none; }
  .pn-toggle:checked ~ .pn-scrim { display: block; position: fixed; inset: 0; z-index: 39; }

  /* ── the panel ────────────────────────────────────────────────────────── */
  .pn-panel {
    display: none;
    position: fixed; right: 14px; bottom: 62px; z-index: 41;
    width: 260px; max-width: calc(100vw - 28px);
    max-height: 60vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
    padding: 14px; border-radius: 16px;
    border: 1px solid var(--line);
    background: color-mix(in oklab, var(--bg) 92%, transparent);
    -webkit-backdrop-filter: blur(18px) saturate(1.4);
    backdrop-filter: blur(18px) saturate(1.4);
    box-shadow: 0 4px 10px rgba(16, 24, 48, .08), 0 24px 60px -18px rgba(16, 24, 48, .38);
  }
  .pn-toggle:checked ~ .pn-panel { display: block; }

  /* the home panel's own heading — the column has none, so the panel needs one */
  .pn-phead { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin: 0 0 12px; }
  .pn-phead-e { font-family: var(--font-mono); font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
  .pn-phead-t { margin: 3px 0 0; font-size: 17px; font-weight: 800; letter-spacing: -.02em; color: var(--text); text-transform: none; }

  /* ⚑ THE ROWS STAY A LIST. They were a horizontal chip strip while the column
     had nowhere to go; inside a panel they are the tree again, which is the one
     shape that can show a count and a leader per row. */
  .pn-rows { overflow-y: visible; }
  /* ⚑ A SHORTER STOREY IN A NARROWER BOX. The panel is 260px against the
     column's own width but pays 14px of padding on each side, so a third-level row
     at the column's 16px step crowds its own leader against the edge. */
  .pn-row { margin-left: calc(min(var(--d, 0), 3) * 12px); }
  .pn-foot { display: none; }
@media (max-width: 900px) {
  /* the column's foot leaves with the column — the article's own meta serves */
  .pn-colfoot { display: none; }
  /* 🎮 the games ride the drawer as its last block (handoff §4) — on the phone
     that copy hides and the measure's copy under the hero shows instead */
  .pn-games { margin: 12px 0 0; }
  .pn-panel .pn-games { display: none; }
  .pn-games-main { display: block; margin: 0 0 18px; }
  .pn-games-main .pn-games { margin: 0; padding-top: 0; border-top: 0; }
  /* 🎮 TWO ON A ROW (owner 2026-09-02: "in mobile they need to be 2 on a
     row, not 2 rows") — the tree's column stack is the column's; under the
     hero the cards sit side by side, the way the slot's own row does. */
  .pn-games-main .nrb-widget-host.nrb-play-row { flex-direction: row; gap: 12px; }
  .pn-games-main .nrb-play-row .nrb-widget-play { flex: 1 1 0; min-width: 0; }
  /* 📱 THE PHONE'S CARD IS THE BOARD AND THE NAME (owner 2026-09-02: "remove
     the chevron and also easy mid hard … reduce the space between the header,
     description and the games … add space between the hero-slider and the
     header Games"). Two cards in 375px have no room for three lane words and
     an arrow; the levels stay a tap away on the board. The block breathes
     ABOVE (from the hero) and tightens INSIDE. */
  .pn-games-main { margin: 26px 0 18px; }
  .pn-games-main .pn-games-h { margin: 0 0 2px; }
  .pn-games-main .pn-games-d { margin: 0 8px 8px; }
  .pn-games-main .nrb-play-levels, .pn-games-main .nrb-play-row-chev { display: none; }
  /* 🎮📱 THE PHONE GETS ITS PLATE BACK (owner 2026-09-08: "for mobile only …
     the games container semi transparent, the entire clickable area").
     ⚖️ THE COMPACT CHASSIS IS RIGHT AND STAYS RIGHT — it removed the frame
     site-wide on purpose (a framed row inside the tree column was a box inside
     a box). What it could not know is that the phone copy sits under the hero
     on the bare page ground with nothing around it, so two tap targets became
     two floating labels with no edge to aim at. The plate is the AFFORDANCE,
     not decoration: it is the only thing saying how much of the row is
     tappable, and the whole row is.
     ⚑ SEMI-TRANSPARENT, NOT THE OLD OPAQUE SURFACE. The framed row used a solid
     --bg-surface; over the page ground under a hero that reads as a card
     competing with the story cards below it. A 55% mix settles it behind them.
     ⚑ VERTICAL PADDING BARELY MOVES (4 → 7px) — the block's rhythm above was
     tuned by hand on 2026-09-02 ("reduce the space between the header,
     description and the games") and a plate is not a reason to undo it. The
     horizontal padding is new because a plate with none is a stripe. */
  .pn-games-main .nrb-widget-play.nrb-play--compact {
    padding: 7px 10px; border-radius: 12px;
    background: color-mix(in oklab, var(--bg-surface, #fff) 55%, transparent);
  }
}

/* Keep the compact personal-space launcher clear of the navigation row. */
body:has(.pn-launch) .nrb-ps-mobile-trigger { bottom: calc(max(18px, env(safe-area-inset-bottom)) + 56px); }
body.nrb-mm-scope:has(.pn-launch) .nrb-ps-mobile-trigger { bottom: calc(max(18px, env(safe-area-inset-bottom)) + 102px); }
body:has(.pn-toggle:checked) .nrb-ps-mobile-trigger { visibility: hidden; pointer-events: none; }

/* Desktop controls attach to the reading column, not the viewport corners. */
@media (min-width: 901px) {
  body:has(.pn-shell) {
    --nrb-main-left: calc(max(var(--sp-card), (100vw - var(--pn-col) * 2 - 56px - 860px) / 2) + var(--pn-col) + 28px);
    --nrb-main-right: var(--nrb-main-left);
    --nrb-main-width: calc(100vw - var(--nrb-main-left) - var(--nrb-main-right));
  }
  /* A fixed parent creates a stacking context; keep its controls above main. */
  .pn-fix { z-index: 40; }
  .pn-launch, .pn-panel { right: var(--nrb-main-right, 14px); }
  .pn-launch { max-width: calc(var(--nrb-main-width, 100vw) - 28px); }
  .pn-panel { max-width: calc(var(--nrb-main-width, 100vw) - 28px); }
}

@media (min-width: 901px) and (max-width: 1180px) {
  body:has(.pn-shell) { --nrb-main-right: var(--sp-card); }
}

@media (prefers-reduced-motion: reduce) {
  .pn-row { transition: none; }
}

/* ── the frame ───────────────────────────────────────────────────────────── */
/* ⚑ THE WRAPPER EXISTS ONLY WHEN THE COLUMN DOES (pageFrame), so nothing here
   can reach a page that still wears the header. */
/* ⚑ THE FRAME IS ITS OWN PARTS, ADDED UP — two columns, two 28px gaps,
   the measure, and the page's gutter. Written as a sum rather than a rounded
   total so widening the measure cannot leave the frame too narrow for it, which
   is exactly what a hand-kept 1430 would have done. */
.pn-shell {
  display: flex; gap: 28px; align-items: flex-start;
  max-width: calc(var(--pn-col) * 2 + 28px * 2 + 860px + var(--sp-card) * 2);
  margin: 0 auto; padding: 0 var(--sp-card);
}
/* 📏 THE MEASURE IS THE STORY'S (owner 2026-08-31: "the max width in the
   content should be 860 just like when we open page with content"). The base
   .shell is 760 and the article page was designed at 860, so a reader moving
   from a wall into a story watched the text get wider — the one measurement a
   publication should hold still. One constant, both chromes. */
/* ⚠️ FLEX-BASIS 0, NOT auto — the measure must SHRINK, never wrap.
   The frame wraps on purpose so the rail can drop full-width under 1180px, and
   with an auto basis the measure's hypothetical size is its CONTENT: once that
   grew to 860 it no longer fitted beside the column at 1100px and wrapped onto
   the next line — under a column that is position:fixed, so the tree was drawn
   straight over the stories. Measured: measure at x=20 w=860 with the fixed
   column also at x=20. A zero basis makes the measure always fit the line and
   take what is left, which is what it looked like it was doing all along. */
.pn-shell > .shell { flex: 1 1 0; min-width: 0; max-width: 860px; margin: 0; padding-left: 0; padding-right: 0; }

/* ── the games rail ──────────────────────────────────────────────────────── */
/* ⚖️ GAMES LIVE HERE ON WEB, NEVER IN THE MEASURE (design 2026-08-30). In the
   column of stories the puzzle sat between the lead and the feed and read as
   an article that had lost its picture, and it pushed the news down by its own
   height on every visit. Beside them it is an offer, which is what it is. */
/* --pn-rail-max is the rail's VISIBLE height, read by the reader's column
   (personalSpaceCss TAIL) to size its mount so the head and the foot never
   scroll away: the sticky home rail has the viewport; the story rail below
   names its own smaller number, and the column inherits whichever it stands in. */
.pn-rail { width: var(--pn-col); flex: none; position: sticky; top: 0; align-self: start; --pn-rail-max: 100vh; }

/* ⚠️ THE SLOT'S RESET HAS TO FOLLOW IT OUT OF .shell. The play sheet cancels
   the slot's own gutter and its 1680px cap with a CHILD selector on .shell,
   because on a baked page the shell already supplies the column. Move the slot
   into the rail and that selector simply stops matching: back come a 24px
   inset and a max-width, inside a column-width box. The reset is re-stated here
   rather than loosened there, so the measure's copy keeps its own rule. */
.pn-rail > .nrb-slot { padding: 0; max-width: none; }

/* ⚑ THE RESERVE IS THE RAIL'S TO SET. --nrb-slot-h fixes the slot to ONE
   widget's height so that nothing BELOW it can move when the game arrives —
   the whole point of the rotation. In a rail there is nothing below it, and
   the games stack, so the height is the content's: the law is kept by where
   the box is, not by holding it to a number that no longer fits. */
.pn-rail .nrb-slot-layer { height: auto; min-height: var(--nrb-slot-h, 86px); }

/* ⚠️ COMPOUND, TO OUTRANK A COMPOUND. .nrb-widget-host.nrb-play-row is 0,2,0
   and deliberately so (it lost to a later display:block once and stacked the
   games in production). A single .pn-rail prefix on one class would tie on
   specificity and lose on source order, so this names both classes too. */
.pn-rail .nrb-widget-host.nrb-play-row { flex-direction: column; gap: 10px; }
/* the masthead is the slot's static occupant, and 40px does not fit the rail */
.pn-rail .nrb-widget-title-h { font-size: 23px; }
.pn-rail .nrb-widget-title-d { font-size: 13px; }
/* the corner marks hang outside the card; a sticky rail is not a clipping box,
   but the rules link sits over the scrollbar's gutter without this */
.pn-rail .nrb-play-rules-lnk { right: 0; }

/* ── the STORY's rail (20.2010 §6) ───────────────────────────────────────── */
/* ⚖️ THE SITE ON THE LEFT, THE STORY ON THE RIGHT (owner 2026-08-31: "same
   logic for open post layout"). An article page has THREE things that want a
   side column — the portal tree, the colophon and the chronicle — and the frame
   has two slots. The tree takes the left one because it is the same on every
   page, which is what makes it furniture; the colophon and the chronicle are
   about THIS story and share the right, exactly where the home page keeps its
   games. Nothing here is a new idea: it is the games rail with two occupants.

   ⚑ AND IT CLOSES THE ASYMMETRY. The article's own grid spent two 320px tracks,
   so at 1448 the story measured 716 against the wall's 860 — the reading page
   narrower than the browsing page. In this frame both are 780 at 1448 and 860
   from 1528 up, by construction rather than by two numbers agreeing.

   ⚑ ONE SCROLLPORT, NOT TWO. The rail is the mirror of .pn-fix: pinned to the
   viewport's height, giving internally when its content is taller. The
   chronicle's own overflow is handed back (below) so a tall thread and a long
   cast cannot each take a scrollbar and leave the reader guessing which one
   their wheel is in. */
.pn-rail--story {
  --pn-rail-max: calc(100vh - 48px);
  max-height: var(--pn-rail-max);
  overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
  /* 🔒 THE HEAD NEVER SLIDES OFF AT THE PAGE'S END (owner 2026-09-14: the
     reader's greeting and tokens "should always be visible, just like the left
     column with the nowbar"). A sticky box is fenced by its containing block's
     MARGIN-BOX rule: once the frame's bottom edge scrolls above the rail's
     bottom, the rail is pushed up with it. The frame ends at the measure's
     last pixel; the footer (its 44px margin + its own height) lives OUTSIDE
     the frame, so at the true bottom of the page the frame's edge sits ~108px
     above the viewport's while the rail's bottom sits 48px above it — and the
     rail rides up by the difference, ~60px, which is exactly the header's
     height (measured 2026-09-14, 900px viewport: head at y=-60). The left
     column never suffers this because .pn-fix is position:fixed and answers to
     nothing. A negative bottom margin shortens the rail's MARGIN box without
     moving a pixel (the frame's height is the measure's, not the rail's), so
     the fence sits far below the rail's border edge and the frame's end can
     no longer catch it. -200px covers a footer that wraps to two lines; the
     rail's visible content still ends 48px above the viewport, so nothing it
     draws reaches the footer. Undone below 1181px where the rail is in flow. */
  margin-bottom: -200px;
}


/* ⚠️ THE CHRONICLE'S DEFAULT SHAPE IS THE DRAWER, and the drawer is not
   media-scoped — the narrow shape is the one a phone parses first, and the DOCK
   query promotes it. Dropped into the rail it therefore arrives fixed to the
   viewport, translated a full column off-screen and invisible. Every one of
   these declarations is undoing a drawer, not styling a rail. */
.pn-fix > .nrb-tl, .pn-rail--story .nrb-tl {
  position: static; z-index: auto; inset: auto;
  width: auto; max-width: none; max-height: none;
  transform: none; visibility: visible; transition: none;
  overflow: visible;
  background: none; border: 0; box-shadow: none;
}
/* the drawer's inner padding was a panel's; in a rail the gutter is the rail's */
.pn-fix > .nrb-tl .nrb-tl-inner, .pn-rail--story .nrb-tl-inner { padding: 0 0 var(--sp-card); height: auto; }
/* ⚑ NEITHER CONTROL HAS ANYTHING LEFT TO DO. The opener lived in the masthead
   and the masthead is gone; the scrim closed a panel that no longer opens. The
   baker stops emitting both on this path — these are the belt to that braces,
   for a page baked before the frame and still on disk. */
.pn-rail--story .nrb-tl-opener, .pn-rail--story .nrb-tl-scrim { display: none; }

/* Keep long chronicles and the navigation reachable in a short viewport. */
@media (min-width: 901px) {
  .pn-fix:has(> .nrb-left-context) { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
  .pn-fix > .nrb-tl { flex: none; margin-bottom: 18px; }
  .pn-fix:has(> .nrb-left-context) > * { flex-shrink: 0; }
}

/* ── the colophon joins the COLUMN (owner 2026-09-01) ────────────────────── */
/* ⚖️ THE STORY'S IDENTITY BELONGS ON THE READER'S OWN EDGE (owner 2026-09-01:
   "move it again to the left side"). It crossed to the right rail when the
   column took the left track, which was the arithmetic answer — three tenants,
   two slots — and the owner reversed it: the block sits under the tree, in the
   space the dock was already holding open at the foot of the column. What stays
   on the right is the story's TIME, which is all that rail has ever been.

   ⚑ IT IS THE SAME COMPONENT, NOT A COPY. PortalNavView takes it as a child
   (see that view's aside parameter); every rule below is PLACEMENT, and nothing
   here restyles a colophon.

   ⚠️ ITS OWN SHEET'S SWITCH IS 1180, AND THIS COLUMN EXISTS FROM 901. Down here
   the column is still a fixed column with 286px and a dock at its foot, so the
   block has a home a full breakpoint earlier than the third track ever gave it.
   colophonCss keeps owning the ARTICLE-GRID answer; this owns the column's, and
   they are different questions about different layouts. */
.pn-fix .nrb-colo { display: none; }
@media (min-width: 901px) {
  /* ⚖️ THE TREE IS THE ONLY THING IN HERE THAT SCROLLS (owner 2026-09-01: "in
     certain situations i see two scrolls that should never be like that").

     This block first shipped shrinkable WITH ITS OWN SCROLL, so a long cast and
     a deep tree could share the squeeze. That bought one thing and cost two.

     ⚠️ A SECOND SCROLLPORT IN A 286px COLUMN. Two bars, and a reader with a
     wheel over the column cannot tell which one they are in.

     ⚠️ AND overflow-y:auto IS NEVER ONE AXIS. The spec resolves the OTHER axis
     to auto the moment one of them is not visible, so this block silently
     became a HORIZONTAL scrollport too — and left overflow is unreachable in
     LTR, so it CLIPPED. The actor rows carry margin: 0 -8px (the same breathing
     room the tree rows use, absorbed until now by the 20px the spine needed),
     and the first letter of every line in the block was cut off. One
     declaration, two bugs, and the second invisible to anyone reading the rule.

     ⚑ SO THE COLOPHON DOES NOT GIVE — the tree does, which is the honest
     priority on a story page: the tree is long by nature and already scrolls,
     while this is a stamp and at most four actors. The tree can shrink to
     nothing (min-height: 0) and both this block and the dock survive. */
  .pn-fix .nrb-colo { display: block; flex: none; }
}
/* ⚠️ THE COLOPHON ARRIVES STICKY, AND STICKY INSIDE A FIXED BOX IS A DIFFERENT
   THING. Its top:84px was measured against the PAGE, where it pinned under the
   masthead; in a column that is already pinned it would hold itself 84px down
   from the column's own top edge and leave a hole under the tree. The column
   does the pinning now, so the block inside it simply flows. */
.pn-fix .colo-inner { position: static; top: auto; }
/* ⚖️ NO FRAME AROUND IT IN HERE (owner 2026-09-01: "there is one frame left
   top, that we need to remove").

   The colophon arrives wearing a hairline down its LEFT edge — the spine it was
   drawn with, which tied three unrelated facts together when it stood alone in
   a track of its own — and it was given a matching rule along its TOP when it
   moved in, on the reasoning that the dock uses one to say "a new kind of thing
   starts here". Both are gone: the column already draws that boundary with
   WHITESPACE and with the eyebrow the block starts on, and two hairlines in a
   286px column read as a box someone forgot to finish rather than as structure.

   ⚑ AND THE INDENT GOES WITH THE SPINE. The 20px of padding existed to clear
   the rule; with no rule it is a block that starts further in than the tree
   above it for no reason a reader could name. Every line in the column now
   begins on one edge — which is exactly what the spine was there to promise. */
.pn-fix .colo-inner { margin-top: 20px; padding: 0; border: 0; }

/* ⚖️ ONE HOME PER READER, RESTATED FOR THIS FRAME. The article's own .meta
   lines carry the publication and the dates everywhere the colophon does not
   exist; where it does, they hand over, so nothing is said twice at any width.

   ⚠️ THE MARKER NAMES THE COLOPHON. Its twin in topicTimelineCss keys the same
   hand-over off .shell--tl, which names the CHRONICLE — a story with a thread
   and no colophon would lose a dateline nothing else was saying. This one is
   emitted on the colophon, so it cannot.

   ⚠️ BOTH SHAPES, BECAUSE EVERY REAL PAGE IS THE SECOND ONE — with the bundle
   on, the baker wraps the article's children in the hydration root and the meta
   lines are a GRANDCHILD. The one-level selector this pattern shipped with once
   matched only the script-off page, and every hydrated page said the dateline
   twice.

   ██ AND THE TWO SWITCHES ARE ONE QUERY, WRITTEN TOGETHER ██ They must flip on
   the same edge or a width exists with NEITHER shape: measured on the first cut,
   at exactly 1180px the colophon drew 1140px wide under the story while the
   dateline it stands in for was already hidden. So the hand-over is written
   against the same breakpoint as the block that replaces it — and moved with it
   when the column took the colophon back. 1180 was the third track's number;
   900 is the column's. */
.shell--story > article > .meta,
.shell--story > article > [data-nrb-view] > .meta { display: none; }
/* ⚠️ TWO WAYS THE COLUMN CANNOT HOLD IT, AND THEY SHARE ONE QUERY LIST — they
   have to, or a viewport exists with neither the column nor the lines it
   replaces.

   BY WIDTH: below 901px the column is a top row and there is no side column.

   BY HEIGHT: measured, a full colophon (a stamp and four actors, 332px) plus
   the column's fixed chrome wants about 576px of viewport. Under that the tree
   has already given everything it has, and the dock — the six editions and
   light/dark — is pushed off the bottom of a box that cannot scroll. An
   unreachable language control is a worse loss than a column of facts the
   article repeats a paragraph later.

   ⚑ AND THE PAIR LIVES DOWN HERE, BELOW BOTH BASE RULES. The height gate was
   written first beside the colophon's own switch, two hundred lines up: the
   query matched, the column went, and the base display:none for the meta lines
   still won on source order — so a short window served a page with the dateline
   in NEITHER place. Measured at 1200x430, not reasoned about. */
@media (max-width: 900px), (max-height: 600px) {
  .pn-fix .nrb-colo { display: none; }
  /* flex, not revert — .meta is a flex row (siteCss), and revert would hand it
     back the browser's block and unwrap the dateline onto two lines */
  .shell--story > article > .meta,
  .shell--story > article > [data-nrb-view] > .meta { display: flex; }
}

/* ── narrow desktop: the rail drops below, full width ────────────────────── */
/* ⚑ IT WRAPS, IT DOES NOT HIDE. A rail that disappears takes the games with
   it; a rail that wraps keeps them one scroll away. 100% cannot share a line,
   so the wrap is a consequence of the width rather than a second breakpoint
   that has to agree with it. */
@media (max-width: 1180px) {
  .pn-shell { flex-wrap: wrap; }
  .pn-rail { width: 100%; position: static; margin-top: 6px; }
  /* ⚑ WRAPPED, THE RAIL IS A PAGE BLOCK AGAIN — a scrollport nested in the page
     scroll is the one shape a reader cannot predict, and there is nothing to
     pin against once the rail is under the story rather than beside it. */
  .pn-rail--story { max-height: none; overflow: visible; margin-bottom: 0; }
  /* ⚑ AND IT LINES UP UNDER THE STORY, NOT UNDER THE COLUMN. A wrapped flex
     item starts at the frame's own left edge, which is where the COLUMN is —
     measured, the thread began at x=20 under a story at x=334, reading as a
     piece that had fallen off the page. The inset is the column plus the gap,
     the same sum the footer pays, so the thread starts on the story's own
     first pixel. Padding rather than a margin because the rail is width:100%
     here: a margin would push it past the frame. */
  .pn-rail--story { padding-left: calc(var(--pn-col) + 28px); }
  /* ⚑ THE THREAD KEEPS A COLUMN'S WIDTH. It is a list of dated cards with a
     spine down the left; stretched to the measure the spine leads text a third
     of a screen away from it and the dates float off alone on the right. 420 is
     the drawer's own designed width — the shape this content was drawn at. */
  .pn-fix > .nrb-tl, .pn-rail--story .nrb-tl { max-width: 420px; }
  /* full width again, so the games return to their row and their own reserve */
  .pn-rail .nrb-widget-host.nrb-play-row { flex-direction: row; gap: 22px; }
  .pn-rail .nrb-slot-layer { height: var(--nrb-slot-h, 86px); min-height: 0; }
  .pn-rail .nrb-widget-title-h { font-size: 40px; }
  .pn-rail .nrb-widget-title-d { font-size: 15px; }
}

/* ── the footer belongs to the MEASURE ───────────────────────────────────── */
/* ⚖️ ONLY BELOW THE MAIN CONTAINER (owner 2026-08-31). The footer was the one
   full-bleed thing under the frame: a rule and a row of links spanning the
   whole viewport, under a column and a rail that stop well short of it. It now
   takes the measure's own frame — same max-width, same centring, and a left
   inset of exactly the column plus the gap — so its links start on the same
   pixel as the stories above them. Measured before: 302 against the measure's
   294, eight pixels of nearly-aligned, which reads worse than either.

   ⚑ THE RULE MOVES WITH IT, onto the inner shell. A border on the footer
   itself would still be drawn edge to edge, which is the thing being fixed.

   ⚠️ A GENERAL SIBLING, NOT AN ADJACENT ONE. The scope switcher (20.2000) is
   emitted between the frame and the footer, so the adjacent combinator matched
   nothing and the rule silently did nothing — the footer only cleared the
   column because its own centred shell happened to land past it at 1440px. */
/* ⚠️ THE SAME SUM AS THE FRAME, not a number that once matched it. A hand-kept
   1430 here against the frame's own total left the footer centred five pixels
   off the moment the measure widened to 860 — the exact drift this file already
   fixed once by aligning the two, reappearing because only one of them was
   written as its parts. */
.pn-shell ~ .site-footer {
  max-width: calc(var(--pn-col) * 2 + 28px * 2 + 860px + var(--sp-card) * 2);
  margin-left: auto; margin-right: auto;
  padding-left: calc(var(--sp-card) + var(--pn-col) + 28px);
  padding-right: calc(var(--sp-card) + var(--pn-col) + 28px);
  border-top: 0;
}
.pn-shell ~ .site-footer > .shell {
  /* the measure's width, or the footer's links stop short of the stories above */
  max-width: 860px;
  margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;
  border-top: 1px solid var(--line);
}

/* ⚖️ AND IT IS ONLY EVER SEEN AT THE BOTTOM, never above (owner: "just like the
   languages"). The dock sits at the foot of a column that owns the screen's
   height; the footer had no such anchor, so on any page shorter than the
   viewport it stopped wherever the stories did and left the last fifth of the
   screen empty beneath it. Measured on the Energy wall: a 1,143px document in a
   1,328px viewport, the footer floating 185px clear of the bottom.

   ⚑ THE FRAME GROWS, THE FOOTER DOES NOT MOVE. flex-grow on the frame is what
   pushes it down, rather than an auto margin on the footer — an auto margin
   would win over the 44px gap the footer keeps from the content, and the two
   are different jobs. On a page taller than the viewport nothing changes: the
   frame is already past 100vh and the footer follows it as it always did.

   ⚑ SCOPED BY :has(), so only a page wearing the portal frame becomes a flex
   column. Where the selector is not understood the footer simply stays where it
   was, which is today's page. */
/* ⚠️ width:100% IS NOT DECORATION HERE. Both the frame and the footer centre
   themselves with AUTO SIDE MARGINS, and an auto margin in the cross axis beats
   align-items:stretch — so as flex items they shrank to fit their content and
   the whole page collapsed inward (measured: the 760px measure became 331px at
   x=516). The explicit width restores the stretch the auto margins cancelled,
   and the margins then do what they were always for: centre it inside its
   max-width. */
/* 🔀 BOTH PARENTS (prod incident 2026-09-09, the owner's phone). Soft
   navigation (20.2110) wraps the page in <div id="nrb-page"> with
   display: contents — no box, so the frame and the footer are STILL body's flex
   items — but a child combinator reads the DOM, not the box tree, and
   'body > .pn-shell' stopped matching. Without width:100% the frame shrank to
   its content exactly as the note above predicts: measured 743px wide at a
   375px viewport, the hero rail spilling off the right of the phone. Every
   direct-child-of-body rule in this sheet names both parents from here on, and
   softNav.test pins that no rule is ever written with one. */
body:has(.pn-shell) { min-height: 100vh; display: flex; flex-direction: column; }
body:has(.pn-shell) > .pn-shell, body:has(.pn-shell) > #nrb-page > .pn-shell { flex: 1 0 auto; width: 100%; }
body:has(.pn-shell) > .site-footer, body:has(.pn-shell) > #nrb-page > .site-footer { width: 100%; }

@media (max-width: 1180px) {
  /* the rail has wrapped below the measure, so there is nothing to clear on the right */
  .pn-shell ~ .site-footer { padding-right: var(--sp-card); }
}
@media (max-width: 900px) {
  /* the column becomes the top row, so the frame stacks */
  .pn-shell { display: block; padding: 0; }
  /* ⚠️ THE MEASURE TAKES ITS CENTRING BACK DOWN HERE. Its margin is zeroed
     above because an AUTO margin in a flex row absorbs the free space and beats
     align-items:stretch — the 2026-08-31 collapse, where the 760 measure became
     331 at x=516. In a BLOCK frame that danger is gone and the zero is the bug
     instead: measured at 900px the 860 measure sat at x=0 with 40px of dead
     page down the right. Both halves are the same rule read at two widths. */
  .pn-shell > .shell { padding: 0 var(--sp-card); margin-left: auto; margin-right: auto; }
  .pn-rail { padding: 0 var(--sp-card); margin-top: 0; }
  /* no side column down here, so the footer is the page's own again */
  .pn-shell ~ .site-footer { padding-left: 0; padding-right: 0; }
  .pn-shell ~ .site-footer > .shell { margin-left: auto; margin-right: auto; padding-left: var(--nrb-page-pad); padding-right: var(--nrb-page-pad); }
}

/* Shared chrome: the clock stays put while the timeline alone scrolls. */
/* 🌍 THE EDITION SWITCHER, PLACED (owner 2026-09-13, design card
   components/feed/language-selector.html). Everything the control LOOKS like —
   the 36px trigger, the panel, the code plates, the endonyms, the tick, the
   open state, the entry keyframe and its reduced-motion suppression — arrives
   with DESIGN_CHROME_CSS, which every baked page already loads. All that is
   left here is the one thing a design card cannot know: where in THIS column
   the control stands.

   ⚠️ WHAT IT REPLACED WAS PAINTED WITH A TOKEN THAT DOES NOT EXIST HERE. The
   old summary asked for background:var(--bg-card); the baked palette defines
   --bg-surface and --bg-elevated and has never defined --bg-card, so the pill
   resolved to transparent and the reader saw bare text behind the browser's
   own disclosure triangle — which no rule had turned off either, because
   list-style:none was written for details.nrb-lang and this was .pn-language.

   ⚠️ details.pn-language, NOT .pn-language. The baked-page adapter carries
   details.nrb-lang { position: relative } at the same weight; a bare class
   here ties on specificity and then loses on nothing but source order, and the
   day that order changes the switcher falls out of the corner into the flow,
   under the mark. One element in the selector settles it for good. */
details.pn-language { position:absolute; top:0; right:0; z-index:50; }
.article-colophon { margin:18px 0 24px; }
.article-colophon .colo-inner { position:static; padding:0; border:0; gap:12px; }
.article-colophon .colo-cast { display:flex; flex-direction:row; flex-wrap:wrap; align-items:center; gap:8px 16px; padding:0; border:0; }
.article-colophon .colo-h { flex-basis:100%; margin:0; text-align:left; }
.article-colophon .colo-foot { display:flex; flex-direction:row; flex-wrap:wrap; gap:8px 16px; padding:0; margin:0 0 12px; border:0; }
.article-colophon .colo-actor { margin:0; }
.nrb-mode-toggle { display:none !important; }
@media(min-width:901px) {
 .pn-fix:has(>.nrb-left-context) { overflow:visible; }
 .pn-fix>.nrb-tl.nrb-left-context { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain; scrollbar-width:thin; }
 .pn-fix>.nrb-tl .nrb-nowbar { position:sticky; top:0; z-index:5; }
}

/* 📱 On a phone the column is the page's top row and the switcher rides its
   end cell. The panel needs no rule of its own down here any more: the design
   already anchors it absolutely under the trigger at its own 178px measure,
   which is what the old .pn-lang override was reaching for at 160. */
@media(max-width:900px) { details.pn-language { position:relative; grid-column:2; grid-row:1; justify-self:end; } }



/* ── the floating scope ──────────────────────────────────────────────────── */
/* ⚖️ FIXED, NOT STICKY — and this is the one place on the site where that is
   right. The column sticks because it belongs to the document and scrolls with
   it; this belongs to the VIEWPORT. It is the answer to "where am I reading
   from", which a reader may want at the bottom of the feed as much as the top,
   so it stays put while everything else moves. */
.sc {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: 40; pointer-events: none;
}
.sc-rail {
  pointer-events: auto;
  display: flex; align-items: center; gap: 4px;
  /* ⚑ 4px ALL ROUND — the design's own metric, adopted now that the rail is
     bare segments (2026-09-08). The 0 on the left used to make room for the
     globe's own margin; with the globe retired there is nothing to make room
     for, and an asymmetric pad would show as a lopsided pill. */
  padding: 4px; border-radius: 999px;
  /* ── THE MODE FORK, AS THREE NUMBERS AND AN INK ───────────────────────── */
  /* ⚖️ THE DESIGN FORKS LIGHT MODE with a [data-mode="light"] block: a surface
     ground at 88% (not the base at 82%), a 14% ring (not 12%), and PRIMARY ink
     on the selection — "gold ink on the pale pill misses AA, so the selection
     keeps the accent pill and ring but takes primary ink".
     ⚠️ THIS SITE HAS NO SINGLE "LIGHT" SELECTOR. Light is the default here and
     dark arrives two ways — the OS scheme unless data-mode says light, or an
     explicit data-mode=dark (siteCss.ts, where --bg itself flips by exactly
     these two rules). So the fork is carried as custom properties set LIGHT
     here and overridden to the dark values by the same two selectors the
     tokens use, and the rules below read the properties. Writing
     [data-mode="light"] alone would have left an OS-light reader with no
     attribute — every first visit — wearing the dark numbers. */
  --sc-ground: 88%; --sc-ring: 14%; --sc-on-ink: var(--text);
  /* ⚑ AN ACCENT HAIRLINE, NOT THE NEUTRAL LINE (design pull 2026-09-08). The
     rail floats over a grid of cards that are themselves outlined in --line, so
     a --line border made the pill read as one more card edge among many. The
     accent says it is a CONTROL. */
  border: 1px solid color-mix(in oklab, var(--accent) 55%, transparent);
  /* ⚑ IT SITS OVER THE FEED, so it needs a ground of its own AND a blur: over
     a card's picture a translucent pill with no backdrop is unreadable, and an
     opaque one is a hole punched in the page. Both, so it reads either way.
     ⚑ THE DESIGN'S OWN PRESCRIPTION, in prose rather than in its test bed's
     numbers: "it sits on a 20px blur with a drop shadow beneath, so it holds
     against the card grid it covers" (scope-switcher.html, 2026-09-03). The
     card offers six candidate treatments and says of all of them "nothing here
     is shipped — it is a test bed", so the blur and the shadow are the spec and
     the winning swatch's exact alpha is not. */
  /* ⚑ NEAR-OPAQUE, UP FROM 62% (design 2026-09-08: "near-opaque --bg-base tint
     on an 18px blur"). 62% was tuned when the labels were MUTED and the rail was
     asking to be ignored; with primary ink on it the ground has to carry that
     ink over a photograph, and 82% (dark) / 88% (light) is what the design
     settled on. --bg is this site's base in dark and its surface in light,
     which is the pair the design names for the two modes. */
  background: color-mix(in oklab, var(--bg) var(--sc-ground), transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.1);
  backdrop-filter: blur(18px) saturate(1.1);
  /* ⚖️ A THEME TOKEN, NOT THE SWATCH'S LITERAL rgba(0,0,0,.75). The design
     preview has two modes; this site has six accent themes, each with its own
     shadow tint already tuned — a hardcoded black would be the one shadow on
     the page that does not belong to its theme. --shadow-lg IS "a drop shadow
     beneath" in this system's words, and it already flips with the mode, which
     is why the design's lighter light-mode shadow needs no rule of its own. */
  /* ⚑ AND A FAINT OUTER RING BEFORE THE SHADOW (design 2026-09-08) — 3px of
     12% accent in dark, 14% in light, which is not a glow: it is the separation
     that lets the pill survive landing on a card border of almost its own
     value. */
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) var(--sc-ring), transparent),
              var(--shadow-lg), 0 1px 2px rgba(0, 0, 0, .18);
  /* the gap animates with the collapse (phone block, below) */
  transition: gap var(--dur-med) var(--ease-out);
}
/* the dark numbers, by the two rules that make the page dark (siteCss.ts) */
@media (prefers-color-scheme: dark) {
  :root:not([data-mode="light"]) .sc-rail { --sc-ground: 82%; --sc-ring: 12%; --sc-on-ink: var(--accent); }
}
:root[data-mode="dark"] .sc-rail { --sc-ground: 82%; --sc-ring: 12%; --sc-on-ink: var(--accent); }
/* ⚠️ NO BACKDROP FILTER, NO GLASS — a ground instead. Firefox shipped
   backdrop-filter late and it is still switchable off; without this the 62%
   ground above is all that stands between white text and a hero photograph.
   The pill goes opaque rather than illegible. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sc-rail { background: var(--bg); }
}
/* ⚑ THE CHECKBOX IS THE STATE, AND ON DESKTOP THERE IS NO STATE — every scope
   simply shows, so the input is display:none and reaches neither the layout
   nor the a11y tree. Its label is the selected word, inert here.
   (A .sc-now label and the globe it wrapped stood here until 2026-09-08; the
   design retired the globe — "globe marker retired" — and with it the only
   element the label existed to hold.) */
.sc-toggle { display: none; }

.sc-seg {
  display: block; padding: 6px 13px; border-radius: 999px; text-decoration: none;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .1em;
  /* ⚖️ PRIMARY INK, NOT MUTED (design 2026-09-08: "labels take primary ink" —
     the readability half of this pull). A muted label on a translucent pill
     over a photograph is the one thing on this control that could not be read,
     and it was the DEFAULT state: three of the four scopes wore it. The active
     one was always legible, so the rail was easiest to read where it mattered
     least. */
  text-transform: uppercase; white-space: nowrap; color: var(--text);
  /* ⚑ THE COLLAPSE'S CEILING AND ITS CLIP live on every segment, at every
     width (design 2026-09-08: "animate max-width, not width: buttons keep
     width:auto so labels of any length work with one rule"; "12rem is a
     ceiling, not a target: the box stops at its intrinsic width"). On desktop
     neither has any effect — no segment reaches 12rem and nothing overflows —
     they are here so the phone block below changes only the values that move,
     and so a segment is the same box in both poses. */
  max-width: 12rem; overflow: hidden;
  /* ⚑ THE DESIGN'S TIMING (its §2 table): --dur-med on the colours and the
     box, --dur-fast on opacity "so that on open the labels are fully legible
     while the boxes are still gliding to size, and on close the text
     disappears before the boxes have visibly squeezed it". No delay in either
     direction — except visibility, which is the one property the design does
     not list and this port needs (phone block). */
  transition: color var(--dur-med) var(--ease-out),
              background var(--dur-med) var(--ease-out),
              max-width var(--dur-med) var(--ease-out),
              padding var(--dur-med) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out),
              visibility 0s linear var(--sc-vis-delay, var(--dur-med));
}
a.sc-seg:hover { color: var(--text); background: color-mix(in oklab, var(--text) 7%, transparent); text-decoration: none; }
a.sc-seg:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* ── the scope you are reading — never a link (see ScopeSwitcherView) ──────── */
/* ⚑ MARKED THE WAY THE DESIGN MARKS A SELECTION: "accent ink on --accent-soft
   with a hairline accent ring; there is no sliding indicator". The grey fill it
   replaces said only "this one is different"; accent says WHICH ONE IS ON, in
   the same language every other selected control on this site speaks — and it
   survives the blur behind it, which a 12% grey over a photograph does not.
   ⚖️ THE RING IS INSET, so it costs no layout: an outer ring would grow the
   segment and shove the two links sideways the moment the scope changed. */
.sc-seg--on {
  /* ⚑ ACCENT IN DARK, PRIMARY IN LIGHT — --sc-on-ink, set on the rail above.
     The design's reason is contrast: gold on the pale pill misses AA. */
  color: var(--sc-on-ink); background: var(--accent-soft); font-weight: 700;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 45%, transparent);
}

/* ── the room it takes from the page ─────────────────────────────────────── */
/* ⚠️ A FIXED PILL STANDS ON WHATEVER REACHES THE BOTTOM. Once the footer was
   pinned there (20.1980, owner: "just like the languages"), the switcher landed
   squarely on its copyright line — the two had only ever missed each other
   because the footer used to stop wherever the stories did.
   ⚑ THE PAGE RESERVES THE BAND, rather than the pill being moved out of the
   middle where the design puts it. Paid by the footer's own shell so the flex
   column still ends at exactly 100vh and no scrollbar appears on a short page —
   body padding would have added to min-height and made one. */
body:has(.sc) .site-footer > .shell { padding-bottom: 76px; }

/* ── the phone ───────────────────────────────────────────────────────────── */
/* ⚖️ IT MOVES TO THE LEFT, off the thumb's path. Bottom-centre is where a phone
   browser puts its own toolbar and where a reader's thumb rests; the design
   makes the same move for the same reason.

   ⚠️ 900px, NOT THE DESIGN'S 800 — a deliberate, reported deviation. The design
   card says "below 800px the rail collapses automatically", a number written
   for a component with no shell around it. THIS shell turns into a phone at
   900 (portalNavCss, every breakpoint in it). Taking 800 literally would leave
   an 800–900 band where the page is already a phone — nav folded into its
   panel, one column — while the rail is still in its desktop pose: expanded,
   bottom-centre, lying straight across the thumb's path. The design's own note
   settles which side gives: "placement is the only thing the shell overrides".
   One number, and it is the shell's. */
/* The same animated, selected-only disclosure at every viewport width. */
  .sc { left: 14px; right: auto; bottom: 16px; transform: none; }
  /* ⚑ 15px = 10 + 50%, the same ask as the launcher's opposite it (owner
     2026-09-08). ⚖️ THE TWO MUST MOVE TOGETHER: they are the only two things
     fixed to the foot of a phone page, one at each corner on the same line, so
     growing one alone would make the pair look like a mistake rather than a
     size. The DESKTOP rule (10.5px, above) is deliberately left alone — the
     complaint was about a thumb-height control, and the wide rail is read at
     arm's length in a row of three. */
  .sc-seg { padding: 6px 11px; font-size: 15px; letter-spacing: .08em; }

  /* ── it shrinks to the scope you are in ───────────────────────────────── */
  /* ⚖️ THREE SEGMENTS IS A BAR ACROSS THE THUMB'S PATH. Collapsed it names
     where you are reading from and nothing else — the selected word, exactly
     the design card's .is-collapsed state (owner 2026-09-03: "even collapsed
     the selected region always visible, like the design"). Tapping it opens
     the rail; choosing a scope navigates, which shrinks it again.

     ⚑ IN PLACE, NOT HOISTED. The selected segment stays wherever it sits in
     the row and its siblings shrink to nothing, so the visible order when the
     rail opens is the model's order every time. The earlier design of this
     block moved the word into the globe's label — which is precisely what made
     the rail reorder itself around the selection (fixed in c3b0f3138), and the
     bare circle that replaced it for one commit lost the word altogether.

     ⚑ IT ANIMATES (design 2026-09-08: "width/opacity rather than display:none,
     so opening and closing animate"). The siblings go to max-width 0, no
     inline padding and opacity 0; opening restores all three, and the same
     transition runs in both directions, so a tap that reverses mid-motion
     reverses smoothly from wherever it is. display:none, which stood here
     from 2026-09-03 to 2026-09-08, cannot transition at all.

     ⚠️ VISIBILITY TOO, WHICH THE DESIGN DOES NOT LIST — a deliberate addition,
     not a misreading. A link at max-width 0 and opacity 0 is still a link: it
     is in the tab order and a screen reader still offers it, so a collapsed
     rail would present four scopes to a keyboard and read as one to a thumb.
     visibility:hidden takes it out of both. And it does NOT drop the
     transition, which is what the design's §5 warns of for display: visibility
     interpolates discretely — visible for the whole run whenever either end is
     visible — so with the delay on the segment above (0s duration, --dur-med
     delay) it flips to hidden only once the box has finished closing, and
     --sc-vis-delay: 0s on the open rule flips it to visible the instant the
     rail opens. */
  .sc-rail > .sc-seg:not(.sc-seg--on) {
    max-width: 0; padding-inline: 0; opacity: 0;
    visibility: hidden; pointer-events: none;
  }
  /* ⚑ NO GAP WHEN CLOSED, AND THE WORD CARRIES ITS OWN 2px (design §5: "zero-
     width flex items still eat their flex gaps and would push the label off
     centre by up to 12px") — three zero-width siblings and their three gaps
     would have left the pill wider on whichever side the selection was not. */
  .sc-toggle:not(:checked) ~ .sc-rail { gap: 0; }
  .sc-toggle:not(:checked) ~ .sc-rail > .sc-seg--on { margin-inline: 2px; }
  .sc-seg--on { cursor: pointer; }

  /* ── open, it takes the row ─────────────────────────────────────────────── */
  /* ⚠️ THE LAUNCHER IS z-index 41 AND THIS RAIL IS 40, so an opened rail would
     be drawn UNDER Рубрики — a half-covered segment the reader cannot tap. The
     open state lifts above it and fades the launcher out for exactly as long as
     the rail is open: two controls cannot be used at once, and choosing a scope
     navigates, which closes this and brings the launcher back. */
  .sc-toggle:checked ~ .sc-rail { position: relative; z-index: 42; }
  /* ⚠️ THE OPEN RAIL MUST NOT RUN OFF THE SCREEN. It is fixed at left:14px with
     no width of its own, so its size is the sum of four words — and at 320px,
     with the 15px type above, the Bulgarian row reached 357px against a 320px
     screen: the last scope sat past the edge, unreachable, with no scrollbar to
     say so. Measured before and after, which is the only reason this is here.
     ⚖️ THE GUARD IS LOCALE-PROOF, NOT A PATCH FOR ONE TYPE SIZE. The fourth
     segment is the DOMESTIC label, so the row is as long as the edition makes
     it — Deutschland is longer than Bulgaria, and a future edition can be
     longer still. Capping it to the viewport and letting the contents scroll
     costs nothing at any width where the row already fits (375px and up on
     every edition today) and keeps every scope reachable where it does not. */
  .sc-toggle:checked ~ .sc-rail {
    max-width: calc(100vw - 28px);
    overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .sc-toggle:checked ~ .sc-rail::-webkit-scrollbar { display: none; }
  body:has(.sc-toggle:checked) .pn-launch {
    opacity: 0; pointer-events: none; transition: opacity var(--dur-med) var(--ease-out);
  }
  /* ⚠️ :checked ALONE. A :focus-within twin looked like the keyboard's way in
     and was actually a latch: clicking a label focuses the control it labels,
     so the rail was still focused the instant after the tap that unchecked it
     and it never shrank again. The checkbox is itself focusable, so Tab and
     Space already reach the same state — the second rule bought nothing and
     cost the close. */
  .sc-toggle:checked ~ .sc-rail > .sc-seg {
    max-width: 12rem; padding-inline: 11px; opacity: 1;
    visibility: visible; pointer-events: auto; --sc-vis-delay: 0s;
  }
  /* the input is the state, never a picture — hidden by SIZE, so it stays
     focusable and the pill can light up with it */
  .sc-toggle { display: block; position: fixed; width: 1px; height: 1px; opacity: 0; margin: 0; pointer-events: none; }
  .sc-toggle:focus-visible ~ .sc-rail { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (min-width: 901px) {
  .sc { left: var(--nrb-main-left, 14px); }
  .sc-toggle:checked ~ .sc-rail { max-width: var(--nrb-main-width, calc(100vw - 28px)); }
}

/* ⚑ END STATES UNCHANGED, MOTION REMOVED (design §5: "transition-duration:
   0ms"). The delay goes too, or a reduced-motion reader would see the hidden
   segments linger for 220ms after the rail closed. */
@media (prefers-reduced-motion: reduce) {
  .sc-seg, .sc-rail { transition-duration: 0s; transition-delay: 0s; }
}



/* ── the band ────────────────────────────────────────────────────────────── */
/* ⚖️ THE BAND HAS NO PLATE OF ITS OWN (handoff §1) — no ground, no border, no
   label. Six tabs and the gaps between them, and nothing that would read as a
   container the tabs live inside: they hang from the top edge of the column
   itself, which is what makes them tabs rather than buttons.

   ⚑ 59px IS AN ALIGNMENT, NOT A TASTE. It lands the band bottom on the same
   line as the masthead underline in the nav column (24px top offset + 26px
   logo + 9px padding) — the same measure the category wall band already keeps,
   which is why the two pages read as one system.

   ⚑ THE COLUMN COUNT COMES FROM THE MARKUP. --hn-n is written by the view from
   the number of top-level rubrics the registry actually holds; the 6 here is a
   fallback for a band rendered without it, never the truth. */
.hn-head { position: relative; box-sizing: border-box; padding: 0; margin: 0 0 18px; }
.hn-track {
  box-sizing: border-box;
  display: grid; grid-template-columns: repeat(var(--hn-n, 6), minmax(0, 1fr));
  align-items: stretch; gap: 8px; height: 59px;
}

/* ⚖️ THE TAB COLOUR IS DERIVED, NEVER THE RAW HUE (handoff §6). The six rubric
   hues are pastel: white ink fails on the yellow, the mint and the pink, and
   dark ink fails on the blue and the red — so no single ink passes on the raw
   palette. Mixing every hue 38% toward the near-black in OKLab produces one
   set where WHITE passes on all six, in light mode as well as dark, and each
   hue stays recognisably its own.

   ⚑ ONE SET OF NUMBERS FOR BOTH MODES, deliberately: the tab is a saturated
   fill either way, so it is not reading the page ground and has nothing to
   adapt to. Forking per mode would be two palettes to keep true instead of one.

   ⚠️ --h FALLS BACK TO THE ACCENT. A registry row may legitimately carry no
   ink; without the fallback color-mix() would receive an undefined custom
   property, the whole declaration would be invalid, and the tab would render
   with no fill at all rather than with a neutral one. */
.hn-it {
  position: relative; min-width: 0;
  display: flex; align-items: flex-end; gap: 7px;
  padding: 8px 12px 9px;
  border-radius: 0 0 14px 14px;
  --hd: color-mix(in oklab, var(--h, var(--accent)) 62%, #0a0f1c);
  background: color-mix(in oklab, var(--hd) 55%, transparent);
  border: 1px solid var(--hd); border-top: 0;
  color: #fff; text-decoration: none;
}
/* ⚑ BRIGHTNESS ONLY. Six differently-coloured surfaces cannot share a hover
   COLOUR, and giving each its own would be a seventh palette; a filter is the
   one affordance that means the same thing on all of them. */
.hn-it:hover { filter: brightness(1.06); text-decoration: none; }
.hn-it:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.hn-tx { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
/* ⚠️ THE NAME ELLIPSISES RATHER THAN WRAPS. A second line would push the row
   below it out of the 59px band and break the alignment the height exists for,
   so a long rubric loses its tail instead of the band losing its line. */
.hn-n {
  font-family: var(--font-display); font-size: 12.5px; font-weight: 400;
  letter-spacing: -.01em; line-height: 1; color: #fff;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hn-r { display: flex; align-items: center; gap: 6px; margin-top: 1px; }
.hn-ic { flex: none; display: flex; width: 18px; height: 18px; color: #fff; }
.hn-ic svg {
  width: 100%; height: 100%; fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
/* The figure is the loudest thing in the tab, and that is the point: the band
   exists to publish it. Mono so the six line up as a row of numbers. */
.hn-d {
  flex: none; font-family: var(--font-mono); font-size: 17px; font-weight: 700;
  letter-spacing: -.02em; line-height: 1; color: #fff;
}

/* ── the phone: one swipeable row ────────────────────────────────────────── */
/* ⚖️ SIX EQUAL COLUMNS CANNOT SURVIVE 390px — each tab would be 57px wide and
   every name would be an ellipsis. The grid becomes a scroller showing THREE
   at a time, which keeps the name readable and makes the rest a swipe away.

   ⚑ IT BLEEDS TO THE FRAME EDGES while its contents stay on the page gutter
   (negative margin + equal padding), so a tab scrolling out does not stop at
   an invisible wall inset from the screen. */
@media (max-width: 900px) {
  .hn-head {
    height: auto;
    margin: 0 calc(-1 * var(--sp-card)) 14px; padding: 0 var(--sp-card);
    overflow-x: auto; scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
    /* ⚠️ THE SNAP MUST RESPECT THE GUTTER OR THE GUTTER IS NOT THERE. With
       scroll-snap-align:start and a padded scrollport, the browser aligns
       the first tab's edge to the PADDING BOX edge — it scrolls the padding
       away, and the row lands flush against the screen edge (measured: it came
       to rest at scrollLeft 20, exactly the inset it had just eaten). Telling
       the scrollport where its start really is puts the tab back on the page's
       own gutter, at rest and after every snap. */
    scroll-padding-left: var(--sp-card);
  }
  .hn-head::-webkit-scrollbar { display: none; }
  /* ⚖️ EQUAL COLUMNS, SIZED TO THE WIDEST TAB (owner 2026-09-08: "hug space to
     the largest text and then all other have the same width"). The handoff's
     three-per-screen width rule fitted exactly three whatever
     they held, which on a wide phone or a tablet is three tabs of mostly
     nothing. A 1fr track inside a max-content grid is the opposite rule: every track
     takes the size of the largest, and the row is as wide as it needs to be —
     so a short roster may not even scroll, and a long one scrolls by exactly
     what it overflows. */
  .hn-track {
    grid-template-columns: none;
    grid-auto-flow: column; grid-auto-columns: 1fr;
    width: max-content; height: auto; gap: 6px;
  }
  .hn-it {
    scroll-snap-align: start;
    border-radius: 0 0 10px 10px; padding: 6px 10px 8px;
  }
}
/* Article paths use the same tabs above the hero, replacing the floating
   path disclosure until the band scrolls away. The home page and pages
   without a path keep their menu. */
.pn-shell:not(.pn-path-scrolled):has(> .shell > .hn-head--path) .pn-toggle,
.pn-shell:not(.pn-path-scrolled):has(> .shell > .hn-head--path) .pn-launch,
.pn-shell:not(.pn-path-scrolled):has(> .shell > .hn-head--path) .pn-panel,
.pn-shell:not(.pn-path-scrolled):has(> .shell > .hn-head--path) .pn-scrim { display:none; }
@media (prefers-reduced-motion: no-preference) {
  .pn-path-scrolled .pn-launch { animation: hn-path-enter 180ms ease-out; }
  @keyframes hn-path-enter {
    from { opacity:0; translate:0 12px; }
    to { opacity:1; translate:0 0; }
  }
}



/* ── the mount ───────────────────────────────────────────────────────────── */
/* ⚖️ AN EMPTY MOUNT IS NOT A BAR. The baker leaves a box and two words and the
   bundle builds the rest, so between first paint and the bundle running this
   element has no contents — and an empty bordered plate reads as a component
   that failed rather than one that has not arrived. Hidden while empty; the
   moment the bundle fills it, it is a bar.

   ⚑ THIS IS ALSO THE NO-SCRIPT ANSWER, and the right one: the now bar is reader
   UX, not content. A reader without the bundle gets the page they have today,
   not a hole where a clock was going to be. STATIC FIRST governs what a page
   SAYS; a clock says nothing.

   ⚠️ NOTHING IS RESERVED YET, deliberately. The bar is the only thing in this
   rail, so its arrival moves nothing below it and a reserved height would just
   be a hole on a page whose bundle never lands. The day the recommendation and
   the constellation sit under it, this is where their height goes. */
.nrb-nowbar-mount:empty { display: none; }

/* ── the now bar ─────────────────────────────────────────────────────────── */
/* ⚖️ IT IS TINTED WITH --info, NEVER THE ACCENT (design card): "it reads as
   instrument, not story". Everything else in this column will be editorial —
   a recommendation, read covers, a constellation — and the one surface that
   reports rather than invites has to be legible as a different KIND of thing at
   a glance. --info is the site's universal semantic blue; it is declared once
   in :root and does not fork per theme, which is right for an instrument: the
   bar means the same thing on all six skins.

   ⚑ THE GLOW IS A drop-shadow ON THE WHOLE BOX, not a box-shadow on the head.
   The head and the tab are two boxes sharing one silhouette (square join in the
   middle, rounded outside), and a box-shadow on each would draw the join. */
.nrb-nowbar {
  --nb-ink: var(--info);
  position: relative; display: flex; flex-direction: column;
  font-family: var(--font-mono); color: var(--text);
  filter: drop-shadow(0 0 14px color-mix(in oklab, var(--nb-ink) 35%, transparent));
}
/* ⚖️ ONE HEAD, BOTH POSES (owner 2026-09-11: "can we make the main layout like
   the one when we open the post … this will leave use the center of the nowbar
   for tokens later"). The column used to centre its clock between two fences,
   per the design card; the post put the words left, a reserved track in the
   middle and the clock hard right. They MORPH into one another across a
   navigation, so two geometries meant the head reordering itself mid-animation
   — and the middle track is the only cell wide enough for the balance F01 will
   bring, which is the owner's second reason in the same sentence.

   ⚑ THE MIDDLE IS RESERVED, NOT DISCOVERED. When it was a 1fr cell in the
   chronicle, the arrow's arrival squeezed the two auto columns and the clock
   lost its last digit at the narrowest docked width — and nowhere else. A fixed
   track costs an empty 30px and buys a head that cannot move.

   ⚑ minmax(0, 1fr) ON THE WORDS so the two cells that must never be clipped
   never are. ⚠️ Pixel-tuning that column is a trap: it was once sized against
   Bulgarian "понеделник", and Portuguese ships "quarta-feira". */
.nrb-nowbar-head {
  box-sizing: border-box;
  display: grid; grid-template-columns: minmax(0, 1fr) 30px auto;
  align-items: center; gap: 10px; padding: 18px;
  border-radius: var(--r-lg);
  border: 1px solid color-mix(in oklab, var(--nb-ink) 60%, transparent);
  background: color-mix(in oklab, var(--nb-ink) 9%, var(--bg-surface));
}
/* with a tab hanging below, the head keeps only its top corners */
.nrb-nowbar[data-tab] .nrb-nowbar-head { border-radius: var(--r-lg) var(--r-lg) 0 0; }
.nrb-nowbar-when { min-width: 0; }
/* ⚑ THE DAY WEARS THE ACCENT — the one accent note on an info-tinted plate, and
   it is what makes the bar read as TODAY rather than as a widget.
   ⚑ UPPERCASE IN THE SHEET, NOT IN THE DATA. Intl gives each edition its own
   abbreviation (bg chetvurtuk as ch-t, en Thu, de Do); asking for it in capitals
   would mean six hand-written tables of weekday names, which is exactly the
   Bulgarian-default trap 20.1250 is the scar of. */
.nrb-nowbar-day {
  font-size: 15px; font-weight: 700; color: var(--accent);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nrb-nowbar-date {
  font-size: 11.5px; color: var(--muted); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  font-variant-numeric: tabular-nums;
}
/* ⚖️ HAIRLINE FENCES, NOT A BOX. The clock is the instrument's reading and the
   two rules are what set it apart from the labels either side without adding a
   second plate inside the first one. */
.nrb-nowbar-clock {
  display: flex; align-items: center; justify-content: flex-end; gap: 9px;
  padding: 2px 0 2px 10px;
  border-left: 1px solid var(--line); border-right: 0;
}
/* ⚠️ TABULAR DIGITS ARE LOAD-BEARING HERE, not typography. The baked clock is
   an em-dash pair and the live one is five digits; equal-width figures are what
   make the swap cost zero layout shift — and, once it ticks, keep 11:11 and
   22:22 the same width so the fences do not breathe every minute. */
.nrb-nowbar-time {
  font-size: 17px; font-weight: 600; letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
}
/* ── the attached tab ────────────────────────────────────────────────────── */
/* ⚑ IT HANGS FROM THE HEAD: square top, head radius at the bottom, and NO top
   border — the head's own bottom edge is the join, so a border here would draw
   it twice and read as a 2px seam.
   ⚖️ PALER THAN THE HEAD (4% wash, 45% border, against 9% and 60%) so the head
   stays the instrument and the tab reads as its caption. */
.nrb-nowbar-tab {
  box-sizing: border-box;
  display: flex; align-items: center; gap: 10px; min-height: 34px;
  padding: 7px var(--sp-card);
  border: 1px solid color-mix(in oklab, var(--nb-ink) 45%, transparent);
  border-top: 0; border-radius: 0 0 var(--r-lg) var(--r-lg);
  background: color-mix(in oklab, var(--nb-ink) 4%, var(--bg-surface));
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted);
}
.nrb-nowbar-tab-l { font-weight: 600; color: var(--text); }
/* ⚑ THE DAYS-LEFT COUNTER KEEPS ITS OWN CASE. It is a localized measurement
   from Intl (4 dni, 4 Tage, 4 days), and uppercasing a word the site did not
   write is how an edition ends up shouting in a language whose conventions
   nobody here checked. */
.nrb-nowbar-tab-s {
  margin-left: auto; letter-spacing: .04em; text-transform: none;
  color: var(--faint); white-space: nowrap; font-variant-numeric: tabular-nums;
}
/* ── the cycle's seven days ──────────────────────────────────────────────── */
/* ⚖️ EQUAL SEGMENTS, ALWAYS SEVEN. They are days, not progress: the row fills
   itself whether the reader reads or not, which is why they are aria-hidden in
   the markup and the words beside them carry the meaning. */
.nrb-nowbar-segs { flex: 1; display: flex; gap: 3px; min-width: 0; }
.nrb-nowbar-seg {
  flex: 1; height: 3px; border-radius: 2px;
  background: color-mix(in oklab, var(--nb-ink) 20%, transparent);
}
/* spent days in the instrument's own ink; today in the accent, like the day */
.nrb-nowbar-seg.is-read { background: var(--nb-ink); }
.nrb-nowbar-seg.is-cur { background: var(--accent); }

/* ── sm — the column's size ──────────────────────────────────────────────── */
/* ⚖️ THIS IS THE RAIL'S DEFAULT, NOT A PHONE VARIANT — a departure from the
   card, which calls sm "Mobile rail".

   ⚠️ AND NOT BECAUSE md OVERFLOWS: it does not. Measured in a browser at the
   rail's narrowest, md's head needs 205px of the 272px available and nothing
   collides — an earlier draft of this comment claimed a collision that is not
   there, which is why the number is now one that was read off a page. What
   decides it is HEIGHT: md stands 121px tall against sm's 88px, a third again
   as much, at the top of a column whose whole job is to be a quiet anchor
   before any content. The owner named NB-04s; these are that variant's own
   numbers, not new ones. */
.nrb-nowbar[data-size="sm"] .nrb-nowbar-tab { min-height: 28px; padding: 5px 12px; font-size: 10px; }

/* ── the STORY pose — the chronicle's head ───────────────────────────────── */
/* ⚖️ SAME INSTRUMENT, SECOND POSE (owner 2026-09-10: "the transformation from
   this to header of the timeline when open gambit or bulletin"). The chronicle
   rail had grown its own day/date/clock head with the same tint and radius;
   this is that head, drawn by the same component.

   ⚠️ EVERY NUMBER OF THIS HEAD WAS MEASURED AGAINST THE RAIL'S 272px CLAMP
   FLOOR and carried over unchanged from topicTimelineCss, where the failures
   that produced them are recorded. They live in the BASE head now — both poses
   share it since 2026-09-11 — and they are still not free to retune:
     · the 30px arrow track is RESERVED, not discovered — when it lived in a 1fr
       cell the arrow's arrival squeezed the two auto columns and the clock lost
       its last digit at the narrowest docked width, and nowhere else;
     · 18px padding, not --sp-card — the head had 190px of content box for
       217.6px of content and overflowed by 27.6;
     · a 17px clock, because 22px plus that reserved track does not fit;
     · minmax(0, 1fr) on the words so the two cells that must never be clipped
       never are. ⚠️ Pixel-tuning that column is a trap: it was once sized
       against Bulgarian "понеделник", and Portuguese ships "quarta-feira". */
/* ⚠️ AND THE GAP TO THE LIST BELOW (owner 2026-09-10: *"the lines and bottom
   items are not properly offset from the now-bar"*). The legacy head carried
   the same bottom margin this rule now restores, and the port dropped it, so
   the first chapter —
   and the thread's own spine with it — sat flush against the head's bottom
   edge. Measured: 0px where the legacy had 20px. The rail's spacing is the
   chronicle's, not this component's, so the number is TAKEN from it rather
   than chosen here. */
.nrb-nowbar[data-pose="story"] { filter: none; margin: 0 0 var(--sp-card); }
/* ⚑ WHAT IS LEFT HERE IS ONLY WHAT THE COLUMN DOES NOT WANT. The grid, the
   padding, the type and the fence moved into the BASE head on 2026-09-11, when
   the owner asked for one layout; what stays is the clip the progress line
   needs and the glow this pose draws with a box-shadow instead of a filter. */
.nrb-nowbar[data-pose="story"] .nrb-nowbar-head {
  /* the progress line rides the bottom edge, absolutely — see below */
  position: relative; overflow: hidden;
  border-radius: var(--r-lg);
  box-shadow: 0 0 24px -8px color-mix(in oklab, var(--nb-ink) 55%, transparent);
}
/* ⚖️ THE WHOLE BAR IS THE BACK-TO-TOP LINK, so it must not look like one until
   it is worth pressing — the arrow appears only once the rail has stuck it.

   ⚠️ AND THE RULE BELONGS TO THE BAR, NOT TO ONE OF ITS POSES (owner
   2026-09-13: "hovering the nowbar is underline everything, should not"). It
   was written the day the story pose was built and qualified with that pose,
   so the COLUMN's bar — which carries no data-pose at all — kept inheriting
   the base sheet's a:hover underline. One anchor wraps the whole widget, so
   that underline ran through the weekday, the date, the clock, the week label
   and its remainder at once: five separate facts scored as if they were a
   phrase. The law was never about the story page; a widget of readings is not
   prose whichever column it stands in.

   ⚑ The focus ring generalises with it. The column's bar had none either, and
   a keyboard reader tabbing onto a control the size of a card deserves to see
   where they are. */
.nrb-nowbar,
.nrb-nowbar:hover,
.nrb-nowbar:focus,
.nrb-nowbar:focus-visible,
.nrb-nowbar:active { color: var(--text); text-decoration: none; }
.nrb-nowbar:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* ⚖️ THE ARROW IS ALSO THE LIVE MARKER (owner 2026-08-22). The widget once
   carried two — a pulsing dot beside the clock and this circle — and the dot
   was the one costing layout: it sat in the tightest cell on the page, so at
   the narrowest docked width it was what pushed the clock into being clipped.
   One marker, on the control that is already a circle, and the ring it throws
   is drawn OUTSIDE its own box (box-shadow, not size) so the pulse cannot push
   anything either. Ported whole from the chronicle's own head. */
.nrb-nowbar-jump {
  /* ⚑ HIDDEN, NOT ABSENT. It holds its 30px track from the first paint, so the
     moment it becomes visible nothing around it has to move. */
  visibility: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  justify-self: center;
  width: 30px; height: 30px; border-radius: 50%;
  color: var(--nb-ink);
  border: 1px solid color-mix(in oklab, var(--nb-ink) 45%, transparent);
  background: color-mix(in oklab, var(--nb-ink) 14%, transparent);
}
.nrb-nowbar.is-stuck { cursor: pointer; }
.nrb-nowbar.is-stuck .nrb-nowbar-jump { visibility: visible; }
.nrb-nowbar.is-stuck:hover .nrb-nowbar-jump { color: var(--text); border-color: var(--nb-ink); }
/* ⚑ ITS OWN KEYFRAME, not the chronicle's. nrb-tl-pulse is defined in
   topicTimelineCss, which is the sheet this component exists to stop depending
   on — borrowing it would leave the arrow silently un-animated the day that
   block is retired. One component, one sheet. */
@keyframes nrb-nowbar-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--nb-ink) 50%, transparent); }
  70% { box-shadow: 0 0 0 9px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: no-preference) {
  .nrb-nowbar.is-stuck .nrb-nowbar-jump { animation: nrb-nowbar-pulse 2.4s infinite; }
  .nrb-nowbar-jump { transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
}

/* ██ THE READING PROGRESS, ALONG THE HEAD'S BOTTOM EDGE ██
   ⚖️ ABSOLUTE, SO IT COSTS THE HEAD NO LAYOUT — and that is the pose's whole
   difference from the column's attached tab. The chronicle head is sticky in a
   rail, so a tab would cost it permanent height; worse, a flow child that
   arrives with the script would GROW the head at hydration, which 20.1170
   forbids outright. Blue-to-gold is not decoration: the sheet spends blue on
   live/now and gold on the reading position, so the fill travels from the
   story's colour to the reader's. */
/* ⚠️ IT IS A CHILD OF THE HEAD, and that is load-bearing: the head owns the
   radius and the overflow clip, so only a DESCENDANT of it gets trimmed to
   the curve. As a sibling it resolved against the root — which has neither —
   and drew a flat bar across the rounded corners (fixed 2026-09-10).
   ⚑ NO INSET NEEDED: an abspos child of a GRID container resolves against the
   container's padding box, which is the border box less its borders; the 18px
   padding lies inside it. So zero on three sides is the inner edge exactly. */
.nrb-nowbar-line {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: color-mix(in oklab, var(--nb-ink) 20%, transparent);
}
.nrb-nowbar-line-fill {
  display: block; height: 100%; width: 0;
  border-radius: 0 2px 2px 0;
  background: linear-gradient(90deg, var(--nb-ink), var(--accent));
  /* Short: the bar answers a scroll, and a bar that lags its own scroll reads
     as a broken bar rather than a smooth one. */
  transition: width .18s var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .nrb-nowbar-line-fill { transition: none; }
}

/* ⚑ THE MORPH ITSELF. Soft navigation (20.2110) runs a View Transition on every
   internal turn; naming both poses the same makes the browser treat the column
   bar and the chronicle head as ONE element across the navigation, so opening a
   story literally transforms one into the other instead of cross-fading two
   strangers. Costs nothing where the API is absent, and nothing with the soft
   nav flag off — an unmatched name is simply never paired. */
@media (prefers-reduced-motion: no-preference) {
  .nrb-nowbar { view-transition-name: nrb-nowbar; }
}

/* ── the phone ───────────────────────────────────────────────────────────── */
/* ⚠️ THE RAIL BECOMES A FULL-WIDTH BLOCK UNDER 900px (portalNavCss), so the bar
   would stretch to the whole screen and its 3px day segments would be finger-
   wide bars of nothing. Capped to the measure and left-aligned, it stays the
   same instrument it is on a laptop. */
@media (max-width: 900px) {
  .nrb-nowbar { max-width: 420px; }
}



/* PERSONAL SPACE layer — the right-column reader space (02.0610): header + token
   balance, recommendation, weekly constellation, completed-reading rail, continue.
   design-side, pre-port. Class prefix .nrb-ps*. Tokens only; no .a-* atoms. */
.nrb-ps,.nrb-ps-token,.nrb-ps-bal,.nrb-ps-cat,.nrb-ps-eyebrow{--ps-cat-tech:#3b82f6;--ps-cat-sport:#ef4444;--ps-cat-cult:#a855f7;--ps-cat-world:#10b981;--ps-cat-money:#f59e0b;--ps-cat-sci:#22d3ee;--ps-tok-amber:#f59e0b;--ps-tok-amber-hi:#fcd34d;--ps-tok-blue:#38bdf8;--ps-tok-blue-hi:#bae6fd}
.nrb-ps{--ps-w:100%;width:var(--ps-w);display:flex;flex-direction:column;gap:20px;color:var(--text-primary);font-family:var(--font-body);font-size:var(--fs-body)}
.nrb-ps *{box-sizing:border-box}
.nrb-ps a{color:inherit;text-decoration:none}
.nrb-ps button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
.nrb-ps :focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}
.nrb-ps-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* ---- header ---- */
.nrb-ps-head{display:flex;align-items:center;gap:12px;min-height:56px}
.nrb-ps-avatar{width:48px;height:48px;border-radius:50%;flex:none;background:var(--bg-elevated);border:1px solid var(--border-default);display:grid;place-items:center;color:var(--text-tertiary);overflow:hidden;background-size:cover;background-position:center}
.nrb-ps-avatar svg{width:22px;height:22px}
.nrb-ps-id{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.nrb-ps-name{font-family:var(--font-display);font-weight:600;font-size:21px;letter-spacing:-.02em;line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nrb-ps-rank{font-size:var(--fs-meta);color:var(--text-secondary);line-height:1.3}
.nrb-ps-bal{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:0 6px;margin:0 -6px;border-radius:10px;color:var(--text-primary)}
.nrb-ps-bal:hover{background:var(--border-subtle)}
.nrb-ps-bal-n{font-family:var(--font-display);font-weight:600;font-size:24px;letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1}
.nrb-ps-bal-n[data-state="unknown"]{color:var(--text-secondary)}
.nrb-ps-bal-n[data-state="cached"]{color:var(--text-secondary)}
.nrb-ps-bal-pend{font-family:var(--font-mono);font-size:11px;color:var(--text-tertiary);align-self:flex-end;margin-bottom:3px}
.nrb-ps-bal-load{width:38px;height:22px;border-radius:6px;background:linear-gradient(90deg,var(--bg-elevated),var(--bg-overlay),var(--bg-elevated));background-size:200% 100%;animation:nrb-ps-shimmer 1.4s linear infinite}
@keyframes nrb-ps-shimmer{to{background-position:-200% 0}}
@media (prefers-reduced-motion:reduce){.nrb-ps-bal-load{animation:none;background:var(--bg-elevated)}}
.nrb-ps-menu{width:44px;height:44px;border-radius:10px;display:grid;place-items:center;color:var(--text-secondary);margin-right:-10px}
.nrb-ps-menu:hover{background:var(--border-subtle);color:var(--text-primary)}

/* ---- token ---- v27: blue glass shell, gold inner hex */
.nrb-ps-token{--sz:24px;width:var(--sz);height:var(--sz);display:block;flex:none}
.nrb-ps-token .tk-glass{fill:var(--ps-tok-blue);fill-opacity:.22;stroke:var(--ps-tok-blue);stroke-width:2.2;stroke-linejoin:round}
.nrb-ps-token .tk-core{fill:url(#nrb-tk-g);stroke:var(--ps-tok-amber-hi);stroke-width:.8;stroke-linejoin:round}
.nrb-ps-token[data-art="glow"]{filter:drop-shadow(0 0 3px rgba(56,189,248,.45))}
.nrb-ps-token[data-art="flat"] .tk-glass{fill-opacity:.16}
.nrb-ps-token[data-art="flat"] .tk-core{fill:var(--ps-tok-amber);stroke:none}
.nrb-ps-token[data-art="mono"] .tk-glass{fill:none;stroke:currentColor;stroke-width:2.4}
.nrb-ps-token[data-art="mono"] .tk-core{fill:currentColor;stroke:none}
.nrb-ps-token .tk-rim,.nrb-ps-token .tk-glow,.nrb-ps-token .tk-trace,.nrb-ps-token .tk-dot{display:none}
@media (forced-colors:active){.nrb-ps-token{filter:none}.nrb-ps-token .tk-glass{stroke:CanvasText;fill:none}.nrb-ps-token .tk-core{fill:CanvasText;stroke:none}}

/* ---- recommendation ---- */
.nrb-ps-rec{display:grid;grid-template-columns:48px 1fr 40px;gap:12px;align-items:center;min-height:48px}
.nrb-ps-rec-img{width:48px;height:48px;border-radius:10px;background:var(--bg-elevated) center/cover;overflow:hidden;border:1px solid var(--border-subtle)}
.nrb-ps-rec-body{min-width:0;display:flex;flex-direction:column;gap:4px}
.nrb-ps-eyebrow{font-size:var(--fs-caption);color:var(--text-secondary);display:flex;gap:6px;align-items:center;flex-wrap:wrap}
.nrb-ps-eyebrow b{font-weight:500;color:var(--text-primary)}
.nrb-ps-eyebrow i{font-style:normal;color:var(--text-tertiary)}
.nrb-ps-eyebrow em{font-style:normal;font-weight:500}
.nrb-ps-rec-title{margin:0;font-family:var(--font-display);font-weight:600;font-size:16px;line-height:1.28;letter-spacing:-.015em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-wrap:pretty}
.nrb-ps-rec-why{font-size:var(--fs-caption);color:var(--text-tertiary);line-height:1.4;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
.nrb-ps-go{width:44px;height:44px;margin-right:-4px;border-radius:50%;display:grid;place-items:center;color:var(--text-secondary);border:1px solid var(--border-default)}
.nrb-ps-go:hover{border-color:var(--accent);color:var(--accent)}
.nrb-ps-rec-empty{padding:14px 0;color:var(--text-tertiary);font-size:var(--fs-meta);border-top:1px solid var(--border-subtle);border-bottom:1px solid var(--border-subtle)}

/* ---- section heads ---- */
.nrb-ps-sech{display:flex;align-items:center;gap:10px;min-height:32px}
.nrb-ps-sech h2{margin:0;font-family:var(--font-display);font-size:11px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);line-height:1.2}
.nrb-ps-sech .sp{flex:1}
.nrb-ps-time{font-size:var(--fs-meta);color:var(--text-secondary);display:inline-flex;gap:6px;align-items:center;min-height:44px;margin:-6px 0;padding:0 6px;border-radius:8px}
.nrb-ps-time:hover{background:var(--border-subtle)}

/* ---- constellation ---- */
.nrb-ps-week{display:flex;flex-direction:column;gap:6px}
.nrb-ps-sky{position:relative;width:100%;aspect-ratio:380/250;display:block;overflow:visible}
.nrb-ps-sky .bg{fill:var(--text-secondary);opacity:.32;pointer-events:none}
.nrb-ps-sky .bg.f{opacity:.55}
.nrb-ps-sky .edge{stroke:var(--text-secondary);stroke-opacity:.45;stroke-width:1.1;stroke-linecap:round}
.nrb-ps-sky .edge.dim{stroke-opacity:.16;stroke-dasharray:2 4}


.nrb-ps-sky .edge.tmp.on{stroke-opacity:.34}
.nrb-ps-sky .edge:not(.dim):not(.tmp){stroke-opacity:.55;stroke-width:1.2}
.nrb-ps-sky .node{cursor:pointer}
.nrb-ps-sky .node .hit{fill:transparent}
.nrb-ps-sky .node .halo{fill:var(--c);opacity:.22;transition:opacity var(--dur-med) var(--ease-out)}
.nrb-ps-sky .node .core{fill:var(--c);stroke:var(--c);stroke-width:2}
.nrb-ps-sky .node[data-state="unopened"] .core{fill:var(--bg-canvas);stroke:var(--text-tertiary);stroke-width:1.5;opacity:.7}
.nrb-ps-sky .node[data-state="unopened"] .halo{opacity:0}
.nrb-ps-sky .node[data-state="unopened"] .inner{display:none}
.nrb-ps-sky .node[data-state="opened"] .core{fill:var(--bg-canvas);stroke-width:2.4}
.nrb-ps-sky .node[data-state="opened"] .inner{fill:var(--c)}
.nrb-ps-sky .node[data-state="completed"] .inner{display:none}
.nrb-ps-sky .node:hover .halo,.nrb-ps-sky .node:focus-visible .halo,.nrb-ps-sky .node[aria-pressed="true"] .halo{opacity:.42}
.nrb-ps-sky .node:focus-visible{outline:none}
.nrb-ps-sky .node:focus-visible .ring{stroke:var(--accent);stroke-width:1.5;fill:none}
.nrb-ps-sky .ring{fill:none;stroke:none}
@media (prefers-reduced-motion:reduce){.nrb-ps-sky .node .halo{transition:none}}
.nrb-ps-count{display:flex;align-items:center;gap:12px;margin-top:-4px}






.nrb-ps-today-dots{display:inline-flex;gap:4px}
.nrb-ps-today-dots i{width:6px;height:6px;border-radius:50%;background:var(--border-default)}
.nrb-ps-today-dots i[data-on="true"]{background:var(--accent)}
.nrb-ps-count b{white-space:nowrap;font-family:var(--font-display);font-weight:600;font-size:18px;letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1}
.nrb-ps-count > span{font-size:var(--fs-caption);color:var(--text-tertiary)}.nrb-ps-count b span{font:inherit;color:inherit}





.nrb-ps-week[data-gate="reached"] .nrb-ps-count b{color:var(--accent-strong)}
.nrb-ps-reveal{display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:0 14px;border-radius:10px;border:1px solid var(--accent);color:var(--accent-strong);font-weight:500;font-size:var(--fs-meta);align-self:flex-start}
.nrb-ps-reveal:hover{background:var(--accent-soft)}

/* ---- completed rail ---- */
.nrb-ps-rail{display:flex;flex-direction:column;gap:10px}
.nrb-ps-ib{width:44px;height:44px;margin:-6px -2px;border-radius:50%;display:grid;place-items:center;color:var(--text-secondary);border:1px solid var(--border-default)}
.nrb-ps-ib:hover{border-color:var(--accent);color:var(--accent)}
.nrb-ps-ib:disabled{opacity:.35;cursor:default}
.nrb-ps-ib:disabled:hover{border-color:var(--border-default);color:var(--text-secondary)}
.nrb-ps-track{display:grid;grid-auto-flow:column;grid-auto-columns:calc((100% - 24px)/3);gap:12px;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;padding-bottom:2px}
.nrb-ps-track::-webkit-scrollbar{display:none}
.nrb-ps-track[data-peek="on"]{grid-auto-columns:calc((100% - 40px)/2.6)}
.nrb-ps-card{scroll-snap-align:start;display:flex;flex-direction:column;gap:7px;min-width:0;text-align:left;border-radius:10px}
.nrb-ps-card-img{aspect-ratio:16/9;border-radius:10px;background:var(--bg-elevated) center/cover;border:1px solid var(--border-subtle);display:grid;place-items:center;color:var(--text-tertiary);font-family:var(--font-mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase}
.nrb-ps-card-img[data-unavailable]{background:repeating-linear-gradient(135deg,var(--bg-elevated) 0 6px,var(--bg-surface) 6px 12px)}
.nrb-ps-cat{display:flex;align-items:center;gap:6px;font-size:var(--fs-caption);color:var(--text-secondary)}
.nrb-ps-cat i{width:9px;height:9px;border-radius:50%;background:var(--c);flex:none}
.nrb-ps-card-t{margin:0;font-size:var(--fs-meta);line-height:1.35;color:var(--text-primary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-wrap:pretty}
.nrb-ps-card[data-selected] .nrb-ps-card-img{outline:2px solid var(--accent);outline-offset:2px}



/* deck (stack) treatment — the compact default */
.nrb-ps-deck{display:flex;align-items:center;gap:14px;min-height:44px;text-align:left;width:100%}
.nrb-ps-stack{display:flex;flex:none}
.nrb-ps-stack i{display:block;width:40px;height:40px;border-radius:8px;background:var(--bg-elevated) center/cover;margin-left:-14px;box-shadow:0 0 0 2px var(--bg-canvas);border:1px solid var(--border-subtle)}
.nrb-ps-stack i:first-child{margin-left:0}
.nrb-ps-deck-body{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px}
.nrb-ps-deck-n{font-size:var(--fs-meta);color:var(--text-primary)}
.nrb-ps-deck-last{font-size:var(--fs-caption);color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* ---- continue ---- */
.nrb-ps-cont{display:flex;flex-direction:column;gap:10px}
.nrb-ps-cont-row{display:grid;grid-template-columns:40px 1fr 20px;gap:12px;align-items:center;min-height:44px;text-align:left}
.nrb-ps-cont-img{width:40px;height:40px;border-radius:8px;background:var(--bg-elevated) center/cover;border:1px solid var(--border-subtle)}
.nrb-ps-cont-body{min-width:0;display:flex;flex-direction:column;gap:5px}
.nrb-ps-cont-t{margin:0;font-family:var(--font-display);font-weight:600;font-size:15px;letter-spacing:-.01em;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nrb-ps-prog{display:flex;align-items:center;gap:10px}
.nrb-ps-prog .bar{flex:1;height:4px;border-radius:2px;background:var(--border-default);overflow:hidden}
.nrb-ps-prog .bar i{display:block;height:100%;width:var(--p,0%);background:var(--accent);border-radius:2px}
.nrb-ps-prog span{font-size:var(--fs-meta);font-variant-numeric:tabular-nums;color:var(--text-secondary);min-width:3.2ch;text-align:right}
.nrb-ps-cont-row svg{color:var(--text-tertiary)}

.nrb-ps-cont-row:hover svg{color:var(--text-primary)}

/* ---- ghosted site frame (baseline surroundings) ---- */




/* ---- focus panel (mobile-style drill-in) ---- open section fills the column under the header ---- */
.nrb-ps-focus{display:flex;flex-direction:column;gap:14px;flex:1;min-height:0;animation:nrb-ps-focus-in var(--dur-fast,.18s) var(--ease-out,ease)}
@keyframes nrb-ps-focus-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.nrb-ps-focus{animation:none}}
.nrb-ps-focus-head{display:flex;align-items:center;gap:10px;min-height:44px;padding-bottom:10px;border-bottom:1px solid var(--border-subtle)}
.nrb-ps-focus-head h2{margin:0;font-family:var(--font-display);font-weight:600;font-size:18px;letter-spacing:-.015em}
.nrb-ps-focus-n{margin-left:auto;font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--text-tertiary)}
.nrb-ps-list{display:flex;flex-direction:column;gap:2px;flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-width:none;margin:0 -8px;padding:0 8px}
/* 🚫 NO SCROLLBAR IN „Прочетени" (owner 2026-09-14: "horizontal scroll (not
   acceptable) and when first open also vertical scroll, this should never be
   visible"). Each row bleeds 8px past the list's content edge for its hover
   wash (margin:0 -8px into the list's own 8px padding); Chrome counts the
   END-side bleed of a scroll container as scrollable overflow — measured
   scrollWidth 328 in a 320 box — and drew a horizontal bar under the rows.
   Nothing lives in those 8px but the hover wash, so clipping x loses no pixel.
   The bar itself is hidden in both engines; the wheel still scrolls. */
.nrb-ps-list::-webkit-scrollbar{display:none}
.nrb-ps-lrow{display:grid;grid-template-columns:56px 1fr auto;gap:12px;align-items:center;min-height:64px;padding:8px;margin:0 -8px;border-radius:10px}
.nrb-ps-lrow:hover{background:var(--border-subtle)}
.nrb-ps-lrow+.nrb-ps-lrow{box-shadow:0 -1px 0 var(--border-subtle)}
.nrb-ps-lrow-img{width:56px;height:56px;border-radius:8px;background:var(--bg-elevated) center/cover;border:1px solid var(--border-subtle)}
.nrb-ps-lrow-b{min-width:0;display:flex;flex-direction:column;gap:4px}
.nrb-ps-lrow-t{margin:0;font-family:var(--font-display);font-weight:600;font-size:15px;line-height:1.3;letter-spacing:-.01em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}

.nrb-ps-lrow-tok{display:inline-flex;align-items:center;gap:5px;font-family:var(--font-mono);font-size:12px;font-variant-numeric:tabular-nums;color:var(--text-secondary);white-space:nowrap}

/* ---- generate image ---- paid action: reading context + personality + chosen style ---- */
.nrb-ps-gen{display:flex;flex-direction:column;gap:8px;margin-top:auto}






.nrb-ps-gen-tx{min-width:0;display:flex;flex-direction:column;gap:2px}
.nrb-ps-gen-t{font-family:var(--font-display);font-weight:600;font-size:15px;letter-spacing:-.01em;line-height:1.2}
.nrb-ps-gen-s{font-size:var(--fs-caption);line-height:1.4;color:var(--text-secondary)}

.nrb-ps-gen-note{font-size:var(--fs-caption);line-height:1.45;color:var(--text-tertiary)}
/* progress-to-price card (exploration 14) */
.nrb-ps-gen-card{display:flex;flex-direction:column;gap:9px;padding:12px;border:1px solid var(--border-default);border-radius:12px;background:var(--bg-surface)}
.nrb-ps-gen-top{display:flex;justify-content:space-between;align-items:baseline;gap:10px}
.nrb-ps-gen-cost{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:13px;font-variant-numeric:tabular-nums;color:var(--text-secondary);white-space:nowrap}
.nrb-ps-gen-bar{height:3px;border-radius:2px;background:var(--border-subtle);overflow:hidden}
.nrb-ps-gen-bar i{display:block;height:100%;border-radius:0 2px 2px 0;background:linear-gradient(90deg,var(--info),var(--accent));transition:width var(--dur-slow,.4s) var(--ease-out,ease)}
.nrb-ps-gen-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:36px}
.nrb-ps .nrb-ps-gen-go{display:inline-flex;align-items:center;gap:8px;min-height:36px;padding:0 14px;border-radius:9px;border:1px solid var(--ps-accent-bg);background:var(--ps-accent-bg);color:var(--ps-accent-ink);font-family:var(--font-display);font-weight:600;font-size:13.5px}
.nrb-ps .nrb-ps-gen-go:hover{background:var(--ps-accent-bg-h);border-color:var(--ps-accent-bg-h)}
.nrb-ps .nrb-ps-gen-go[disabled]{background:none;color:var(--text-tertiary);border-color:var(--border-default);cursor:not-allowed}
.nrb-ps .nrb-ps-gen-go[disabled]:hover{background:none;border-color:var(--border-default)}
@media (prefers-reduced-motion:reduce){.nrb-ps-gen-bar i{transition:none}}

/* queued — generation is never immediate */
.nrb-ps-gen-q{display:grid;grid-template-columns:40px 1fr;gap:12px;align-items:center;padding:10px 12px;border:1px solid var(--border-subtle);border-radius:12px;background:var(--bg-surface)}
.nrb-ps-gen-q .sp{width:20px;height:20px;justify-self:center;border-radius:50%;border:2px solid color-mix(in oklab,var(--info) 30%,transparent);border-top-color:var(--info);animation:nrb-ps-spin 1s linear infinite}
@keyframes nrb-ps-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.nrb-ps-gen-q .sp{animation:none}}
/* register gate — guests land here instead of the queue */
.nrb-ps-reg{display:flex;flex-direction:column;gap:12px;flex:1;min-height:0;padding-top:4px}
.nrb-ps-reg h2{margin:0;font-family:var(--font-display);font-weight:600;font-size:20px;letter-spacing:-.02em;line-height:1.2}
.nrb-ps-reg p{margin:0;font-size:13.5px;line-height:1.55;color:var(--text-secondary);text-wrap:pretty}
.nrb-ps-reg-b{display:flex;gap:10px;flex-wrap:wrap;margin-top:2px}
.nrb-ps .nrb-ps-btn{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 18px;border-radius:10px;font-family:var(--font-display);font-weight:600;font-size:14px;border:1px solid var(--ps-accent-bg);color:var(--ps-accent-ink);background:var(--ps-accent-bg)}
.nrb-ps .nrb-ps-btn:hover{background:var(--ps-accent-bg-h);border-color:var(--ps-accent-bg-h)}
.nrb-ps .nrb-ps-btn[data-variant="ghost"]{background:none;color:var(--text-primary);border-color:var(--border-default)}
.nrb-ps .nrb-ps-btn[data-variant="ghost"]:hover{background:var(--border-subtle)}
.nrb-ps-reg-l{display:flex;flex-direction:column;gap:8px;margin:6px 0 0;padding:0;list-style:none;font-size:13px;color:var(--text-secondary)}
.nrb-ps-reg-l li{display:grid;grid-template-columns:16px 1fr;gap:10px;align-items:start;line-height:1.45}
.nrb-ps-reg-l svg{width:16px;height:16px;margin-top:1px;color:var(--success);fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}

/* on-accent pairing — ink and fill flip together per mode so contrast holds */
:root{--ps-accent-bg:var(--accent);--ps-accent-bg-h:var(--accent-strong);--ps-accent-ink:var(--bg-base)}
[data-mode="light"],[data-theme="light"]{--ps-accent-bg:var(--accent-strong);--ps-accent-bg-h:var(--accent);--ps-accent-ink:var(--text-primary)}

/* ---- guest join column (V09 · „Добрите истории остават с теб") ---- S0/S1 only ---- */
.nrb-ps-join{display:flex;flex-direction:column;gap:14px}
.nrb-ps-join h2{margin:0;font-family:var(--font-display);font-weight:700;font-size:24px;line-height:1.14;letter-spacing:-.025em;text-wrap:balance}
.nrb-ps-join p{margin:0;font-size:14px;line-height:1.55;color:var(--text-secondary);text-wrap:pretty}
.nrb-ps-bal-n[data-state="pending"]{color:var(--text-secondary)}
.nrb-ps-bal-n[data-state="pending"]::after{content:attr(data-pending-label);white-space:nowrap;margin-left:7px;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--text-tertiary)}
.nrb-ps-deck[data-locked]{opacity:.88}
.nrb-ps-deck[data-locked] .nrb-ps-stack:empty{width:44px;height:44px;border-radius:8px;border:1px dashed var(--border-default);flex:none}
.nrb-ps-deck[data-locked] .nrb-ps-deck-last{color:var(--text-tertiary)}
.nrb-ps-join-ex{font-family:var(--font-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-tertiary)}
.nrb-ps-join-scene{display:grid;grid-template-columns:84px 1fr;gap:12px;align-items:center}
.nrb-ps-join-scene .th{position:relative;height:56px;border-radius:8px;background:linear-gradient(150deg,#1b3450,#0d1b2f);border:1px solid var(--border-subtle);overflow:hidden}
.nrb-ps-join-scene .th b{position:absolute;left:4px;top:4px;padding:2px 6px;border-radius:999px;background:color-mix(in oklab,var(--bg-base) 74%,transparent);border:1px solid var(--border-subtle);font-family:var(--font-mono);font-size:8.5px;font-weight:400;letter-spacing:.08em;text-transform:uppercase;color:var(--text-secondary)}
.nrb-ps-join-scene p{font-size:12.5px;color:var(--text-tertiary)}
.nrb-ps-join-foot{display:flex;flex-direction:column;gap:10px}
.nrb-ps .nrb-ps-join-cta{display:inline-flex;align-items:center;justify-content:center;gap:9px;width:100%;min-height:46px;padding:0 18px;border:1px solid var(--ps-accent-bg);border-radius:10px;background:var(--ps-accent-bg);color:var(--ps-accent-ink);font-family:var(--font-display);font-weight:600;font-size:15px}
.nrb-ps .nrb-ps-join-cta:hover{background:var(--ps-accent-bg-h);border-color:var(--ps-accent-bg-h);color:var(--ps-accent-ink)}
.nrb-ps-join-login{font-size:12.5px;color:var(--text-tertiary);text-align:center}
.nrb-ps .nrb-ps-join-login a{color:var(--text-secondary);text-decoration:underline;text-underline-offset:3px}
/* Runtime personal controls and the approved horizontal reference rail. */
.nrb-ps-track { scrollbar-width: thin; padding-bottom: 10px; }
.nrb-ps-track::-webkit-scrollbar { display: block; height: 4px; }
.nrb-ps-track::-webkit-scrollbar-thumb { background: var(--border-subtle); border-radius: 4px; }
.nrb-ps-track a, .nrb-ps-rec { color: inherit; text-decoration: none; }
.nrb-ps-menu-item { display: block; width: 100%; padding: 14px 0; color: var(--text-primary); background: none; border: 0; text-align: start; cursor: pointer; }
.nrb-ps-menu-item:focus-visible, .nrb-ps-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.nrb-ps-sync{margin:8px 0;padding:12px;border:1px solid var(--border-subtle);border-radius:12px;background:var(--bg-elevated);color:var(--text-primary);font-size:var(--fs-meta);overflow-wrap:anywhere}
.nrb-ps-sync p{margin:6px 0}.nrb-ps-sync summary{cursor:pointer;min-height:44px;display:flex;align-items:center}
.nrb-ps-sync summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.nrb-ps-sync ul{list-style:none;padding:0;margin:0}.nrb-ps-sync li{padding:12px 0;border-top:1px solid var(--border-subtle)}
.nrb-ps-sync .nrb-ps-menu-item{min-height:44px}
/* 🔒 Поверителност на данни — the ⋯ menu's privacy panel (2026-09-23): a titled section, quiet notes, full-width actions. */
.nrb-ps-privacy h3{margin:0 0 4px;font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--text-tertiary)}
.nrb-ps-privacy p{margin:6px 0;font-size:13px;line-height:1.45;color:var(--text-secondary)}
.nrb-ps-privacy p:empty{display:none}
.nrb-ps-privacy .nrb-ps-menu-item{min-height:44px;border-top:1px solid var(--border-subtle)}
.nrb-ps-privacy [role=group]{margin-top:8px;padding:10px 12px;border:1px solid var(--border-subtle);border-radius:12px;background:var(--bg-elevated)}
#nrb-saved-feedback{position:fixed;z-index:10020;inset-inline:16px;bottom:calc(80px + env(safe-area-inset-bottom));max-width:480px;margin-inline:auto;padding:16px;border:1px solid var(--border-default);border-radius:12px;background:var(--bg-elevated);color:var(--text-primary);font-family:var(--font-body);font-size:var(--fs-body)}
#nrb-saved-feedback button{display:block;min-height:44px;margin-top:8px;color:var(--text-primary);background:none;border:0;font:inherit;cursor:pointer}
#nrb-saved-feedback button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.nrb-ps-mobile-trigger { position: fixed; bottom: max(18px, env(safe-area-inset-bottom)); right: 16px; z-index: 90; color: var(--text-primary); background: var(--bg-surface); border: 0; border-radius: 28px; padding: 12px 18px; box-shadow: 0 3px 24px #0005; backdrop-filter: blur(18px); cursor: pointer; }
.nrb-ps-mobile-trigger[hidden] { display: none; }
.nrb-ps-mobile-balance { display: flex; gap: 10px; align-items: center; font-size: 20px; }
.nrb-ps-mobile-balance small { font-size: 11px; color: var(--text-secondary); }
.nrb-ps-mobile-dialog { width: min(100%, 430px); max-width: 100%; max-height: 92dvh; margin: auto 0 0 auto; padding: 16px; padding-bottom: max(16px, env(safe-area-inset-bottom)); background: var(--bg-base, #061020); color: var(--text-primary); border: 0; border-radius: 24px 24px 0 0; }
.nrb-ps-mobile-dialog::backdrop { background: #0008; backdrop-filter: blur(5px); }
.nrb-ps-mobile-close { display: block; margin-left: auto; width: 40px; height: 40px; font-size: 28px; border: 0; background: none; color: inherit; cursor: pointer; }
.nrb-ps-mobile-dialog .nrb-ps { width: 100%; max-width: 100%; }

/* ── THE MOUNT ── the only part of this sheet the BAKER's output ever meets.
   An empty box with no height, so a rail with the flag on and a reader who has
   not hydrated yet is exactly as tall as a rail with the flag off. */
.nrb-ps-mount { display: block; }
/* Identity stays above the nowbar while the story rail scrolls. Empty SPA
   placeholders reserve no height; private UI is still created only at runtime. */
.pn-rail > .nrb-ps-mount:not(:empty) { position: sticky; top: 0; z-index: 2; padding-bottom: 16px; background: transparent; }
.pn-rail .nrb-ps { width: 100%; }
/* 🏷 THE COLUMN'S HEADS ARE THE HOUSE'S KICKER (owner 2026-09-13; DS card SH-02).
   personalSpaceParts renders every head through SectionHeaderView, so the words
   are dressed by sectionHeaderCss and the ported .nrb-ps-sech h2 recipe below
   no longer has an h2 of its own to reach. Three placements are this sheet's:
   the card's 32px min-height is the ACTION's touch target (section-header TRAP
   2) and none of these carry one; inside the row the component takes the width
   so a trailing control stays at the row's end; and the join statement — a
   sentence, not a name — may wrap where the card's kicker ellipsises. */
.nrb-ps .nrb-sech { min-height: 0; }
.nrb-ps-sech > .nrb-sech { flex: 1 1 auto; min-width: 0; }
.nrb-ps-join > .nrb-ps-join-h .nrb-sech-t { white-space: normal; overflow: visible; text-overflow: clip; }
/* 🔒 THE HEAD AND THE FOOT NEVER SCROLL AWAY (owner, 2026-09-13: "we need never
   to see scroll there for the upper part and the footer ... for the things in
   between we will see"). On a desktop the rail is a sticky sidebar; the column's
   mount takes exactly the rail's visible height (--pn-rail-max, set by
   portalNavCss on .pn-rail and .pn-rail--story) and hides its own overflow, so
   the RAIL never scrolls. Inside, the header and the join foot are fixed and the
   body between them — PersonalSpaceView's .nrb-ps-body — is the one box allowed
   to scroll when the column is taller than the rail. The foot therefore stands at
   the bottom of the column always: it takes the slack when the body is short.
   ⚠️ NOT min-height: 100vh on the mount — that shipped for one bake (30f1acefa),
   was 48px taller than the story rail's max-height and made the rail itself
   scroll, sliding the head off the top (the very thing this rules out). The
   explicit min-height: 0 is the belt to that braces: a min-height from any
   sheet would beat the height and bring the rail's scroll back. Below 1180px the rail flows with the page
   and nothing here applies. */
.nrb-ps-body { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
@media (min-width: 1181px) {
  .pn-rail > .nrb-ps-mount:not(:empty) { display: flex; flex-direction: column; box-sizing: border-box; height: var(--pn-rail-max, 100vh); min-height: 0; overflow: hidden; }
  .pn-rail .nrb-ps { flex: 1 1 auto; min-height: 0; }
  .pn-rail .nrb-ps-head, .pn-rail .nrb-ps-join-foot { flex: none; }
  .pn-rail .nrb-ps-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; scrollbar-width: none; }
  .pn-rail .nrb-ps-body::-webkit-scrollbar { display: none; }
  /* ⚑ ONE SCROLLER, NOT TWO. In the rail the BODY is the box that scrolls, so
     the read list must not be a second scrollport nested in it (that is the
     vertical bar the owner saw on first open): the focus panel and its list
     take their content's height and the body carries the scroll. The list's
     own scroller stays for the mobile dialog, which has no such body. */
  .pn-rail .nrb-ps-focus, .pn-rail .nrb-ps-list { flex: none; min-height: auto; }
  .pn-rail .nrb-ps-list { overflow: visible; }
  .pn-rail .nrb-ps-join-foot { margin-top: auto; }
}
/* ⚖️ ONE IDENTITY CONTROL IN THE RAIL, NEVER TWO. auth/ui.tsx prepends its own
   .nrb-auth-rail card — avatar, name, a sign-in button — and that was the personal
   surface before this column existed. 02.0610 §2 forbids the overlap outright
   ("avoid duplicate focusable balance controls"), and two avatars 40px apart is
   also just wrong. The attribute is set by mountPersonalSpace only after a
   column is really on screen, so a mount that could not find its words leaves
   the auth card exactly where it is. The DIALOG is untouched: it is still the
   only sign-in surface, and the column's CTA opens it. */
[data-nrb-ps="on"] .nrb-auth-rail { display: none; }
/* The rail is narrower than the specimen at the smallest desktop step; below
   340px the design drops the rank and nothing is allowed to clip (handoff
   rules: „Под 340px рангът отпада … нищо не се отрязва"). */
@media (max-width: 340px) {
  .nrb-ps-rank { display: none; }
  .nrb-ps-name { font-size: 19px; }
}

/* The profile and token controls share the category band's label/value rhythm. */
.nrb-ps .nrb-ps-id { display:flex; flex:1; min-width:0; flex-direction:column; align-items:flex-start; gap:5px; text-align:start; }
.nrb-ps .nrb-ps-label { display:block; font-family:var(--font-display); font-size:12.5px; font-weight:400; letter-spacing:-.01em; line-height:1; color:var(--text-secondary); }
.nrb-ps .nrb-ps-value { display:flex; align-items:center; gap:6px; height:18px; min-width:0; max-width:100%; }
.nrb-ps .nrb-ps-avatar { width:18px; height:18px; font-size:14px; line-height:1; }
.nrb-ps .nrb-ps-avatar svg { width:14px; height:14px; }
.nrb-ps .nrb-ps-name { font-size:17px; line-height:1; font-weight:600; letter-spacing:-.01em; }
.nrb-ps .nrb-ps-bal { flex:none; flex-direction:column; align-items:flex-start; justify-content:center; gap:5px; margin:0; padding:0; }
.nrb-ps .nrb-ps-bal-n { font-size:17px; line-height:1; }
.nrb-ps .nrb-ps-bal-n[data-state="pending"]::after { display:block; margin:2px 0 0; font-size:8px; }
.nrb-ps .nrb-ps-bal-pend { font-size:9px; margin:0; }
.nrb-ps .nrb-ps-menu { flex:none; width:28px; margin:0; }
.nrb-ps-id--act:hover .nrb-ps-name { color:var(--accent); }
.pn-rail .nrb-ps-head { min-height: 59px; gap: 10px; align-items:center; }
/* Match homeBandCss: 12.5px label, 5px separation, 18px value, then the
   category tab's 9px bottom padding + 1px border. Keep the whole button tappable. */
.pn-rail .nrb-ps-id, .pn-rail .nrb-ps-bal { min-height:59px; justify-content:flex-end; padding-bottom:10px; }

/* 🗂 THE DECK'S TILES SEPARATE (owner, 2026-09-15: "the read posts are not
   well separated"; the design rings each cover so the stack reads as five
   tiles, not one strip). One fact about this site the specimen could not know:
   THE RING IS CUT FROM THE SURFACE THE DECK STANDS ON. The handoff rings each
   tile in --bg-canvas because its frame was painted --bg-canvas; here the
   column sits on the page's own --bg-base (the rail is transparent, and the
   phone dialog paints --bg-base too), so the specimen's 2px ring read as a
   faint lighter outline instead of a gap. 3px of the true surface reads as the
   stack's own edge — the design's 2px was measured against gradient swatches,
   and a dark photograph needs one more — and --border-default over
   --border-subtle so the rim shows on a dark picture. */
.nrb-ps-stack i { box-shadow: 0 0 0 3px var(--bg-base); border-color: var(--border-default); }
/* 🗂 THE DECKS ARE ROWS (owner 2026-09-21, the ps-decks isolation, pick R1): the pile, the word, the
   chevron — flush on the column's ground, no plate and no inset, and the kicker gone with the block. The
   two decks differ by their piles alone: SAVED carries covers (four and a "+N" chip past five), FINISHED
   carries one tile holding its total. THE WORD IS FIRST and the pile sits beside the chevron (owner, the
   same evening: "move the strings to the most left and the tiles and the number tiles go right next to the
   chevron for more details") — which is also what lines the rows up: every word starts at the column's own
   edge whatever its pile holds, with no reserved stack width to keep in sync. */
.nrb-ps .nrb-ps-deck { display: flex; align-items: center; gap: 12px; min-height: 44px; width: 100%; text-align: left; padding-bottom: 12px; border-bottom: 1px solid var(--border-subtle); }
/* a HAIRLINE UNDER EACH ROW AND EACH SECTION TITLE (owner 2026-09-21: "lets add also line down, nearly
   transparent for both rows and also section title"). --border-subtle is the theme's own faintest edge
   (a light blue at 8 % on the dark skins, an ink at 7 % on the paper ones), so "nearly transparent" stays
   true on all six editions and in both modes rather than being one hard-coded alpha that is right on one.
   ⚠️ SPECIFICITY AGAIN: the snapshot's .nrb-ps button (0,1,1) zeroes border and padding on every button in
   the column, so a bare .nrb-ps-deck (0,1,0) drew no line at all. Two classes beat one class and a type. */
.nrb-ps .nrb-ps-sech { padding-bottom: 8px; border-bottom: 1px solid var(--border-subtle); }
/* the two decks under one head (owner 2026-09-21: "Колекции, instead of keep reading"): a list, so the rows
   sit flush and their own hairlines do the separating - the column's 20px body gap would tear the group apart */
.nrb-ps-collections { display: flex; flex-direction: column; gap: 0; }
.nrb-ps-deck-label { flex: 1 1 auto; min-width: 0; font-size: var(--fs-meta); color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nrb-ps-deck .nrb-ps-stack { flex: none; }
.nrb-ps-deck .nrb-ps-ib { flex: none; width: 28px; height: 28px; margin: 0; border: 0; background: color-mix(in oklab, var(--text-primary) 8%, transparent); color: var(--text-secondary); }
.nrb-ps-stack i { width: 44px; height: 44px; border-radius: 10px; margin-left: -14px; }
.nrb-ps-stack i:first-child { margin-left: 0; }
/* ⚖️ THE NEWEST TILE IS ON TOP (owner 2026-09-21: "make sure that the last is ontop so from top to bottom
   ordering"). The tiles arrive NEWEST FIRST and the pile overlaps LEFTWARD, so plain DOM paint order laid
   the OLDEST cover over the newest — the story just finished was the one the reader could not see. Five
   rules, because the pile is at most five; the tiles already carry position: relative above. */
.nrb-ps-stack i { z-index: 1; }
.nrb-ps-stack i:nth-child(1) { z-index: 5; }
.nrb-ps-stack i:nth-child(2) { z-index: 4; }
.nrb-ps-stack i:nth-child(3) { z-index: 3; }
.nrb-ps-stack i:nth-child(4) { z-index: 2; }
/* the fifth seat ("+N", what the pile does not show) and the finished deck's tally tile (its total).
   ⚠️ SELECTOR SPECIFICITY, NOT !important: the snapshot's own .nrb-ps-stack i (0,1,1) sets display:block and
   outranks a bare .nrb-ps-more (0,1,0) — the number rendered in the tile's top-left corner. i.nrb-ps-more wins. */
.nrb-ps-stack i.nrb-ps-more { display: grid; place-items: center; background: var(--bg-elevated); }
.nrb-ps-stack i.nrb-ps-more[data-total] { background: color-mix(in oklab, var(--text-primary) 7%, transparent); }
.nrb-ps-more-n { font: 600 13px/1 var(--font-display); letter-spacing: -.01em; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
/* a LOCKED deck keeps its hint: the only second line left in the row, and only a guest sees it */
.nrb-ps-deck[data-locked] { flex-wrap: wrap; }
.nrb-ps-deck[data-locked] .nrb-ps-deck-last { flex: 1 0 100%; order: 3; margin-top: 2px; font-size: 12px; color: var(--text-tertiary); }
/* 🔷 A COVERLESS TILE WEARS THE FORGE (owner, 2026-09-15: "use the same approach
   for bulletins" — the hero's green economy cover). The box carries the forge's
   SVG (render/covers/, the card grid's own) as its child: a 640×360 viewBox that
   slices to the square, absolutely filling the box, the box's own radius
   clipping it. Same three boxes as the photograph: the stack tile, the panel
   row's picture, the continue row's picture. */
.nrb-ps-stack i, .nrb-ps-lrow-img, .nrb-ps-cont-img { position: relative; overflow: hidden; }
.nrb-ps-tile-cover { display: block; position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }
/* ⏳ „Продължи да четеш" AS A LIST (owner, 2026-09-15): the design drew one row;
   the section is a flex column, so five rows stack on its own 10px gap. The
   „view all (N)" entry is the rail's own quiet link — meta size, a chevron, the
   accent on hover — and never a pill. In the panel the same rows breathe a
   little and take the read list's hover wash. */
.nrb-ps-cont-all { align-self: flex-start; display: inline-flex; align-items: center; gap: 6px; min-height: 32px; margin: -4px 0 0; padding: 0; font-size: var(--fs-meta); color: var(--text-secondary); }
.nrb-ps-cont-all:hover { color: var(--accent); }
.nrb-ps-cont-all svg { width: 14px; height: 14px; }
.nrb-ps-list--continue { gap: 6px; }
.nrb-ps-list--continue .nrb-ps-cont-row { padding: 6px 8px; margin: 0 -8px; border-radius: 10px; }
.nrb-ps-list--continue .nrb-ps-cont-row:hover { background: var(--border-subtle); }
/* ✂️ ONE ROW, THEN AN ELLIPSIS (owner, 2026-09-15: "a story only have 1 row and
   then '...' we may go to two rows in the future but not today"). The design
   already says nowrap on .nrb-ps-cont-t, but the guest column wraps every row in
   .nrb-ps-join, whose paragraph rule carries text-wrap: pretty — and text-wrap is a
   shorthand that resets text-wrap-mode, so it silently re-enabled wrapping and
   also shrank the title to 14px secondary ink. Two classes outrank one class
   and a tag; both spellings of nowrap so either engine agrees. */
.nrb-ps .nrb-ps-cont-t { white-space: nowrap; text-wrap: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 15px; line-height: 1.25; color: var(--text-primary); }
/* 📍 THE STORY OPEN IN THE MAIN COLUMN, LIT THE SAME WAY ON EVERY SURFACE (owner,
   2026-09-15: "a nice highlight so that i can easily identify them all as the
   same"): the accent's wash and a 3px accent rule on the row, the title in the
   strong accent; on the deck's tile the same accent as a ring outside the
   surface cut. aria-current="page" is the mark — a fact for a screen reader
   first, a style second. */
.nrb-ps .nrb-ps-cont-row[aria-current="page"], .nrb-ps .nrb-ps-lrow[aria-current="page"] { background: color-mix(in oklab, var(--accent) 12%, transparent); box-shadow: inset 3px 0 0 var(--accent); border-radius: 10px; }
.nrb-ps-cont .nrb-ps-cont-row[aria-current="page"] { padding: 6px 8px; margin: 0 -8px; }
.nrb-ps .nrb-ps-cont-row[aria-current="page"] .nrb-ps-cont-t, .nrb-ps .nrb-ps-lrow[aria-current="page"] .nrb-ps-lrow-t { color: var(--accent-strong); }
.nrb-ps-stack i[data-current] { box-shadow: 0 0 0 3px var(--bg-base), 0 0 0 5px var(--accent); }


/* ✋ the open story's action row in the column (2026-09-15): like · views · share · the read/unread toggle — the colophon's look, the column's width */
.nrb-ps .nrb-ps-acts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 0 0 18px; }
.nrb-ps .nrb-ps-act { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--border-subtle); background: color-mix(in oklab, var(--bg-base) 40%, transparent); color: var(--text-secondary); font: inherit; font-size: 13px; line-height: 1; cursor: default; }
.nrb-ps .nrb-ps-act svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.nrb-ps .nrb-ps-act-n { min-width: 1ch; font-variant-numeric: tabular-nums; }
.nrb-ps .nrb-ps-act-n:empty { display: none; }
.nrb-ps button.nrb-ps-act { cursor: pointer; }
.nrb-ps button.nrb-ps-act:hover, .nrb-ps button.nrb-ps-act:focus-visible { color: var(--text-primary); border-color: var(--text-tertiary); }
.nrb-ps .nrb-ps-act--like[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
.nrb-ps .nrb-ps-act--like[aria-pressed="true"] svg { fill: currentColor; }
.nrb-ps .nrb-ps-act--mark { width: 32px; padding: 0; justify-content: center; border-radius: 50%; }
.nrb-ps .nrb-ps-act--mark svg { width: 18px; height: 18px; }
.nrb-ps .nrb-ps-act--mark[aria-pressed="true"] { color: var(--bg-base); background: var(--success); border-color: var(--success); }
.nrb-ps .nrb-ps-act--mark[aria-pressed="true"] svg circle { fill: var(--success); }
.nrb-ps .nrb-ps-act--mark[aria-pressed="true"]:hover, .nrb-ps .nrb-ps-act--mark[aria-pressed="true"]:focus-visible { color: var(--bg-base); border-color: var(--success); }
/* the views are a figure, not a control: right end, no pill */
.nrb-ps .nrb-ps-act--views { margin-left: auto; padding: 0; border: 0; background: transparent; color: var(--text-tertiary); }
/* the colophon's own row steps aside while the column's stands (static-first: without the attribute, i.e. without script, it stays) */
html[data-nrb-ps-acts="column"] .article-colophon .colo-actwrap { display: none; }

/* ---- 📬 the reader's notices (PRD 20.2780 P4, 2026-10-01) — how a story fund they
   gave to ended. First cut on the column's own values (the rec/continue rows);
   no tuned recipe yet. The dot carries the tone: gold done · green a new story ·
   muted returned — and the words always say it too, never the colour alone. */
.nrb-ps-notices { display: flex; flex-direction: column; gap: 8px; }
.nrb-ps-notice-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.nrb-ps-notice { display: grid; grid-template-columns: 10px 1fr 28px; gap: 10px; align-items: start; padding: 10px 8px 10px 12px;
  border-radius: 10px; background: var(--bg-elevated); border: 1px solid var(--border-subtle); }
.nrb-ps-notice-dot { width: 8px; height: 8px; margin-top: 6px; border-radius: 50%; background: var(--accent); }
.nrb-ps-notice[data-tone="new"] .nrb-ps-notice-dot { background: var(--ok, #34d399); }
.nrb-ps-notice[data-tone="returned"] .nrb-ps-notice-dot { background: var(--text-tertiary); }
.nrb-ps-notice-body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.nrb-ps-notice-t { margin: 0; font-family: var(--font-display); font-weight: 600; font-size: 15px; line-height: 1.28; letter-spacing: -.01em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.nrb-ps a.nrb-ps-notice-t:hover { color: var(--accent); }
.nrb-ps-notice-x { margin: 0; font-size: var(--fs-meta); line-height: 1.4; color: var(--text-secondary); }
.nrb-ps .nrb-ps-notice-x a { color: var(--accent); font-weight: 600; }
.nrb-ps-notice-e { margin: 0; font-family: var(--font-mono); font-size: 11px; line-height: 1.4; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
.nrb-ps .nrb-ps-notice-x-btn { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; color: var(--text-tertiary); font-size: 18px; line-height: 1; }
.nrb-ps .nrb-ps-notice-x-btn:hover { background: var(--border-subtle); color: var(--text-primary); }



/* Paid reader preference; applies to feed cards only, never shared HTML. */
[data-nrb-card][data-nrb-hidden-read][hidden] { display: none !important; }
/* ✅ READ STATE — „Прочетено" and „наполовина" over a feed card
   (design_handoff_read_state, ported 2026-09-15 to attributes + pseudo-elements;
   readStateCss.ts explains why and what the owner changed).
   The card carries data-nrb-read="read|partial", data-nrb-read-art="media|none",
   data-nrb-read-size="sm|xs" and, when partial, data-nrb-read-fill="10…90";
   data-nrb-read-tokens="N" (read, only when N > 0) and data-nrb-read-progress="P"
   (partial) sit on the picture box, or on the card when there is none. Nothing
   here changes a card's box: the layer is absolute inside it, so 20.1170's
   second test (scripting on ⇒ nothing moves) holds by construction. */
[data-nrb-read="read"], [data-nrb-read="partial"] { position: relative; }

/* read — THE ENTIRE CONTAINER RECEDES, THE BADGE AT FULL (owner, by the
   tuner 2026-09-15: "veil 88% of page ground · words 60% · picture saturate
   100% · hover veil 0% · badge 100%"): one flat wash over the whole card at
   88 % of the page's ground, the text block at 60 % of its own beneath it,
   both lifted on hover or keyboard focus; the badge sits above everything. */
[data-nrb-read="read"]::before { content: ""; position: absolute; inset: 0; z-index: 3; border-radius: inherit; pointer-events: none; background: color-mix(in oklab, var(--bg-base) 88%, transparent); transition: background 180ms ease-in-out; }
[data-nrb-read="read"]:hover::before, [data-nrb-read="read"]:focus-within::before { background: transparent; }
/* the words sit at 60 % of their own under the veil (the tuner's recipe); the picture keeps its colour */
[data-nrb-read="read"] .p1a-body, [data-nrb-read="read"] .p1b-body { opacity: .6; transition: opacity 180ms ease-in-out; }
[data-nrb-read="read"]:hover .p1a-body, [data-nrb-read="read"]:hover .p1b-body, [data-nrb-read="read"]:focus-within .p1a-body, [data-nrb-read="read"]:focus-within .p1b-body { opacity: 1; }

/* the read badge — check + token + count, bottom-left of the picture. The two
   icons are background layers (tick at 15px, token at 53px); the count is the
   pseudo-element's text and the left padding clears both icons plus the
   design's 13px gaps. */
[data-nrb-read="read"] .c-shot::after { content: attr(data-nrb-read-tokens); left: 16px; bottom: 16px; position: absolute; z-index: 4; pointer-events: none; display: inline-flex; align-items: center; box-sizing: border-box; min-height: 45px; padding: 9px 19px 9px 95px; border-radius: 999px; border: 1px solid var(--border-subtle); background-color: color-mix(in oklab, var(--bg-base) 78%, transparent); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8.5 3.2 3.2L13 5'/%3E%3C/svg%3E"), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M10 3H22Q24 3 25 5L30 14Q31 16 30 18L25 27Q24 29 22 29H10Q8 29 7 27L2 18Q1 16 2 14L7 5Q8 3 10 3Z' fill='%2378b9ef38' stroke='%23c9e6ff' stroke-width='0.8'/%3E%3Cpath d='M11 6H21Q22.5 6 23.3 7.5L27.5 14.5Q28.3 16 27.5 17.5L23.3 24.5Q22.5 26 21 26H11Q9.5 26 8.7 24.5L4.5 17.5Q3.7 16 4.5 14.5L8.7 7.5Q9.5 6 11 6Z' fill='%23071429' stroke='%23ffc447' stroke-width='2.4' stroke-linejoin='round'/%3E%3Cpath d='M17 8V13L12 18V23M24 15H20L17 18M8 13H11L14 16' fill='none' stroke='%2340cfff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='17' cy='18' r='1.2' fill='%23071429' stroke='%2383eaff' stroke-width='1'/%3E%3Cpath d='M10 4H22M26 7L29 13' stroke='%23effaff' stroke-width='0.8' stroke-linecap='round' opacity='0.75'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-size: 25px 25px, 29px 29px; background-position: 15px center, 53px center; font-family: var(--font-mono); font-size: 23px; line-height: 1; font-variant-numeric: tabular-nums; color: var(--text-secondary); backdrop-filter: blur(3px); }
[data-nrb-read="read"] .c-shot:not([data-nrb-read-tokens])::after { content: ""; width: 45px; height: 45px; padding: 0; border-radius: 50%; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8.5 3.2 3.2L13 5'/%3E%3C/svg%3E"); background-size: 25px 25px; background-position: center; }

/* a short story has no picture: the badge sits on the card's own bottom-right
   (its actions live bottom-left). */
[data-nrb-read="read"][data-nrb-read-art="none"]::after { content: attr(data-nrb-read-tokens); right: 14px; bottom: 14px; position: absolute; z-index: 4; pointer-events: none; display: inline-flex; align-items: center; box-sizing: border-box; min-height: 45px; padding: 9px 19px 9px 95px; border-radius: 999px; border: 1px solid var(--border-subtle); background-color: color-mix(in oklab, var(--bg-base) 78%, transparent); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8.5 3.2 3.2L13 5'/%3E%3C/svg%3E"), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M10 3H22Q24 3 25 5L30 14Q31 16 30 18L25 27Q24 29 22 29H10Q8 29 7 27L2 18Q1 16 2 14L7 5Q8 3 10 3Z' fill='%2378b9ef38' stroke='%23c9e6ff' stroke-width='0.8'/%3E%3Cpath d='M11 6H21Q22.5 6 23.3 7.5L27.5 14.5Q28.3 16 27.5 17.5L23.3 24.5Q22.5 26 21 26H11Q9.5 26 8.7 24.5L4.5 17.5Q3.7 16 4.5 14.5L8.7 7.5Q9.5 6 11 6Z' fill='%23071429' stroke='%23ffc447' stroke-width='2.4' stroke-linejoin='round'/%3E%3Cpath d='M17 8V13L12 18V23M24 15H20L17 18M8 13H11L14 16' fill='none' stroke='%2340cfff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='17' cy='18' r='1.2' fill='%23071429' stroke='%2383eaff' stroke-width='1'/%3E%3Cpath d='M10 4H22M26 7L29 13' stroke='%23effaff' stroke-width='0.8' stroke-linecap='round' opacity='0.75'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-size: 25px 25px, 29px 29px; background-position: 15px center, 53px center; font-family: var(--font-mono); font-size: 23px; line-height: 1; font-variant-numeric: tabular-nums; color: var(--text-secondary); backdrop-filter: blur(3px); }
[data-nrb-read="read"][data-nrb-read-art="none"]:not([data-nrb-read-tokens])::after { content: ""; width: 45px; height: 45px; padding: 0; border-radius: 50%; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8.5 3.2 3.2L13 5'/%3E%3C/svg%3E"); background-size: 25px 25px; background-position: center; }

/* partial — THE OVERLAY OF PROGRESS (owner): a 4px bar in --info along the
   card's top edge, as wide as the read share (the fill steps below), on every
   begun card; on a picture card also a wash over the picture from the left,
   the same width, 30 % of the page's ground and 15 % under the pointer or the
   keyboard, with a hairline in --info at its edge — the reading position.
   The wash sits above the picture, below the section chip and the clock. The
   card itself is not veiled — it is not done. */
[data-nrb-read="partial"]::before { content: ""; position: absolute; top: 0; left: 0; height: 4px; width: 0; z-index: 3; pointer-events: none; background: var(--info); border-radius: 0 2px 2px 0; }
[data-nrb-read="partial"] .c-shot::before { content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 0; z-index: 1; pointer-events: none; background: color-mix(in oklab, var(--bg-base) 30%, transparent); border-right: 2px solid color-mix(in oklab, var(--info) 70%, transparent); transition: background var(--dur-fast, .18s) var(--ease-out, ease); }
[data-nrb-read="partial"]:hover .c-shot::before, [data-nrb-read="partial"]:focus-within .c-shot::before { background: color-mix(in oklab, var(--bg-base) 15%, transparent); }
/* the partial badge — the ring and the percent; no check (nothing is finished),
   no token (the column's continue row shows none either) */
[data-nrb-read="partial"] .c-shot::after { left: 16px; bottom: 16px; position: absolute; z-index: 4; pointer-events: none; display: inline-flex; align-items: center; box-sizing: border-box; min-height: 45px; padding: 9px 19px 9px 95px; border-radius: 999px; border: 1px solid var(--border-subtle); background-color: color-mix(in oklab, var(--bg-base) 78%, transparent); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8.5 3.2 3.2L13 5'/%3E%3C/svg%3E"), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M10 3H22Q24 3 25 5L30 14Q31 16 30 18L25 27Q24 29 22 29H10Q8 29 7 27L2 18Q1 16 2 14L7 5Q8 3 10 3Z' fill='%2378b9ef38' stroke='%23c9e6ff' stroke-width='0.8'/%3E%3Cpath d='M11 6H21Q22.5 6 23.3 7.5L27.5 14.5Q28.3 16 27.5 17.5L23.3 24.5Q22.5 26 21 26H11Q9.5 26 8.7 24.5L4.5 17.5Q3.7 16 4.5 14.5L8.7 7.5Q9.5 6 11 6Z' fill='%23071429' stroke='%23ffc447' stroke-width='2.4' stroke-linejoin='round'/%3E%3Cpath d='M17 8V13L12 18V23M24 15H20L17 18M8 13H11L14 16' fill='none' stroke='%2340cfff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='17' cy='18' r='1.2' fill='%23071429' stroke='%2383eaff' stroke-width='1'/%3E%3Cpath d='M10 4H22M26 7L29 13' stroke='%23effaff' stroke-width='0.8' stroke-linecap='round' opacity='0.75'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-size: 25px 25px, 29px 29px; background-position: 15px center, 53px center; font-family: var(--font-mono); font-size: 23px; line-height: 1; font-variant-numeric: tabular-nums; color: var(--text-secondary); backdrop-filter: blur(3px); content: attr(data-nrb-read-progress) "%"; padding-left: 53px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2338bdf8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6' opacity='.35'/%3E%3Cpath d='M8 2a6 6 0 0 1 6 6'/%3E%3C/svg%3E"); background-size: 25px 25px; background-position: 15px center; }
[data-nrb-read="partial"][data-nrb-read-art="none"]::after { right: 14px; bottom: 14px; position: absolute; z-index: 4; pointer-events: none; display: inline-flex; align-items: center; box-sizing: border-box; min-height: 45px; padding: 9px 19px 9px 95px; border-radius: 999px; border: 1px solid var(--border-subtle); background-color: color-mix(in oklab, var(--bg-base) 78%, transparent); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8.5 3.2 3.2L13 5'/%3E%3C/svg%3E"), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M10 3H22Q24 3 25 5L30 14Q31 16 30 18L25 27Q24 29 22 29H10Q8 29 7 27L2 18Q1 16 2 14L7 5Q8 3 10 3Z' fill='%2378b9ef38' stroke='%23c9e6ff' stroke-width='0.8'/%3E%3Cpath d='M11 6H21Q22.5 6 23.3 7.5L27.5 14.5Q28.3 16 27.5 17.5L23.3 24.5Q22.5 26 21 26H11Q9.5 26 8.7 24.5L4.5 17.5Q3.7 16 4.5 14.5L8.7 7.5Q9.5 6 11 6Z' fill='%23071429' stroke='%23ffc447' stroke-width='2.4' stroke-linejoin='round'/%3E%3Cpath d='M17 8V13L12 18V23M24 15H20L17 18M8 13H11L14 16' fill='none' stroke='%2340cfff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='17' cy='18' r='1.2' fill='%23071429' stroke='%2383eaff' stroke-width='1'/%3E%3Cpath d='M10 4H22M26 7L29 13' stroke='%23effaff' stroke-width='0.8' stroke-linecap='round' opacity='0.75'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-size: 25px 25px, 29px 29px; background-position: 15px center, 53px center; font-family: var(--font-mono); font-size: 23px; line-height: 1; font-variant-numeric: tabular-nums; color: var(--text-secondary); backdrop-filter: blur(3px); content: attr(data-nrb-read-progress) "%"; padding-left: 53px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2338bdf8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6' opacity='.35'/%3E%3Cpath d='M8 2a6 6 0 0 1 6 6'/%3E%3C/svg%3E"); background-size: 25px 25px; background-position: 15px center; }
/* the fill steps — the bar's and the wash's width from the card's
   data-nrb-read-fill, one rule per step */
[data-nrb-read-fill="10"]::before, [data-nrb-read-fill="10"] .c-shot::before { width: 10%; }
[data-nrb-read-fill="20"]::before, [data-nrb-read-fill="20"] .c-shot::before { width: 20%; }
[data-nrb-read-fill="30"]::before, [data-nrb-read-fill="30"] .c-shot::before { width: 30%; }
[data-nrb-read-fill="40"]::before, [data-nrb-read-fill="40"] .c-shot::before { width: 40%; }
[data-nrb-read-fill="50"]::before, [data-nrb-read-fill="50"] .c-shot::before { width: 50%; }
[data-nrb-read-fill="60"]::before, [data-nrb-read-fill="60"] .c-shot::before { width: 60%; }
[data-nrb-read-fill="70"]::before, [data-nrb-read-fill="70"] .c-shot::before { width: 70%; }
[data-nrb-read-fill="80"]::before, [data-nrb-read-fill="80"] .c-shot::before { width: 80%; }
[data-nrb-read-fill="90"]::before, [data-nrb-read-fill="90"] .c-shot::before { width: 90%; }

/* the read badge IN THE MIDDLE, WITH A GREEN HALO (owner, by the tuner: "glow
   reach 480px · glow intensity 70% · glow centre 30% · halo spread 210px ·
   badge centred · animate veil + words + halo · 180ms · ease-in-out") — the
   success colour reaches the card that is done. The halo is the badge's own
   shadow, so it is centred on the badge wherever the picture's height puts
   it, sits above the veil with it, and reaches the whole card. Last in
   the sheet so it outranks the size rules' left / right. The picture box
   stops clipping on a read card (owner: "expand the green to the entire
   container"): the halo then reaches the words and the actions, as it does
   on a short story; the card's own overflow: hidden still rounds the corners. */
[data-nrb-read="read"] .c-shot { overflow: visible; }
[data-nrb-read="read"] .c-shot::after, [data-nrb-read="read"][data-nrb-read-art="none"]::after { left: 50%; right: auto; transform: translateX(-50%); box-shadow: 0 0 480px 210px color-mix(in oklab, color-mix(in oklab, var(--success) 70%, var(--bg-base)) 30%, transparent); transition: box-shadow 180ms ease-in-out; }
[data-nrb-read="read"]:hover .c-shot::after, [data-nrb-read="read"]:focus-within .c-shot::after, [data-nrb-read="read"][data-nrb-read-art="none"]:hover::after, [data-nrb-read="read"][data-nrb-read-art="none"]:focus-within::after { box-shadow: 0 0 0 0 transparent; }

/* sm — narrow cards: the design's 17 / 19 / 15 numbers, 9px gaps */
[data-nrb-read-size="sm"] .c-shot::after, [data-nrb-read-size="sm"][data-nrb-read-art="none"]::after { min-height: 31px; padding: 6px 13px 6px 64px; background-size: 17px 17px, 19px 19px; background-position: 10px center, 36px center; font-size: 15px; }
[data-nrb-read-size="sm"] .c-shot::after { left: 10px; bottom: 10px; }
[data-nrb-read-size="sm"][data-nrb-read-art="none"]::after { right: 10px; bottom: 10px; }
[data-nrb-read="read"][data-nrb-read-size="sm"] .c-shot:not([data-nrb-read-tokens])::after, [data-nrb-read="read"][data-nrb-read-size="sm"][data-nrb-read-art="none"]:not([data-nrb-read-tokens])::after { width: 31px; height: 31px; padding: 0; background-size: 17px 17px; background-position: center; }
[data-nrb-read="partial"][data-nrb-read-size="sm"] .c-shot::after, [data-nrb-read="partial"][data-nrb-read-size="sm"][data-nrb-read-art="none"]::after { padding-left: 36px; background-size: 17px 17px; background-position: 10px center; }
/* xs — thumbnail rows, 56–72px art: the check alone, whatever was earned; the ring alone when begun */
[data-nrb-read-size="xs"] .c-shot::after, [data-nrb-read-size="xs"][data-nrb-read-art="none"]::after { content: ""; left: 6px; bottom: 6px; right: auto; width: 22px; height: 22px; min-height: 0; padding: 0; border-radius: 50%; border-color: transparent; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 8.5 3.2 3.2L13 5'/%3E%3C/svg%3E"); background-size: 14px 14px; background-position: center; }
[data-nrb-read="partial"][data-nrb-read-size="xs"] .c-shot::after, [data-nrb-read="partial"][data-nrb-read-size="xs"][data-nrb-read-art="none"]::after { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2338bdf8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6' opacity='.35'/%3E%3Cpath d='M8 2a6 6 0 0 1 6 6'/%3E%3C/svg%3E"); }

@media (prefers-reduced-motion: reduce) { [data-nrb-read="read"]::before, [data-nrb-read="read"] .p1a-body, [data-nrb-read="read"] .p1b-body, [data-nrb-read="read"] .c-shot::after, [data-nrb-read="read"][data-nrb-read-art="none"]::after, [data-nrb-read="partial"] .c-shot::before { transition: none; } }
@media (forced-colors: active) {
  [data-nrb-read="read"] .c-shot::after, [data-nrb-read="read"][data-nrb-read-art="none"]::after { box-shadow: none; }
  [data-nrb-read="read"]::before { background: none; outline: 2px dashed CanvasText; outline-offset: -2px; }
  [data-nrb-read="partial"]::before { background: CanvasText; }
  [data-nrb-read="partial"] .c-shot::before { background: none; border-right-color: CanvasText; }
  [data-nrb-read="read"] .c-shot::after, [data-nrb-read="read"][data-nrb-read-art="none"]::after, [data-nrb-read="partial"] .c-shot::after, [data-nrb-read="partial"][data-nrb-read-art="none"]::after { background-color: Canvas; border-color: CanvasText; }
}



/* ── the dock: hidden until a media block places it (the phone's at the edges, the web's centred) ── */
.nrb-dock { display: none; }
/* owner 2026-09-17 (latest): beside a dock the РУБРИКИ launcher is never drawn — the categories seat is the launcher — at EVERY width */
body:has(.nrb-dock) .pn-launch { display: none; }
@media (max-width: 900px) {
  /* ⚖️ FIXED, like the two floaters it replaces: it belongs to the VIEWPORT.
     z-index 41 = the launcher's, so the open panel (41, above it in the
     stacking order by source) and the open rail (42) still draw over it. */
  /* ⚓ THE FLOOR (owner 2026-09-21: "move the dock lower so it almost touches the browser controls"): 4px — the
     browser's own toolbar keeps ~12px above its capsule, so the two bars read as one stack; the safe-area inset
     wins where a viewport extends under the home indicator. The panel and the rail above the dock ride on it. */
  body:has(.nrb-dock) { --dock-floor: max(4px, env(safe-area-inset-bottom, 0px)); }
  .nrb-dock { display: block; position: fixed; left: 14px; right: 14px; bottom: var(--dock-floor, 16px); z-index: 41; }
  /* ⚠️ the display:block above outranks the UA's [hidden] — a dock the bundle put under (storyDock.ts) must still vanish */
  .nrb-dock[hidden], .nrb-dock[data-nrb-dock-under] { display: none; }
  /* the article's chronicle strip owns the very foot (pageMinimapCss) — lift, as the launcher did */
  body.nrb-mm-scope .nrb-dock { bottom: 62px; }

  /* ── 🔵 THE NOW BAR'S PLATE — the dock's colours are the now bar's (nowBarCss.ts): the ink is --info, the
     plate its 9 % mix into --bg-surface, so every theme dresses both the same way; the lit seat's capsule
     has a LIGHT and a DARK face by the theme's own color-scheme (the dark face first, the fallback). */
  .nrb-dock {
    --dock-ink: var(--info);
    --dock-plate: color-mix(in oklab, var(--dock-ink) 9%, var(--bg-surface));
    --dock-ring: color-mix(in oklab, var(--dock-ink) 60%, transparent);
    --dock-glow: color-mix(in oklab, var(--dock-ink) 35%, transparent);
    --dock-lit: rgba(255,255,255,.14); --dock-lit-top: rgba(255,255,255,.22); --dock-lit-ring: rgba(255,255,255,.06); --dock-lit-shadow: rgba(0,0,0,.25);
    --dock-edge: color-mix(in oklab, var(--bg-base, var(--bg)) 85%, transparent);
  }
  @supports (color: light-dark(#000, #fff)) {
    .nrb-dock {
      --dock-lit: light-dark(rgba(5,16,60,.07), rgba(255,255,255,.14)); --dock-lit-top: light-dark(rgba(255,255,255,.80), rgba(255,255,255,.22));
      --dock-lit-ring: light-dark(rgba(5,16,60,.06), rgba(255,255,255,.06)); --dock-lit-shadow: light-dark(rgba(10,20,60,.10), rgba(0,0,0,.25));
    }
  }
  /* the bar IS the plate: opaque, the ring as an inset shadow (the bar keeps its height), a white top light,
     the now bar's glow and a contact shadow. No lens, no rim, no sheen (owner: n1). */
  .nrb-dock-bar {
    display: flex; align-items: stretch; padding: 4px; border-radius: 999px; border: 0;
    background: var(--dock-plate);
    box-shadow: inset 0 0 0 1px var(--dock-ring), inset 0 1px 0 rgba(255,255,255,.07), 0 0 14px var(--dock-glow), 0 1px 2px rgba(0,0,0,.25);
  }
  /* the SCROLL EDGE EFFECT (owner 2026-09-21, late: "you proposed something i really like, can we add it" — the
     ladder's E1, back after an evening without it): the floor's last 110px dissolve — a masked 6px blur under a
     ground fade — so content never meets the bar with a hard line. A child of the dock: with it, gone with it;
     under the bar (z -1 inside the dock's own stacking context), above the page. ⚠️ fixed to the VIEWPORT — the
     web block, whose dock is transformed, re-anchors it as an absolute strip. */
  .nrb-dock::before {
    content: ""; position: fixed; left: 0; right: 0; bottom: 0; height: 110px; pointer-events: none; z-index: -1;
    background: linear-gradient(to top, var(--dock-edge), transparent);
    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
    -webkit-mask: linear-gradient(to top, #000 35%, transparent); mask: linear-gradient(to top, #000 35%, transparent);
  }
  /* ⚑ THREE EQUAL SEATS. flex:1 1 0 so the widest word does not buy its seat
     more room than the others — the bar reads as one object, not three pills.
     CONCENTRIC: a capsule inside the capsule (999 − the 4px pad); NO HAIRLINES — items that share a
     background are one group, the lit seat's capsule is the only separation. */
  .nrb-dock-seat {
    flex: 1 1 0; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 9px;
    padding: 6px 10px; border: 0; border-radius: 995px; background: none; color: var(--text);
    text-decoration: none; cursor: pointer; font-family: var(--font-display);
  }
  .nrb-dock-seat[hidden] { display: none; }
  .nrb-dock-i, .nrb-dock-seat > .nrb-ps-token { flex: none; display: block; width: 20px; height: 20px; }
  .nrb-dock-i { color: var(--accent); }
  /* value over label — the column's own two-row type (.nrb-ps-value / .nrb-ps-label) */
  .nrb-dock-l { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; }
  .nrb-dock-v {
    display: flex; align-items: center; height: 18px; font-weight: 600; font-size: 17px; line-height: 1;
    letter-spacing: -.01em; font-variant-numeric: tabular-nums; white-space: nowrap;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }
  .nrb-dock-k { display: block; font-size: 12.5px; font-weight: 400; letter-spacing: -.01em; line-height: 1; color: var(--text-secondary); white-space: nowrap; }
  /* ⚖️ THE KEY IS LOWERCASE WHERE THE LANGUAGE WRITES COMMON NOUNS LOWERCASE —
     a typographic rule, not a word: the launcher's word arrives capitalised
     (Рубрики) and the column's does not (токени), and a bar with both cases
     reads as two components. German keeps its capitals. */
  .nrb-dock-k:lang(bg), .nrb-dock-k:lang(en), .nrb-dock-k:lang(es), .nrb-dock-k:lang(fr), .nrb-dock-k:lang(pt) { text-transform: lowercase; }
  /* THE LIT SEAT — one language for every seat kind (categories · scope while their layer is up; tokens ·
     compass while their panel is up; save · mark-read while on): a lighter capsule concentric with
     the bar, its own top light and a hairline ring — NEUTRAL (the ladder's T0, kept under n1): the accent
     lives in the type and the icon, never in the capsule */
  body:has(.pn-toggle:checked) .nrb-dock-seat--cats, body:has(.sc-toggle:checked) .nrb-dock-seat--scope,
  .nrb-dock-seat--tokens[aria-pressed="true"], .nrb-dock-seat--compass[aria-pressed="true"],
  .nrb-dock-seat--save[aria-pressed="true"], .nrb-dock-seat--mark[aria-pressed="true"] {
    background: var(--dock-lit);
    box-shadow: inset 0 1px 0 var(--dock-lit-top), inset 0 0 0 1px var(--dock-lit-ring), 0 1px 3px var(--dock-lit-shadow);
  }
  body:has(.pn-toggle:checked) .nrb-dock-seat--cats .nrb-dock-v,
  body:has(.sc-toggle:checked) .nrb-dock-seat--scope .nrb-dock-v { color: var(--accent); }
  .pn-toggle:focus-visible ~ .nrb-dock .nrb-dock-seat--cats,
  body:has(.sc-toggle:focus-visible) .nrb-dock-seat--scope { outline: 2px solid var(--accent); outline-offset: 2px; }
  /* people can reduce transparency — the edge's blur goes (the plate is opaque already); more contrast — the ring at full ink */
  @media (prefers-reduced-transparency: reduce) { .nrb-dock::before { display: none; } }
  @media (prefers-contrast: more) { .nrb-dock-bar { box-shadow: inset 0 0 0 1px var(--dock-ink), 0 1px 2px rgba(0,0,0,.25); } }

  /* ── what the dock replaces, keyed on its presence ──────────────────────── */
  /* the launcher retires; its panel rises ABOVE the dock, still at the right */
  body:has(.nrb-dock) .pn-launch { display: none; }
  body:has(.nrb-dock) .pn-toggle:checked ~ .pn-panel { bottom: calc(var(--dock-floor, 16px) + 66px); }
  body.nrb-mm-scope:has(.nrb-dock) .pn-toggle:checked ~ .pn-panel { bottom: 130px; }
  /* the rail moves to the right, above ITS seat; collapsed it is not drawn at
     all — the seat carries the word now — and it fades in when the seat opens it */
  body:has(.nrb-dock) .sc { left: auto; right: 14px; bottom: calc(var(--dock-floor, 16px) + 66px); transform: none; }
  body.nrb-mm-scope:has(.nrb-dock) .sc { bottom: 130px; }
  body:has(.nrb-dock) .sc-toggle:not(:checked) ~ .sc-rail {
    opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity var(--dur-fast) var(--ease-out), visibility 0s linear var(--dur-med);
  }
  body:has(.nrb-dock) .sc-toggle:checked ~ .sc-rail { opacity: 1; visibility: visible; }
  /* the body-level pill never draws beside a dock (the bundle adopts the seat;
     this is the belt) */
  body:has(.nrb-dock) .nrb-ps-mobile-trigger { display: none; }
  /* the sheet covers the dock (owner: the dock hides under it, today's rule for the pill) */
  body:has(.nrb-ps-mobile-dialog[open]) .nrb-dock { display: none; }
  /* ⚖️ THE JOIN FOOT IS STICKY (owner 2026-09-16: "always keep the register or
     i have an account sticky and slide the content under"): the sheet scrolls,
     the foot stays on its bottom edge, the column slides under a ground fade */
  body:has(.nrb-dock) .nrb-ps-mobile-dialog { overflow-y: auto; padding-bottom: 0; }
  body:has(.nrb-dock) .nrb-ps-mobile-dialog .nrb-ps-join-foot {
    position: sticky; bottom: 0; z-index: 2; margin: 0 -16px;
    padding: 12px 16px max(16px, env(safe-area-inset-bottom));
    background: linear-gradient(to bottom, color-mix(in oklab, var(--bg-base, #061020) 0%, transparent), var(--bg-base, #061020) 28%);
  }
  body:has(.nrb-dock) .nrb-ps-mobile-dialog .nrb-ps-join-foot::before {
    content: ""; position: absolute; left: 16px; right: 16px; top: 0; height: 1px; background: var(--line); opacity: .6;
  }
  /* a story page: the dock waits, as the launcher did, until the path head has
     scrolled off. ⚑ A SIBLING RULE, NOT body:has(…) — the dock bakes beside the
     scope rail inside the soft-nav region, a sibling of .pn-shell, and the
     region wrapper's law (softNav.test) forbids a body-anchored child rule. */
  .pn-shell:not(.pn-path-scrolled):has(> .shell > .hn-head--path) ~ .nrb-dock { display: none; }

  /* ── 📱 THE STORY'S DOCK (owner 2026-09-16/17; records story-dock.isolation.md and
        dock-unification.isolation.md, pick A2 „the home's skin on the story";
        render/StoryDockView.ts, DRAWN BY THE BUNDLE on the client flag STORY_DOCK —
        web/src/storyDock.ts; these rules reach the browser inside the bundle's sheet) ──
     ONE SKIN, TWO STATES: the story dock wears the home dock's seats — 20px accent
     icons, a 12.5px word under each, the tokens seat the home's own — on a second
     row the progress line. Read off the unification tuner (its defaults):
     icon 20 · word 12.5 · value 17 · seat pad 6 · icon↔word 5 · corners 26 · hex 20 ·
     line 6 · knob 14 · line↔seats 21 · minimal pill 32 · minimal icons 16 · floor 16 */
  .nrb-dock--story .nrb-dock-bar { flex-wrap: wrap; border-radius: 26px; }
  /* 🫧 concentric with the 26px bar: 26 − the 4px pad */
  .nrb-dock--story .nrb-dock-seat { border-radius: 22px; flex-direction: column; gap: 5px; padding: 6px 4px; }
  .nrb-dock--story .nrb-dock-l { align-items: center; }
  /* the tokens seat is the home's: hex beside the number over the word — the CENTRE seat, a little wider */
  .nrb-dock--story .nrb-dock-seat--tokens { flex: 1.6 1 0; flex-direction: row; gap: 9px; }
  .nrb-dock--story .nrb-dock-seat--tokens .nrb-dock-l { align-items: flex-start; }
  /* the save seat lit: the reader's keep — the capsule is DOCK_SKIN's one lit rule; here only the ink */
  .nrb-dock-seat--save[aria-pressed="true"] .nrb-dock-i { fill: var(--accent); }
  /* the mark-read seat (the web, 2026-09-17): the column's own action, lit when this story is read */
  .nrb-dock-seat--mark[aria-pressed="true"] .nrb-dock-i { color: var(--accent); }
  /* the line: accent from the start to HERE, a solid knob at HERE; bakes hidden, the bundle drives it */
  .nrb-dock-pg { flex: 0 0 calc(100% - 32px); position: relative; height: 6px; margin: 21px 16px 10px; border-radius: 999px; background: color-mix(in oklab, var(--text) 14%, transparent); cursor: grab; touch-action: none; -webkit-tap-highlight-color: transparent; }
  .nrb-dock-pg[hidden] { display: none; }
  .nrb-dock-pg::before { content: ""; position: absolute; left: -8px; right: -8px; top: -20px; bottom: -14px; }
  .nrb-dock-pg-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; border-radius: 999px; background: var(--accent); pointer-events: none; }
  .nrb-dock-pg-knob { position: absolute; top: 50%; left: 0; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); transform: translate(-50%, -50%); pointer-events: none; transition: box-shadow var(--dur-fast); }
  .nrb-dock--story.is-drag .nrb-dock-pg { cursor: grabbing; }
  .nrb-dock--story.is-drag .nrb-dock-pg-knob { box-shadow: 0 0 0 6px color-mix(in oklab, var(--accent) 28%, transparent); }
  /* the line REPLACES the chronicle strip: the story dock stands at the floor and the article clears it */
  body:has(.nrb-dock--story) .nrb-mm { display: none; }
  body.nrb-mm-scope:has(.nrb-dock--story) .nrb-dock { bottom: var(--dock-floor, 16px); }
  .pn-shell:has(~ .nrb-dock--story) > .shell, .pn-shell:has(~ .nrb-dock-host > .nrb-dock--story) > .shell { padding-bottom: 130px; }
  .nrb-dock-host { display: contents; }

  /* ── THE COMPASS PANEL (owner 2026-09-17, night; dock-unification.isolation.md § the compass panel —
     the recipe read off the tuner: top 0 · edge to edge · corners 7 · pad 8 · scrim 58 % · no blur ·
     8px between the list's end and the dock · slide 200ms · behind the dock · the day bar sticky).
     A panel the DOCK OWNS, not a dialog: the page's chronicle rail rises from behind the dock to the
     top of the screen over a scrim; the dock stays (z 41) above the panel (40) above the scrim (39);
     the chronicle seat reads pressed in the save seat's language. The list's end clears the dock:
     the bottom padding = the dock (96) + its floor (16) + the gap (8). */
  /* 🪟 THE DRAWER (owner 2026-09-21): the panel stops 44px short of the top so the page shows through the scrim's
     BLUR above it; a handle at its top (::before, sticky) says it slides; web/src/dockSheet.ts drags it down to close */
  .nrb-dock-scrim { position: fixed; inset: 0; z-index: 39; background: rgb(0 0 0 / .58); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); opacity: 0; pointer-events: none; transition: opacity var(--dur-fast) linear; }
  .nrb-dock-scrim.is-open { opacity: 1; pointer-events: auto; }
  .nrb-dock-panel { position: fixed; left: 0; right: 0; top: 44px; bottom: 0; z-index: 40; box-sizing: border-box; padding: 8px 8px 120px; border-radius: 18px 18px 0 0; touch-action: pan-y; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none;
    background: color-mix(in oklab, var(--bg-base, #061020) 96%, transparent); box-shadow: 0 -8px 40px #0008;
    transform: translateY(100%); opacity: 0; pointer-events: none; transition: transform 200ms var(--ease-out), opacity var(--dur-fast) linear; }
  .nrb-dock-panel.is-open { transform: none; opacity: 1; pointer-events: auto; }
  .nrb-dock-panel::before { content: ""; display: block; position: sticky; top: 0; z-index: 4; width: 40px; height: 5px; margin: 2px auto 10px; border-radius: 999px; background: color-mix(in oklab, var(--text) 32%, transparent); }
  .nrb-dock-panel.is-dragging { overflow: hidden; }
  /* ⚠️ the rail carries the desktop column's 286px width — in the panel it fills the width */
  .nrb-dock-panel .pn-rail--story, .nrb-dock-panel .nrb-tl, .nrb-dock-panel .nrb-tl-inner { width: auto; max-width: none; padding: 0; margin: 0; max-height: none; overflow: visible; }
  .nrb-dock-panel .nrb-tl-inner { padding-bottom: 0; }
  /* the day bar (also „to the top") never leaves: sticky on the panel's ground, a fade under it */
  .nrb-dock-panel .nrb-nowbar { position: sticky; top: 9px; z-index: 3; margin-top: -8px; padding-top: 8px; background: color-mix(in oklab, var(--bg-base, #061020) 96%, transparent); }
  .nrb-dock-panel .nrb-nowbar::after { content: ""; position: absolute; left: 0; right: 0; bottom: -12px; height: 12px; background: linear-gradient(color-mix(in oklab, var(--bg-base, #061020) 96%, transparent), transparent); pointer-events: none; }
  /* ── THE TOKEN PANEL (owner 2026-09-17, night; dock-unification.isolation.md § the token panel): the reader's
     column in the same panel, opened by the TOKENS seat — no head (the seat says the number), the join foot
     ONE ROW (the CTA + a ghost „Влез", 40px), scrolling with the column; the seat lit while open */
  .nrb-dock-panel--ps .nrb-ps { width: 100%; max-width: 100%; }
  /* owner 2026-09-21 (evening): the column's HEAD (avatar · name · balance) is back — "it was better with the user
     profile on top"; the 2026-09-17 "no head" is withdrawn */
  /* owner: the column's actions row (like · share · mark read · views) is not needed here — the dock carries save and share */
  .nrb-dock-panel--ps .nrb-ps-acts { display: none; }
  /* owner (late): the foot SCROLLS with the column — sticky kept it glued above the dock, never gone */
  .nrb-dock-panel--ps .nrb-ps-join-foot { position: static; margin: 12px -8px 0; padding: 8px; background: var(--bg-base, #061020); border-top: 1px solid var(--line); display: flex; flex-direction: row; align-items: center; gap: 8px; }
  .nrb-dock-panel--ps .nrb-ps-join-foot::before { display: none; }
  .nrb-dock-panel--ps .nrb-ps-join-cta { flex: 1 1 auto; min-height: 40px; height: 40px; padding: 0 14px; font-size: 14px; }
  .nrb-dock-panel--ps .nrb-ps-join-login { margin: 0; font-size: 0; line-height: 0; flex: 0 0 auto; }
  .nrb-dock-panel--ps .nrb-ps-join-login button { font: 500 14px var(--font-display); height: 40px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--text); cursor: pointer; }
  /* owner: the masthead's language selector must not show over an open panel — the whole masthead
     (z 40, the panel's own band) steps under the scrim while any dock panel is open */
  body:has(.nrb-dock-panel.is-open) .pn-fix { z-index: 38; }
  body:has(.nrb-dock-panel.is-open) .nrb-lang { visibility: hidden; }
  /* the tokens and compass seats lit: the capsule is DOCK_SKIN's one lit rule; here only the compass's ink */
  .nrb-dock-seat--compass[aria-pressed="true"] .nrb-dock-i { color: var(--accent); }

  /* ── MINIMAL ⇄ FULL on EVERY dock (owner 2026-09-17: "minimal — progress only — when minimal
     UI, animate to the full dock on tap"; then the unification's C1: the home folds too). While the
     browser is in its collapsed-toolbar minimal UI (20.2260) the story dock is the LINE alone and the
     home dock a pill of its three icons; the seats fold up and back down. The bundle sets .is-min
     from the visual viewport's height and clears it on a tap or when the toolbar returns. */
  .nrb-dock .nrb-dock-bar { transition: padding var(--dur-med) var(--ease-out), gap var(--dur-med) var(--ease-out); }
  .nrb-dock .nrb-dock-seat { max-height: 64px; transition: max-height var(--dur-med) var(--ease-out), padding var(--dur-med) var(--ease-out), opacity var(--dur-fast) linear, background var(--dur-med) var(--ease-out), box-shadow var(--dur-med) var(--ease-out); }
  .nrb-dock--story .nrb-dock-pg { transition: margin var(--dur-med) var(--ease-out); }
  .nrb-dock--story.is-min .nrb-dock-seat { max-height: 0; padding-top: 0; padding-bottom: 0; opacity: 0; overflow: hidden; pointer-events: none; }
  .nrb-dock--story.is-min .nrb-dock-bar { padding-bottom: 4px; }
  .nrb-dock--story.is-min .nrb-dock-pg { margin: 8px 16px 8px; }
  /* the home's fold: a 32px pill of the three icons at 16px, no words; the pill itself is the tap that unfolds */
  .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-bar { padding: 4px 12px; gap: 18px; justify-content: center; min-height: 32px; box-sizing: border-box; }
  .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-seat { flex: 0 0 auto; padding: 2px 4px; border-left: 0; pointer-events: none; }
  .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-l { display: none; }
  .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-i, .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-seat > .nrb-ps-token { width: 16px; height: 16px; }
  @media (prefers-reduced-motion: reduce) {
    .nrb-dock .nrb-dock-bar, .nrb-dock .nrb-dock-seat, .nrb-dock--story .nrb-dock-pg, .nrb-dock-panel, .nrb-dock-scrim { transition: none; }
  }
  /* ⚖️ THE STORY DOCK IS ALWAYS THERE (owner 2026-09-17: "visible all the time — if I go to the top it disappears"):
     the path-head wait above is the home dock's rule; the story's bar does not wait */
  .pn-shell:not(.pn-path-scrolled):has(> .shell > .hn-head--path) ~ .nrb-dock--story { display: block; }

}
@media (min-width: 901px) {

  /* ── 🔵 THE NOW BAR'S PLATE — the dock's colours are the now bar's (nowBarCss.ts): the ink is --info, the
     plate its 9 % mix into --bg-surface, so every theme dresses both the same way; the lit seat's capsule
     has a LIGHT and a DARK face by the theme's own color-scheme (the dark face first, the fallback). */
  .nrb-dock {
    --dock-ink: var(--info);
    --dock-plate: color-mix(in oklab, var(--dock-ink) 9%, var(--bg-surface));
    --dock-ring: color-mix(in oklab, var(--dock-ink) 60%, transparent);
    --dock-glow: color-mix(in oklab, var(--dock-ink) 35%, transparent);
    --dock-lit: rgba(255,255,255,.14); --dock-lit-top: rgba(255,255,255,.22); --dock-lit-ring: rgba(255,255,255,.06); --dock-lit-shadow: rgba(0,0,0,.25);
    --dock-edge: color-mix(in oklab, var(--bg-base, var(--bg)) 85%, transparent);
  }
  @supports (color: light-dark(#000, #fff)) {
    .nrb-dock {
      --dock-lit: light-dark(rgba(5,16,60,.07), rgba(255,255,255,.14)); --dock-lit-top: light-dark(rgba(255,255,255,.80), rgba(255,255,255,.22));
      --dock-lit-ring: light-dark(rgba(5,16,60,.06), rgba(255,255,255,.06)); --dock-lit-shadow: light-dark(rgba(10,20,60,.10), rgba(0,0,0,.25));
    }
  }
  /* the bar IS the plate: opaque, the ring as an inset shadow (the bar keeps its height), a white top light,
     the now bar's glow and a contact shadow. No lens, no rim, no sheen (owner: n1). */
  .nrb-dock-bar {
    display: flex; align-items: stretch; padding: 4px; border-radius: 999px; border: 0;
    background: var(--dock-plate);
    box-shadow: inset 0 0 0 1px var(--dock-ring), inset 0 1px 0 rgba(255,255,255,.07), 0 0 14px var(--dock-glow), 0 1px 2px rgba(0,0,0,.25);
  }
  /* the SCROLL EDGE EFFECT (owner 2026-09-21, late: "you proposed something i really like, can we add it" — the
     ladder's E1, back after an evening without it): the floor's last 110px dissolve — a masked 6px blur under a
     ground fade — so content never meets the bar with a hard line. A child of the dock: with it, gone with it;
     under the bar (z -1 inside the dock's own stacking context), above the page. ⚠️ fixed to the VIEWPORT — the
     web block, whose dock is transformed, re-anchors it as an absolute strip. */
  .nrb-dock::before {
    content: ""; position: fixed; left: 0; right: 0; bottom: 0; height: 110px; pointer-events: none; z-index: -1;
    background: linear-gradient(to top, var(--dock-edge), transparent);
    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
    -webkit-mask: linear-gradient(to top, #000 35%, transparent); mask: linear-gradient(to top, #000 35%, transparent);
  }
  /* ⚑ THREE EQUAL SEATS. flex:1 1 0 so the widest word does not buy its seat
     more room than the others — the bar reads as one object, not three pills.
     CONCENTRIC: a capsule inside the capsule (999 − the 4px pad); NO HAIRLINES — items that share a
     background are one group, the lit seat's capsule is the only separation. */
  .nrb-dock-seat {
    flex: 1 1 0; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 9px;
    padding: 6px 10px; border: 0; border-radius: 995px; background: none; color: var(--text);
    text-decoration: none; cursor: pointer; font-family: var(--font-display);
  }
  .nrb-dock-seat[hidden] { display: none; }
  .nrb-dock-i, .nrb-dock-seat > .nrb-ps-token { flex: none; display: block; width: 20px; height: 20px; }
  .nrb-dock-i { color: var(--accent); }
  /* value over label — the column's own two-row type (.nrb-ps-value / .nrb-ps-label) */
  .nrb-dock-l { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; }
  .nrb-dock-v {
    display: flex; align-items: center; height: 18px; font-weight: 600; font-size: 17px; line-height: 1;
    letter-spacing: -.01em; font-variant-numeric: tabular-nums; white-space: nowrap;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }
  .nrb-dock-k { display: block; font-size: 12.5px; font-weight: 400; letter-spacing: -.01em; line-height: 1; color: var(--text-secondary); white-space: nowrap; }
  /* ⚖️ THE KEY IS LOWERCASE WHERE THE LANGUAGE WRITES COMMON NOUNS LOWERCASE —
     a typographic rule, not a word: the launcher's word arrives capitalised
     (Рубрики) and the column's does not (токени), and a bar with both cases
     reads as two components. German keeps its capitals. */
  .nrb-dock-k:lang(bg), .nrb-dock-k:lang(en), .nrb-dock-k:lang(es), .nrb-dock-k:lang(fr), .nrb-dock-k:lang(pt) { text-transform: lowercase; }
  /* THE LIT SEAT — one language for every seat kind (categories · scope while their layer is up; tokens ·
     compass while their panel is up; save · mark-read while on): a lighter capsule concentric with
     the bar, its own top light and a hairline ring — NEUTRAL (the ladder's T0, kept under n1): the accent
     lives in the type and the icon, never in the capsule */
  body:has(.pn-toggle:checked) .nrb-dock-seat--cats, body:has(.sc-toggle:checked) .nrb-dock-seat--scope,
  .nrb-dock-seat--tokens[aria-pressed="true"], .nrb-dock-seat--compass[aria-pressed="true"],
  .nrb-dock-seat--save[aria-pressed="true"], .nrb-dock-seat--mark[aria-pressed="true"] {
    background: var(--dock-lit);
    box-shadow: inset 0 1px 0 var(--dock-lit-top), inset 0 0 0 1px var(--dock-lit-ring), 0 1px 3px var(--dock-lit-shadow);
  }
  body:has(.pn-toggle:checked) .nrb-dock-seat--cats .nrb-dock-v,
  body:has(.sc-toggle:checked) .nrb-dock-seat--scope .nrb-dock-v { color: var(--accent); }
  .pn-toggle:focus-visible ~ .nrb-dock .nrb-dock-seat--cats,
  body:has(.sc-toggle:focus-visible) .nrb-dock-seat--scope { outline: 2px solid var(--accent); outline-offset: 2px; }
  /* people can reduce transparency — the edge's blur goes (the plate is opaque already); more contrast — the ring at full ink */
  @media (prefers-reduced-transparency: reduce) { .nrb-dock::before { display: none; } }
  @media (prefers-contrast: more) { .nrb-dock-bar { box-shadow: inset 0 0 0 1px var(--dock-ink), 0 1px 2px rgba(0,0,0,.25); } }

  /* ── 📱 THE STORY'S DOCK (owner 2026-09-16/17; records story-dock.isolation.md and
        dock-unification.isolation.md, pick A2 „the home's skin on the story";
        render/StoryDockView.ts, DRAWN BY THE BUNDLE on the client flag STORY_DOCK —
        web/src/storyDock.ts; these rules reach the browser inside the bundle's sheet) ──
     ONE SKIN, TWO STATES: the story dock wears the home dock's seats — 20px accent
     icons, a 12.5px word under each, the tokens seat the home's own — on a second
     row the progress line. Read off the unification tuner (its defaults):
     icon 20 · word 12.5 · value 17 · seat pad 6 · icon↔word 5 · corners 26 · hex 20 ·
     line 6 · knob 14 · line↔seats 21 · minimal pill 32 · minimal icons 16 · floor 16 */
  .nrb-dock--story .nrb-dock-bar { flex-wrap: wrap; border-radius: 26px; }
  /* 🫧 concentric with the 26px bar: 26 − the 4px pad */
  .nrb-dock--story .nrb-dock-seat { border-radius: 22px; flex-direction: column; gap: 5px; padding: 6px 4px; }
  .nrb-dock--story .nrb-dock-l { align-items: center; }
  /* the tokens seat is the home's: hex beside the number over the word — the CENTRE seat, a little wider */
  .nrb-dock--story .nrb-dock-seat--tokens { flex: 1.6 1 0; flex-direction: row; gap: 9px; }
  .nrb-dock--story .nrb-dock-seat--tokens .nrb-dock-l { align-items: flex-start; }
  /* the save seat lit: the reader's keep — the capsule is DOCK_SKIN's one lit rule; here only the ink */
  .nrb-dock-seat--save[aria-pressed="true"] .nrb-dock-i { fill: var(--accent); }
  /* the mark-read seat (the web, 2026-09-17): the column's own action, lit when this story is read */
  .nrb-dock-seat--mark[aria-pressed="true"] .nrb-dock-i { color: var(--accent); }
  /* the line: accent from the start to HERE, a solid knob at HERE; bakes hidden, the bundle drives it */
  .nrb-dock-pg { flex: 0 0 calc(100% - 32px); position: relative; height: 6px; margin: 21px 16px 10px; border-radius: 999px; background: color-mix(in oklab, var(--text) 14%, transparent); cursor: grab; touch-action: none; -webkit-tap-highlight-color: transparent; }
  .nrb-dock-pg[hidden] { display: none; }
  .nrb-dock-pg::before { content: ""; position: absolute; left: -8px; right: -8px; top: -20px; bottom: -14px; }
  .nrb-dock-pg-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; border-radius: 999px; background: var(--accent); pointer-events: none; }
  .nrb-dock-pg-knob { position: absolute; top: 50%; left: 0; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); transform: translate(-50%, -50%); pointer-events: none; transition: box-shadow var(--dur-fast); }
  .nrb-dock--story.is-drag .nrb-dock-pg { cursor: grabbing; }
  .nrb-dock--story.is-drag .nrb-dock-pg-knob { box-shadow: 0 0 0 6px color-mix(in oklab, var(--accent) 28%, transparent); }
  /* the line REPLACES the chronicle strip: the story dock stands at the floor and the article clears it */
  body:has(.nrb-dock--story) .nrb-mm { display: none; }
  body.nrb-mm-scope:has(.nrb-dock--story) .nrb-dock { bottom: var(--dock-floor, 16px); }
  .pn-shell:has(~ .nrb-dock--story) > .shell, .pn-shell:has(~ .nrb-dock-host > .nrb-dock--story) > .shell { padding-bottom: 130px; }
  .nrb-dock-host { display: contents; }

  /* ── THE COMPASS PANEL (owner 2026-09-17, night; dock-unification.isolation.md § the compass panel —
     the recipe read off the tuner: top 0 · edge to edge · corners 7 · pad 8 · scrim 58 % · no blur ·
     8px between the list's end and the dock · slide 200ms · behind the dock · the day bar sticky).
     A panel the DOCK OWNS, not a dialog: the page's chronicle rail rises from behind the dock to the
     top of the screen over a scrim; the dock stays (z 41) above the panel (40) above the scrim (39);
     the chronicle seat reads pressed in the save seat's language. The list's end clears the dock:
     the bottom padding = the dock (96) + its floor (16) + the gap (8). */
  /* 🪟 THE DRAWER (owner 2026-09-21): the panel stops 44px short of the top so the page shows through the scrim's
     BLUR above it; a handle at its top (::before, sticky) says it slides; web/src/dockSheet.ts drags it down to close */
  .nrb-dock-scrim { position: fixed; inset: 0; z-index: 39; background: rgb(0 0 0 / .58); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); opacity: 0; pointer-events: none; transition: opacity var(--dur-fast) linear; }
  .nrb-dock-scrim.is-open { opacity: 1; pointer-events: auto; }
  .nrb-dock-panel { position: fixed; left: 0; right: 0; top: 44px; bottom: 0; z-index: 40; box-sizing: border-box; padding: 8px 8px 120px; border-radius: 18px 18px 0 0; touch-action: pan-y; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none;
    background: color-mix(in oklab, var(--bg-base, #061020) 96%, transparent); box-shadow: 0 -8px 40px #0008;
    transform: translateY(100%); opacity: 0; pointer-events: none; transition: transform 200ms var(--ease-out), opacity var(--dur-fast) linear; }
  .nrb-dock-panel.is-open { transform: none; opacity: 1; pointer-events: auto; }
  .nrb-dock-panel::before { content: ""; display: block; position: sticky; top: 0; z-index: 4; width: 40px; height: 5px; margin: 2px auto 10px; border-radius: 999px; background: color-mix(in oklab, var(--text) 32%, transparent); }
  .nrb-dock-panel.is-dragging { overflow: hidden; }
  /* ⚠️ the rail carries the desktop column's 286px width — in the panel it fills the width */
  .nrb-dock-panel .pn-rail--story, .nrb-dock-panel .nrb-tl, .nrb-dock-panel .nrb-tl-inner { width: auto; max-width: none; padding: 0; margin: 0; max-height: none; overflow: visible; }
  .nrb-dock-panel .nrb-tl-inner { padding-bottom: 0; }
  /* the day bar (also „to the top") never leaves: sticky on the panel's ground, a fade under it */
  .nrb-dock-panel .nrb-nowbar { position: sticky; top: 9px; z-index: 3; margin-top: -8px; padding-top: 8px; background: color-mix(in oklab, var(--bg-base, #061020) 96%, transparent); }
  .nrb-dock-panel .nrb-nowbar::after { content: ""; position: absolute; left: 0; right: 0; bottom: -12px; height: 12px; background: linear-gradient(color-mix(in oklab, var(--bg-base, #061020) 96%, transparent), transparent); pointer-events: none; }
  /* ── THE TOKEN PANEL (owner 2026-09-17, night; dock-unification.isolation.md § the token panel): the reader's
     column in the same panel, opened by the TOKENS seat — no head (the seat says the number), the join foot
     ONE ROW (the CTA + a ghost „Влез", 40px), scrolling with the column; the seat lit while open */
  .nrb-dock-panel--ps .nrb-ps { width: 100%; max-width: 100%; }
  /* owner 2026-09-21 (evening): the column's HEAD (avatar · name · balance) is back — "it was better with the user
     profile on top"; the 2026-09-17 "no head" is withdrawn */
  /* owner: the column's actions row (like · share · mark read · views) is not needed here — the dock carries save and share */
  .nrb-dock-panel--ps .nrb-ps-acts { display: none; }
  /* owner (late): the foot SCROLLS with the column — sticky kept it glued above the dock, never gone */
  .nrb-dock-panel--ps .nrb-ps-join-foot { position: static; margin: 12px -8px 0; padding: 8px; background: var(--bg-base, #061020); border-top: 1px solid var(--line); display: flex; flex-direction: row; align-items: center; gap: 8px; }
  .nrb-dock-panel--ps .nrb-ps-join-foot::before { display: none; }
  .nrb-dock-panel--ps .nrb-ps-join-cta { flex: 1 1 auto; min-height: 40px; height: 40px; padding: 0 14px; font-size: 14px; }
  .nrb-dock-panel--ps .nrb-ps-join-login { margin: 0; font-size: 0; line-height: 0; flex: 0 0 auto; }
  .nrb-dock-panel--ps .nrb-ps-join-login button { font: 500 14px var(--font-display); height: 40px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--text); cursor: pointer; }
  /* owner: the masthead's language selector must not show over an open panel — the whole masthead
     (z 40, the panel's own band) steps under the scrim while any dock panel is open */
  body:has(.nrb-dock-panel.is-open) .pn-fix { z-index: 38; }
  body:has(.nrb-dock-panel.is-open) .nrb-lang { visibility: hidden; }
  /* the tokens and compass seats lit: the capsule is DOCK_SKIN's one lit rule; here only the compass's ink */
  .nrb-dock-seat--compass[aria-pressed="true"] .nrb-dock-i { color: var(--accent); }

  /* ── MINIMAL ⇄ FULL on EVERY dock (owner 2026-09-17: "minimal — progress only — when minimal
     UI, animate to the full dock on tap"; then the unification's C1: the home folds too). While the
     browser is in its collapsed-toolbar minimal UI (20.2260) the story dock is the LINE alone and the
     home dock a pill of its three icons; the seats fold up and back down. The bundle sets .is-min
     from the visual viewport's height and clears it on a tap or when the toolbar returns. */
  .nrb-dock .nrb-dock-bar { transition: padding var(--dur-med) var(--ease-out), gap var(--dur-med) var(--ease-out); }
  .nrb-dock .nrb-dock-seat { max-height: 64px; transition: max-height var(--dur-med) var(--ease-out), padding var(--dur-med) var(--ease-out), opacity var(--dur-fast) linear, background var(--dur-med) var(--ease-out), box-shadow var(--dur-med) var(--ease-out); }
  .nrb-dock--story .nrb-dock-pg { transition: margin var(--dur-med) var(--ease-out); }
  .nrb-dock--story.is-min .nrb-dock-seat { max-height: 0; padding-top: 0; padding-bottom: 0; opacity: 0; overflow: hidden; pointer-events: none; }
  .nrb-dock--story.is-min .nrb-dock-bar { padding-bottom: 4px; }
  .nrb-dock--story.is-min .nrb-dock-pg { margin: 8px 16px 8px; }
  /* the home's fold: a 32px pill of the three icons at 16px, no words; the pill itself is the tap that unfolds */
  .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-bar { padding: 4px 12px; gap: 18px; justify-content: center; min-height: 32px; box-sizing: border-box; }
  .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-seat { flex: 0 0 auto; padding: 2px 4px; border-left: 0; pointer-events: none; }
  .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-l { display: none; }
  .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-i, .nrb-dock.is-min:not(.nrb-dock--story) .nrb-dock-seat > .nrb-ps-token { width: 16px; height: 16px; }
  @media (prefers-reduced-motion: reduce) {
    .nrb-dock .nrb-dock-bar, .nrb-dock .nrb-dock-seat, .nrb-dock--story .nrb-dock-pg, .nrb-dock-panel, .nrb-dock-scrim { transition: none; }
  }
  /* ⚖️ THE STORY DOCK IS ALWAYS THERE (owner 2026-09-17: "visible all the time — if I go to the top it disappears"):
     the path-head wait above is the home dock's rule; the story's bar does not wait */
  .pn-shell:not(.pn-path-scrolled):has(> .shell > .hn-head--path) ~ .nrb-dock--story { display: block; }

  /* ── 🖥 THE WEB (owner 2026-09-17, latest; dock-unification.isolation.md § the WEB's chrome — the
     recipe off the desk tuner: dock 520px · floor 20px · panels 520px, top 24px, corners 12px · scrim
     45 % · the chronicle stays in the LEFT column (no chronicle seat) · the right column keeps its head
     and loses its ACTIONS ROW (the dock carries them: ‹ · share · TOKENS · save · mark read + the line) ·
     the pills as they are). Then (owner: "execute H1, W1") THE HOME DOCK TOO: on the home and the walls the
     baked dock — categories · TOKENS · scope — stands at the same floor; the categories seat opens the
     tree as a 520px panel above the dock, the scope seat opens the rail there, the СВЯТ pill is gone
     while closed (the seat carries the word). */
  .nrb-dock { display: block; position: fixed; left: 50%; right: auto; width: 520px; transform: translateX(-50%); bottom: 20px; z-index: 41; }
  .nrb-dock[hidden], .nrb-dock[data-nrb-dock-under] { display: none; }
  /* the scroll edge under a TRANSFORMED dock: fixed would anchor to the dock, so an absolute strip as wide as the viewport, its foot at the floor */
  .nrb-dock::before { position: absolute; left: 50%; right: auto; width: 100vw; transform: translateX(-50%); bottom: -20px; }
  .pn-shell:has(~ .nrb-dock) > .shell { padding-bottom: 120px; }
  /* the home dock on a story waits for nothing on the web either (the story dock hides it anyway) */
  /* the categories seat: the tree (display:contents in the left column on the web) becomes a panel above the dock while checked */
  body:has(.nrb-dock) .pn-toggle:checked ~ .pn-scrim { display: block; position: fixed; inset: 0; z-index: 39; }
  body:has(.nrb-dock) .pn-toggle:checked ~ .pn-panel {
    display: block; position: fixed; left: 50%; right: auto; transform: translateX(-50%); bottom: 92px; z-index: 41;
    width: 520px; max-width: calc(100vw - 28px); max-height: 60vh; overflow-y: auto;
    padding: 14px; border-radius: 16px; border: 1px solid var(--line);
    background: color-mix(in oklab, var(--bg) 92%, transparent);
    -webkit-backdrop-filter: blur(18px) saturate(1.4); backdrop-filter: blur(18px) saturate(1.4);
    box-shadow: 0 4px 10px rgba(16, 24, 48, .08), 0 24px 60px -18px rgba(16, 24, 48, .38);
  }
  /* the scope seat: the rail rises above the dock, centred; closed it is not drawn — the seat carries the word */
  body:has(.nrb-dock) .sc { left: 50%; right: auto; bottom: 92px; transform: translateX(-50%); }
  body:has(.nrb-dock) .sc-toggle:not(:checked) ~ .sc-rail { opacity: 0; visibility: hidden; pointer-events: none; }
  body:has(.nrb-dock) .sc-toggle:checked ~ .sc-rail { opacity: 1; visibility: visible; }
  /* the web's rail collapses its siblings at every width but opens them only in the phone's block — beside a dock the seat opens them here */
  body:has(.nrb-dock) .sc-toggle:checked ~ .sc-rail > .sc-seg { max-width: 12rem; padding-inline: 11px; opacity: 1; visibility: visible; pointer-events: auto; --sc-vis-delay: 0s; }
  body.nrb-mm-scope:has(.nrb-dock--story) .nrb-dock { bottom: 20px; }
  .nrb-dock-panel { left: 50%; right: auto; width: 520px; top: 24px; border-radius: 12px 12px 0 0; padding-bottom: 124px; transform: translate(-50%, 100%); }
  .nrb-dock-panel.is-open { transform: translate(-50%, 0); }
  .nrb-dock-scrim { background: rgb(0 0 0 / .45); }
  /* owner (latest): on the web the TOKENS seat is hidden for now, on every dock state — the right column carries the number */
  .nrb-dock-seat--tokens { display: none; }
  /* the column's actions row (like · share · mark read · views) leaves the right column beside the dock */
  body:has(.nrb-dock--story) .pn-rail .nrb-ps-acts { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  body:has(.nrb-dock) .sc-rail { transition-duration: 0s; transition-delay: 0s; }
}



/* ── 🔀 SOFT NAVIGATION (20.2110) ─────────────────────────────────────── */
#nrb-page { display: contents; }
::view-transition-old(root), ::view-transition-new(root) {
  animation-duration: 240ms;
  animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}
::view-transition-group(nrb-hero) { animation-duration: 320ms; }
.nrb-nav-bar {
  position: fixed; top: 0; left: 0; height: 2px; width: 0;
  background: var(--accent, #c9a227);
  z-index: 80; pointer-events: none; opacity: 0;
  transition: width .2s ease-out, opacity .2s ease-out;
}
.nrb-nav-bar.is-on { opacity: 1; }
.nrb-nav-fade { transition: opacity 160ms ease-out; }
.nrb-nav-preload { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
.nrb-nav-fresh { position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 0); z-index: 81;
  padding: 10px 18px; border-radius: 999px; border: 1px solid var(--accent, #c9a227);
  background: var(--bg-elevated, #102258); color: var(--text-primary, #f2f6ff); font: inherit; font-weight: 600;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .35); cursor: pointer; opacity: 0; transition: opacity 200ms ease-out, transform 200ms ease-out; }
.nrb-nav-fresh.is-on { opacity: 1; }
.nrb-nav-shield { position: absolute; z-index: 2; overflow: hidden; pointer-events: none; will-change: opacity, filter, transform; }
.nrb-nav-shield > img { display: block; width: 100%; height: 100%; object-fit: cover; }
.nrb-nav-shield > .nrb-nav-shield-sheen { position: absolute; inset: 0;
  background: linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, .11) 50%, transparent 62%);
  background-size: 260% 100%; animation: nrb-nav-sheen 1.5s ease-in-out infinite; }
@keyframes nrb-nav-sheen { from { background-position: 130% 0; } to { background-position: -30% 0; } }
.nrb-nav-shield.is-dust {
  -webkit-mask-image: linear-gradient(110deg, #000 42%, rgba(0, 0, 0, .55) 56%, transparent 72%);
  mask-image: linear-gradient(110deg, #000 42%, rgba(0, 0, 0, .55) 56%, transparent 72%);
  -webkit-mask-size: 280% 100%; mask-size: 280% 100%;
  animation: nrb-nav-dust 950ms cubic-bezier(.4, 0, .2, 1) forwards; }
.nrb-nav-shield.is-dust > .nrb-nav-shield-sheen { animation: none; opacity: 0; }
@keyframes nrb-nav-dust {
  from { -webkit-mask-position: 0% 0; mask-position: 0% 0; filter: blur(0); transform: scale(1); opacity: 1; }
  to   { -webkit-mask-position: 100% 0; mask-position: 100% 0; filter: blur(10px); transform: scale(1.05); opacity: 0; }
}
.nrb-nav-fresh::before { content: '\2191\00a0'; }
.nrb-nav-fade.is-out { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
  .nrb-nav-fade { transition: none; }
  .nrb-nav-shield > .nrb-nav-shield-sheen { animation: none; }
  .nrb-nav-shield.is-dust { animation: none; opacity: 0; }
}



[data-live-stat]:empty { visibility: hidden; }
.pn-n { min-inline-size: 4ch; justify-content: flex-end; }
.pn-n i { display: inline-block; min-inline-size: 4ch; text-align: end; }
.hn-d { display: inline-block; min-inline-size: 5ch; }
.cat-num { min-block-size: 1em; }
.catwall-meta { min-block-size: 1.5em; min-inline-size: 12ch; }
.archive-list [data-live-stat] { min-inline-size: 5ch; }
.hero-status-count { display: inline-block; min-inline-size: 12ch; min-block-size: 1.2em; }
[data-live-year] { display: inline-block; min-inline-size: 4ch; }
