/* ==========================================================================
   Self-hosted approved typefaces.

   Phase 4E chose these and Phase 4G proved the metrics are load-bearing: real
   Plex line boxes are ~19px taller per card than the character-count estimate,
   which is why the fold math moved from 1.48 rows to 1.33. Falling back to a
   system face would silently change every measurement the design was verified
   against, so the fonts ship with the application rather than being hoped for.

   `font-display: swap` - text is readable immediately and reflows once, which
   is the right trade for a reading product on a constrained network.
   ========================================================================== */

/* UI - Arabic */
@font-face {
  font-family: 'IBM Plex Sans Arabic'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/fonts/plex-arabic-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans Arabic'; font-style: normal; font-weight: 500;
  font-display: swap; src: url('/fonts/plex-arabic-500.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans Arabic'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('/fonts/plex-arabic-600.woff2') format('woff2');
}

/* UI - Latin. A mixed-script catalogue needs both, and the Latin face is what
   renders a title like "The Chronicles of a Forgotten Kingdom…" correctly. */
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/fonts/plex-latin-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 500;
  font-display: swap; src: url('/fonts/plex-latin-500.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('/fonts/plex-latin-600.woff2') format('woff2');
}

/* ==========================================================================
   BRAND WORDMARK - a brand asset, NOT a UI typeface.

   This face exists to draw ONE thing: the visual wordmark. It is applied by
   exactly one selector, `.brand-wordmark`, and `Phase5CWordmarkScopeTest`
   fails the suite if it ever reaches --font-ui, --font-prose, a heading, the
   navigation, a button, a card, the reader or a form.

   That guard is not paranoia. The supplied file is a COMPLETE Latin face - 87
   mapped characters, upper and lower case, digits and punctuation - so nothing
   about the asset itself prevents it being used as body type. Only the scope
   rule does.

   It carries no Arabic at all, which is the second reason the scope matters:
   this is an Arabic-first product, and a leak would silently fall back for
   every Arabic string while looking fine in a Latin-only review.

   The family is the font's OWN production family name, which is the
   convention every other @font-face in this file follows - the eight Plex and
   Naskh faces all declare their real names. The brand-neutral hook is the
   SELECTOR, `.brand-wordmark`, which is what the rest of the system addresses.

   The file is the AUTHORISED production derivative, not the supplied source.
   The source OTF and TTF stay in resources/brand/wordmark/ as provenance and
   are never modified.
   ========================================================================== */
@font-face {
  font-family: 'KUNATSU Wordmark';
  font-style: normal;
  /* The face declares usWeightClass 550. Declaring the SAME weight here is
     what stops the browser synthesising a bolder one - synthesis is exactly
     the "distortion to imitate a reference" the brand contract forbids. */
  font-weight: 550;
  font-display: swap;
  src: url('/fonts/KUNATSU-Wordmark.woff2') format('woff2');
}

/* Novel prose ONLY. Arabic has no serif/sans distinction, so this is not a
   "serif for reading" choice - Naskh is the traditional book face and it is
   used for the text reader and nowhere else. */
@font-face {
  font-family: 'Noto Naskh Arabic'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/fonts/naskh-arabic-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Noto Naskh Arabic'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('/fonts/naskh-arabic-600.woff2') format('woff2');
}

/* CATALOGUE PROSE — the synopsis, and only the synopsis.
   A third role, not a third decoration: the synopsis is written by the
   publisher about the work and shown on a catalogue page, so it is neither the
   product's UI voice nor the reader's long-form face. Arabic subset only
   (~9KB per weight), self-hosted like every other face here. SIL OFL. */
@font-face {
  font-family: 'Tajawal'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/fonts/tajawal-arabic-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Tajawal'; font-style: normal; font-weight: 500;
  font-display: swap; src: url('/fonts/tajawal-arabic-500.woff2') format('woff2');
}
/* ==========================================================================
   PHASE 5A — public frontend stylesheet.

   Implements the APPROVED system. Nothing here is a new design decision:

     tokens       Phase 4E, contrast-verified by phase4e/verify-contrast.py
     breakpoints  Phase 4J responsive contract, mechanically derived
     geometry     Phase 4H (mobile) / 4I (desktop) anchors

   Two anchors this file must reproduce exactly, because the contract was
   derived from them:  390px -> 2 columns, 173px cover
                      1280px -> 6 columns, 186px cover
   ========================================================================== */

/* ---------------------------------------------------------------- tokens */
:root {
  /* Surfaces */
  --surface:          #FFFFFF;
  --surface-elevated: #F7F7F8;
  --surface-sunken:   #EFEFF1;
  --surface-reading:  #FFFFFF;
  --surface-reading-warm: #FBF8F4;   /* 17.5:1 with --text-primary */

  /* Lines. Separator and control boundary are DIFFERENT tokens: a hairline
     that divides content may be faint; a boundary that says "this is a
     control" must clear 3:1. Phase 4E separated them after review. */
  --border:         #E3E3E6;   /* 1.28:1 - structural separator, advisory */
  --control-border: #767680;   /* 4.49:1 on surface - meets the 3:1 minimum */

  /* Text - every value below was verified, not chosen by eye */
  --text-primary:   #16161A;   /* 18.04:1 */
  --text-secondary: #5A5A63;   /*  6.82:1 */
  --text-tertiary:  #6B6B74;   /*  5.28:1 - the 12px timestamp colour */
  --text-inverse:   #FFFFFF;

  --interactive: #16161A;
  --focus:       #16161A;

  /* The default avatar mark. Its own token rather than borrowing a text
     colour, so the empty state can be tuned without moving type. */
  --avatar-fallback: #6B6B74;

  /* SHARED CONTROL GEOMETRY. Buttons, chips, tabs, inputs and segments each
     had their own heights and radii, set locally as each was built - so
     three things that sit on one row were three different heights. */
  --control-h: 44px;      /* anything a finger presses */
  --control-h-sm: 32px;   /* chips, badges, dense metadata */
  --control-r: 10px;
  --card-pad: var(--space-5);

  /* Placeholder artwork tones (covers that have not loaded / do not exist) */
  --art-1: #D9D9DE;
  --art-2: #C6C6CC;
  --art-3: #B3B3BA;
  --skel:  #EFEFF1;

  /* VS-1 is deferred and the Product B accent is unchosen. Resolving to
     --interactive keeps every surface exactly as the approved boards were
     verified. Assigning a hue here would invalidate the contrast proofs. */
  --brand-accent: var(--interactive);

  /* Spacing */
  --space-3: 8px;  --space-4: 12px; --space-5: 16px; --space-6: 20px;
  --space-7: 24px; --space-8: 32px; --space-9: 40px; --space-10: 48px;
  --space-11: 64px; --space-12: 80px;

  /* Type scale */
  --text-xs: 12px; --text-sm: 13px; --text-base: 15px; --text-md: 17px;
  --text-lg: 20px; --text-xl: 28px; --text-2xl: 32px; --text-3xl: 40px;
  /* A DISPLAY NUMERAL, added as a deliberate design-system change rather than
     typed inline. The scale topped out at 40px, which is a heading size; there
     was no size for the ONE figure a surface is about - the count of what a
     reader has recorded from their own reading. Set at 40 it read as metadata.
     The role is narrow on purpose: at most one per page, and only for a
     number. Anything else that wants 64px is a composition problem. */
  --text-display: 64px;

  --radius-sm: 2px; --radius-md: 8px; --radius-lg: 12px;

  /* Locked content caps - viewport-independent (Phase 4J §4.3) */
  --w-listing: 1280px;
  --w-title:   1100px;
  --w-reader-image: 900px;
  --w-reading: 640px;   /* the desktop width buys a rail, never a wider column */

  /* Responsive band values. XS is the default; media queries below step them.
     Column COUNT is a breakpoint decision because a fluid auto-fill rule
     cannot reproduce a fixed approved anchor - it gives 7 columns at 1440
     where Phase 4I approved 6. */
  --cols: 2;
  --pad:  16px;
  --gap:  12px;
  --cover-cap: 190px;   /* bounds the wide side; without it covers reach 208px */

  --font-ui: 'IBM Plex Sans Arabic', 'IBM Plex Sans', system-ui, sans-serif;
  --font-prose: 'Noto Naskh Arabic', serif;
  /* CATALOGUE PROSE. A synopsis is written by the publisher about the work and
     read on a catalogue page - not the product's UI voice, and not the reader's
     long-form face. One role, one place. */
  --font-synopsis: 'Tajawal', 'IBM Plex Sans Arabic', system-ui, sans-serif;
}

/* Dark tokens. Re-verified in Phase 4E; the reading surface is WARM, which is
   a deliberate difference from the neutral page dark. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --surface:          #0E0E11;
    --surface-elevated: #17171C;
    --surface-sunken:   #000000;
    --surface-reading:  #141210;
    --surface-reading-warm: #141210;
    --border:         #26262D;
    --control-border: #8B8B95;   /* 5.71:1 */
    --text-primary:   #F2F2F4;   /* 17.24:1 */
    --text-secondary: #A6A6B0;   /*  7.99:1 */
    --text-tertiary:  #9A9AA4;   /*  6.91:1 */
    --text-inverse:   #0E0E11;
    --interactive: #F2F2F4;
    --focus:       #F2F2F4;
    --avatar-fallback: #9A9AA4;
    --art-1: #2A2A31; --art-2: #33333B; --art-3: #3D3D46; --skel: #1C1C22;
  }
}
:root[data-theme='dark'] {
  --surface: #0E0E11; --surface-elevated: #17171C; --surface-sunken: #000000;
  --surface-reading: #141210; --surface-reading-warm: #141210;
  --border: #26262D; --control-border: #8B8B95;
  --text-primary: #F2F2F4; --text-secondary: #A6A6B0; --text-tertiary: #9A9AA4;
  --text-inverse: #0E0E11; --interactive: #F2F2F4; --focus: #F2F2F4;
  --avatar-fallback: #9A9AA4;
  --art-1: #2A2A31; --art-2: #33333B; --art-3: #3D3D46; --skel: #1C1C22;
}

/* ------------------------------------------------- responsive contract */
/* Derived in Phase 4J and verified at 16 widths, including one pixel BELOW
   every edge. These are NOT framework defaults and must not be replaced. */
@media (min-width: 560px)  { :root { --cols: 3; --pad: 20px; --gap: 16px; } }
@media (min-width: 740px)  { :root { --cols: 4; --pad: 24px; --gap: 20px; } }
@media (min-width: 940px)  { :root { --cols: 5; --pad: 32px; --gap: 20px; } }
@media (min-width: 1120px) { :root { --cols: 6; --pad: 32px; --gap: 20px; } }

/* ------------------------------------------------------------- reset */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--surface);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  line-height: 1.6;
  /* Arabic needs more leading than Latin at the same size, and renders
     optically smaller - both established in Phase 4E. */
}

img, svg { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
ul, ol { list-style: none; }

/* Keyboard focus must be VISIBLE. The ring uses --focus, which equals
   --interactive, so it clears contrast on every surface by construction. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
:focus:not(:focus-visible) { outline: none; }

.skip-link {
  position: absolute; inset-inline-start: -9999px; top: 0; z-index: 100;
  background: var(--surface); color: var(--text-primary);
  padding: var(--space-4) var(--space-6);
}
.skip-link:focus { inset-inline-start: 0; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

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

/* --------------------------------------------------------- direction */
/* UI chrome is RTL. Catalogue and user strings are NOT: they carry dir="auto"
   so the Unicode first-strong rule decides, and isolation so a Latin title
   cannot reorder the Arabic around it. Forcing RTL globally was the real bug
   Phase 4G caught. */
.ui { direction: rtl; unicode-bidi: isolate; }
.cat { unicode-bidi: isolate; text-align: right; }
.clamp-2 {
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden;
  text-align: right; unicode-bidi: isolate;
}

/* ----------------------------------------------------------- layout */
.wrap { max-width: var(--w-listing); margin-inline: auto; padding-inline: var(--pad); }
.wrap--title   { max-width: var(--w-title); }
.wrap--reading { max-width: var(--w-reading); }

.page-heading { font-size: var(--text-lg); line-height: 1.4; font-weight: 600; }
@media (min-width: 560px) { .page-heading { font-size: var(--text-xl); } }

.meta  { font-size: var(--text-sm); line-height: 1.6; color: var(--text-secondary); }
.tiny  { font-size: var(--text-xs); line-height: 1.5; color: var(--text-tertiary); }
.tabular { font-variant-numeric: tabular-nums; }

/* ----------------------------------------------------------- header */
.site-header {
  display: flex; align-items: center; gap: var(--space-6);
  min-height: 52px; padding-inline: var(--pad);
  border-bottom: 1px solid var(--border); background: var(--surface);
}
@media (min-width: 560px) { .site-header { min-height: 60px; gap: var(--space-8); } }

/* The typographic wordmark. When no artwork is configured this IS the
   wordmark - not a placeholder. It shrinks before the fixed controls do,
   because at 320px the controls claim their space first. */
.wordmark {
  font-size: var(--text-md); font-weight: 600; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
@media (min-width: 560px) { .wordmark { font-size: 22px; } }

/* --------------------------------------------------------------------------
   THE BRAND WORDMARK SLOT.

   The ONLY selector permitted to name the wordmark face.

   SIZE IS DERIVED, NOT CHOSEN. The two faces have different cap heights -
   IBM Plex Sans is 698/1000, the wordmark face is 520/1000 (its own OS/2
   sCapHeight, confirmed against the K U N A T glyph bounds, which are uniform
   at 520). Rendered at the same font-size the wordmark would stand 25%
   shorter than the approved header and read as a mistake.

     multiplier = 698 / 520 = 1.3423
     17px -> 22.8px      22px -> 29.5px

   That is a size correction, not a distortion: no letter-spacing, no
   transform, no stretching, no synthesis, no altered geometry. The letterforms
   are exactly as supplied.

   FALLBACK: if the font never arrives the mark renders in the UI face at the
   wordmark's larger size - correctly spelled, legible, slightly larger than
   the approved board. Preferred over `font-display: block`, which would hide
   the brand entirely while waiting.
   -------------------------------------------------------------------------- */
.brand-wordmark {
  font-family: 'KUNATSU Wordmark', var(--font-ui);
  font-weight: 550;
  font-synthesis: none;
  font-size: 22.8px;
  letter-spacing: normal;
  /* The face ships kerning in a legacy `kern` table and has no GPOS. Asking
     for kerning explicitly is what keeps the six pairs inside K-U-N-A-T-S-U
     applied rather than left to a default that varies by engine. */
  font-kerning: normal;
}
@media (min-width: 560px) { .brand-wordmark { font-size: 29.5px; } }

/* --------------------------------------------------------------------------
   BRAND LOGO ARTWORK — the other wordmark mode.

   Height is set and width left AUTO, deliberately. The supplied aspect ratio
   is then preserved exactly by the browser at every breakpoint, with no
   rounding of our own and no risk of stretching a mark that predates this
   application.

   The heights come from the Phase 5D measurements: the header is 52px at 390
   and 60px from 560 up, and the text wordmark it replaces occupies 27.19px
   and 35.19px line boxes respectively. 28 and 32 sit inside those without
   changing the header.

   `max-width` is a guard rather than a fit: the supplied 1.44:1 mark is ~40px
   wide at 28px tall, against 184px of measured free space. If a future mark
   were far wider, it letterboxes instead of pushing the row into overflow.
   -------------------------------------------------------------------------- */
.brand-logo {
  display: block;
  height: 28px;
  width: auto;
  max-width: 184px;
  object-fit: contain;
}
@media (min-width: 560px) {
  .brand-logo { height: 32px; max-width: 320px; }
}

/* Inline navigation exists only from SM up. Below that the drawer carries it -
   the cluster measures 525px at the band's gap and cannot fit 559px. */
.nav-inline { display: none; }
@media (min-width: 560px) {
  .nav-inline { display: flex; gap: var(--space-6); align-items: center; }
}
.nav-inline a { font-size: var(--text-base); white-space: nowrap; }
.nav-inline a[aria-current='page'] { font-weight: 600; }

.header-actions { margin-inline-start: auto; display: flex; align-items: center; gap: 2px; }

/* Every interactive element gets a >= 44x44 HIT AREA. The visual control may
   be smaller - a genre chip is still 36px tall inside a 44px target. */
.hit {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px;
  /* A flex ITEM may shrink below its min-width unless this is set, and the
     header is a flex row. Without it the icon buttons compressed to 38x44 at
     1440 - the contract held in the stylesheet and broke in the layout. */
  flex: 0 0 auto;
}
.icon-button {
  width: 44px; height: 44px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
}

/*
 * The header icons are LINKS now, not decorative spans, so they inherit link
 * colour and underline. Neither belongs on an icon - and a control a reader can
 * tab to must show where focus is, which a span never had to.
 */
.header-action {
  color: inherit;
  text-decoration: none;
  border-radius: 6px;
}
.header-action:hover { color: var(--accent, currentColor); }
.header-action:focus-visible {
  outline: 2px solid var(--accent, currentColor);
  outline-offset: 2px;
}

.menu-toggle { display: flex; }
@media (min-width: 560px) { .menu-toggle { display: none; } }

.icon-search {
  width: 18px; height: 18px; border: 1.8px solid currentColor;
  border-radius: 50%; position: relative;
}
.icon-search::after {
  content: ''; position: absolute; width: 7px; height: 1.8px;
  background: currentColor; bottom: -1px; left: -5px; transform: rotate(45deg);
}
.icon-burger { width: 18px; height: 12px; position: relative; }
.icon-burger i { position: absolute; inset-inline: 0; height: 1.8px; background: currentColor; }
.icon-burger i:nth-child(1) { top: 0; }
.icon-burger i:nth-child(2) { top: 5px; }
.icon-burger i:nth-child(3) { top: 10px; }

.icon-avatar {
  width: 24px; height: 24px; border: 1.5px solid var(--control-border);
  border-radius: 50%; position: relative; overflow: hidden;
}
.icon-avatar::before {
  content: ''; position: absolute; width: 9px; height: 9px; border-radius: 50%;
  background: var(--control-border); top: 3px; left: 7px;
}
.icon-avatar::after {
  content: ''; position: absolute; width: 18px; height: 10px;
  border-radius: 9px 9px 0 0; background: var(--control-border); bottom: -1px; left: 2px;
}

/* Directional carets use PHYSICAL borders and a semantic class. Logical
   properties resolve to the wrong edge under RTL - that bug shipped once in
   Phase 4H and every caret pointed up-left. */
.caret {
  width: 8px; height: 8px; display: inline-block; vertical-align: middle;
  border-top: 1.7px solid currentColor; border-right: 1.7px solid currentColor;
}
.caret--back, .caret--prev { transform: rotate(45deg); }   /* points RIGHT */
.caret--next { transform: rotate(-135deg); }               /* points LEFT  */
.caret--down { transform: rotate(135deg); }
.caret--up   { transform: rotate(-45deg); }

/* ----------------------------------------------------------- drawer */
.scrim {
  position: fixed; inset: 0; background: rgba(0, 0, 0, .45);
  opacity: 0; visibility: hidden; transition: opacity .18s ease; z-index: 40;
}
.scrim[data-open='true'] { opacity: 1; visibility: visible; }

/* RTL: the drawer opens from the RIGHT. */
.drawer {
  position: fixed; inset-block: 0; right: 0; width: min(78%, 320px);
  background: var(--surface); padding: var(--space-6) var(--pad);
  transform: translateX(100%); transition: transform .2s ease;
  z-index: 50; overflow-y: auto;
}
.drawer[data-open='true'] { transform: translateX(0); }
.drawer__list { display: flex; flex-direction: column; gap: 2px; margin-top: var(--space-8); }
.drawer__list a {
  min-height: 44px; display: flex; align-items: center;
  padding-inline: 14px; border-radius: var(--radius-sm);
}
.drawer__list a[aria-current='page'] { background: var(--surface-elevated); font-weight: 600; }

/* ----------------------------------------------------------- catalogue */
/* justify-content: space-between with a capped track is what keeps the covers
   inside 158-190px across every width while the column count steps. */
.catalogue {
  display: grid; justify-content: space-between;
  grid-template-columns: repeat(var(--cols), minmax(0, var(--cover-cap)));
  gap: var(--space-6) var(--gap);
}

.cover {
  width: 100%; aspect-ratio: 2 / 3; background: var(--art-1);
  position: relative; overflow: hidden; border-radius: var(--radius-sm);
}
.cover img { width: 100%; height: 100%; object-fit: cover; }
.cover--novel { aspect-ratio: 3 / 4; background: var(--surface-sunken);
  display: flex; align-items: center; justify-content: center; }
.cover--novel img { width: 75%; height: 100%; object-fit: contain; }

/* A missing cover is a NORMAL state, not an error: it gets a designed
   treatment rather than a broken image icon. */
.cover--missing {
  background: var(--surface-sunken); border: 1px solid var(--border);
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--space-3);
}
.cover--missing .plus { width: 36px; height: 36px; position: relative; }
.cover--missing .plus::before, .cover--missing .plus::after {
  content: ''; position: absolute; background: var(--control-border);
}
.cover--missing .plus::before { inset-inline: 0; top: 17px; height: 1.5px; }
.cover--missing .plus::after  { inset-block: 0; left: 17px; width: 1.5px; }

/* A1 cards are CONTAINERLESS - that is the structural argument of Quiet
   Utility against Modern Library, and it must survive implementation. */
.card__title {
  margin-top: var(--space-3); font-size: var(--text-base); line-height: 1.75;
  height: calc(var(--text-base) * 1.75 * 2);
}
.card__chapter { font-size: var(--text-sm); line-height: 1.6; color: var(--text-secondary); }

.rank-badge {
  position: absolute; top: 0; inset-inline-start: 0;
  min-width: 28px; height: 28px; padding-inline: 6px;
  display: flex; align-items: center; justify-content: center;
  background: var(--interactive); color: var(--text-inverse);
  font-size: var(--text-sm); font-weight: 600; font-variant-numeric: tabular-nums;
}

/* Product B: soft container. This is the whole visual difference from A1 and
   it is why the two products cannot share one card partial. */
.card--soft {
  background: var(--surface-elevated); border-radius: var(--radius-md); padding: 10px;
}
.card--soft .cover { border-radius: 6px; }
.card--soft .card__title {
  margin-top: 8px; font-size: var(--text-sm); line-height: 1.5; height: auto;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-align: right; unicode-bidi: isolate;
}

/* ----------------------------------------------------------- controls */
.button {
  min-height: 44px; min-width: 44px; padding-inline: 16px;
  border: 1px solid var(--control-border); border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  font-size: var(--text-sm);
}
.button--primary {
  min-height: 48px; padding-inline: var(--space-8);
  background: var(--interactive); color: var(--text-inverse);
  border-color: var(--interactive); font-size: var(--text-md); font-weight: 500;
}
.button--dominant { min-height: 56px; width: 100%; }

/*
 * ONE FAMILY.
 *
 * A primary at 48px beside a bare .button at 44px, one stretched to 100% and
 * the other at its natural 111px, read as two unrelated products. Related
 * actions now share a height and a rhythm; hierarchy comes from weight and
 * fill, which is what hierarchy is for - not from one control being three
 * times the size of its neighbour.
 */
.button--secondary {
  min-height: 48px; padding-inline: var(--space-7);
  background: transparent; color: var(--text-primary);
  border-color: var(--control-border);
  font-size: var(--text-base); font-weight: 500;
}
.button--secondary:hover { background: var(--surface-elevated); }

/* Actions that belong together, laid out once instead of per page. */
.actions {
  display: flex; flex-wrap: wrap; gap: var(--space-4);
  margin-top: var(--space-7);
}
.actions > * { flex: 0 1 auto; }
/* The primary leads, but does not swallow the row. */
.actions .button--primary { flex: 1 1 260px; max-width: 420px; }
.actions .button--secondary { flex: 0 1 auto; }
@media (max-width: 559px) {
  .actions > .button--primary,
  .actions > .button--secondary,
  .actions > form,
  .actions > form > span,
  .actions > form .button { width: 100%; max-width: none; }
}

/* A consistent field, so forms stop being styled by their neighbours.
   `.field` is what the markup has always used - `.input` was the design-system
   name and matched nothing, so every form in the product was drawn by the
   browser. Both names resolve here; the views were not renamed to suit the CSS. */
.field,
.input {
  min-height: 44px; width: 100%; padding-inline: var(--space-5);
  background: var(--surface); color: var(--text-primary);
  border: 1px solid var(--control-border); border-radius: var(--radius-sm);
  font-size: var(--text-base); font-family: inherit;
}
textarea.field { min-height: 0; padding-block: var(--space-4); line-height: 1.6; }
/* A file input has browser chrome inside it; the padding that suits a text
   field crops that chrome, so it keeps its own box. */
.field[type='file'] { padding: var(--space-3); min-height: 0; }
.field:focus-visible,
.input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.field:disabled { opacity: .6; }

/* The label above a field. 29 of them carried this class and none of them had
   a rule, so a label was whatever the surrounding text happened to be. */
.lbl {
  display: block; margin-bottom: var(--space-3);
  font-size: var(--text-sm); font-weight: 500; color: var(--text-secondary);
}

/* Vertical rhythm between page sections, one value instead of ad-hoc margins. */
.section { margin-top: var(--space-10); }
.section__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-5); margin-bottom: var(--space-6);
}
.section__title { font-size: var(--text-lg); font-weight: 600; }

.chip {
  min-height: 36px; padding-inline: 15px;
  border: 1px solid var(--control-border); border-radius: var(--radius-md);
  display: inline-flex; align-items: center; gap: 8px; font-size: var(--text-sm);
}
.chip[aria-pressed='true'] {
  background: var(--interactive); color: var(--text-inverse); border-color: var(--interactive);
}
.chip-row { display: flex; gap: var(--space-3); flex-wrap: wrap; }

/* .segmented is defined with the reader chrome, below. */

/* --------------------------------------------------------- pagination */
.pager {
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-4); padding-block: var(--space-8);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------- chapter list */
.chapter-row {
  min-height: 48px; display: flex; align-items: center; gap: 8px;
  border-bottom: 1px solid var(--border);
  font-size: var(--text-base); font-variant-numeric: tabular-nums;
}
.chapter-row__time { margin-inline-start: auto; font-size: var(--text-sm); color: var(--text-tertiary); }
.chapter-row__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--control-border); }

.volume-row {
  min-height: 48px; display: flex; align-items: center; gap: 8px;
  border-bottom: 1px solid var(--border); font-size: var(--text-base); font-weight: 500;
}
.volume-row__count { margin-inline-start: auto; font-size: var(--text-sm); color: var(--text-tertiary); }

/* ------------------------------------------------------- title pages */
.identity { display: flex; align-items: flex-start; gap: var(--space-5); }
.identity__cover { width: 96px; flex: 0 0 auto; }
.identity__title {
  font-size: var(--text-xl); line-height: 1.45; font-weight: 600;
  height: calc(var(--text-xl) * 1.45 * 2);
}

/* From LG the title becomes a page heading across the full content width and
   the page splits. Below that, porting the desktop split truncates a title on
   a 1440px screen - which Phase 4I refused. */
.title-layout { display: grid; grid-template-columns: 1fr; gap: var(--space-8); }
@media (min-width: 940px) {
  .title-layout { grid-template-columns: 380px 1fr; gap: var(--space-10); align-items: start; }
  .identity { display: block; }
  /* The cover IS the identity of the work. At 180px in a 380px column it read
     as a thumbnail beside a heading, with the column half empty around it. */
  .identity__cover { width: 260px; margin-bottom: var(--space-6); }
  .identity__title { font-size: var(--text-2xl); line-height: 1.35;
    height: calc(var(--text-2xl) * 1.35 * 2); }
}
.title-layout > * { min-width: 0; }

/* The synopsis: readable measure, clear separation from the actions above it. */
/* THE SYNOPSIS. Two selectors, because `.prose` is declared later in this file
   and a single class would lose to it - which is why this block has had no
   effect at all and the synopsis has been rendering at 19px/1.95. */
.prose.title-synopsis {
  margin-top: var(--space-7);
  max-width: 62ch;
  color: var(--text-secondary);
  font-family: var(--font-synopsis);
  /* 15px, not 17. A synopsis is four lines of orientation before the reader
     decides; at 17 it read as body copy for a page nobody came to read. */
  font-size: var(--text-base);
  /* A summary, not a chapter. 1.95 is a reading line-height and made four lines
     look like a page; 1.75 keeps Arabic comfortable without inflating it. */
  line-height: 1.75;
}
.prose.title-synopsis p + p { margin-top: var(--space-4); }

/* --------------------------------------------------------- spotlight */
/*
 * THE HERO.
 *
 * It used to be a text column beside `.spotlight__art` - an EMPTY div at 55%
 * width and 16/9, which is both a large blank rectangle and the wrong shape for
 * a book cover. Two problems in one: the page looked under-filled, and the one
 * piece of artwork the title actually has was not used.
 *
 * Now the cover is the art, at its own portrait ratio, and the text column sits
 * beside it. Calm, not promotional: no gradient, no overlay, no animation - the
 * same elevated surface as before, composed properly.
 */
.spotlight {
  background: var(--surface-elevated); border-radius: var(--radius-lg);
  padding: var(--space-6);
  display: grid; grid-template-columns: 1fr; gap: var(--space-6);
  align-items: center;
}
/* The COVER column comes first, so the artwork sits at the reading start -
   the right in Arabic - the same way it leads on a phone. It used to be the
   second column, which in RTL put it at the far left and opened a void on the
   right where the eye begins. */
@media (min-width: 740px) {
  .spotlight { grid-template-columns: 200px 1fr; gap: var(--space-8);
    padding: var(--space-7); }
  .spotlight__cover { grid-column: 1; grid-row: 1; }
  .spotlight__body  { grid-column: 2; grid-row: 1; }
}
@media (min-width: 940px) {
  .spotlight { grid-template-columns: 260px 1fr; gap: var(--space-10);
    padding: var(--space-8); }
}

/* Capped to a readable measure and held at the inline-START, so the text sits
   beside the cover and any slack falls at the reading end rather than in front
   of it. */
.spotlight__body { min-width: 0; max-width: 46ch; justify-self: start; }
.spotlight__eyebrow { margin-bottom: var(--space-4); }
.spotlight__meta { margin-top: var(--space-4); }
.spotlight__cover { width: 100%; }

/* On phones the cover leads and the text follows - a natural vertical read,
   and it stops the cover being a full-bleed slab. */
@media (max-width: 739px) {
  .spotlight__cover { order: -1; max-width: 190px; margin-inline: auto; }
}

.spotlight h2 { font-size: var(--text-xl); line-height: 1.25; font-weight: 600; }
@media (min-width: 940px) { .spotlight h2 { font-size: var(--text-2xl); } }

/* Each rail says where "all of it" lives, instead of ending in nothing. */
.section__head .tiny { color: var(--text-secondary); text-decoration: none; }
.section__head .tiny:hover { color: var(--text-primary); text-decoration: underline; }

/* ------------------------------------------------------------ rails */
.rail { display: grid; gap: var(--space-4);
  grid-auto-flow: column; grid-auto-columns: 128px;
  overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: var(--space-3); }
.rail > * { scroll-snap-align: start; }
@media (min-width: 740px) {
  .rail { grid-auto-flow: row; grid-auto-columns: auto; overflow-x: visible;
    grid-template-columns: repeat(var(--cols), minmax(0, var(--cover-cap)));
    justify-content: space-between; gap: var(--gap); }
}

.section { margin-top: var(--space-11); }
.section__head { display: flex; align-items: baseline; justify-content: space-between; }
.section__head h2 { font-size: var(--text-lg); line-height: 1.6; font-weight: 600; }

/* ----------------------------------------------------------- readers */
.reader-chrome {
  position: sticky; top: 0; z-index: 30;
  min-height: 52px; display: flex; align-items: center; gap: var(--space-4);
  padding-inline: var(--pad); border-bottom: 1px solid var(--border);
  background: var(--surface); transition: transform .2s ease;
}
.reader-chrome[data-hidden='true'] { transform: translateY(-100%); }

.reader-strip { max-width: var(--w-reader-image); margin-inline: auto; }
.reader-strip img { width: 100%; height: auto; background: var(--surface-sunken); }
/* Product B frames the strip in an elevated container; A1 frames it against
   true black. Aliasing one to the other erases the difference. */
.reader-strip--soft { background: var(--surface-elevated);
  border-radius: var(--radius-md); padding: 8px; }
.reader-strip--soft img { border-radius: 6px; }

.reader-page { position: relative; }
.reader-page__retry {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--space-4);
  background: var(--surface-sunken); min-height: 200px;
}

/* Side rails only exist from XL: below 1120 the strip cannot hold its 900 cap
   AND leave 88px each side. */
.reader-rail { display: none; }
@media (min-width: 1120px) {
  .reader-rail {
    position: fixed; inset-block: 52px 0; width: 180px;
    display: flex; flex-direction: column; align-items: center;
    justify-content: center; gap: 8px; color: var(--text-secondary);
  }
  .reader-rail--prev { right: 0; }   /* RTL: previous is on the RIGHT */
  .reader-rail--next { left: 0; }    /* RTL: next is on the LEFT      */
  .reader-rail .caret { width: 12px; height: 12px; border-width: 2px; }
}

.reader-nav { display: flex; gap: var(--space-4); max-width: var(--w-reader-image);
  margin-inline: auto; padding-block: var(--space-5); }
.reader-nav .button:last-child { margin-inline-start: auto; }

.reader-end { max-width: var(--w-reader-image); margin: var(--space-11) auto; }

/* --------------------------------------------------- text reader (B1) */
.reading-surface { background: var(--surface-reading); }
:root[data-theme='dark'] .reading-surface,
/*
 * THE WARM READING SURFACE.
 *
 * This was `background: #141210` - a hardcoded DARK warm tone applied
 * unconditionally, while the template always sets data-warm. In dark mode
 * it looked right by coincidence; in LIGHT mode it painted a dark panel and
 * left --text-primary (#16161A) on top of it, at about 1.05:1. The novel
 * reader was effectively unreadable in daylight.
 *
 * Warmth was the intent and is kept. It is a token now, so each theme picks
 * its own warm tone and the two can never drift apart again.
 */
.reading-surface[data-warm='true'] { background: var(--surface-reading-warm); }

.prose {
  font-family: var(--font-prose);
  font-size: 19px; line-height: 1.95;
  max-width: var(--w-reading); margin-inline: auto;
  text-align: right; unicode-bidi: isolate;
}
.prose p + p { margin-top: var(--space-6); }

/*
 * A readable measure. Long-form Arabic at full container width is a wall;
 * ~62 characters is the comfortable range, and the reader is the one place
 * on the site where line length actually decides whether people stay.
 */
.prose--reading { max-width: 62ch; margin-inline: auto;
  font-size: var(--text-md); line-height: 1.9; }
@media (max-width: 559px) {
  .prose--reading { font-size: var(--text-base); line-height: 1.85; }
}

/* THE EXTREME TOP OF THE VIEWPORT.
   It was `sticky; top: 52px` inside the reading column, which put it 132px down
   the page and over the artwork - it belonged to the content it was measuring.
   It is page-level chrome now: fixed, at the very top, owned by no page and no
   image, and above every other reader layer. */
.progress-bar {
  position: fixed; inset-inline: 0; top: 0; height: 3px;
  background: transparent;
  z-index: 60;
  pointer-events: none;
}

/* GROWS FROM THE READING ORIGIN. Arabic reads right to left, so progress grows
   from the RIGHT - written logically, so it is also correct the other way round
   rather than accidentally correct in one. */
.progress-bar__fill {
  height: 100%; width: 0;
  margin-inline-start: 0;
  background: var(--interactive);
  transition: width 120ms linear;
}
@media (prefers-reduced-motion: reduce) { .progress-bar__fill { transition: none; } }

/* ==========================================================================
   READER CHROME — a manual toggle, on every size
   ========================================================================== */

/* Hidden means HIDDEN. Fading it left the strip in the layout still taking its
   space, which is not what "give me the page" means. */
/* DESCENDANT, not sibling. The toolbar and the resume prompt are both INSIDE
   `.reader` - checked in the blade rather than assumed, because a `~` here
   silently matches nothing and the bar would simply never hide. */
.reader[data-chrome='hidden'] .reader-bar,
.reader[data-chrome='hidden'] .resume-prompt { display: none; }

.reader-reveal {
  position: fixed; z-index: 41;
  inset-inline-end: var(--space-4);
  bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
  width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface-elevated);
  /* Quiet until wanted: present enough to find, faint enough not to compete
     with the page it is sitting on. */
  color: var(--text-tertiary);
  opacity: .55;
}
.reader-reveal:hover { opacity: 1; color: var(--text-primary); }
.reader-reveal:focus-visible { opacity: 1; outline: 2px solid var(--focus); outline-offset: 2px; }

.progress-percent { font-size: var(--text-xs); color: var(--text-tertiary);
  padding: 6px var(--pad) 0; display: block; text-align: left; direction: ltr; }

/* The pinned contents rail is an XL affordance. The reading measure never
   grows - the width buys a rail, not a longer line. */
.contents-rail { display: none; }
@media (min-width: 1120px) {
  .contents-rail {
    position: fixed; inset-inline-start: var(--space-8); top: 180px; width: 200px;
  }
}

/* ----------------------------------------------------------- footer */
.site-footer {
  border-top: 1px solid var(--border); background: var(--surface-elevated);
  padding: var(--space-8) var(--pad) var(--space-10); margin-top: var(--space-11);
}
.site-footer nav { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-7); }
.site-footer a { font-size: var(--text-sm); color: var(--text-secondary); }

/* ------------------------------------------------- states & feedback */
.skeleton { background: var(--skel); border-radius: var(--radius-sm); }
@media (prefers-reduced-motion: no-preference) {
  .skeleton { animation: pulse 1.4s ease-in-out infinite; }
  @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .55 } }
}

.empty-state { padding-block: var(--space-12); text-align: center; }

.alert {
  padding: var(--space-4) var(--space-5); border-radius: var(--radius-sm);
  border: 1px solid var(--control-border); font-size: var(--text-sm);
}

/* Personalised regions are hidden until /api/me/state answers. The cached
   HTML therefore contains no viewer-specific data at all - it contains an
   empty container. */
/* PERSONAL REGIONS ARE ABOUT VISIBILITY, NOT LAYOUT.
   These two lines decide whether a per-reader region has arrived. The `block`
   is only there to undo the `none` - but it is a LAYOUT declaration, and at
   0,2,0 it outranks any class that gave the element its own display.
   The bell is the one control that is both an `.icon-button` and a
   `[data-personal]` region, so on hydration it silently stopped being a centred
   flex box: its glyph jumped to the corner of the 44px target while every
   neighbouring icon stayed centred. That is what read as the bell "floating"
   away from the utility cluster - measured at 24,0 against siblings at 12,12. */
[data-personal] { display: none; }
[data-personal][data-ready='true'] { display: block; }

/* An element that brought its own layout keeps it when it arrives. */
.icon-button[data-personal][data-ready='true'] {
  display: flex; align-items: center; justify-content: center;
}

/* ======================================================================
   PHASE 5B — search, auth, account, engagement
   ====================================================================== */

/* --------------------------------------------------------- search ---- */
/* The sidebar appears at the SEARCH TRANSITION the contract already fixed:
   740, where the results area can still hold two covers beside a 280px
   sidebar. Below it the same seven dimensions live in the sheet. */
.searchgrid { display: grid; grid-template-columns: 1fr; gap: var(--space-6); }
.search-sidebar { display: none; }
.search-filters-toggle { display: inline-flex; }

@media (min-width: 740px) {
  .searchgrid { grid-template-columns: 280px 1fr; gap: var(--space-8); align-items: start; }
  .search-sidebar {
    display: block; background: var(--surface-elevated);
    border-radius: var(--radius-md); padding: var(--space-6);
  }
  .search-filters-toggle { display: none; }
}
.searchgrid > * { min-width: 0; }

.fblock {
  padding-bottom: var(--space-6); margin-bottom: var(--space-6);
  border-bottom: 1px solid var(--border);
}
.fblock:last-child { border-bottom: 0; margin-bottom: 0; padding-bottom: 0; }

/* A content filter's switch. Not an age gate. */
.sw {
  width: 44px; height: 26px; border-radius: 13px; flex: 0 0 auto;
  background: var(--surface-sunken); border: 1px solid var(--control-border);
  position: relative;
}
.sw::after {
  content: ''; position: absolute; width: 18px; height: 18px; border-radius: 50%;
  background: var(--control-border); top: 3px; right: 3px;
}
.sw--on { background: var(--interactive); border-color: var(--interactive); }
.sw--on::after { right: auto; left: 3px; background: var(--text-inverse); }

input:focus-visible + .chip,
input:focus-visible + .sw,
input:focus-visible + span { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ---------------------------------------------------------- sheet ---- */
/* Scrollable body, PINNED footer. A filter sheet whose apply action is
   off-screen is not a filter sheet - Phase 4J found that the hard way. */
.sheet {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 50;
  background: var(--surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  max-height: 88vh; display: flex; flex-direction: column;
  transform: translateY(100%); transition: transform .2s ease;
}
.sheet[data-open='true'] { transform: translateY(0); }
.sheet__head { padding: var(--space-6) var(--pad) 0; }
.sheet__body { overflow-y: auto; padding: var(--space-5) var(--pad); flex: 1; min-height: 0; }
.sheet__foot {
  padding: var(--space-5) var(--pad) var(--space-7);
  border-top: 1px solid var(--border); background: var(--surface);
}
.grab {
  display: block; width: 36px; height: 4px; border-radius: 2px;
  background: var(--border); margin: 0 auto var(--space-5);
}

/* --------------------------------------------------------- dialog ---- */
.dialog {
  position: fixed; top: 50%; inset-inline-start: 50%;
  transform: translate(50%, -50%); width: min(420px, calc(100vw - 32px));
  background: var(--surface); border-radius: var(--radius-lg);
  padding: var(--space-8); z-index: 50;
}
.dialog[hidden] { display: none; }

/* -------------------------------------------------------- account ---- */
.account-layout { display: grid; grid-template-columns: 1fr; gap: var(--space-6); }
/* A GRID ITEM DEFAULTS TO `min-width: auto`, WHICH MEANS "AS WIDE AS MY
   WIDEST CHILD". So the moment this column held something that scrolls - a
   ladder of six milestones - the column grew to the ladder's full width and
   took the entire page sideways with it at 390. The child's own
   `overflow-x: auto` never got a chance to do anything, because it was never
   narrower than its content.
   This is the whole fix for horizontal overflow on the account pages, and it
   is one line because the containment was always correct - the container just
   refused to be smaller than what it contained. */
.account-layout > * { min-width: 0; }
@media (min-width: 940px) {
  .account-layout { grid-template-columns: 240px 1fr; gap: var(--space-10); align-items: start; }
  /* A sidebar: same chips, one per line. The mobile strip wraps rather than
     scrolls, so there is no overflow left to reset here. */
  .account-tabs { flex-direction: column; flex-wrap: nowrap; gap: 2px; }
}
/* WRAPPED, not scrolled. Five tabs need ~550px and a phone has ~360px of
   content width, so a single row put the last two off-screen with nothing to
   say they were there. Wrapping costs one line of vertical space and makes
   every tab visible without any interaction at all.

   No border-bottom: a rule under a strip that wraps has no edge to sit on. The
   current page is marked the same way the desktop sidebar marks it. */
.account-tabs {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
}
.account-tabs a {
  min-height: 44px; display: flex; align-items: center;
  padding-inline: var(--space-5); border-radius: var(--radius-sm);
  font-size: var(--text-base); color: var(--text-secondary);
  /* Labels stay whole: a tab whose text is cut is not a tab you can read. */
  white-space: nowrap;
}
.account-tabs a[aria-current='page'] {
  color: var(--text-primary); font-weight: 600;
  background: var(--surface-elevated);
}
.account-tabs a:focus-visible {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

.list-row {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-4) 0; border-bottom: 1px solid var(--border); min-height: 64px;
}
.list-row .cover { width: 44px; flex: 0 0 auto; }
.list-row__body { flex: 1; min-width: 0; }

/* ----------------------------------------------------- engagement ---- */
.comment { padding: var(--space-5) 0; border-bottom: 1px solid var(--border); }
.comment__meta { font-size: var(--text-xs); color: var(--text-tertiary); }
.comment--pending { border-inline-start: 2px solid var(--control-border);
  padding-inline-start: var(--space-4); }

.rating-control { display: flex; gap: 4px; }
.rating-control button { min-width: 44px; min-height: 44px; font-size: var(--text-lg); }

.field-group { margin-top: var(--space-5); }
.field-error { color: var(--danger, #B3261E); font-size: var(--text-sm); margin-top: 6px; }


/* ------------------------------------------------------ account identity */
/* A quiet header, not a profile banner - Wave 2 owns public profiles. */
.account-identity-bar {
  display: flex; align-items: center; gap: var(--space-5);
  margin-top: var(--space-6); padding: var(--space-6);
  background: var(--surface-elevated); border-radius: var(--radius-md);
}
/* An outline icon here read as a loading spinner. An initial reads as a
   person - and needs no storage, which matters because user media has none
   yet. This is the finished state, not a waiting one. */
.account-identity-bar__mark {
  width: 56px; height: 56px; flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface-sunken); color: var(--text-secondary);
  font-family: var(--font-ui); font-size: var(--text-lg);
  font-weight: 500; line-height: 1; user-select: none;
}
.account-identity-bar__text { min-width: 0; }
.account-identity-bar__name {
  font-size: var(--text-md); font-weight: 600; line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The profile identity block. It was a flex row with a raw file input in
   it, which on an RTL page renders the browser own English chrome in the
   middle of Arabic. Given a row of its own it at least sits straight. */
.identity-edit {
  display: flex; gap: var(--space-6); align-items: center; flex-wrap: wrap;
}
.identity-edit__mark { width: 72px; height: 72px; font-size: var(--text-xl); }
.identity-edit input[type='file'] { max-width: 100%; }

/* The spacer. Seven places put this on the second item in a flex row meaning
   "and you go to the far end" - and it had no rule, so the login footer read as
   one run-on string with two links stuck together. */
.sp { margin-inline-start: auto; }

/* The remove glyph on an applied filter chip. Also undefined, so an applied
   filter drew no affordance at all - the link worked, but nothing said so.
   Built from borders rather than a character so it needs no icon font and
   inherits the chip's colour. */
.x {
  inline-size: 8px; block-size: 8px; display: inline-block;
  position: relative; margin-inline-start: var(--space-3);
  vertical-align: middle; opacity: .75;
}
.x::before,
.x::after {
  content: ''; position: absolute; inset-block-start: 3px; inset-inline-start: -1px;
  inline-size: 10px; block-size: 1.5px; background: currentColor;
}
.x::before { transform: rotate(45deg); }
.x::after  { transform: rotate(-45deg); }

/* An uploaded avatar occupies the same box as the initial that stands in for
   it, so the page does not move when one becomes the other. Cropped square by
   the service, so object-fit only guards against a stale object. */
.identity-photo {
  object-fit: cover; background: var(--surface-sunken); border: 1px solid var(--border);
}

/* ---------------------------------------------------------- avatar ---- */
/* One box, three sizes, two contents. The uploaded picture and the fallback
   mark occupy exactly the same space, so a page does not move when one becomes
   the other - which is the whole reason the fallback has a size at all. */
.avatar {
  display: inline-grid; place-items: center; flex: 0 0 auto;
  border-radius: 999px; overflow: hidden;
  background: var(--surface-sunken); border: 1px solid var(--border);
  object-fit: cover;
}
.avatar--sm { width: 24px; height: 24px; border-width: 1px; }
.avatar--md { width: 40px; height: 40px; }
.avatar--lg { width: 56px; height: 56px; }
@media (min-width: 740px) { .avatar--lg { width: 72px; height: 72px; } }

/* The mark is authored with fill:currentColor, so ONE colour token drives it
   and it follows a theme change immediately - no second file, no raster pair,
   no JavaScript. Never a literal colour here: the token is the contract. */
.avatar--fallback { color: var(--avatar-fallback); }
.avatar--fallback svg { width: 52%; height: 52%; display: block; }
.avatar--fallback .icon-avatar { border-color: currentColor; }

/* The paged-mode chunk pager. Same language as the chapter nav above it, so a
   reader does not have to learn a second set of controls inside one page. */
.reader-pager {
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-5); padding: var(--space-6) 0;
}

/* ==========================================================================
   TITLE PAGE — identity, facts, panels
   ========================================================================== */

/* The cover carries the page. It was 260px for both kinds, which made a novel
   look like a thumbnail of itself: a novel cover is the ONLY picture the work
   has, where a manga has a whole strip behind it. */
.identity {
  display: grid; grid-template-columns: 1fr; gap: var(--space-6);
  align-items: start;
}
.identity__cover { max-width: 260px; }
@media (min-width: 740px) {
  .identity { grid-template-columns: 300px 1fr; gap: var(--space-9); }
  .identity__cover { max-width: none; }
  .identity--novel { grid-template-columns: 340px 1fr; }
}
@media (min-width: 1120px) {
  .identity { grid-template-columns: 340px 1fr; }
  .identity--novel { grid-template-columns: 380px 1fr; }
}
.identity__title { font-size: var(--text-2xl); line-height: 1.25; margin-top: var(--space-2); }
.identity__alt { margin-top: var(--space-2); max-width: 52ch; }

/* Facts read as a row of small labelled numbers, not a table. */
.factbar {
  display: flex; flex-wrap: wrap; gap: var(--space-5) var(--space-7);
  margin-top: var(--space-5);
}
.factbar__item dt { color: var(--text-secondary); font-size: var(--text-xs); }
.factbar__item dd { margin: 2px 0 0; font-weight: 600; }

.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-5); list-style: none; }
.chip {
  display: inline-flex; align-items: center; min-height: 32px;
  padding-inline: var(--space-4); border-radius: 999px;
  background: var(--surface-sunken); border: 1px solid var(--border);
  font-size: var(--text-sm); color: var(--text-secondary);
}
a.chip:hover { color: var(--text-primary); border-color: var(--interactive); }
.chip--kind { background: var(--surface-elevated); color: var(--text-primary); font-weight: 600; }

.credits { margin-top: var(--space-7); display: grid; gap: var(--space-4); }
.credits__row { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-4); align-items: baseline; }
.credits__row dt { color: var(--text-secondary); }
.credits__row dd { margin: 0; }
@media (max-width: 520px) { .credits__row { grid-template-columns: 1fr; gap: 2px; } }

/* DETAILS / CHAPTERS. The :target rules are the no-JavaScript path: the
   anchors work on their own, and the script only adds the aria bookkeeping. */
.panels { margin-top: var(--space-9); }
.panels__tabs { display: flex; gap: var(--space-2); border-bottom: 1px solid var(--border); }
.panels__tab {
  min-height: 48px; display: inline-flex; align-items: center;
  padding-inline: var(--space-5); color: var(--text-secondary);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.panels__tab[aria-selected='true'] { color: var(--text-primary); font-weight: 600; border-bottom-color: var(--interactive); }
.panels__tab:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.panels__panel { padding-top: var(--space-7); }
.panels__panel[hidden] { display: none; }

/* ==========================================================================
   READER — chrome that gets out of the way
   ========================================================================== */

.reader { padding-bottom: 96px; }

.reader-head {
  display: flex; align-items: baseline; gap: var(--space-4);
  padding-top: var(--space-5); padding-bottom: var(--space-4);
  flex-wrap: wrap;
}
.reader-head__work { font-weight: 600; }
.reader-head__chapter { color: var(--text-secondary); font-size: var(--text-sm); }

.reader-bar {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 40;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); padding: var(--space-2) var(--space-4);
  padding-bottom: max(var(--space-2), env(safe-area-inset-bottom));
  background: var(--surface); border-top: 1px solid var(--border);
}
.reader-bar__group { display: flex; align-items: center; gap: var(--space-1); }
.reader-bar__group--center { flex: 1; justify-content: center; }
.reader-bar__btn {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; min-width: 56px; min-height: 48px; padding-inline: var(--space-3);
  border-radius: var(--radius-sm); color: var(--text-secondary); background: none; border: 0;
}
.reader-bar__btn:hover { color: var(--text-primary); background: var(--surface-sunken); }
.reader-bar__btn:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.reader-bar__btn.is-disabled { opacity: .35; }
.reader-bar__label { font-size: 11px; }
/* Below 420px the labels are what overflows, not the buttons. */
/* AT 390 THE THREE GROUPS HAVE TO FIT, AND THE TARGETS STAY 44px.
   Measured: the previous-chapter link sat at left:-14, so 14px of a 44px target
   was off the screen. Eight controls at 44 is 352; the bar's 16px side padding
   and the 8px gaps between the three groups took it past 390.
   The padding and the gaps give way - never the target. */
/* AT 390 THE THREE GROUPS HAVE TO FIT, AND THE TARGETS STAY 44px.
   Measured: the previous-chapter link sat at left:-18, so 18px of a 44px
   target was off the screen - and the three groups were CONTIGUOUS, which is
   what gave it away. `space-between` was distributing nothing because the
   centre group was not absorbing the slack, so the row behaved as one 352px
   block that overflowed the start edge instead of spreading across 390.
   The centre group takes the free space; the nav groups keep their size; the
   targets never shrink. */
@media (max-width: 420px) {
  .reader-bar__label { display: none; }
  .reader-bar__btn { min-width: 44px; padding-inline: 0; }
  .reader-bar { padding-inline: var(--space-1, 4px); gap: 0; }
  .reader-bar__group { gap: 0; }
  .reader-bar__group--nav { flex: 0 0 auto; }
  .reader-bar__group--center { flex: 1 1 0; min-width: 0; }
}

/* ==========================================================================
   RESUME PROMPT - an offer, never an action taken on the reader's behalf
   ========================================================================== */

/* Fixed, so its arrival after the state call cannot shift the pages under
   somebody who has already started reading. Above the reader bar, never over
   it: the way out stays reachable. */
.resume-prompt {
  position: fixed; z-index: 39;
  inset-inline: var(--space-4);
  bottom: calc(72px + env(safe-area-inset-bottom));
  margin-inline: auto;
  max-width: 460px;

  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  padding: var(--space-4);

  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  /* This is not a shadowed product; a hairline and a raised surface are
     enough to read as "above" against the page. */
}
.resume-prompt[hidden] { display: none; }

.resume-prompt__text { flex: 1 1 180px; min-width: 0; }
.resume-prompt__q { font-size: var(--text-base); line-height: 1.5; }
.resume-prompt__where { margin-top: 2px; color: var(--text-tertiary); }

.resume-prompt__actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* On a narrow phone the two answers get the full width rather than being
   squeezed into half-buttons beside a wrapped question. */
@media (max-width: 419px) {
  .resume-prompt__actions { width: 100%; }
  .resume-prompt__actions .button { flex: 1; }
}

/* Focus mode fades the chrome. An unanswered question is chrome. */
.reader[data-focus-mode='on'] .resume-prompt { opacity: 0; pointer-events: none; }

/* FOCUS MODE. Everything that is not the reading surface goes away; one tap
   anywhere brings it back, so it can never become a trap. */
/* FOCUS MODE RECEDES THE CHROME. IT NEVER REMOVES THE WAY BACK.
   This used to be opacity:0 + pointer-events:none, with the only "off" switch
   inside the panel that the bar opens - so one tap could leave a reader in a
   chapter with no navigation and no idea why. It fades now, and any intent
   brings it straight back. */
.reader[data-focus-mode='on'] .reader-bar,
.reader[data-focus-mode='on'] .reader-head { opacity: 0; }
.reader[data-focus-mode='on'] .reader-bar { pointer-events: none; }

/* Intent: the pointer near the bar, a tap, a key, or a scroll up. */
.reader[data-focus-mode='on'][data-chrome='shown'] .reader-bar,
.reader[data-focus-mode='on'][data-chrome='shown'] .reader-head { opacity: 1; }
.reader[data-focus-mode='on'][data-chrome='shown'] .reader-bar { pointer-events: auto; }
.reader[data-focus-mode='on'] .reader-bar:hover,
.reader[data-focus-mode='on'] .reader-bar:focus-within { opacity: 1; pointer-events: auto; }

.reader[data-focus-mode='on'] { padding-bottom: var(--space-7); }
body:has(.reader[data-focus-mode='on']) .site-header,
body:has(.reader[data-focus-mode='on']) .site-footer { display: none; }
.reader-bar, .reader-head { transition: opacity .18s ease; }

/* THE ESCAPE HATCH. Always visible while focus mode is on, and the one thing
   on screen that is guaranteed to bring the controls back. */
.reader-escape { display: none; }
.reader[data-focus-mode='on'] .reader-escape {
  display: flex; align-items: center; gap: var(--space-2);
  position: fixed; inset-block-end: var(--space-4); inset-inline-start: var(--space-4);
  z-index: 40; min-height: 40px; padding-inline: var(--space-4);
  border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text-secondary);
  font-size: var(--text-sm); box-shadow: var(--shadow-sm);
  opacity: .75;
}
.reader[data-focus-mode='on'] .reader-escape:hover,
.reader[data-focus-mode='on'] .reader-escape:focus-visible { opacity: 1; color: var(--text-primary); }

/* ------------------------------------------- a segment that looks chosen --
   The account settings render <label><input hidden><span aria-current>>, and
   a span is neither an `a` nor a `button` - so the old rule painted a colour
   onto a box with no height and no padding. Everything interactive inside a
   segmented control gets the box now, whatever tag it happens to be. */
.segmented { display: inline-flex; flex-wrap: wrap; gap: 2px;
  border: 1px solid var(--control-border); border-radius: var(--radius-sm);
  padding: 2px; background: var(--surface-sunken); }
.segmented a, .segmented button, .segmented label, .segmented label > span {
  min-height: 40px; display: flex; align-items: center; justify-content: center;
  padding-inline: 16px; font-size: var(--text-sm); border-radius: calc(var(--radius-sm) - 2px);
  color: var(--text-secondary); white-space: nowrap;
}
.segmented label { padding: 0; }
.segmented label > span { width: 100%; }
.segmented a:hover, .segmented button:hover, .segmented label:hover > span {
  color: var(--text-primary); background: var(--surface);
}
.segmented [aria-current='true'], .segmented [aria-pressed='true'] {
  background: var(--interactive); color: var(--text-inverse); font-weight: 600;
}
.segmented label:hover > span[aria-current='true'] {
  background: var(--interactive); color: var(--text-inverse);
}
/* The real radio is visually hidden, so the ring has to come from the label. */
.segmented label:focus-within > span {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

/* ---------------------------------------- the reader's own setting chips --
   These had NO css at all, so aria-current was invisible: the setting applied
   and the panel gave no sign of which one was chosen. */
.setting-row { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.setting-chip {
  min-height: 40px; display: inline-flex; align-items: center; padding-inline: 14px;
  border-radius: 999px; border: 1px solid var(--control-border);
  background: var(--surface); color: var(--text-secondary);
  font-size: var(--text-sm); white-space: nowrap;
}
.setting-chip:hover { color: var(--text-primary); border-color: var(--text-secondary); }
.setting-chip:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.setting-chip[aria-current='true'] {
  background: var(--interactive); color: var(--text-inverse);
  border-color: var(--interactive); font-weight: 600;
}
.setting-group + .setting-group { margin-top: var(--space-6); }
.setting-group__label { font-size: var(--text-sm); color: var(--text-secondary); }

/* ---- drawers: chapters and settings ---- */
.reader-drawer { position: fixed; inset: 0; z-index: 60; }
.reader-drawer[hidden] { display: none; }
.reader-drawer__scrim { position: absolute; inset: 0; background: rgba(0,0,0,.45); }
.reader-drawer__panel {
  position: absolute; inset-inline: 0; bottom: 0; max-height: 78vh;
  display: flex; flex-direction: column;
  background: var(--surface); border-top-left-radius: var(--radius-lg);
  border-top-right-radius: var(--radius-lg); padding: var(--space-5) var(--pad) var(--space-7);
}
@media (min-width: 740px) {
  .reader-drawer__panel {
    inset-inline: auto 0; top: 0; bottom: 0; width: min(420px, 92vw); max-height: none;
    border-radius: 0;
  }
}
.reader-drawer__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-5); }
.reader-drawer__body { overflow-y: auto; margin-top: var(--space-4); }
.chapter-row.is-current { background: var(--surface-elevated); font-weight: 600; }

/* ==========================================================================
   THE CHAPTER INDEX - a window onto a query, not a list in the document
   ========================================================================== */

.chapter-index__controls {
  display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: wrap;
}
.chapter-index__search {
  flex: 1 1 220px; min-width: 0;
  justify-content: flex-start;
  background: var(--surface-sunken);
}

/* A DIVIDED PAIR, not two buttons with a gap: one outline, one hairline, two
   states - so it reads as a single control that is currently set to one of
   them. The same reasoning as the fact panel above it. */
.chapter-index__order {
  display: inline-flex; flex: none;
  border: 1px solid var(--control-border, var(--border));
  border-radius: var(--control-r);
  overflow: hidden;
  align-self: stretch;
}
.chapter-index__order-btn {
  /* The SAME height as the search beside it. Two controls sharing a row that
     do not share a height read as one control and one afterthought. */
  min-height: var(--control-h);
  padding: 0 var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  background: transparent;
  border: 0;
  cursor: pointer;
}
.chapter-index__order-btn + .chapter-index__order-btn {
  border-inline-start: 1px solid var(--border);
}
.chapter-index__order-btn.is-on {
  background: var(--surface-elevated);
  color: var(--text-primary);
  font-weight: 600;
}

.chapter-row__label { min-width: 0; }

/* Always reserved, so the column stays aligned and nothing shifts when the
   private state arrives after paint. */
.chapter-row__mark {
  width: 14px; height: 14px; flex: none;
  background: currentColor; opacity: 0;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M9.6 16.2 4.8 11.4l-1.4 1.4 6.2 6.2L21 7.6l-1.4-1.4z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M9.6 16.2 4.8 11.4l-1.4 1.4 6.2 6.2L21 7.6l-1.4-1.4z'/%3E%3C/svg%3E");
}

/* FINISHED. It recedes - and it is a check as well as a weight, never a colour
   on its own. */
.chapter-row.is-read .chapter-row__mark { opacity: .5; }
.chapter-row.is-read .chapter-row__label { color: var(--text-tertiary); font-weight: 400; }

/* WHERE THEY ARE. Said in words, not only drawn. */
.chapter-row__state { color: var(--text-tertiary); white-space: nowrap; }

.chapter-row.is-current-read { background: var(--surface-elevated); }
.chapter-row.is-current-read .chapter-row__label { font-weight: 600; color: var(--text-primary); }
.chapter-row.is-current-read .chapter-row__state { color: var(--text-secondary); }
.chapter-row.is-current-read { box-shadow: inset 2px 0 0 0 var(--text-secondary); }
[dir='rtl'] .chapter-row.is-current-read { box-shadow: inset -2px 0 0 0 var(--text-secondary); }

/* THE CONTINUE CTA - an action and its destination, not one long label. */
.cta-read {
  flex-direction: column; gap: 1px;
  padding-block: var(--space-2);
  line-height: 1.25;
}
.cta-read__action { font-weight: 600; }
.cta-read__where { opacity: .75; font-weight: 400; }
.cta-read__where[hidden] { display: none; }

/* Quiet: it is an offer, not the reason the page exists. */
.chapter-index__jump {
  display: block; width: 100%;
  margin-top: var(--space-4);
  min-height: var(--control-h-sm, 32px);
  padding: 0 var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--control-r);
  cursor: pointer;
}
.chapter-index__jump[hidden] { display: none; }

.chapter-index__more {
  justify-content: center; width: 100%;
  color: var(--text-secondary);
}
.chapter-index__more[disabled] { opacity: .55; cursor: default; }

.chapter-index__empty {
  margin-top: var(--space-5);
  text-align: center;
  color: var(--text-tertiary);
}

/* WHERE THIS READER STOPPED. A rule on the inline start edge rather than a
   colour: this chrome has no hue, so a coloured row would be the only saturated
   thing on the page and would read as a warning rather than a bookmark. */
.chapter-row.is-last-read {
  background: var(--surface-elevated);
  box-shadow: inset 2px 0 0 0 var(--text-secondary);
}
[dir='rtl'] .chapter-row.is-last-read {
  box-shadow: inset -2px 0 0 0 var(--text-secondary);
}

/* ==========================================================================
   READER SETTINGS — the stylesheet owns what every step MEANS
   ========================================================================== */

/* image width */
.reader[data-image-width='fit']     .reader-page img { width: 100%; max-width: 900px; height: auto; }
.reader[data-image-width='natural'] .reader-page img { width: auto; max-width: 100%; height: auto; }
.reader[data-image-width='full']    .reader-page img { width: 100%; max-width: none; height: auto; }
.reader-page { display: flex; justify-content: center; }

/* image gap */
.reader[data-image-gap='none']   .reader-page + .reader-page { margin-top: 0; }
.reader[data-image-gap='small']  .reader-page + .reader-page { margin-top: var(--space-3); }
.reader[data-image-gap='medium'] .reader-page + .reader-page { margin-top: var(--space-6); }
.reader[data-image-gap='large']  .reader-page + .reader-page { margin-top: var(--space-9); }

/* reading background. `theme` deliberately sets nothing - it is the page. */
.reader[data-reading-bg='white'] .reader-strip { background: #FFFFFF; }
.reader[data-reading-bg='black'] .reader-strip { background: #000000; }
.reader[data-reading-bg='sepia'] .reader-strip { background: #F4ECD8; }
.reader-strip { padding-block: var(--space-4); border-radius: var(--radius-lg); }

.reader-paging {
  display: flex; align-items: center; justify-content: center; gap: var(--space-5);
  margin-top: var(--space-6);
}

/* ---- novel typography ---- */
.reader[data-font-family='naskh']  .prose--reading { font-family: 'Noto Naskh Arabic', serif; }
.reader[data-font-family='plex']   .prose--reading { font-family: 'IBM Plex Sans Arabic', sans-serif; }
.reader[data-font-family='system'] .prose--reading { font-family: system-ui, sans-serif; }

/* Bounded steps, so no combination can reach unreadable. */
.reader[data-font-size='sm'] .prose--reading { font-size: 17px; }
.reader[data-font-size='md'] .prose--reading { font-size: 19px; }
.reader[data-font-size='lg'] .prose--reading { font-size: 21px; }
.reader[data-font-size='xl'] .prose--reading { font-size: 24px; }

.reader[data-line-height='tight']  .prose--reading { line-height: 1.7; }
.reader[data-line-height='normal'] .prose--reading { line-height: 1.95; }
.reader[data-line-height='loose']  .prose--reading { line-height: 2.25; }

/* The measure is capped in ch at every step: a wide screen buys a wider
   MARGIN, never a longer line, because line length is what makes long-form
   Arabic tiring rather than the window. */
.reader[data-measure='narrow'] .prose--reading { max-width: 48ch; }
.reader[data-measure='medium'] .prose--reading { max-width: 62ch; }
.reader[data-measure='wide']   .prose--reading { max-width: 76ch; }
.prose--reading { margin-inline: auto; }

.reader[data-paragraph-gap='tight']  .prose--reading p + p { margin-top: var(--space-4); }
.reader[data-paragraph-gap='normal'] .prose--reading p + p { margin-top: var(--space-6); }
.reader[data-paragraph-gap='loose']  .prose--reading p + p { margin-top: var(--space-8); }

.reader[data-text-align='start']   .prose--reading { text-align: start; }
.reader[data-text-align='justify'] .prose--reading { text-align: justify; text-justify: inter-word; }

.reader-text__title { font-size: var(--text-xl); line-height: 1.4; font-weight: 600; }

/* ---- toolbar icons: drawn, never imported ---- */
.icon-book, .icon-list, .icon-comment, .icon-sliders, .icon-expand, .icon-close {
  width: 20px; height: 20px; display: block; position: relative; color: currentColor;
}
.icon-book::before {
  content: ''; position: absolute; inset: 2px 3px;
  border: 1.5px solid currentColor; border-radius: 2px;
}
.icon-book::after {
  content: ''; position: absolute; top: 2px; bottom: 2px; left: 50%;
  border-left: 1.5px solid currentColor;
}
.icon-list::before, .icon-list::after {
  content: ''; position: absolute; left: 3px; right: 3px; height: 1.5px; background: currentColor;
}
.icon-list::before { top: 5px; box-shadow: 0 5px 0 currentColor; }
.icon-list::after { bottom: 5px; }
.icon-comment::before {
  content: ''; position: absolute; inset: 3px 2px 6px;
  border: 1.5px solid currentColor; border-radius: 3px;
}
.icon-comment::after {
  content: ''; position: absolute; bottom: 3px; left: 6px;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-top: 4px solid currentColor;
}
.icon-sliders::before, .icon-sliders::after {
  content: ''; position: absolute; left: 2px; right: 2px; height: 1.5px; background: currentColor;
}
.icon-sliders::before { top: 6px; }
.icon-sliders::after { bottom: 6px; }
.icon-expand::before {
  content: ''; position: absolute; inset: 3px;
  border: 1.5px solid currentColor; border-radius: 2px;
}
.icon-close::before, .icon-close::after {
  content: ''; position: absolute; top: 9px; left: 2px; right: 2px;
  height: 1.5px; background: currentColor;
}
.icon-close::before { transform: rotate(45deg); }
.icon-close::after { transform: rotate(-45deg); }

/* A house, in the same hairline language as the other reader icons: a roof
   drawn with a rotated square corner over a body. */
.icon-home {
  width: 18px; height: 18px; position: relative; display: inline-block;
}
/* PHYSICAL properties, deliberately. This was the only icon in the file built
   from `inset-inline-start` / `border-inline-start`; in RTL those resolve to the
   right-hand side, so the rotated corner that draws the roof pointed the wrong
   way and the house read as an arrow. A house is a symbol, and symbols do not
   mirror with writing direction - every other icon here already uses `left`. */
.icon-home::before {
  content: ''; position: absolute; left: 1px; top: 7px;
  width: 16px; height: 10px; border: 1.5px solid currentColor;
  border-top: 0; border-radius: 0 0 2px 2px;
}
.icon-home::after {
  content: ''; position: absolute; left: 2px; top: 1px;
  width: 12px; height: 12px; border-top: 1.5px solid currentColor;
  border-left: 1.5px solid currentColor; transform: rotate(45deg);
  transform-origin: center;
}

/* A comment thread needs more room than a chapter list. On a phone it is a
   full-height sheet either way. */
@media (min-width: 740px) {
  .reader-drawer--wide .reader-drawer__panel { width: min(560px, 92vw); }
}

/* ------------------------------------------------------------ comments --
   A comment is an avatar, an identity line, and a body - in that order and at
   that hierarchy. It used to be one grey metadata line with the name, the time
   and the moderation state all run together at the same weight. */
.comment-list { margin-top: var(--space-5); }
.comment {
  display: grid; grid-template-columns: auto 1fr; gap: var(--space-4);
  padding: var(--space-5) 0; border-bottom: 1px solid var(--border);
}
.comment:last-child { border-bottom: 0; }
.comment__avatar { grid-row: 1 / -1; }
.comment__head { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-3); }
.comment__author { font-weight: 600; color: var(--text-primary); font-size: var(--text-sm); }
.comment__when { color: var(--text-secondary); }
.comment__text {
  margin-top: 6px; color: var(--text-primary); line-height: 1.7;
  /* Long comments stay readable instead of running the full page width. */
  max-width: 68ch; overflow-wrap: anywhere;
}
.comment__badge {
  color: var(--text-secondary); border: 1px solid var(--border);
  border-radius: 999px; padding: 2px 10px;
}
/* A held comment is the author's own, and should read as waiting rather than
   as an error. */
.comment--pending { opacity: .82; }

/* The composer. A bare textarea reads as an unfinished form; this reads as a
   place to write. */
.composer {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-4);
  margin-top: var(--space-6); width: 100%;
}
.composer__field {
  border: 1px solid var(--control-border); border-radius: var(--radius-md);
  background: var(--surface); overflow: hidden;
  /* Without this a grid child refuses to shrink below its content, and the
     whole composer collapses to the width of its placeholder. */
  min-width: 0;
}
.composer__field:focus-within { border-color: var(--text-secondary); }
.composer__input {
  display: block; width: 100%; border: 0; background: transparent;
  padding: 12px 14px; height: 96px; min-height: 96px; resize: vertical;
  color: var(--text-primary); font: inherit; line-height: 1.7;
  /* `ui` letter-spacing is for labels; on running text it reads as spaced out. */
  letter-spacing: normal;
}
.composer__input:focus { outline: none; }
.composer__note {
  padding: 8px 14px; border-top: 1px solid var(--border);
  color: var(--text-secondary); background: var(--surface-sunken);
}
@media (max-width: 560px) {
  .comment { grid-template-columns: auto 1fr; gap: var(--space-3); }
  .composer { grid-template-columns: 1fr; }
  .composer__avatar { display: none; }
}

/* ------------------------------------------------------------- toast ----
   Quiet by design: one line, bottom centre, out of the way of the reader
   toolbar, and it never stacks. */
.toast-host {
  position: fixed; inset-block-end: calc(var(--space-7) + env(safe-area-inset-bottom));
  inset-inline: 0; display: flex; justify-content: center;
  pointer-events: none; z-index: 60; padding-inline: var(--space-5);
}
.toast {
  max-width: min(420px, 100%); padding: 10px var(--space-5);
  border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text-primary);
  font-size: var(--text-sm); box-shadow: var(--shadow-md);
  opacity: 0; transform: translateY(6px);
  transition: opacity .18s ease, transform .18s ease;
}
.toast[data-shown] { opacity: 1; transform: none; }
.toast--error { border-color: var(--danger, #B3261E); color: var(--danger, #B3261E); }
@media (prefers-reduced-motion: reduce) {
  .toast { transition: opacity .01s; transform: none; }
}
/* The reader has its own bottom bar; keep the toast clear of it. */
body:has(.reader) .toast-host { inset-block-end: calc(var(--space-10) + 44px); }

/* ---------------------------------------------------------- featured ----
   A CENTRED CINEMATIC CAROUSEL, not a rail.

   One slide occupies the middle; the previous and next sit in the margins,
   smaller and dimmed, so the component says "there is more" without asking the
   reader to compare five things at once.

   The centring is native: a scroll-snap container whose slides are narrower
   than it, with inline padding equal to half the leftover space. A slide can
   only come to rest in the middle. Touch swipe, trackpad and momentum come free
   with that, and nothing measures from a physical edge, so RTL is correct by
   construction rather than by a mirrored stylesheet.  */
.featured {
  position: relative;
  margin-block: var(--space-6) var(--space-9);
  /* The width one slide occupies, and therefore how much of its neighbours
     shows. Widened at each breakpoint below.

     A LENGTH, NOT A PERCENTAGE, and that is the whole point. As a percentage
     this value was resolved against two different boxes: the track's PADDING
     resolves percentages against its parent, while a flex-basis resolves them
     against the track's own CONTENT box - which the padding had just shrunk.
     The slide therefore computed itself smaller than intended, and at 1920 the
     "dominant" slide was 40% of the band. Viewport units resolve identically
     in both places. */
  --slide: min(560px, 86vw);
  --peek: calc((100% - var(--slide)) / 2);
}
.featured:focus { outline: none; }

/* Full-bleed: a cinematic band should not be inset by the page gutter. */
.featured__viewport {
  width: 100vw;
  margin-inline-start: 50%;
  transform: translateX(50%);
}
html[dir="ltr"] .featured__viewport { transform: translateX(-50%); }

.featured__track {
  display: flex;
  gap: var(--space-5);
  overflow-x: auto;
  /* `none`, NOT `contain`. Both stop the scroll chaining to the parent, but
     `contain` still allows this scroller's OWN default overflow behaviour - and
     on a horizontal scroller that behaviour is the browser's back-swipe. With
     `contain` here, swiping past the last card navigated to the previous page;
     the track was doing it, not the document. */
  overscroll-behavior-inline: none;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding-inline: var(--peek);
  padding-block: var(--space-4);
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.featured__track::-webkit-scrollbar { display: none; }

/* A drag across the band is a drag, not a text selection. Selection is what
   takes the pointer away from the track mid-gesture, which is how the carousel
   used to end up stuck. */
.featured__track { -webkit-user-select: none; user-select: none; }

.featured__slide {
  position: relative;
  /* The ground UNDER the backdrop. Several works have no cover yet, and their
     slides were empty outlined rectangles - which reads as broken rather than
     as "no picture". */
  background:
    radial-gradient(120% 100% at 80% 0%, color-mix(in srgb, var(--interactive) 22%, transparent), transparent 70%),
    var(--surface-sunken);
  flex: 0 0 var(--slide);
  scroll-snap-align: center;
  scroll-snap-stop: always;
  border-radius: var(--radius-lg, 18px);
  overflow: hidden;
  isolation: isolate;
  min-height: 300px;
  display: grid;
  /* The neighbours recede. Calm on purpose - a featured area that lurches is
     an interruption, and this one is behind the first thing a reader reads. */
  transform: scale(.93);
  opacity: .45;
  filter: saturate(.65);
  transition: transform .45s ease, opacity .45s ease, filter .45s ease;
}
.featured__slide.is-active {
  transform: scale(1);
  opacity: 1;
  filter: none;
}
/* Without the script nothing is marked active, and a permanently dimmed
   carousel would be worse than an undifferentiated one. */
html:not(.js) .featured__slide { transform: none; opacity: 1; filter: none; }

.featured__backdrop { position: absolute; inset: 0; z-index: 0; }
.featured__backdrop img {
  width: 100%; height: 100%; object-fit: cover;
  /* Enlarged so the blur has material at the edges instead of fading out.
     BRIGHTENED because a lot of cover art is nearly black, and a backdrop you
     cannot see is not a backdrop. */
  transform: scale(1.15);
  filter: blur(22px) saturate(1.25) brightness(1.45);
}

/* Dark where the WORDS are, clear where the ARTWORK is.
   The copy sits on the inline-end side and the cover on the inline-start side,
   so the gradient runs from the reading side outward. `to right` in RTL puts
   the dark stop on the left, which is where the text is - the previous
   direction had it exactly backwards and shaded the cover instead. */
.featured__scrim {
  position: absolute; inset: 0; z-index: 1;
  background:
    linear-gradient(to bottom, rgb(0 0 0 / .05) 0%, rgb(0 0 0 / .48) 100%),
    linear-gradient(to var(--scrim-dir, right),
      rgb(0 0 0 / .72) 0%, rgb(0 0 0 / .42) 52%, rgb(0 0 0 / .06) 100%);
}
html[dir="rtl"] .featured__scrim { --scrim-dir: right; }
html[dir="ltr"] .featured__scrim { --scrim-dir: left; }

/* On a phone the copy is UNDER the cover, not beside it, so the gradient has
   to run down rather than across. */
@media (max-width: 699px) {
  .featured__scrim {
    background: linear-gradient(to bottom,
      rgb(0 0 0 / .18) 0%, rgb(0 0 0 / .45) 45%, rgb(0 0 0 / .78) 100%);
  }
}

.featured__inner {
  position: relative; z-index: 2;
  display: grid; gap: var(--space-6);
  align-items: center;
  padding: var(--space-7) var(--space-6);
  grid-template-columns: 1fr;
  justify-items: center;
  text-align: center;
}

/* The copy sits ON the artwork, so it carries its own colour rather than
   inheriting the page's - the picture underneath is not ours to predict. */
.featured__slide .featured__eyebrow { color: rgb(255 255 255 / .78); }
.featured__slide .featured__title,
.featured__slide .featured__facts,
.featured__slide .featured__synopsis { color: #fff; }
.featured__slide .featured__facts { color: rgb(255 255 255 / .86); }
.featured__slide .featured__synopsis { color: rgb(255 255 255 / .80); }

.featured__cover { width: min(150px, 34vw); }
.featured__body { min-width: 0; max-width: 46ch; }

.featured__eyebrow {
  text-transform: uppercase; letter-spacing: .09em;
  font-size: var(--text-xs); font-weight: 700;
}
.featured__title {
  margin-top: var(--space-2);
  font-size: clamp(1.4rem, 1.1rem + 1.6vw, 2.3rem);
  font-weight: 700; line-height: 1.2;
  text-wrap: balance;
}
.featured__facts {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: center; justify-content: center;
  margin-top: var(--space-3); font-size: var(--text-sm);
}
.featured__synopsis {
  margin-top: var(--space-4); line-height: 1.65;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.featured__chips { margin-top: var(--space-4); justify-content: center; }
.featured__actions { margin-top: var(--space-5); justify-content: center; }

/* The chips and badge sit on artwork too, so they carry their own surface. */
.featured__slide .chip {
  background: rgb(255 255 255 / .14); border-color: rgb(255 255 255 / .26); color: #fff;
}
.featured__slide .chip:hover { background: rgb(255 255 255 / .24); }
.featured__slide .badge { background: rgb(255 255 255 / .18); color: #fff; border-color: transparent; }
.featured__slide .button--secondary {
  background: rgb(255 255 255 / .12); color: #fff; border-color: rgb(255 255 255 / .34);
}
.featured__slide .button--secondary:hover { background: rgb(255 255 255 / .22); }

/* --- MOBILE: one slide dominates, a sliver of the next remains ---------- */
@media (max-width: 559px) {
  /* 88vw left a peek narrower than the gap between slides, so on a phone the
     band read as one card rather than as a carousel. 82vw keeps a visible
     sliver of the next work. */
  .featured { --slide: 82vw; }
  /* The gap eats the peek on a narrow screen: 9vw of margin minus a 24px gap
     left about ten pixels of the next slide, which is not a peek, it is an
     edge. A tighter gap gives the sliver back. */
  .featured__track { gap: var(--space-3); }
  .featured__slide { min-height: 0; }
  .featured__inner { padding: var(--space-6) var(--space-5); gap: var(--space-5); }

  /* Nearly half the slide. At 122px the cover was a thumbnail sitting on top
     of its own atmosphere - the strongest material there is, shown small. */
  .featured__cover { width: min(168px, 44vw); }

  /* The copy is one block, so the eye reads it as one thing rather than as
     five separately centred fragments. */
  .featured__body { display: grid; justify-items: center; gap: 0; }
  .featured__title { font-size: clamp(1.25rem, 1rem + 2.2vw, 1.6rem); }
  .featured__synopsis { -webkit-line-clamp: 3; margin-top: var(--space-3); }
  .featured__facts { margin-top: var(--space-3); }
  .featured__chips { margin-top: var(--space-3); }

  /* Two full-width actions, stacked. Side by side at this width they were two
     short buttons with a gap between them that read as one broken row. */
  .featured__actions { display: grid; width: 100%; gap: var(--space-3); }
  .featured__actions .button { width: 100%; }
}

/* --- TABLET UP: side by side, and the peeks become obvious -------------- */
@media (min-width: 700px) {
  .featured { --slide: min(820px, 78vw); }
  .featured__slide { min-height: 380px; }
  .featured__inner {
    grid-template-columns: 210px 1fr;
    justify-items: start; text-align: start;
    gap: var(--space-8);
    padding: var(--space-8) var(--space-8);
  }
  .featured__cover { width: 100%; }
  .featured__facts, .featured__chips, .featured__actions { justify-content: flex-start; }
}

@media (min-width: 1100px) {
  .featured { --slide: min(1180px, 76vw); }
  .featured__slide { min-height: 420px; }
  .featured__inner { grid-template-columns: 250px 1fr; padding: var(--space-9); }
}

/* --- controls ----------------------------------------------------------- */
.featured__nav {
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-5); margin-top: var(--space-5);
}
.featured__arrow {
  display: grid; place-items: center; width: 40px; height: 40px;
  border: 1px solid var(--border); border-radius: 999px;
  color: var(--text-secondary); background: var(--surface);
  transition: color .15s, border-color .15s, transform .15s;
}
.featured__arrow:hover {
  color: var(--text-primary); border-color: var(--text-secondary); transform: scale(1.06);
}
.featured__arrow:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.featured__dots { display: flex; align-items: center; gap: var(--space-3); }
.featured__dot { display: grid; place-items: center; width: 24px; height: 24px; }
.featured__dot > span {
  display: block; width: 8px; height: 8px; border-radius: 999px;
  background: var(--border); transition: width .25s, background .25s;
}
.featured__dot[aria-selected='true'] > span { background: var(--interactive); width: 22px; }
.featured__dot:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 999px; }

@media (prefers-reduced-motion: reduce) {
  .featured__track { scroll-behavior: auto; }
  .featured__slide { transition: none; }
  .featured__dot > span, .featured__arrow { transition: none; }
}

/* -------------------------------------------------------------- chips ---- */
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip {
  display: inline-flex; align-items: center; min-height: 32px; padding-inline: 12px;
  border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text-secondary); font-size: var(--text-sm);
}
.chip:hover { color: var(--text-primary); border-color: var(--text-secondary); }
.chip:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.badge {
  display: inline-flex; align-items: center; min-height: 26px; padding-inline: 10px;
  border-radius: 999px; background: var(--surface-sunken); color: var(--text-primary);
  font-size: var(--text-sm); font-weight: 600;
}

/* THE COVER IS BOUNDED BY ITS WIDTH, NOT ITS HEIGHT.

   `max-height` capped the height while the width stayed put, so the box stopped
   being 2/3 and became 0.78 - and `object-fit: contain` then letterboxed the
   artwork inside it with 16px of grey down each side. Measured, not guessed.

   Width is the only dimension that can be constrained without breaking the
   ratio, because `aspect-ratio` derives the other one. The fit mode goes back
   to the house default (`cover`), and there is nothing left for a gutter to
   appear in. */
.featured__cover { align-self: center; }

/* A rail of two cards used to stretch across the full grid, leaving most of the
   row blank. Rails pack from the start and scroll when they outgrow the row. */
.rail {
  display: flex; gap: var(--space-5); overflow-x: auto;
  /* Same reasoning as the featured track: a horizontal scroller that reaches
     its end must not hand the gesture to history. */
  overscroll-behavior-inline: none;
  /* START, not space-between. A leftover from when this was a grid: with
     seven cards it is invisible, with two it throws one to each end of the
     row - and Continue Reading is the rail that usually has two. */
  justify-content: flex-start;
  scroll-snap-type: x proximity; scrollbar-width: none; padding-bottom: 4px;
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { flex: 0 0 clamp(140px, 40vw, 180px); scroll-snap-align: start; }

/* ------------------------------------------------------------- stats ----
   Counted facts, not a dashboard. Two of them, because two is what the
   product can honestly answer today. */
.stat-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); }
@media (min-width: 740px) { .stat-row { max-width: 460px; } }
.stat {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--space-5) var(--space-6); border-radius: var(--radius-md);
  background: var(--surface-elevated); border: 1px solid var(--border);
}
.stat:hover { border-color: var(--text-secondary); }
.stat:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.stat__value { font-size: var(--text-2xl); font-weight: 600; color: var(--text-primary); line-height: 1.1; }
.stat__label { font-size: var(--text-sm); color: var(--text-secondary); }

/* ------------------------------------------------- system consolidation --
   These were each defined where they were first needed, which is how a button,
   a chip and an input on the same row ended up three different heights. They
   read from the shared tokens now; a new component inherits the decision
   instead of re-making it slightly differently. */
.button, .input, .field, .composer__input { border-radius: var(--control-r); }
.button { min-height: var(--control-h); }
.chip, .badge { min-height: var(--control-h-sm); }
.setting-chip, .segmented a, .segmented button, .segmented label > span {
  min-height: 40px; border-radius: var(--control-r);
}
.card--soft, .stat, .featured__slide, .composer__field { border-radius: var(--radius-md); }
.stat, .composer__field { padding: var(--card-pad); }
.composer__field { padding: 0; }   /* its input supplies the padding */

/* ONE empty state, everywhere. Three surfaces had three ways of saying
   "nothing here", and one of them said it in muted 11px. */
.empty {
  padding: var(--space-8) var(--space-6); text-align: center;
  color: var(--text-secondary); border: 1px dashed var(--border);
  border-radius: var(--radius-md); background: var(--surface);
}
.empty__title { color: var(--text-primary); font-weight: 600; }
.empty__hint { margin-top: var(--space-2); font-size: var(--text-sm); }

/* ONE loading state. Only where a reader would otherwise see nothing: a
   skeleton over content that is already on the page is decoration. */
.loading {
  display: flex; align-items: center; justify-content: center; gap: var(--space-3);
  padding: var(--space-7); color: var(--text-secondary); font-size: var(--text-sm);
}
.loading::before {
  content: ''; width: 14px; height: 14px; border-radius: 999px;
  border: 2px solid var(--border); border-top-color: var(--text-secondary);
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .loading::before { animation-duration: 2.4s; }
}

/* A failure a reader can act on, rather than a blank space. */
.failed { text-align: center; padding: var(--space-7); color: var(--text-secondary); }
.failed button { margin-top: var(--space-3); }

/* Disabled reads as unavailable everywhere, not just in the reader bar. */
.button[disabled], .button.is-disabled, [aria-disabled='true'] {
  opacity: .45; pointer-events: none;
}

/* -------------------------------------------------- list-row content measure
   A library row spanning 1440px separated the title from its own remove
   action by most of a screen. A list is easier to scan narrow than wide. */
.list-row { max-width: 760px; }
.list-row__body { flex: 1 1 auto; min-width: 0; }
.list-row form, .list-row .tiny { flex: 0 0 auto; }

/* The pager reads as one control, not three floating pieces. */
.pager { gap: var(--space-5); }
.pager .button { min-width: 96px; justify-content: center; }

/* ------------------------------------------------- reader direction ----
   NEXT on the right, PREVIOUS on the left, in both writing directions.

   The bar is a flex row inside an RTL document, so its first group naturally
   sat on the right - which put Previous there and Next on the left. Correct
   for prose, wrong for turning a page. The order is stated rather than
   inherited. */
.reader-bar__group--next { order: -1; }
.reader-bar__group--center { order: 0; }
.reader-bar__group--prev { order: 1; }
[dir='ltr'] .reader-bar__group--next { order: 1; }
[dir='ltr'] .reader-bar__group--prev { order: -1; }

/* Home sat in the middle group and read as an odd sixth icon. It is a
   destination, so it sits with the destinations and gets the same weight. */
.reader-bar__group--nav { min-width: 96px; }
.reader-bar__group--next { justify-content: flex-end; }
.reader-bar__group--prev { justify-content: flex-start; }

/* ----------------------------------------------------- theme toggle ----
   A sun that becomes a moon: one shape, filled or ringed, so it reads at
   24px without a second asset. */
.icon-theme {
  width: 18px; height: 18px; border-radius: 999px;
  border: 1.6px solid currentColor; position: relative; display: inline-block;
}
.icon-theme::after {
  content: ''; position: absolute; inset: 2px;
  border-radius: 999px; background: currentColor;
  clip-path: inset(0 0 0 50%);
}
[data-theme='dark'] .icon-theme::after { clip-path: inset(0 50% 0 0); }

/* -------------------------------------------------- library feedback ----
   The saved state has to be legible without reading the label - a filled
   button for saved, an outline for not. */
[data-bookmark-state='on'] {
  background: var(--interactive); color: var(--text-inverse);
  border-color: var(--interactive);
}

/* A mark as well as a fill. The fill already separates saved from unsaved
   without relying on colour, but it still asks the reader to compare two
   buttons; a check answers at a glance. Drawn from borders so it needs no icon
   font and inherits the button's own colour - no accent, no asset. */
[data-bookmark-state='on']::before {
  content: '';
  display: inline-block;
  width: 6px; height: 11px;
  margin-inline-end: var(--space-3);
  border: solid currentColor;
  border-width: 0 2px 2px 0;
  transform: translateY(-1px) rotate(45deg);
}
.saved-by { display: inline-block; margin-inline-start: var(--space-4); color: var(--text-secondary); }

/* ------------------------------------------------- comment interaction --
   Actions stay quiet until the comment is hovered or focused, so a thread
   reads as prose rather than as a row of controls. */
.comment__actions {
  display: flex; gap: var(--space-4); margin-top: var(--space-3);
  opacity: 0; transition: opacity .15s ease;
}
.comment:hover .comment__actions,
.comment:focus-within .comment__actions { opacity: 1; }
@media (hover: none) { .comment__actions { opacity: 1; } }

.comment__action {
  color: var(--text-secondary); background: none; border: 0; padding: 0;
  font: inherit; cursor: pointer;
}
.comment__action:hover { color: var(--text-primary); text-decoration: underline; }
.comment__action:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.comment__action[disabled] { opacity: .5; cursor: default; text-decoration: none; }

/* A reply is visibly attached to what it answers: indented, and hung from a
   rule that runs back up to the parent. */
.comment--reply {
  margin-inline-start: var(--space-8);
  border-inline-start: 2px solid var(--border);
  padding-inline-start: var(--space-5);
}
@media (max-width: 560px) {
  .comment--reply { margin-inline-start: var(--space-4); padding-inline-start: var(--space-4); }
}

/* Being told what you are replying to, and how to stop. */

.composer__foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap;
  padding: 6px 10px; border-top: 1px solid var(--border); background: var(--surface-sunken);
}
.composer__foot .composer__note { padding: 0; border: 0; background: none; }
.composer__emoji { display: flex; gap: 2px; flex-wrap: wrap; }
.composer__emoji-btn {
  width: 30px; height: 30px; display: grid; place-items: center;
  border: 0; background: none; border-radius: var(--radius-sm);
  font-size: 15px; cursor: pointer; line-height: 1;
}
.composer__emoji-btn:hover { background: var(--surface); }
.composer__emoji-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

/* ------------------------------------------------------------- hero ----
   The featured slide's own artwork, enormous and soft, behind everything.

   A scrim sits over it so text contrast never depends on what a given cover
   happens to look like - a pale cover and a black one must both leave the
   title readable. */
/* The band's OWN backdrop rules used to live here: a cover blurred to 38px at
   half opacity under a near-opaque layer of the page colour. That was right
   for a flat panel that had to match the page, and it is the reason the
   cinematic carousel showed no artwork at all - being later in the file, it
   won every property. The carousel defines its own backdrop and scrim above. */
.featured__cover .cover { box-shadow: var(--shadow-lg, 0 18px 40px rgba(0,0,0,.22)); }

/* A section says what it is for, next to its name. */
.section__hint { color: var(--text-secondary); margin-inline-start: var(--space-4); flex: 1; }
.section__head { display: flex; align-items: baseline; gap: var(--space-3); }

/* NEWLY ADDED is a grid, because it answers a different question from the
   rails around it and looked identical to them. */
.grid-cards {
  display: grid; gap: var(--space-5);
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
@media (min-width: 740px) {
  .grid-cards { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
}
@media (max-width: 560px) {
  .section__hint { display: none; }
}

/* ------------------------------------------------------ title banner ----
   A band behind the identity block. Restrained on purpose: it anchors the
   page, it does not become the page. */
.title-banner {
  position: relative; height: 132px; margin-bottom: calc(var(--space-7) * -1);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0; overflow: hidden;
  background: var(--surface-sunken);
}
@media (min-width: 740px) { .title-banner { height: 190px; } }
.title-banner img { width: 100%; height: 100%; object-fit: cover; }

/* The band fades into the page so the identity block sits ON it rather than
   below a hard edge. */
.title-banner::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(to bottom, transparent 35%, var(--surface) 100%);
}

/* THE FALLBACK. One shared mark, tinted by a token - no generated object and
   nothing duplicated per title. */
.title-banner--mark { display: grid; place-items: center; }
.title-banner__mark {
  display: flex; align-items: center; gap: var(--space-4);
  color: var(--banner-mark); opacity: .5;
}
.title-banner__mark svg { width: 34px; height: 34px; }
/* Size and spacing only. The FACE comes from .brand-wordmark, which is the
   single rule allowed to name it. */
.title-banner__word { font-size: 26px; letter-spacing: .22em; }
:root { --banner-mark: #C9C9D1; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) { --banner-mark: #34343C; }
}
:root[data-theme='dark'] { --banner-mark: #34343C; }

/* ---------------------------------------------------------- credits ----
   A definition list, because that is what it is: a role and the people who
   did it. Roles with nobody in them are not drawn at all. */
.credits { margin-top: var(--space-7); padding-top: var(--space-6); border-top: 1px solid var(--border); }
.credits__title { font-size: var(--text-sm); font-weight: 600; color: var(--text-secondary); }
.credits__list { margin-top: var(--space-4); display: grid; gap: var(--space-3); }
.credits__row { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-4); align-items: baseline; }
.credits__role { color: var(--text-secondary); font-size: var(--text-sm); }
.credits__names { color: var(--text-primary); }
@media (max-width: 560px) {
  .credits__row { grid-template-columns: 1fr; gap: 2px; }
}

/* --------------------------------------------------- comment images ----
   Bounded, so a tall photograph cannot turn one comment into a page. */
.comment__image { display: block; margin-top: var(--space-4); max-width: 420px; }
.comment__image img {
  display: block; width: 100%; height: auto; max-height: 380px; object-fit: contain;
  border-radius: var(--radius-md); border: 1px solid var(--border);
  background: var(--surface-sunken);
}
@media (max-width: 560px) { .comment__image { max-width: 100%; } }

.composer__attach {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  color: var(--text-secondary); padding: 4px 8px; border-radius: var(--radius-sm);
}
.composer__attach:hover { color: var(--text-primary); background: var(--surface); }
.composer__attach:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
.composer__preview {
  display: flex; align-items: flex-start; gap: var(--space-4);
  padding: var(--space-4); border-top: 1px solid var(--border);
}
.composer__preview img {
  max-width: 140px; max-height: 100px; object-fit: contain;
  border-radius: var(--radius-sm); border: 1px solid var(--border);
}

/* ------------------------------------------------------------- thread --
   A comment thread is a conversation, so it has to LOOK like one: replies sit
   under what they answer, joined by a line, on a slightly quieter surface.

   Presentation depth stops at one. A reply to a reply sits beside its sibling
   and names who it answers, because a staircase of indents is unreadable on a
   phone and buys nothing a name does not.  */
.thread__heading {
  display: flex; align-items: center; gap: var(--space-3);
  font-size: var(--text-lg); font-weight: 600;
}
.thread__count {
  font-size: var(--text-sm); font-weight: 600;
  color: var(--text-secondary); background: var(--surface-sunken);
  border-radius: 999px; padding: 2px 10px; font-variant-numeric: tabular-nums;
}
.thread__item { border-bottom: 1px solid var(--border); }
.thread__item:last-child { border-bottom: 0; }
.thread__item .comment { border-bottom: 0; }

/* The replies of one comment: an indented column with a rail down its start
   edge. `padding-inline-start` and `inset-inline-start` are RTL-correct by
   construction - nothing here mentions left or right. */
.thread__replies { position: relative; padding-inline-start: var(--space-7); }
.thread__replies::before {
  content: ""; position: absolute; inset-block: 0; inset-inline-start: 19px;
  width: 2px; background: var(--border); border-radius: 2px;
}
.thread__reply-list { display: grid; }
.thread__reply-list[hidden] { display: none; }

.comment--reply { padding-block: var(--space-4); }
.comment--reply .comment__text { font-size: var(--text-sm); }

/* Who this answers. The arrow is drawn rather than typed so it mirrors with the
   writing direction instead of pointing the wrong way in Arabic. */
.comment__to {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--text-secondary); max-width: 12rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.comment__to-arrow {
  width: 10px; height: 10px; flex: none;
  border-inline-start: 2px solid currentColor; border-block-end: 2px solid currentColor;
  border-end-start-radius: 3px; opacity: .55; transform: translateY(-2px);
}

/* Show/hide the rest of a conversation. A control, not a link: it changes what
   is on screen and changes nothing else. */
.thread__toggle {
  display: flex; align-items: center; gap: var(--space-3);
  background: none; border: 0; padding: var(--space-3) 0; cursor: pointer;
  color: var(--text-secondary); font-weight: 600;
}
.thread__toggle:hover { color: var(--text-primary); }
.thread__toggle-line { width: 22px; height: 2px; background: var(--border); border-radius: 2px; flex: none; }

/* ---------------------------------------------------------- reply chip --
   Contextual. It exists while a reply is aimed and at no other time, and the
   rule is written so the `hidden` attribute still decides. */
.reply-chip { display: none; }
.reply-chip:not([hidden]) {
  display: flex; align-items: flex-start; gap: var(--space-3);
  margin-top: var(--space-5); padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface-sunken);
}
.reply-chip__mark {
  width: 3px; align-self: stretch; border-radius: 3px;
  background: var(--accent, var(--text-secondary)); flex: none;
}
.reply-chip__text { display: grid; gap: 2px; min-width: 0; }
.reply-chip__to { color: var(--text-primary); font-weight: 600; }
.reply-chip__excerpt {
  color: var(--text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.reply-chip__cancel {
  margin-inline-start: auto; background: none; border: 0; cursor: pointer;
  font-size: 20px; line-height: 1; color: var(--text-secondary); padding: 0 4px;
}
.reply-chip__cancel:hover { color: var(--text-primary); }

/* ------------------------------------------------------------ composer --
   It no longer reserves a large empty area for a reply that is not happening. */
.composer-form { margin-top: var(--space-7); }
.composer__submit {
  display: flex; align-items: center; gap: var(--space-4); margin-top: var(--space-5);
}
.composer__submit .button { min-width: 180px; }
.composer__counter { color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
.composer__counter.is-near { color: var(--text-secondary); }

button[data-comment-submit][disabled] { opacity: .55; cursor: not-allowed; }
button[data-comment-submit].is-sending [data-submit-label]::after {
  content: "…"; animation: ku-dots 1.2s steps(4, end) infinite;
}
@keyframes ku-dots { to { content: "…"; } }

/* The chosen image, before it is sent. */
.composer__preview { display: none; }
.composer__preview:not([hidden]) {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-3) var(--space-4); border-block-start: 1px solid var(--border);
}
.composer__thumb {
  width: 56px; height: 56px; border-radius: var(--radius-sm); flex: none;
  background-size: cover; background-position: center; background-color: var(--surface-sunken);
}
.composer__file { display: grid; gap: 2px; min-width: 0; }
.composer__file span { color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.composer__file-actions { display: flex; gap: var(--space-4); margin-inline-start: auto; }

.composer__attach { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.composer__attach-icon {
  width: 14px; height: 12px; border: 1.5px solid currentColor; border-radius: 2px;
  position: relative; display: inline-block; opacity: .8;
}
.composer__attach-icon::after {
  content: ""; position: absolute; inset-block-end: 1px; inset-inline-start: 1px;
  width: 7px; height: 5px; background: currentColor;
  clip-path: polygon(0 100%, 40% 25%, 70% 100%);
}

.composer__emoji-btn {
  background: none; border: 0; cursor: pointer; font-size: 17px; line-height: 1;
  padding: 4px; border-radius: var(--radius-sm); transition: transform .12s, background .12s;
}
.composer__emoji-btn:hover { background: var(--surface-sunken); transform: scale(1.15); }

@media (prefers-reduced-motion: reduce) {
  .composer__emoji-btn { transition: none; }
  button[data-comment-submit].is-sending [data-submit-label]::after { animation: none; }
}

@media (max-width: 560px) {
  .thread__replies { padding-inline-start: var(--space-5); }
  .thread__replies::before { inset-inline-start: 13px; }
  .composer__preview:not([hidden]) { flex-wrap: wrap; }
  .composer__file-actions { margin-inline-start: 0; }
}

/* ------------------------------------------------------------ updates --
   "What got a new chapter?" - a dense list, not another shelf of posters. The
   chapter is the destination, so it is the one thing with a border around it. */
.updates { display: grid; gap: var(--space-3); }
.update {
  display: grid;
  grid-template-columns: 44px 1fr auto;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  transition: border-color .15s, background .15s;
}
.update:hover { border-color: var(--text-tertiary); background: var(--surface-raised, var(--surface)); }
.update__cover { display: block; border-radius: var(--radius-sm); overflow: hidden; }
.update__cover img { display: block; width: 100%; height: auto; }
.update__body { min-width: 0; }
.update__title { font-size: var(--text-sm); font-weight: 600; line-height: 1.45; }
.update__title a { color: var(--text-primary); }
.update__title a:hover { color: var(--interactive); }
.update__meta { display: flex; gap: var(--space-3); color: var(--text-tertiary); margin-top: 2px; }
.update__kind { color: var(--text-secondary); }

.update__chapter {
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: 6px var(--space-4);
  border: 1px solid var(--border); border-radius: 999px;
  color: var(--text-secondary); white-space: nowrap;
  font-size: var(--text-xs); font-weight: 600;
  transition: color .15s, border-color .15s, background .15s;
}
.update__chapter:hover {
  color: var(--text-primary); border-color: var(--text-secondary);
  background: var(--surface-sunken);
}
.update__chapter-go { display: grid; place-items: center; opacity: .7; }

@media (max-width: 559px) {
  .update { grid-template-columns: 40px 1fr; row-gap: var(--space-3); }
  .update__chapter { grid-column: 2; justify-self: start; }
}

/* ------------------------------------------------------------ railing --
   A horizontal strip that looks scrollable must BE movable with whatever the
   reader has in their hand. */
.rail-wrap { position: relative; }
.rail-wrap .rail { scroll-behavior: smooth; }
.rail-wrap.is-dragging .rail { scroll-behavior: auto; cursor: grabbing; }

.rail-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 2;
  display: grid; place-items: center;
  width: 36px; height: 36px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-secondary);
  box-shadow: 0 2px 10px rgb(0 0 0 / .12);
  opacity: 0; pointer-events: none;
  transition: opacity .18s, color .15s, border-color .15s;
}
.rail-nav--prev { inset-inline-start: -6px; }
.rail-nav--next { inset-inline-end: -6px; }
.rail-wrap:hover .rail-nav:not([disabled]),
.rail-wrap:focus-within .rail-nav:not([disabled]) { opacity: 1; pointer-events: auto; }
.rail-nav:hover { color: var(--text-primary); border-color: var(--text-secondary); }
.rail-nav:focus-visible { opacity: 1; pointer-events: auto; outline: 2px solid var(--focus); outline-offset: 2px; }
.rail-nav[disabled] { opacity: 0; pointer-events: none; }

/* A pointer device gets a grab cursor; touch does not need the hint. */
@media (hover: hover) and (pointer: fine) {
  .rail-wrap .rail { cursor: grab; }
}
@media (prefers-reduced-motion: reduce) {
  .rail-wrap .rail { scroll-behavior: auto; }
  .rail-nav { transition: none; }
}

/* ---------------------------------------------------------- title hero --
   Layers, in order: backdrop, veil, cover, identity, metadata, actions.

   The veil is load-bearing, not decoration. A banner is whatever the operator
   uploaded - it can be bright, dark, or covered in its own lettering - so the
   artwork is faded into the page BEFORE the work's name begins. Nothing that
   has to be read is drawn over it. */
.title-hero { position: relative; }

.title-banner {
  position: relative;
  height: clamp(140px, 26vw, 260px);
  border-radius: var(--radius-lg, 18px) var(--radius-lg, 18px) 0 0;
  overflow: hidden;
  margin-inline: calc(var(--space-5) * -1);
  /* The earlier design pulled the identity block UP onto the band with a
     negative bottom margin, which is precisely why the kind badge was being
     read against artwork. The veil below does that job now - the band fades
     into the page - so the pull is cancelled rather than left to fight it. */
  margin-bottom: 0;
}
.title-banner > img {
  width: 100%; height: 100%;
  /* object-position comes from the operator's focal point, inline. The image
     is never cropped; only the window onto it moves. */
  object-fit: cover;
  display: block;
}

/* Artwork -> page. The last third is the page's own background, which is what
   the identity block sits on. */
.title-banner__veil {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgb(0 0 0 / .18) 0%,
    transparent 28%,
    color-mix(in srgb, var(--bg) 55%, transparent) 66%,
    var(--bg) 100%
  );
}

.title-hero .identity {
  position: relative;
  z-index: 1;
  padding-inline: 0;
}

/* ONLY THE COVER OVERLAPS, and it does so WITHOUT MOVING ANYTHING.

   A negative margin was the obvious tool and the wrong one: it shrinks the
   grid row, which pulled the identity column up with it and put the kind badge
   24px inside the artwork - the exact thing this composition exists to
   prevent. `position: relative` offsets the cover visually and leaves the
   layout untouched, so the text below cannot be dragged upward by it. */
.title-hero .identity__cover {
  position: relative;
  inset-block-start: calc(var(--space-8) * -1);
}

@media (max-width: 559px) {
  .title-banner { height: clamp(120px, 34vw, 170px); }
  .title-hero .identity__cover { inset-block-start: calc(var(--space-6) * -1); }
}

/* ------------------------------------------------------------ entities --
   A credited party in a frame that states its role. The glyph is drawn, not a
   portrait: nobody supplied an image, and inventing one for a real person is
   not a decision a stylesheet gets to make. */
.entity-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}
.entity {
  display: flex; align-items: center; gap: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  min-width: 0;
}
.entity__mark {
  position: relative; flex: none;
  width: 38px; height: 38px; border-radius: 10px;
  display: grid; place-items: center; overflow: hidden;
  background: var(--surface-sunken);
  color: var(--text-tertiary);
}
.entity__mark img { width: 100%; height: 100%; object-fit: cover; }
.entity__initial {
  position: relative; z-index: 1;
  font-weight: 700; font-size: var(--text-sm); color: var(--text-secondary);
}
/* The role glyph sits behind the initial, quietly: it says "publisher" at a
   glance without pretending to be a logo. */
.entity__glyph {
  position: absolute; inset: 0;
  background: currentColor; opacity: .16;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: 20px 20px; mask-size: 20px 20px;
}
.entity__mark--author .entity__glyph,
.entity__mark--person .entity__glyph {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-4.4 0-8 2.7-8 6v2h16v-2c0-3.3-3.6-6-8-6Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-4.4 0-8 2.7-8 6v2h16v-2c0-3.3-3.6-6-8-6Z'/%3E%3C/svg%3E");
}
.entity__mark--artist .entity__glyph {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m3 17.2 9.9-9.9 3.8 3.8-9.9 9.9H3v-3.8ZM17.9 6.1l1.4-1.4a1.5 1.5 0 0 0-2.1-2.1l-1.4 1.4 2.1 2.1Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m3 17.2 9.9-9.9 3.8 3.8-9.9 9.9H3v-3.8ZM17.9 6.1l1.4-1.4a1.5 1.5 0 0 0-2.1-2.1l-1.4 1.4 2.1 2.1Z'/%3E%3C/svg%3E");
}
.entity__mark--publisher .entity__glyph,
.entity__mark--team .entity__glyph {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M3 21V6l7-3v4l7-2v16H3Zm2-2h4v-3H5v3Zm0-5h4v-3H5v3Zm0-5h4V6H5v3Zm6 10h6v-3h-6v3Zm0-5h6v-3h-6v3Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M3 21V6l7-3v4l7-2v16H3Zm2-2h4v-3H5v3Zm0-5h4v-3H5v3Zm0-5h4V6H5v3Zm6 10h6v-3h-6v3Zm0-5h6v-3h-6v3Z'/%3E%3C/svg%3E");
}
.entity__mark--translate .entity__glyph,
.entity__mark--edit .entity__glyph,
.entity__mark--check .entity__glyph,
.entity__mark--type .entity__glyph {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M4 4h16v2H4V4Zm0 5h10v2H4V9Zm0 5h16v2H4v-2Zm0 5h10v2H4v-2Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M4 4h16v2H4V4Zm0 5h10v2H4V9Zm0 5h16v2H4v-2Zm0 5h10v2H4v-2Z'/%3E%3C/svg%3E");
}

.entity__text { display: grid; gap: 1px; min-width: 0; }
.entity__role { color: var(--text-tertiary); }
.entity__name {
  font-weight: 600; color: var(--text-primary); font-size: var(--text-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* --------------------------------------------------------- synopsis ----- */
.synopsis {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-5) var(--space-6);
  background: var(--surface);
}
.synopsis__title, .credits__title {
  font-size: var(--text-sm); font-weight: 600; color: var(--text-secondary);
  margin-bottom: var(--space-3);
}
.title-synopsis { max-width: 72ch; }
.credits { margin-top: var(--space-6); }

/* The comment you just wrote, or the one a link pointed at. Brief, and only a
   background - moving or outlining it would be shouting. */
.comment.is-anchored {
  animation: ku-anchored 2.6s ease-out;
  border-radius: var(--radius-md);
}
@keyframes ku-anchored {
  0%, 55% { background: color-mix(in srgb, var(--interactive) 14%, transparent); }
  100% { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .comment.is-anchored { animation: none; background: var(--surface-sunken); }
}

/* ------------------------------------------------------- title identity --
   The hero is a COVER and a COLUMN at every width. It used to be a block that
   stacked on a phone and occupied half of a two-column space on desktop, which
   is why one side was empty and nothing grouped.

   The rhythm carries the grouping: tight inside a group, loose between them.
   Every value is from the scale. */
.title-hero .identity {
  display: grid;
  grid-template-columns: clamp(132px, 38vw, 170px) minmax(0, 1fr);
  /* Stated, not inferred. The actions and the facts are their own groups and
     span the full width; inside the identity column they rendered at half a
     phone screen with the cover's column empty beneath them. */
  grid-template-areas:
    "cover   ident"
    "actions actions"
    "facts   facts";
  gap: var(--space-5);
  align-items: start;
}
.title-hero .identity__cover { grid-area: cover; }
.title-hero .identity__body  { grid-area: ident; }
.title-hero .actions         { grid-area: actions; }
.title-hero .title-facts     { grid-area: facts; }

/* The cover was 96px - on a phone that is SMALLER than the 173px covers in the
   browse grid, so the hero showed the work less prominently than a search
   result did. */
.title-hero .identity__cover { width: 100%; }

.title-hero .identity__body {
  display: grid;
  gap: 0;              /* groups set their own distance; see below */
  align-content: start;
  min-width: 0;
}

/* INSIDE the identity group - tight. */
.title-hero .identity__body > .meta { margin: 0; }
.title-hero .identity__title { margin-top: var(--space-3); }
.title-hero .identity__alt { margin-top: var(--space-3); }
.title-hero .identity__body > .chips { margin-top: var(--space-4); }

/* BETWEEN groups - loose. This is the whole fix for "everything is equally far
   from everything else". */
/* BETWEEN groups. The grid's row gap is the tight rhythm; these open it to the
   loose one, which is the whole fix for "everything is equally far from
   everything else". */
.title-hero .actions { margin-top: var(--space-4); }
.title-hero .title-facts { margin-top: var(--space-3); }

.title-hero .actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-4);
}
/* A primary stretched to the column width beside a natural-width secondary
   reads as two unrelated controls. Both size to their content. */
.title-hero .actions .button { width: auto; }

/* ---------------------------------------------------------- title facts --
   ONE panel, divided - not five separate cards. Five bordered boxes in a row is
   the "collection of disconnected cards" the design language forbids, and it
   made counted facts look like a dashboard readout. */
.title-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface);
}
.title-facts__cell {
  padding: var(--space-4);
  display: grid; gap: 2px; align-content: start;
  /* Hairlines BETWEEN cells, never around them: the panel has one outline. */
  border-inline-start: 1px solid var(--border);
}
.title-facts__cell:first-child { border-inline-start: 0; }
.title-facts__label {
  color: var(--text-tertiary);
  display: inline-flex; align-items: center; gap: 6px;
}

/* FACT GLYPHS. Masks rather than an icon font or an image request: they inherit
   currentColor, cost nothing to load, and carry no hue of their own - the chrome
   has none. Quieter than the label they sit beside, so they identify a cell
   without competing with its value. */
.fact-icon {
  width: 14px; height: 14px; flex: none;
  background: currentColor; opacity: .55;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
.fact-icon--status {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 10.6V6h-2v7.4l5 3 1-1.7-4-2.1Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 10.6V6h-2v7.4l5 3 1-1.7-4-2.1Z'/%3E%3C/svg%3E");
}
.fact-icon--chapters {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M4 4h7v16H4V4Zm9 0h7v16h-7V4Zm2 2v12h3V6h-3ZM6 6v12h3V6H6Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M4 4h7v16H4V4Zm9 0h7v16h-7V4Zm2 2v12h3V6h-3ZM6 6v12h3V6H6Z'/%3E%3C/svg%3E");
}
.fact-icon--year {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M7 2v2H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2V2h-2v2H9V2H7ZM5 9h14v10H5V9Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M7 2v2H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2V2h-2v2H9V2H7ZM5 9h14v10H5V9Z'/%3E%3C/svg%3E");
}
.fact-icon--language {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm6.9 6h-2.9a15.7 15.7 0 0 0-1.4-3.6A8 8 0 0 1 18.9 8ZM12 4.2c.7 1 1.3 2.3 1.7 3.8h-3.4c.4-1.5 1-2.8 1.7-3.8ZM4.3 14a8 8 0 0 1 0-4h3.3a17 17 0 0 0 0 4H4.3Zm.8 2h2.9c.3 1.3.8 2.5 1.4 3.6A8 8 0 0 1 5.1 16Zm2.9-8H5.1a8 8 0 0 1 4.3-3.6C8.8 5.5 8.3 6.7 8 8Zm4 11.8c-.7-1-1.3-2.3-1.7-3.8h3.4c-.4 1.5-1 2.8-1.7 3.8ZM14.1 14H9.9a15 15 0 0 1 0-4h4.2a15 15 0 0 1 0 4Zm.5 5.6c.6-1.1 1.1-2.3 1.4-3.6h2.9a8 8 0 0 1-4.3 3.6ZM16.4 14a17 17 0 0 0 0-4h3.3a8 8 0 0 1 0 4h-3.3Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm6.9 6h-2.9a15.7 15.7 0 0 0-1.4-3.6A8 8 0 0 1 18.9 8ZM12 4.2c.7 1 1.3 2.3 1.7 3.8h-3.4c.4-1.5 1-2.8 1.7-3.8ZM4.3 14a8 8 0 0 1 0-4h3.3a17 17 0 0 0 0 4H4.3Zm.8 2h2.9c.3 1.3.8 2.5 1.4 3.6A8 8 0 0 1 5.1 16Zm2.9-8H5.1a8 8 0 0 1 4.3-3.6C8.8 5.5 8.3 6.7 8 8Zm4 11.8c-.7-1-1.3-2.3-1.7-3.8h3.4c-.4 1.5-1 2.8-1.7 3.8ZM14.1 14H9.9a15 15 0 0 1 0-4h4.2a15 15 0 0 1 0 4Zm.5 5.6c.6-1.1 1.1-2.3 1.4-3.6h2.9a8 8 0 0 1-4.3 3.6ZM16.4 14a17 17 0 0 0 0-4h3.3a8 8 0 0 1 0 4h-3.3Z'/%3E%3C/svg%3E");
}
.fact-icon--views {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 5C6.5 5 2.7 9.6 1.5 12c1.2 2.4 5 7 10.5 7s9.3-4.6 10.5-7c-1.2-2.4-5-7-10.5-7Zm0 12a5 5 0 1 1 0-10 5 5 0 0 1 0 10Zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 5C6.5 5 2.7 9.6 1.5 12c1.2 2.4 5 7 10.5 7s9.3-4.6 10.5-7c-1.2-2.4-5-7-10.5-7Zm0 12a5 5 0 1 1 0-10 5 5 0 0 1 0 10Zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E");
}
.title-facts__value { font-weight: 600; color: var(--text-primary); font-size: var(--text-sm); }

@media (max-width: 559px) {
  /* Two per row on a phone, so the row seams need horizontal rules as well. */
  .title-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .title-facts__cell:nth-child(odd) { border-inline-start: 0; }
  .title-facts__cell:nth-child(n + 3) { border-block-start: 1px solid var(--border); }

  /* An ODD number of facts leaves the last cell alone in its row, so half the
     final row was empty and the panel's bottom edge ran past the last cell into
     nothing - the open border. A divided panel has to be a complete rectangle,
     or the division reads as damage. */
  .title-facts__cell:last-child:nth-child(odd) {
    grid-column: 1 / -1;
    border-inline-start: 0;
  }
}

/* --- desktop: the cover leads, the column takes the rest ----------------- */
@media (min-width: 940px) {
  .title-hero .identity {
    grid-template-columns: 240px minmax(0, 1fr);
    /* The cover spans all three rows, so the column beside it carries the whole
       hierarchy - identity, then actions, then facts - and the facts stop
       drifting away from the work they describe. */
    grid-template-areas:
      "cover ident"
      "cover actions"
      "cover facts";
    grid-template-rows: auto auto 1fr;
    gap: var(--space-6) var(--space-8);
    align-content: start;
  }
  .title-hero .actions { margin-top: var(--space-3); }
  .title-hero .title-facts { margin-top: 0; align-self: start; }
}

/* The cover keeps its weight as the page grows. Fixed at 240px it was 13% of a
   1920 viewport - the strongest material on the page, shrinking relative to
   everything around it. */
@media (min-width: 1120px) {
  .title-hero .identity { grid-template-columns: 296px minmax(0, 1fr); }
}

/* THE FALLBACK BAND IS SHORTER THAN A REAL ONE.

   The brand mark is a deliberate treatment and stays. But at full banner height
   on a title that has no banner it becomes the largest empty region on the
   page, which reads as a space reserved for something missing rather than as a
   considered ground. A real banner is artwork and earns the height; a mark does
   not need it. */
.title-banner--mark { height: clamp(96px, 14vw, 148px); }

/* ==========================================================================
   WAVE 2 — follow, lists, notifications
   ========================================================================== */

/* A bell, drawn. No icon font, no request. */
.icon-bell { position: relative; width: 18px; height: 18px; display: inline-block; }
.icon-bell::before {
  content: ''; position: absolute; left: 3px; top: 2px;
  width: 12px; height: 11px;
  border: 1.5px solid currentColor; border-bottom: 0;
  border-radius: 6px 6px 0 0;
}
.icon-bell::after {
  content: ''; position: absolute; left: 1px; top: 12px;
  width: 16px; height: 1.5px; background: currentColor;
  box-shadow: 0 3px 0 -1px currentColor;
}

/* THE SECOND RANK OF ACTIONS. Smaller and lighter than the two above them, on
   their own line, so the page still answers "read this" before it offers
   anything else. */
/* INSIDE the actions cluster, on its own line. `.actions` is a wrapping flex
   row, so a full basis is what puts these beneath the two primary controls
   instead of beside them - and keeps them attached to the actions they qualify
   rather than drifting to the bottom of the hero. */
.actions-secondary {
  flex: 1 0 100%;
  display: flex; gap: var(--space-2); flex-wrap: wrap;
  margin-top: calc(var(--space-2) * -1);
}
.actions-secondary[hidden] { display: none; }

.button--quiet {
  min-height: var(--control-h-sm);
  padding-inline: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--control-border);
  gap: 6px;
}
.button--quiet:hover { color: var(--text-primary); background: var(--surface-elevated); }

/* FOLLOWED. Fill and weight, never hue alone. */
.button--quiet.is-on {
  background: var(--interactive); color: var(--text-inverse);
  border-color: var(--interactive); font-weight: 600;
}

/* ------------------------------------------------------------ list picker */
.listadd {
  flex: 1 0 100%;
  margin-top: var(--space-2);
  padding: var(--space-4);
  max-width: 420px;
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.listadd[hidden] { display: none; }

.listadd__row {
  display: flex; align-items: center; gap: var(--space-3);
  width: 100%; min-height: var(--control-h);
  padding-inline: var(--space-2);
  background: none; border: 0; cursor: pointer;
  color: var(--text-primary); font-size: var(--text-base); text-align: start;
}
.listadd__row + .listadd__row { border-top: 1px solid var(--border); }
.listadd__row:hover { background: var(--surface-sunken); }

/* An empty square that fills with a check. State by shape, not by colour. */
.listadd__mark {
  width: 16px; height: 16px; flex: none;
  border: 1.5px solid var(--control-border); border-radius: 3px;
}
.listadd__row.is-in .listadd__mark {
  background: var(--interactive); border-color: var(--interactive);
  -webkit-mask-image: none;
  position: relative;
}
.listadd__row.is-in .listadd__mark::after {
  content: ''; position: absolute; left: 4px; top: 1px;
  width: 5px; height: 9px;
  border: solid var(--text-inverse); border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}

.listadd__new { display: flex; gap: var(--space-2); margin-top: var(--space-4); }
.listadd__new input { flex: 1; min-width: 0; justify-content: flex-start; background: var(--surface-sunken); }
.listadd__error { margin-top: var(--space-3); color: var(--text-secondary); }

/* --------------------------------------------------------------- the bell */
.header-bell { position: relative; }
.header-bell[hidden] { display: none; }

/* The one number in this product that must be noticed. Filled, in the
   interactive token, with inverse text - the loudest treatment the achromatic
   palette has, and the same one the primary button uses. */
.header-bell__count {
  /* ANCHORED TO THE GLYPH, NOT THE BUTTON'S PADDING.
     It was `top: 2px; inset-inline-end: 2px` - 2px from the edge of the 44px
     control. But the bell glyph is 20px CENTRED in that control, so its corner
     is 12px in: the badge sat out in the padding, not touching the bell, and
     the pair read as a number floating beside an icon rather than a count ON
     it. That is the "detached" look, and it was never the bell's place in the
     cluster - measured at 390/768/1440/1920, the four utility controls are
     uniformly 44px, identically aligned, 2px apart.
     6px puts the badge over the glyph's corner while the CONTROL keeps its full
     44px target. */
  position: absolute; top: 4px; inset-inline-end: 4px;
  min-width: 16px; height: 16px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 600; line-height: 1;
  background: var(--interactive); color: var(--text-inverse);
  border-radius: 8px;
}
.header-bell__count[hidden] { display: none; }

/* A LIST COLUMN, NOT A FULL-WIDTH BAND.
   A row whose content is at one edge and whose controls are at the other reads
   as two unrelated things on one line. The end alignment is correct - it is the
   width that is wrong, and 740px is the band that keeps a name, a count and
   four controls related. Applied to the SECTION so the heading and its hint
   share the measure rather than drifting past the rows they describe. */
.section--measured { max-width: 740px; }

/* -------------------------------------------------------- collections page */
.collection-new { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.collection-new__name { flex: 1 1 220px; min-width: 0; justify-content: flex-start; background: var(--surface-sunken); }
.collection-error { margin-top: var(--space-3); color: var(--text-secondary); }

.collection-row {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  padding-block: var(--space-4);
  border-bottom: 1px solid var(--border);
}
.collection-row__text { flex: 1 1 220px; min-width: 0; }
.collection-row__name { font-size: var(--text-md); font-weight: 600; }
.collection-row__meta { color: var(--text-tertiary); margin-top: 2px; }
.collection-row__desc { color: var(--text-secondary); margin-top: var(--space-2); max-width: 60ch; }
.collection-row__actions { display: flex; gap: var(--space-2); }

/* The move controls sit BEFORE the destructive ones in the source order, so
   tabbing through a row reaches "move" before "delete". */
.collection-row__order { display: flex; gap: var(--space-1, 4px); flex: none; }

.collection-move {
  /* 44px: this is a touch target on a phone, and the design floor is not
     negotiable for a control whose whole job is being pressed repeatedly. */
  width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--control-border);
  border-radius: var(--control-r);
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
}
.collection-move:hover:not([disabled]) { color: var(--text-primary); background: var(--surface-elevated); }
.collection-move:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* Disabled at the ends. Visibly so - an end-of-list control that looks
   pressable is a control that gets pressed. */
.collection-move[disabled] { opacity: .35; cursor: default; }

/* ------------------------------------------------------ notifications page */
.notification-row {
  display: flex; align-items: center; gap: var(--space-3);
  min-height: 56px; padding-block: var(--space-3);
  border-bottom: 1px solid var(--border);
  color: var(--text-primary);
}
.notification-row:hover { background: var(--surface-elevated); }

/* Reserved on every row so the column edge stays straight, filled only when
   there is something to say. */
.notification-row__mark {
  width: 8px; height: 8px; flex: none; border-radius: 50%;
  background: transparent;
}
.notification-row.is-unread .notification-row__mark { background: var(--interactive); }
.notification-row.is-unread .notification-row__what { font-weight: 600; }

.notification-row__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.notification-row__detail { color: var(--text-tertiary); }
.notification-row__when { color: var(--text-tertiary); white-space: nowrap; }

/* ------------------------------------------------------------- read stats */
.stat-strip {
  display: grid; gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.stat-strip__cell { background: var(--surface); padding: var(--space-4); }
.stat-strip__value { font-size: var(--text-lg); font-weight: 600; }
.stat-strip__label { color: var(--text-tertiary); margin-top: 2px; }

/* ==========================================================================
   PUBLIC READER PROFILE — an identity page, not a social one
   ========================================================================== */

.reader-profile {
  display: flex; align-items: center; gap: var(--space-5);
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--border);
}
.reader-profile__id { min-width: 0; }
.reader-profile__name {
  font-size: var(--text-xl); line-height: 1.3; font-weight: 600;
  overflow-wrap: anywhere;
}
.reader-profile__joined { color: var(--text-tertiary); margin-top: 4px; }

/* The bio reads at a READING measure, not the full width of a 1440 screen.
   It is the only prose on the page and it is somebody's own words. */
.reader-profile__bio {
  margin-top: var(--space-6);
  max-width: 62ch;
  font-family: var(--font-synopsis);
  font-size: var(--text-base);
  line-height: 1.75;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

/* CARDS WITH THEIR OWN BORDERS, not a divided panel.
   THE FIRST FIX WAS WRONG AND THIS NOTE EXISTS SO NOBODY REPEATS IT.
   This panel used the technique the fact panel uses: cells over a
   border-coloured background, with a 1px gap showing through as the divisions.
   That needs a KNOWN cell count. With user-generated content it produces a
   phantom card wherever the last row is short - a grey block that reads as
   broken or still loading.
   Switching `auto-fill` to `auto-fit` did NOT fix it, and claiming it did was
   wrong: `auto-fit` only collapses tracks that are empty in EVERY row, so one
   list looked right and three-in-two-columns still showed the phantom.
   A variable-length list of somebody's own content wants cards. Each one
   carries its own hairline, the gap is real space, and every count from one to
   twenty-four looks deliberate. */
.public-lists {
  margin-top: var(--space-5);
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.public-list {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.public-list__name { font-size: var(--text-base); font-weight: 600; overflow-wrap: anywhere; }
.public-list__count { color: var(--text-tertiary); margin-top: 2px; }
.public-list__desc { color: var(--text-secondary); margin-top: var(--space-2); }

@media (max-width: 559px) {
  .reader-profile { gap: var(--space-4); }
  .reader-profile__name { font-size: var(--text-lg); }
}

/* ==========================================================================
   COLLECTION DETAIL — a catalogue surface
   ========================================================================== */

.collection-head { padding-bottom: var(--space-5); border-bottom: 1px solid var(--border); }
.collection-head__crumb { color: var(--text-tertiary); }
.collection-head__crumb a { color: inherit; }
.collection-head__crumb a:hover { color: var(--text-primary); }

.collection-head__name {
  margin-top: var(--space-2);
  font-size: var(--text-xl); line-height: 1.3; font-weight: 600;
  overflow-wrap: anywhere;
}
.collection-head__meta { margin-top: 4px; color: var(--text-tertiary); }
.collection-head__desc {
  margin-top: var(--space-4); max-width: 62ch;
  color: var(--text-secondary); line-height: 1.7; overflow-wrap: anywhere;
}

/* THE NAME IS THE DOOR. It reads as a link on hover and carries a focus ring,
   because it is the primary way into the collection and the previous row gave
   no sign it could be opened at all. */
.collection-row__open { color: inherit; text-decoration: none; }
.collection-row__open:hover { text-decoration: underline; text-underline-offset: 3px; }
.collection-row__open:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 2px; }

/* A public list card is now a link; it must not start looking like a button. */
a.public-list { display: block; color: inherit; text-decoration: none; }
a.public-list:hover { border-color: var(--control-border); }
a.public-list:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ==========================================================================
   ACHIEVEMENTS — an archive, not an inventory
   ========================================================================== */

.achv-grid {
  margin-top: var(--space-4);
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

.achv {
  display: flex; align-items: flex-start; gap: var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}

/* THE SEAL. Same square, same hairline, earned or not - what changes is
   contrast, never hue. There is no gold here and there will not be one: the
   chrome is achromatic and artwork is the only place saturation belongs. */
.achv__seal {
  flex: none;
  width: 48px; height: 48px;
  display: inline-flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 1px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-tertiary);
}
.achv__num { font-size: var(--text-sm); font-weight: 600; line-height: 1; }

.achv__text { min-width: 0; }
.achv__name { font-size: var(--text-base); font-weight: 600; overflow-wrap: anywhere; }
.achv__desc { color: var(--text-tertiary); margin-top: 2px; overflow-wrap: anywhere; }
.achv__state { color: var(--text-tertiary); margin-top: var(--space-2); }

/* EARNED. A firmer seal and full-contrast text - and the word "مُسجَّل" beside
   it, so the state never depends on noticing a shade. */
.achv.is-earned { border-color: var(--control-border); }
.achv.is-earned .achv__seal { border-color: var(--interactive); color: var(--text-primary); }
.achv.is-earned .achv__name { color: var(--text-primary); }
.achv.is-earned .achv__state { color: var(--text-secondary); }

/* Unearned recedes rather than shouting about being locked. */
.achv:not(.is-earned) .achv__name { color: var(--text-secondary); font-weight: 500; }

.achv__bar {
  display: block; margin-top: 6px; height: 2px;
  background: var(--surface-sunken); border-radius: 1px; overflow: hidden;
}
.achv__bar-fill { display: block; height: 100%; background: var(--text-tertiary); }

/* THE NEAREST ONE. Set apart from the grid because it answers a different
   question - not "what have I done" but "what is next". */
/* THE NEAREST ONE sits in the same column width as the grid cards rather than
   stretching across the section. A single card at 740px puts a short label at
   one edge and nothing at the other - the same "row too wide for its content"
   problem the collections list had. */
.achv-next { margin-top: var(--space-6); max-width: 340px; }
.achv-next__label { color: var(--text-tertiary); margin-bottom: var(--space-2); }

.achv-empty { margin-top: var(--space-5); }
.achv-empty__lead { color: var(--text-secondary); line-height: 1.6; }
.achv-empty__sub { color: var(--text-tertiary); margin-top: 4px; }

@media (max-width: 559px) {
  .achv-grid { grid-template-columns: 1fr; }
}

/* ==========================================================================
   PINNING — a checkbox that looks like the thing it selects
   ==========================================================================

   A tick in a browser box beside a label is not a selection of an emblem. The
   control IS the seal, and the selected state is a filled surface, a firm
   border AND a visible check — never a shade alone. */

.pin-grid {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}
.pin {
  position: relative;
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  cursor: pointer;
  min-block-size: var(--control-h);
}
.pin__input { position: absolute; opacity: 0; pointer-events: none; }
.pin__name { font-size: var(--text-sm); font-weight: 500; overflow-wrap: anywhere; min-inline-size: 0; }
.pin__mark {
  margin-inline-start: auto; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 20px; block-size: 20px;
  border: 1px solid var(--control-border); border-radius: var(--radius-sm);
  color: transparent;
}
.pin[data-on='true'] {
  border-color: var(--interactive);
  background: var(--surface-elevated);
}
.pin[data-on='true'] .pin__name { font-weight: 600; }
.pin[data-on='true'] .pin__mark {
  background: var(--interactive); border-color: var(--interactive); color: var(--surface);
}
.pin:has(.pin__input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
/* A pin that cannot be added because the limit is reached says so by looking
   unavailable, rather than by silently doing nothing when pressed. */
.pin[data-full='true'] { opacity: .55; cursor: not-allowed; }
.pin-form__hint { margin-top: var(--space-4); }
.pin-form__hint[data-full='true'] { color: var(--text-primary); font-weight: 500; }

/* ==========================================================================
   ACCOUNT PANELS — grouped decisions, not a stack of inputs
   ========================================================================== */

.panel {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--space-7);
}
.panel + .panel { margin-top: var(--space-6); }
.panel--quiet { background: var(--surface-elevated); }
.panel__head { margin-bottom: var(--space-6); }
.panel__title { font-size: var(--text-lg); font-weight: 600; }
.panel__hint { color: var(--text-tertiary); margin-top: 4px; }
.panel__divider {
  block-size: 1px; background: var(--border);
  margin-block: var(--space-7) var(--space-6);
}

.field-block + .field-block { margin-top: var(--space-6); }
.field-block .lbl { margin-bottom: var(--space-2); }
.field-block .meta { margin-top: 6px; color: var(--text-tertiary); line-height: 1.5; }
.input--area { min-block-size: 88px; padding-block: var(--space-4); line-height: 1.6; resize: vertical; }

/* A PREFIXED FIELD. The @ belongs to the control, not to the value: it says
   "this is a handle" before a character is typed and keeps the stored string
   clean. The wrapper carries the border so the whole thing reads as one
   control and focus lands on all of it. */
.input--prefixed {
  display: flex; align-items: stretch; padding: 0; overflow: hidden;
}
.input__prefix {
  display: inline-flex; align-items: center;
  padding-inline: var(--space-4);
  background: var(--surface-sunken); color: var(--text-secondary);
  border-inline-end: 1px solid var(--border);
  font-size: var(--text-base);
}
.input__field {
  flex: 1 1 auto; min-inline-size: 0;
  border: 0; background: transparent; color: inherit;
  padding-inline: var(--space-4);
  font: inherit; font-size: var(--text-base);
}
.input__field:focus { outline: none; }
.input--prefixed:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }

/* AVAILABILITY. Three states and each says a sentence — never a bare colour,
   and never a bare "invalid". */
.handle-state { margin-top: 6px; min-block-size: 18px; color: var(--text-tertiary); }
.handle-state[data-state='ok'] { color: var(--text-primary); font-weight: 500; }
.handle-state[data-state='bad'] { color: var(--text-primary); font-weight: 500; }
/* THE MARKER LEADS THE SENTENCE.
   As bare generated text the tick and the cross are neutral characters, so the
   bidi algorithm resolved them against the Arabic run and pushed both to the
   END of the line - a state marker arriving after the state it marks. Given
   its own inline box it sits at the line's start, which in RTL is the right. */
.handle-state::before { display: inline-block; margin-inline-end: 4px; }
.handle-state[data-state='ok']::before { content: '✓'; }
.handle-state[data-state='bad']::before { content: '✕'; }

/* THE ADDRESS, SHOWN. A reader must never have to assemble their own URL. */
.handle-url {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
}
.handle-url__label { color: var(--text-tertiary); flex: none; }
.handle-url__value {
  font-family: var(--font-ui); font-size: var(--text-sm);
  color: var(--text-primary); overflow-wrap: anywhere; min-inline-size: 0;
}
.button--sm { min-height: var(--control-h-sm); padding-inline: var(--space-4); font-size: var(--text-sm); }

/* THE AVATAR CONTROL. The real file input is present and focusable; the LABEL
   is the control. That is the accessible way to replace browser chrome rather
   than the way that breaks the keyboard. */
.avatar-edit {
  display: flex; align-items: flex-start; gap: var(--space-6);
  flex-wrap: wrap;
  padding-bottom: var(--space-7);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-7);
}
.avatar-edit__now { flex: none; }
.avatar-edit__now .avatar { inline-size: 88px; block-size: 88px; }
.avatar-edit__form { min-inline-size: 0; flex: 1 1 260px; }
.avatar-edit__controls { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
.avatar-edit__file { margin-top: var(--space-3); overflow-wrap: anywhere; }
.avatar-edit__limits { margin-top: var(--space-3); color: var(--text-tertiary); }
/* A label acting as a button must look pressable and focus with its input. */
label.button { cursor: pointer; }
.avatar-edit__controls:has(#avatar:focus-visible) label.button {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

/* A SWITCH ROW — one decision per line, with its consequence written under it. */
.switch {
  position: relative;
  display: flex; align-items: flex-start; gap: var(--space-4);
  padding: var(--space-4) 0;
  cursor: pointer;
}
.switch + .switch { border-top: 1px solid var(--border); }
.switch__input { position: absolute; inset-block-start: 0; inset-inline-start: 0; opacity: 0; pointer-events: none; }
.switch__track {
  flex: none; margin-top: 2px;
  inline-size: 44px; block-size: 26px; border-radius: 13px;
  border: 1px solid var(--control-border);
  background: var(--surface-sunken);
  display: inline-flex; align-items: center;
  padding: 2px;
  transition: background-color 140ms ease, border-color 140ms ease;
}
.switch__knob {
  inline-size: 20px; block-size: 20px; border-radius: 50%;
  background: var(--control-border);
  transition: transform 140ms ease, background-color 140ms ease;
}
/* RTL-CORRECT BY CONSTRUCTION. A logical translation moves the knob toward the
   inline end in both directions; a signed `translateX` would run the wrong way
   in one of them. */
.switch__input:checked + .switch__track {
  background: var(--interactive); border-color: var(--interactive);
}
/* Track 44px, minus 2px of border and 4px of padding, minus the 20px knob =
   18px of travel. A TRANSFORM IS PHYSICAL, not logical, so the sign has to be
   flipped for RTL by hand - a bare `translateX(18px)` would send the knob off
   the wrong end in Arabic, which is exactly the class of bug `inset-inline`
   exists to prevent everywhere else. */
.switch__input:checked + .switch__track .switch__knob {
  background: var(--surface);
  transform: translateX(calc(18px * var(--switch-dir, 1)));
}
:root { --switch-dir: 1; }
[dir='rtl'] { --switch-dir: -1; }
.switch__input:focus-visible + .switch__track { outline: 2px solid var(--focus); outline-offset: 2px; }
.switch__text { min-inline-size: 0; }
.switch__label { display: block; font-size: var(--text-base); font-weight: 500; }
.switch__text .meta { display: block; margin-top: 4px; color: var(--text-tertiary); line-height: 1.5; }

.id-form__actions {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  margin-top: var(--space-7);
}
.id-form__private { color: var(--text-tertiary); }

/* NAMED, NOT SUMMARISED. "Your data is protected" is a slogan; a list is a
   promise somebody can check. */
.privacy-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.privacy-list li {
  display: flex; align-items: flex-start; gap: var(--space-3);
  font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.5;
}
.privacy-list svg { flex: none; margin-top: 2px; color: var(--text-tertiary); }
.privacy-note { margin-top: var(--space-5); line-height: 1.6; max-inline-size: 62ch; }
.privacy-note a { color: var(--text-primary); text-decoration: underline; text-underline-offset: 3px; }


/* ==========================================================================
   THE MILESTONE SEAL — second construction
   ==========================================================================

   THE FIRST ONE WAS NOT TOO SMALL, IT WAS TOO QUIET. An 88px octagon with a
   1.5px hairline over `--surface-sunken` has almost no value contrast against
   its card, and the card almost none against the page, so earned and unearned
   sat in the same narrow band of grey. Restraint had been confused with
   absence.

   AN ACHROMATIC SYSTEM HAS ONE WAY TO BE MONUMENTAL: the full value range. An
   earned seal is now SOLID `--interactive` carrying `--text-inverse` content -
   the strongest mark this palette can make, with no hue at all. It reads as
   something struck into the page rather than drawn on it. There is one earned
   state and every earned seal gets all of it: no gold, no rarity, no tiers of
   worth. */

.seal {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  inline-size: var(--seal-size);
  block-size: var(--seal-size);
  color: var(--text-tertiary);
}

/* FIVE SIZES, EACH WITH A JOB, and every numeral lands on the type scale -
   none of these is an invented number.

     hero    the showcase and the public profile's featured marks
     major   the milestone a reader is standing on, in a journey
     minor   milestones already behind them
     quiet   milestones still ahead
     chip    dense strips and notification rows                              */
.seal--hero  { --seal-size: 128px; --seal-num: var(--text-3xl);  --seal-stroke: 1.5px; }
.seal--major { --seal-size:  96px; --seal-num: var(--text-2xl);  --seal-stroke: 1.5px; }
.seal--minor { --seal-size:  72px; --seal-num: var(--text-xl);   --seal-stroke: 1.25px; }
.seal--quiet { --seal-size:  52px; --seal-num: var(--text-lg);   --seal-stroke: 1px; }
.seal--chip  { --seal-size:  40px; --seal-num: var(--text-base); --seal-stroke: 1px; }

.seal__frame {
  position: absolute; inset: 0;
  inline-size: 100%; block-size: 100%;
  overflow: visible;
}

/* UNEARNED: the identical silhouette as a dashed hairline, nothing filled.
   Visibly a future thing rather than a failed one, and never louder than
   something earned. */
.seal__plate {
  fill: none;
  stroke: var(--border);
  stroke-width: var(--seal-stroke);
  stroke-dasharray: 5 4;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}
.seal__inner { display: none; }

/* EARNED: solid ink, inverse content, and an inner hairline in the inverse ink
   so the shape reads as PRESSED rather than as a filled polygon. That inner
   line is the difference between a seal and a black octagon. */
.seal[data-earned] .seal__plate {
  fill: var(--interactive);
  stroke: var(--interactive);
  stroke-dasharray: none;
}
.seal[data-earned] .seal__inner {
  display: block;
  fill: none;
  stroke: var(--text-inverse);
  stroke-width: 1px;
  stroke-opacity: .38;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}
.seal[data-earned] .seal__inner--second { display: none; }

/* THE TIER IS FRAME RHYTHM, NOT WORTH. A second concentric line appears at the
   top of a family - a double-struck stamp. It is a discrete step somebody can
   actually see, unlike a quarter-pixel of stroke, and it says "this is the far
   end of this path" without ranking one reader against another. */
.seal[data-earned][data-tier='4'] .seal__inner--second,
.seal[data-earned][data-tier='5'] .seal__inner--second { display: block; stroke-opacity: .28; }

.seal__body {
  position: relative;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 0;
  line-height: 1;
  color: var(--text-tertiary);
}
.seal[data-earned] .seal__body { color: var(--text-inverse); }

.seal__num {
  font-family: var(--font-ui);
  font-size: var(--seal-num);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
}

/* THE SEAL'S OWN HORIZON. Without it the numeral and the glyph stack into one
   blob; with it the number leads and the glyph reads as a signature under it. */
.seal__rule {
  display: block;
  inline-size: 42%;
  block-size: 1px;
  margin-block: calc(var(--seal-size) * 0.05) calc(var(--seal-size) * 0.045);
  background: currentColor;
  opacity: .45;
}
.seal__body svg { display: block; opacity: .8; }
.seal[data-earned] .seal__body svg { opacity: .92; }

/* A BEGINNING HAS NO NUMBER. The glyph takes the space the numeral would have
   had, because a first chapter is marked by what was done rather than by how
   much - and "1" on a seal reads as a rank. */
.seal[data-mark='glyph'] .seal__body svg { opacity: .95; }

/* ==========================================================================
   THE MARK CARD — the seal, its name, and nothing competing with either
   ========================================================================== */

.mark {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-4);
  text-align: center;
  min-inline-size: 0;
}
/* NO CARD AROUND IT. A border around a seal is chrome competing with the one
   thing on the page that is supposed to be loud - and §4 is explicit that a
   card earns its frame by grouping things that would otherwise float. A seal
   does not float; it is the heaviest object on the page. */
.mark__name {
  font-size: var(--text-md); font-weight: 600;
  color: var(--text-primary); line-height: 1.35;
  overflow-wrap: anywhere;
}
.mark__desc {
  color: var(--text-tertiary); line-height: 1.5;
  max-inline-size: 22ch; overflow-wrap: anywhere;
}
.mark__state { color: var(--text-secondary); }

.seal-shelf {
  display: flex; flex-wrap: wrap;
  gap: var(--space-9) var(--space-8);
  align-items: flex-start;
}
.seal-shelf .mark { inline-size: 168px; }

@media (max-width: 739px) {
  .seal--hero  { --seal-size: 96px; --seal-num: var(--text-2xl); }
  .seal--major { --seal-size: 76px; --seal-num: var(--text-xl); }
  .seal--minor { --seal-size: 60px; --seal-num: var(--text-lg); }
  .seal--quiet { --seal-size: 48px; --seal-num: var(--text-base); }
  .seal-shelf { gap: var(--space-7) var(--space-6); }
  .seal-shelf .mark { inline-size: 124px; }
  .mark__name { font-size: var(--text-base); }
}

/* ==========================================================================
   THE READING RECORD — level 1
   ==========================================================================

   "3 / 15" at 32px is metadata about a settings page. The count of what
   somebody has recorded from their own reading is the SUBJECT of this page,
   and it is now set like one: a single display numeral, with the total present
   but subordinate. The total is context, not a score to complete. */

.record {
  padding-block: var(--space-8) var(--space-10);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-10);
}
.record__label {
  font-size: var(--text-sm); font-weight: 500;
  color: var(--text-tertiary);
  margin-bottom: var(--space-5);
}
.record__body {
  display: flex; align-items: flex-start;
  gap: var(--space-9); flex-wrap: wrap;
}
.record__count {
  display: flex; align-items: baseline; gap: var(--space-4);
  flex: none;
}
.record__n {
  font-family: var(--font-ui);
  font-size: var(--text-display);
  font-weight: 600; line-height: .85;
  letter-spacing: -0.04em;
  color: var(--text-primary);
}
.record__of { font-size: var(--text-md); color: var(--text-tertiary); }

.record__aside { flex: 1 1 280px; min-inline-size: 0; padding-top: 6px; }
.record__lead {
  font-size: var(--text-md); line-height: 1.55;
  color: var(--text-secondary); max-inline-size: 46ch;
}
.record__facts {
  display: flex; flex-wrap: wrap; gap: var(--space-5) var(--space-9);
  margin: var(--space-6) 0 0;
}
.record__facts dt { color: var(--text-tertiary); margin-bottom: 4px; }
.record__facts dd {
  margin: 0; font-size: var(--text-base); font-weight: 600;
  color: var(--text-primary); overflow-wrap: anywhere;
}
.record__frac { font-weight: 500; color: var(--text-secondary); margin-inline-start: 6px; }

/* ==========================================================================
   THE JOURNEY — level 3
   ==========================================================================

   The first ladder drew six equal steps and let earned/unearned be the only
   difference, which is a LIST of milestones rather than a journey through
   them. Three sizes and two line styles now carry past / current / future
   without a legend:

     past     solid seal, `minor`      what is behind
     current  solid seal, `major`      the largest object in the row
     future   dashed seal, `quiet`     what is ahead

   The connector is solid where the reader has been and dashed where they have
   not, and the segment leaving the current step is filled to the honest
   fraction of the way to the next milestone. */

.journey + .journey {
  margin-top: var(--space-10);
  padding-top: var(--space-9);
  border-top: 1px solid var(--border);
}
.journey__head {
  display: flex; align-items: baseline; gap: var(--space-4);
  margin-bottom: var(--space-6);
}
.journey__label { font-size: var(--text-md); font-weight: 600; }
.journey__count { color: var(--text-tertiary); }

.journey__track {
  position: relative;
  display: flex; align-items: flex-start;
  gap: var(--space-6);
  list-style: none; margin: 0; padding: 0 0 var(--space-3);
  /* A family can hold more steps than a phone is wide. It scrolls inside its
     own container; the page body never scrolls sideways. */
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
}
.journey__step {
  position: relative;
  flex: none;
  inline-size: 116px;
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-3);
  text-align: center;
  scroll-snap-align: start;
  /* Every step aligns on the CENTRE LINE of the largest seal, so a row of
     three sizes reads as one path rather than as three rows of debris. */
  padding-top: calc((96px - var(--own, 96px)) / 2);
}
.journey__step[data-pos='past']    { --own: 72px; }
.journey__step[data-pos='future']  { --own: 52px; }
.journey__step[data-pos='current'] { --own: 96px; }

/* THE CONNECTOR, DRAWN PER STEP so it is exactly as long as the journey is -
   a rule spanning the scroll container ran hundreds of pixels past the last
   seal into empty space. */
.journey__step::before {
  content: '';
  position: absolute;
  top: 48px;
  inset-inline-start: 50%;
  inline-size: calc(100% + var(--space-6));
  block-size: 1px;
  background: var(--border);
}
.journey__step:last-child::before { content: none; }
/* Behind them: solid. Ahead: dashed. */
.journey__step[data-pos='past']::before {
  background: var(--interactive);
}
.journey__step[data-pos='future']::before {
  background: none;
  border-top: 1px dashed var(--border);
}
/* THE ONE SEGMENT THAT IS PARTLY TRAVELLED. Two solid colours with a hard
   stop - a progress fill, not a decorative gradient - measured between the two
   thresholds it joins rather than from zero. */
.journey__step[data-pos='current']::before {
  background: linear-gradient(
    to var(--seg-dir, left),
    var(--interactive) 0 var(--seg, 0%),
    var(--border) var(--seg, 0%) 100%
  );
}
:root { --seg-dir: right; }
[dir='rtl'] { --seg-dir: left; }

.seal { position: relative; z-index: 1; }
.journey__step .seal { background: var(--surface); }

.journey__name {
  font-size: var(--text-sm); line-height: 1.35;
  color: var(--text-secondary); overflow-wrap: anywhere;
}
.journey__state { color: var(--text-tertiary); }
.journey__step[data-pos='past'] .journey__name,
.journey__step[data-pos='current'] .journey__name {
  color: var(--text-primary); font-weight: 600;
}
.journey__step[data-pos='current'] .journey__name { font-size: var(--text-base); }
.journey__step[data-pos='past'] .journey__state,
.journey__step[data-pos='current'] .journey__state { color: var(--text-secondary); }

/* A JOURNEY NOBODY HAS STARTED is a map, not a reprimand: it keeps its shape
   and simply has no current step. */
.journey[data-unstarted='true'] .journey__label { color: var(--text-secondary); }

/* ==========================================================================
   THE ARCHIVE STRIP — earned, compact
   ========================================================================== */

.seal-strip {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--space-4);
}
.seal-chip {
  display: flex; align-items: center; gap: var(--space-3);
  padding-inline-end: var(--space-4);
}
.seal-chip__name { font-size: var(--text-sm); font-weight: 500; overflow-wrap: anywhere; }

/* ==========================================================================
   PUBLIC PROFILE — identity as ONE composition
   ==========================================================================

   THE OPERATOR'S READING OF THE LAST VERSION WAS RIGHT. The avatar sat at one
   edge, the name beside it, the featured marks at the far other edge, and
   several hundred pixels of nothing in between - four related things
   distributed across the full width read as four unrelated things.

   The identity cluster is now TIGHT (avatar, name, handle, bio, joined, all
   in one block) and the featured marks sit immediately beside it as part of
   the same composition rather than at the opposite end of the band. At 1440
   the two together fill the measure; the remaining space is the band's own
   margin rather than a hole between two halves. */

.pro-hero {
  background: var(--surface-elevated);
  border-bottom: 1px solid var(--border);
  padding-block: var(--space-10);
}
.pro-hero__inner {
  display: flex; align-items: center;
  gap: var(--space-10);
  flex-wrap: wrap;
}
.pro-hero__who {
  display: flex; align-items: center; gap: var(--space-7);
  flex: 0 1 auto; min-inline-size: 0;
}
.pro-hero__mark { flex: none; }
.pro-hero__mark .avatar { inline-size: 120px; block-size: 120px; }
.pro-hero__id { min-inline-size: 0; max-inline-size: 42ch; }
.pro-hero__name {
  font-size: var(--text-2xl); line-height: 1.15; font-weight: 600;
  letter-spacing: -0.01em; overflow-wrap: anywhere;
}
.pro-hero__handle {
  margin-top: 6px;
  font-family: var(--font-ui);
  font-size: var(--text-md); color: var(--text-secondary);
}
.pro-hero__bio {
  margin-top: var(--space-5);
  max-inline-size: 46ch;
  font-family: var(--font-synopsis);
  font-size: var(--text-base); line-height: 1.7;
  color: var(--text-secondary); overflow-wrap: anywhere;
}
.pro-hero__joined { margin-top: var(--space-4); color: var(--text-tertiary); }

/* THE FEATURED MARKS ARE PART OF THE IDENTITY, not a section under it. A
   reader chose exactly these to represent themselves; at chip size that choice
   was invisible, and a visitor could not answer "why these three" without
   squinting. */
.pro-hero__marks {
  flex: 0 1 auto; min-inline-size: 0;
  padding-inline-start: var(--space-10);
  border-inline-start: 1px solid var(--border);
}
.pro-hero__marks-label {
  color: var(--text-tertiary); margin-bottom: var(--space-6);
}
.seal-shelf--hero { gap: var(--space-7); }
.seal-shelf--hero .mark { inline-size: 132px; }
.seal-shelf--hero .mark__desc { display: none; }

.pro-body { padding-block: var(--space-9) var(--space-11); }
.pro-quiet { color: var(--text-tertiary); padding-block: var(--space-8); }

@media (max-width: 1119px) {
  /* The divider only reads as a divider while the two halves are side by side.
     Once they stack it becomes a stray line across the page. */
  .pro-hero__marks {
    inline-size: 100%;
    padding-inline-start: 0; border-inline-start: 0;
    padding-top: var(--space-8); border-top: 1px solid var(--border);
  }
  .pro-hero__inner { gap: var(--space-8); }
}

@media (max-width: 739px) {
  .record { padding-block: var(--space-7) var(--space-8); margin-bottom: var(--space-8); }
  .record__body { gap: var(--space-6); }
  .record__n { font-size: var(--text-3xl); }
  .record__lead { font-size: var(--text-base); }
  .journey__track { gap: var(--space-5); }
  .journey__step { inline-size: 96px; padding-top: calc((76px - var(--own, 76px)) / 2); }
  .journey__step[data-pos='past']    { --own: 60px; }
  .journey__step[data-pos='future']  { --own: 48px; }
  .journey__step[data-pos='current'] { --own: 76px; }
  .journey__step::before { top: 38px; inline-size: calc(100% + var(--space-5)); }
  .pro-hero { padding-block: var(--space-8); }
  .pro-hero__who { gap: var(--space-5); }
  .pro-hero__mark .avatar { inline-size: 84px; block-size: 84px; }
  .pro-hero__name { font-size: var(--text-xl); }
  .pro-hero__handle { font-size: var(--text-base); }
  .seal-shelf--hero .mark { inline-size: 104px; }
}

/* ==========================================================================
   CARRIED FORWARD from the first visual pass — still correct, still needed
   ==========================================================================

   These are not new. They are the rules from the previous pass that survive
   the rebuild, restated here rather than left in a block that happened not to
   be deleted, so there is one place to read what is in force. */

/* A progress pair is a left-to-right expression made of neutrals; dropped into
   an RTL paragraph the bidi algorithm reverses the run and the reader sees a
   number larger than its own target. */
.numpair { direction: ltr; unicode-bidi: isolate; display: inline-block; }
.handle-url__value { direction: ltr; unicode-bidi: isolate; }

/* `[hidden]` is `display: none` at specificity 0,0,0 in the UA sheet, and any
   author rule that sets `display` outranks it. That cost this product a
   floating bell and an upload button inviting an empty upload, so it is stated
   once, generally. */
[hidden] { display: none !important; }

/* ARTWORK LEADS on a public list card: a list of somebody's works whose card
   shows no work is a database row with a border round it.

   A fixed four-column grid left a hole whenever a list held three, and an empty
   cell inside a bordered card reads as a cover that failed to load. A flex row
   with equal shares always fills, whatever the count; the row's HEIGHT is fixed
   and the covers crop to it, which is what `object-fit: cover` is for. */
.list-grid {
  display: grid; gap: var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.list-card {
  display: flex; flex-direction: column; gap: var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface);
  color: inherit; text-decoration: none;
}
.list-card:hover { border-color: var(--control-border); }
.list-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.list-card__art { display: flex; gap: 4px; block-size: 116px; }
.list-card__cover { flex: 1 1 0; min-inline-size: 0; block-size: 100%; }
.list-card__cover .cover {
  block-size: 100%; inline-size: 100%; aspect-ratio: auto;
  border-radius: var(--radius-sm); overflow: hidden;
}
.list-card__cover .cover--missing .plus { inline-size: 24px; block-size: 24px; }
.list-card__cover .cover--missing .tiny { display: none; }
/* An empty list is a real state, and it gets a deliberate treatment rather
   than a filled slab that reads as a skeleton still loading. */
.list-card__empty {
  flex: 1 1 100%; block-size: 100%;
  display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-tertiary);
}
.list-card__text { display: block; min-inline-size: 0; }
.list-card__name { display: block; font-size: var(--text-base); font-weight: 600; overflow-wrap: anywhere; }
.list-card__count { display: block; color: var(--text-tertiary); margin-top: 2px; }
.list-card__desc { display: block; color: var(--text-secondary); margin-top: var(--space-3); line-height: 1.5; overflow-wrap: anywhere; }

/* Identity form measures, and a status line that reserves no space until it
   has something to say. */
.id-form .field-block { max-inline-size: 620px; }
.handle-state:empty { display: none; }
.field-block .handle-state { margin-top: var(--space-2); }

/* An unlock row announces a mark, so it shows the mark. */
.notification-row .seal { flex: none; margin-top: 2px; }

@media (max-width: 559px) {
  .list-grid { grid-template-columns: 1fr; }
}


/* ==========================================================================
   SECOND VISUAL ROUND — composition catching up with the new weight
   ========================================================================== */

/* --- THE HERO COMPOSES AT EVERY MARK COUNT -------------------------------
   Packed at the start, the band filled beautifully with three featured marks
   and left half the measure empty with one - and completely empty with none.
   Centring the whole composition costs nothing at three (it already spans) and
   turns one mark, or none, into a deliberate name plate instead of a block
   hanging off one edge. */
.pro-hero__inner { justify-content: center; }

/* --- VERTICAL RHYTHM ON THE PUBLIC PAGE ----------------------------------
   The hero already separates itself with a rule and a change of ground, so a
   further 120px before the first section is air nobody asked for. */
.pro-body { padding-block: var(--space-8) var(--space-10); }
.pro-body .section + .section { margin-top: var(--space-10); }

/* --- AN EMPTY PROFILE IS A COMPOSITION ----------------------------------- */
.pro-empty {
  margin-inline: auto;
  max-inline-size: 44ch;
  padding: var(--space-10) var(--space-8);
  text-align: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.pro-empty__lead { font-size: var(--text-md); color: var(--text-secondary); }
.pro-empty__sub { margin-top: var(--space-4); color: var(--text-tertiary); line-height: 1.6; }

/* --- THE JOURNEY HEAD SPANS ITS OWN ROW ---------------------------------- */
.journey__head { justify-content: flex-start; }
.journey__total {
  margin-inline-start: auto;
  color: var(--text-secondary);
}
.journey__total .tabular { color: var(--text-primary); font-weight: 600; }

/* --- THE RECORD HEAD SPANS THE COLUMN ------------------------------------
   Numeral, sentence, and the two facts at the far end - rather than everything
   packed against one edge with a third of the column empty. */
.record__body { justify-content: flex-start; }
.record__aside { flex: 1 1 auto; display: flex; align-items: flex-start; gap: var(--space-9); flex-wrap: wrap; }
.record__lead { flex: 0 1 32ch; }
.record__facts { margin: 0; margin-inline-start: auto; flex: 0 0 auto; }

@media (max-width: 939px) {
  .record__aside { gap: var(--space-6); }
  .record__facts { margin-inline-start: 0; }
}

/* --- THE SHOWCASE USES ITS MEASURE ---------------------------------------
   Three marks at 168px left a third of the account column empty beside the
   most important region on the page. Wider cells and a wider column gap span
   it without inflating the seal, which is already at the size the composition
   wants. */
.seal-shelf { gap: var(--space-9) var(--space-11); }
.seal-shelf .mark { inline-size: 184px; }
@media (max-width: 739px) {
  .seal-shelf { gap: var(--space-7) var(--space-6); }
  .seal-shelf .mark { inline-size: 124px; }
}

/* --- THE HERO'S OWN GEOMETRY, ALL OF IT, IN ONE PLACE ---------------------
   The account showcase and the public hero share `.seal-shelf`, and the
   account page's wider cells AND wider column gap were both overriding the
   hero's - written earlier at equal specificity, so source order decided.
   Measured: identity 459 + gap 48 + marks 573 = 1080 in a 1060 box, which is
   why it wrapped by twenty pixels and left the divider beside a row it no
   longer divided.

   At 132px cells and a 24px gap the marks are 492 and the composition fits
   with room. Everything hero-specific is here, last, so the next disagreement
   has one place to be resolved. */
.pro-hero .seal-shelf--hero { gap: var(--space-7); }
.pro-hero .seal-shelf--hero .mark { inline-size: 132px; }

/* And if the composition ever does wrap - a very long display name, a large
   text-zoom - the divider must go with it rather than becoming a stub. */
@media (max-width: 1199px) {
  .pro-hero__marks { padding-inline-start: 0; border-inline-start: 0; }
}


/* ==========================================================================
   SEARCH PANEL — one control, every size
   ========================================================================== */

.search-panel { position: fixed; inset: 0; z-index: 80; }
.search-panel__scrim { position: absolute; inset: 0; background: rgb(0 0 0 / .45); }

.search-panel__body {
  position: relative;
  margin-inline: auto;
  margin-block-start: min(12vh, 96px);
  max-inline-size: 680px;
  padding-inline: var(--pad);
}

/* THE FIELD IS THE CONTROL, and it carries the border - so the icon, the input
   and the close button read as one object rather than three things that happen
   to be adjacent. */
.search-panel__field {
  display: flex; align-items: center; gap: var(--space-4);
  padding-inline: var(--space-5);
  background: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-md);
  min-block-size: 56px;
}
.search-panel__field svg { flex: none; color: var(--text-tertiary); }
.search-panel__input {
  flex: 1 1 auto; min-inline-size: 0;
  border: 0; background: transparent; color: var(--text-primary);
  font: inherit; font-size: var(--text-md);
  padding-block: var(--space-4);
}
.search-panel__input:focus { outline: none; }
.search-panel__field:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
/* The browser's own clear affordance is chrome this product did not design. */
.search-panel__input::-webkit-search-cancel-button { display: none; }
.search-panel__close { flex: none; }

.search-suggestions {
  list-style: none; margin: var(--space-4) 0 0; padding: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  max-block-size: min(52vh, 460px); overflow-y: auto;
}
.search-suggestion__link {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  color: inherit; text-decoration: none;
}
/* SELECTED IS NOT A COLOUR. A filled surface and a firmer name, so the
   keyboard position is visible to somebody who cannot separate two greys. */
.search-suggestion[aria-selected='true'] .search-suggestion__link,
.search-suggestion__link:hover {
  background: var(--surface-sunken);
}
.search-suggestion[aria-selected='true'] .search-suggestion__name { font-weight: 700; }

/* ONE RATIO for every thumbnail, whatever the artwork is - a row of natural
   ratios is the fastest way to make a list look unfinished. */
.search-suggestion__art {
  flex: none; inline-size: 40px; block-size: 56px;
  border-radius: var(--radius-sm); overflow: hidden;
  background: var(--surface-sunken);
}
.search-suggestion__art img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
.search-suggestion__text { min-inline-size: 0; display: block; }
.search-suggestion__name { display: block; font-size: var(--text-base); font-weight: 600; overflow-wrap: anywhere; }
.search-suggestion__meta { display: block; color: var(--text-tertiary); margin-top: 2px; }

.search-panel__empty {
  margin-top: var(--space-4); padding: var(--space-5);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); color: var(--text-secondary);
}
.search-panel__all { margin-top: var(--space-4); inline-size: 100%; }

@media (max-width: 559px) {
  .search-panel__body { margin-block-start: 0; padding-inline: 0; }
  .search-panel__field { border-radius: 0; border-inline: 0; min-block-size: 64px; }
  .search-suggestions { border-radius: 0; border-inline: 0; max-block-size: calc(100vh - 140px); }
  .search-panel__all { border-radius: 0; }
}

/* ==========================================================================
   BROWSE — the controls a reader arrives with
   ========================================================================== */

.browse-controls {
  display: flex; flex-wrap: wrap; gap: var(--space-4);
  align-items: center; justify-content: space-between;
  margin-top: var(--space-5);
}
/* The sort is the same KIND of decision as the facet, so it is the same
   control - quieter, because it orders a list rather than choosing one. */
.segmented--quiet { --seg-quiet: 1; }
.segmented--quiet a { font-size: var(--text-sm); }

.browse-facets {
  display: grid; gap: var(--space-4);
  margin-top: var(--space-6);
  padding-block: var(--space-5);
  border-block: 1px solid var(--border);
}
.browse-facet { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
.browse-facet__label {
  color: var(--text-tertiary); flex: none;
  min-inline-size: 4.5em;
}
/* SELECTED IS FILL PLUS WEIGHT, never a shade: a chip row read only by hue is
   a chip row half the readers cannot use. */
.browse-facet .chip[aria-current="true"] {
  background: var(--interactive); color: var(--text-inverse);
  border-color: var(--interactive); font-weight: 600;
}

.browse-summary {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap;
  margin-top: var(--space-5);
}
.browse-clear { color: var(--text-secondary); text-decoration: underline; text-underline-offset: 3px; }
.browse-clear:hover { color: var(--text-primary); }

@media (max-width: 559px) {
  .browse-facet__label { min-inline-size: 100%; margin-bottom: 2px; }
  .browse-controls { gap: var(--space-3); }
}

/* ==========================================================================
   RELATED WORKS — the poster language, one step quieter
   ========================================================================== */

.related { margin-top: var(--space-10); }
.related-grid {
  display: grid; gap: var(--space-5);
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.related-card { display: block; color: inherit; text-decoration: none; min-inline-size: 0; }
.related-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--radius-sm); }
/* ARTWORK OUTRANKS THE INTERFACE. No frame around the poster - the poster is
   the card, and a border here would be chrome competing with the one thing on
   the row worth looking at. */
.related-card__text { display: block; margin-top: var(--space-3); }
.related-card__name {
  display: block; font-size: var(--text-sm); font-weight: 600;
  line-height: 1.35; overflow-wrap: anywhere;
}
.related-card:hover .related-card__name { text-decoration: underline; text-underline-offset: 3px; }
/* THE REASON IS SUPPORTING TEXT. It explains the row; it is not the row. */
.related-card__why { display: block; color: var(--text-tertiary); margin-top: 2px; }

/* ==========================================================================
   DISCOVER — one surface, two ways of asking
   ========================================================================== */

/* NOT A CHAT. No bubbles, no avatar, no typing indicator: a reader states a
   request, the page shows what it understood, then it shows works. Dressing a
   vocabulary matcher as a conversation would promise an understanding it does
   not have. */
.ask { margin-top: var(--space-6); max-inline-size: 720px; }
.ask__row { display: flex; gap: var(--space-3); align-items: stretch; margin-top: var(--space-2); }
.ask__input { flex: 1 1 auto; min-inline-size: 0; }
.ask__examples {
  list-style: none; margin: var(--space-4) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--space-3);
}

/* WHAT IT UNDERSTOOD, shown before what it found - so a reader can tell a
   wrong ANSWER from a misunderstood QUESTION. */
.ask-read {
  list-style: none; margin: var(--space-7) 0 var(--space-5); padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center;
}
/* A chip that reports rather than links: same shape, no affordance it cannot
   honour. */
.chip--static { cursor: default; background: var(--surface-sunken); border-color: var(--border); }

.ask-note {
  margin-top: var(--space-7);
  padding: var(--space-7);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  max-inline-size: 56ch;
}
.ask-note__lead { font-size: var(--text-md); color: var(--text-primary); }
.ask-note__sub { margin-top: var(--space-3); color: var(--text-tertiary); line-height: 1.6; }

/* The reason explains the row; it is not the row. */

@media (max-width: 559px) {
  .ask__row { flex-wrap: wrap; }
  .ask__row .button { inline-size: 100%; }
}

/* =========================================================================
   THE ASSISTANT
   =========================================================================
   A floating control and a conversational panel, built from the product's own
   control language rather than from the chat-widget one.

   ACHROMATIC. The control is `--interactive`, the same filled block as every
   primary action. No brand colour, no gradient, no shadow — this is not a
   shadowed product, so the panel reads as raised through an elevated surface
   and a hairline, exactly like `.resume-prompt` does.

   LOGICAL PROPERTIES THROUGHOUT. `inset-inline-end` puts the control in the
   trailing corner, which is bottom-LEFT in this RTL product and bottom-right
   if the direction ever flips. Writing `right:` here would have pinned it to
   the wrong corner the moment anybody looked at it in Arabic.

   IT NEVER APPEARS IN THE READER, so there is no collision with `.reader-bar`
   (z 40) or `.resume-prompt` (z 39). The control sits below both anyway.
   ========================================================================= */

.assistant { position: static; }

/* ------------------------------------------------------------ the control */
.assistant__open {
  position: fixed; z-index: 38;
  inset-block-end: var(--space-5);
  inset-inline-end: var(--space-5);

  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: var(--control-h);
  padding-inline: var(--space-4);

  background: var(--interactive);
  color: var(--text-inverse);
  border: 0;
  border-radius: var(--control-r);
  cursor: pointer;

  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
}
.assistant__open:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
.assistant__open:hover { opacity: .92; }
.assistant__open:active { transform: translateY(1px); }

/* THE LABEL IS THE FIRST-USE EXPLANATION, and it is not permanent clutter:
   once the panel has been opened the control keeps only its mark, because a
   button that has already been understood does not need to keep explaining
   itself. */
.assistant[data-known='1'] .assistant__open-label { display: none; }
.assistant[data-known='1'] .assistant__open { padding-inline: 0; inline-size: var(--control-h); justify-content: center; }

/* -------------------------------------------------------------- the panel */
.assistant__panel {
  position: fixed; z-index: 55;
  inset-block-end: var(--space-5);
  inset-inline-end: var(--space-5);
  inline-size: min(400px, calc(100vw - var(--space-8)));
  max-block-size: min(620px, calc(100vh - var(--space-10)));

  display: flex; flex-direction: column;
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.assistant__panel[hidden] { display: none; }

.assistant__scrim { display: none; }

.assistant__head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-block-end: 1px solid var(--border);
  flex: 0 0 auto;
}
.assistant__title {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-size: var(--text-sm); font-weight: 600; margin: 0;
}
.assistant__head-actions { display: inline-flex; align-items: center; gap: var(--space-2); }

.assistant__text-action {
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-ui); font-size: var(--text-xs);
  color: var(--text-secondary);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
}
.assistant__text-action:hover { color: var(--text-primary); }
.assistant__text-action:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.assistant__context {
  margin: 0; padding: var(--space-2) var(--space-4);
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border-block-end: 1px solid var(--border);
  flex: 0 0 auto;
}
.assistant__context[hidden] { display: none; }

/* ------------------------------------------------------------------- log */
.assistant__log {
  flex: 1 1 auto; overflow-y: auto;
  padding: var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-5);
  /* Grid and flex children default to min-height:auto, which stops a scroll
     container from ever scrolling. Named because this product has hit it. */
  min-block-size: 0;
}

.assistant__intro-text { margin: 0 0 var(--space-3); color: var(--text-secondary); font-size: var(--text-sm); }

.assistant__starters { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.assistant__starter {
  min-height: var(--control-h-sm);
  padding-inline: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--control-r);
  color: inherit; cursor: pointer;
  font-family: var(--font-ui); font-size: var(--text-xs);
}
.assistant__starter:hover { background: var(--surface-sunken); }
.assistant__starter:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* A TURN IS NOT A SPEECH BUBBLE.
   The question is a quiet labelled line; the answer is ordinary prose. That is
   more legible than two rows of coloured lozenges, and it is what this
   product's editorial voice already looks like everywhere else. */
.assistant__turn { display: block; }
.assistant__q {
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm); font-weight: 600;
  color: var(--text-primary);
}
.assistant__q::before {
  content: '';
  display: inline-block;
  inline-size: 2px; block-size: 1em;
  background: var(--interactive);
  margin-inline-end: var(--space-2);
  vertical-align: -2px;
}
.assistant__a {
  margin: 0; font-size: var(--text-sm); line-height: 1.7;
  color: var(--text-primary);
  white-space: pre-wrap;
  /* Long Arabic without spaces (a pasted URL, a long name) must not push the
     panel wide. */
  overflow-wrap: anywhere;
}
.assistant__a--pending { color: var(--text-secondary); }

/* -------------------------------------------------------- grounded results */
.assistant__works { display: flex; flex-direction: column; gap: var(--space-2); margin-block-start: var(--space-3); }

.assistant__work {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: inherit; text-decoration: none;
  min-inline-size: 0;
}
.assistant__work:hover { background: var(--surface-sunken); }
.assistant__work:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.assistant__work-cover {
  inline-size: 36px; flex: 0 0 auto; aspect-ratio: 2 / 3;
  background: var(--surface-sunken); border-radius: 4px; overflow: hidden;
}
.assistant__work-cover img { inline-size: 100%; block-size: 100%; object-fit: cover; }

.assistant__work-text { min-inline-size: 0; }
.assistant__work-name { font-size: var(--text-sm); font-weight: 600; }
.assistant__work-meta { font-size: var(--text-xs); color: var(--text-secondary); }

/* A site-help answer ends in a door, not in "look in your settings". */
.assistant__action {
  display: inline-flex; align-items: center; min-height: var(--control-h-sm);
  margin-block-start: var(--space-3); padding-inline: var(--space-3);
  border: 1px solid var(--control-border); border-radius: var(--control-r);
  color: inherit; text-decoration: none;
  font-size: var(--text-xs); font-weight: 600;
}
.assistant__action:hover { background: var(--surface-sunken); }
.assistant__action:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* --------------------------------------------------------------- composer */
.assistant__composer {
  flex: 0 0 auto;
  display: flex; align-items: flex-end; gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-block-start: 1px solid var(--border);
}

.assistant__input {
  flex: 1 1 auto; min-inline-size: 0;
  resize: none; max-block-size: 120px;
  padding: var(--space-3);
  background: var(--surface);
  color: inherit;
  border: 1px solid var(--control-border);
  border-radius: var(--control-r);
  font-family: var(--font-ui); font-size: var(--text-sm); line-height: 1.5;
}
.assistant__input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.assistant__send {
  flex: 0 0 auto;
  inline-size: var(--control-h); block-size: var(--control-h);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--interactive); color: var(--text-inverse);
  border: 0; border-radius: var(--control-r); cursor: pointer;
}
.assistant__send:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
.assistant__send:disabled { opacity: .5; cursor: not-allowed; }

/* ------------------------------------------------------------- small screens
   A 400px panel pinned to a corner is wrong on a phone: the on-screen keyboard
   takes half the viewport and a floating card ends up squeezed behind it. So
   below the 560 band the panel becomes a sheet anchored to the bottom, sized
   with dvh so the keyboard shrinks it rather than covering it. */
@media (max-width: 559px) {
  .assistant__panel {
    inset-inline: 0;
    inset-block-end: 0;
    inline-size: auto;
    max-block-size: 85dvh;
    block-size: 85dvh;
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    border-inline: 0; border-block-end: 0;
    padding-block-end: env(safe-area-inset-bottom);
  }

  .assistant__scrim {
    display: block; position: fixed; inset: 0; z-index: 54;
    background: rgba(0, 0, 0, .45);
  }
  .assistant__scrim[hidden] { display: none; }

  .assistant__open {
    inset-block-end: calc(var(--space-4) + env(safe-area-inset-bottom));
    inset-inline-end: var(--space-4);
  }
}

/* The panel is a dialog on small screens, so the page behind it must not
   scroll underneath. */
body[data-assistant-open='1'] { overflow: hidden; }
@media (min-width: 560px) {
  body[data-assistant-open='1'] { overflow: auto; }
}

/* =========================================================================
   TRANSLATION REQUESTS
   =========================================================================
   A surface with no artwork, which is the whole design problem. Everywhere
   else in this product the cover supplies the colour and the hierarchy; here
   the title has to do both, so it is set larger than a catalogue card's title
   and given room rather than being wrapped in decoration to compensate.

   The brand name is deliberately not written here: this file is scanned by
   FrontendBrandSafetyTest, and the name must reach a page as configuration
   rather than as text somebody typed into a stylesheet.

   No status colours. The product is achromatic, and a status that is only a
   hue is unreadable to anybody who cannot separate them - the word carries it.
   ========================================================================= */

.requests-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-6); flex-wrap: wrap;
  margin-block-end: var(--space-6);
}
.requests-head__text { min-inline-size: 0; }
.requests-head__note {
  margin: var(--space-3) 0 0;
  color: var(--text-secondary);
  font-size: var(--text-sm); line-height: 1.6;
  max-inline-size: 52ch;
}

.requests-search {
  display: flex; gap: var(--space-3); align-items: center;
  margin-block-end: var(--space-5);
}
.requests-search .input { flex: 1 1 auto; min-inline-size: 0; }

.requests-controls {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap;
  padding-block-end: var(--space-5);
  border-block-end: 1px solid var(--border);
  margin-block-end: var(--space-6);
}

/* auto-fill rather than the anchored cover grid: these cells are sized by
   TEXT, not by a cover ratio, so the column count follows the measure. */
.requests-grid {
  display: grid; gap: var(--space-5);
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

.request-card {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-5);
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: inherit; text-decoration: none;
  /* Grid items default to min-width:auto, which lets a long unbroken title
     push the whole column wider. This product has hit that repeatedly. */
  min-inline-size: 0;
}
.request-card:hover { border-color: var(--control-border); }
.request-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

.request-card__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3);
  color: var(--text-secondary);
}
.request-card__kind { font-weight: 600; }
.request-card__status { text-align: end; }

.request-card__title {
  margin: 0;
  font-size: var(--text-lg); font-weight: 600; line-height: 1.4;
  overflow-wrap: anywhere;
}
.request-card__native {
  margin: 0; color: var(--text-secondary);
  overflow-wrap: anywhere;
}
.request-card__support {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  /* The count is the point of the card, so it is not tertiary small print. */
  color: var(--text-primary);
}

/* ------------------------------------------------------------------ detail */
.request-detail { max-inline-size: 760px; }
.request-detail__back { margin-block-end: var(--space-5); }

.request-detail__badges {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  margin-block-end: var(--space-4);
}
.request-detail__status {
  font-size: var(--text-sm); font-weight: 600;
  color: var(--text-secondary);
}
.request-detail__title {
  margin: 0; font-size: var(--text-2xl); font-weight: 600; line-height: 1.35;
  overflow-wrap: anywhere;
}
.request-detail__native {
  margin: var(--space-3) 0 0; color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.request-detail__demand {
  margin-block-start: var(--space-7);
  padding: var(--space-6);
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.request-detail__count {
  margin: 0 0 var(--space-5);
  font-size: var(--text-lg); font-weight: 600;
}
.request-detail__promise {
  margin: var(--space-4) 0 0; color: var(--text-secondary); line-height: 1.6;
}

.request-detail__merged {
  margin-block-start: var(--space-7);
  padding: var(--space-6);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap;
}
.request-detail__merged p { margin: 0; }

/* SUPPORTED IS A STATE, and it is visible without colour: the fill inverts and
   the label changes. A pressed control that only shifts hue is a control that
   half the readers cannot read. */
.request-support[aria-pressed='true'] {
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--control-border);
}

.request-detail__facts {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin-block-start: var(--space-7);
}
.request-fact {
  display: flex; flex-direction: column; gap: 4px;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  min-inline-size: 0;
}
.request-fact__label { color: var(--text-secondary); }
.request-fact__value { overflow-wrap: anywhere; }

.request-detail__linked {
  margin-block-start: var(--space-6);
  color: var(--text-secondary); font-size: var(--text-sm);
}

/* -------------------------------------------------------------------- form */
.request-form-wrap { max-inline-size: 640px; }
.request-form__note {
  margin: var(--space-4) 0 var(--space-6);
  color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6;
}
.request-form__actions {
  display: flex; gap: var(--space-3); margin-block-start: var(--space-7);
}

.request-refusal {
  padding: var(--space-5);
  background: var(--surface-sunken);
  border: 1px solid var(--control-border);
  border-radius: var(--radius-md);
  margin-block-end: var(--space-6);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap;
}
.request-refusal p { margin: 0; }

/* Two options, both worth seeing: a segmented pair rather than a select, and
   never the browser's own radio on a public surface. */
.request-kind { display: flex; gap: var(--space-3); }
.request-kind__option {
  flex: 1 1 0; min-inline-size: 0;
  display: flex; align-items: center; justify-content: center;
  min-height: var(--control-h);
  border: 1px solid var(--control-border);
  border-radius: var(--control-r);
  cursor: pointer;
  font-size: var(--text-sm);
}
.request-kind__option input { position: absolute; opacity: 0; pointer-events: none; }
.request-kind__option:has(input:checked) {
  background: var(--interactive); color: var(--text-inverse);
  border-color: var(--interactive); font-weight: 600;
}
.request-kind__option:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ------------------------------------------------------- duplicate suggestions */
.request-suggest {
  margin-block-start: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.request-suggest[hidden] { display: none; }

.request-suggest__group + .request-suggest__group { border-block-start: 1px solid var(--border); }
.request-suggest__heading {
  margin: 0; padding: var(--space-3) var(--space-4);
  background: var(--surface-sunken);
  font-size: var(--text-xs); font-weight: 600;
  color: var(--text-secondary);
}
.request-suggest__item {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  color: inherit; text-decoration: none;
  min-inline-size: 0;
}
.request-suggest__item + .request-suggest__item { border-block-start: 1px solid var(--border); }
.request-suggest__item:hover { background: var(--surface-sunken); }
.request-suggest__item:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.request-suggest__name { font-size: var(--text-sm); font-weight: 600; overflow-wrap: anywhere; }
.request-suggest__meta { font-size: var(--text-xs); color: var(--text-secondary); white-space: nowrap; }

/* ------------------------------------------------------------- small screens */
@media (max-width: 559px) {
  .requests-head { flex-direction: column; align-items: stretch; }
  .requests-head__cta { inline-size: 100%; }
  .requests-grid { grid-template-columns: 1fr; }
  .request-detail__merged { flex-direction: column; align-items: stretch; }
  .request-form__actions { flex-direction: column; }
  .request-form__actions .button { inline-size: 100%; }
}

/* A request in a reader's own list: one line, three facts, one optional door.
   Rows rather than cards here because this is a personal inventory, not a
   discovery surface - the reader already knows what they asked for. */
.request-rows { list-style: none; margin: var(--space-5) 0 0; padding: 0; }
.request-row {
  display: flex; align-items: center; gap: var(--space-4);
  padding-block: var(--space-4);
  border-block-start: 1px solid var(--border);
  flex-wrap: wrap;
}
.request-row:first-child { border-block-start: 0; }
.request-row__main {
  flex: 1 1 220px; min-inline-size: 0;
  display: flex; flex-direction: column; gap: 2px;
  color: inherit; text-decoration: none;
}
.request-row__main:hover .request-row__title { text-decoration: underline; text-underline-offset: 3px; }
.request-row__main:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--radius-sm); }
.request-row__title { font-weight: 600; overflow-wrap: anywhere; }
.request-row__meta { color: var(--text-secondary); }
.request-row__count { color: var(--text-secondary); white-space: nowrap; }

/* Where a failed search meets the request system. Visually secondary to the
   relax action above it - the reader may still just have mistyped. */
.search-zero__requests {
  margin-block-start: var(--space-8);
  padding-block-start: var(--space-6);
  border-block-start: 1px solid var(--border);
  display: flex; flex-direction: column; align-items: center; gap: var(--space-4);
}
.search-zero__requests p { margin: 0; color: var(--text-secondary); }
.search-zero__list { list-style: none; margin: 0; padding: 0; inline-size: 100%; max-inline-size: 420px; }
.search-zero__list li + li { margin-block-start: var(--space-2); }
.search-zero__match {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: inherit; text-decoration: none; min-inline-size: 0;
}
.search-zero__match:hover { background: var(--surface-sunken); }
.search-zero__match:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* WAVE 5, after looking at the page rather than at the assertions.
   The cards were squat and the support count - the one fact the surface exists
   to communicate - carried the same weight as everything around it, so the
   most-wanted request did not look most-wanted. */
.request-card { gap: var(--space-3); padding: var(--space-6); }

.request-card__title {
  font-size: var(--text-xl);
  /* Room to breathe: with no artwork the title IS the card, and it was
     competing with its own metadata for attention. */
  margin-block: var(--space-2) 0;
}

.request-card__support {
  margin-block-start: auto;
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--border);
  font-weight: 600;
}

/* `margin-block-start:auto` only pushes the count down if the card is a
   full-height flex column, which is what makes a row of cards line their
   counts up instead of floating them at ragged heights. */
.requests-grid { align-items: stretch; }

/* =========================================================================
   ACHROMATIC IS NOT LOW-PRESENCE
   =========================================================================
   Operator correction: the newer surfaces read as pale, thin and
   under-designed - "HTML with minimal CSS". The black-and-white direction is
   right; the weakness came from expressing EVERY boundary as the same 1px
   advisory hairline, so a page had no hierarchy left to spend.

   The fix is structure, not colour. Four deliberate levels instead of one:

     page      a heading that anchors the screen
     section   a visibly grouped step, numbered where it is a sequence
     card      a real component boundary, on its own surface
     control   unmistakably pressable, with a real selected state

   Nothing here introduces hue, gradient, glass or shadow. It buys presence
   with weight, contrast, spacing and fill - which is what "premium" meant in
   this product's own design authority all along.
   ========================================================================= */

/* ------------------------------------------------------------- sections */
.step {
  /* A SURFACE, not a hairline. A step is a place you are in, and a box that
     differs from the page reads as one where a single top border does not. */
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-6);
}
.step + .step { margin-block-start: var(--space-5); }

.step__head {
  display: flex; align-items: center; gap: var(--space-4);
  margin-block-end: var(--space-4);
}

/* The number says "this is a sequence with an order", which four identical
   headings cannot. Filled, because an outlined circle would be another
   hairline. */
.step__number {
  inline-size: 28px; block-size: 28px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--interactive); color: var(--text-inverse);
  font-size: var(--text-sm); font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.step__title { margin: 0; font-size: var(--text-lg); font-weight: 600; }

.step__hint {
  margin: 0 0 var(--space-5);
  color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6;
}

/* ------------------------------------------------------ source cards */
.source-grid {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.source-grid[hidden] { display: none; }

.source-card {
  display: flex; align-items: flex-start; gap: var(--space-4);
  padding: var(--space-5);
  background: var(--surface);
  /* 2px, because this is a CHOICE and a chosen one has to be unmistakable.
     A 1px selected state next to a 1px unselected state is not a state. */
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  min-inline-size: 0;
}
.source-card:hover { border-color: var(--control-border); }
.source-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.source-card.is-selected {
  border-color: var(--interactive);
  /* Selection is carried by the border AND the mark inverting - never by
     colour alone, and never by something a screenshot cannot show. */
  background: var(--surface-elevated);
}

/* A provider mark that costs no request and cannot break: the initial, set
   with intent. No remote logos, no fetching, no broken image box. */
.source-card__mark {
  inline-size: 40px; block-size: 40px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  font-size: var(--text-lg); font-weight: 700;
  color: var(--text-secondary);
}
.source-card.is-selected .source-card__mark {
  background: var(--interactive); color: var(--text-inverse); border-color: var(--interactive);
}

.source-card__body { flex: 1 1 auto; min-inline-size: 0; }
.source-card__name { margin: 0; font-weight: 600; font-size: var(--text-base); }
.source-card__host { margin: 2px 0 0; color: var(--text-secondary); }
.source-card__note { margin: var(--space-3) 0 0; color: var(--text-secondary); line-height: 1.5; }
.source-card__actions { flex: 0 0 auto; }

/* --------------------------------------------------------- url state */
.url-state { margin: var(--space-3) 0 0; min-block-size: 1.2em; line-height: 1.5; }
.url-state--ok  { color: var(--text-primary); font-weight: 600; }
.url-state--bad { color: var(--text-primary); font-weight: 600; }
/* The marker carries the meaning without relying on hue: a solid square
   before the message, filled for good and hollow for bad. */
.url-state--ok::before,
.url-state--bad::before {
  content: ''; display: inline-block;
  inline-size: 8px; block-size: 8px; margin-inline-end: var(--space-3);
  vertical-align: middle;
}
.url-state--ok::before  { background: var(--interactive); }
.url-state--bad::before { border: 2px solid var(--interactive); }
.url-state__link { margin-inline-start: var(--space-3); text-decoration: underline; }

/* ----------------------------------------------------------- notices */
.notice {
  padding: var(--space-5);
  border-radius: var(--radius-md);
  margin-block-end: var(--space-6);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap;
  background: var(--surface-elevated);
  /* A notice that needs reading gets a heavier edge on its leading side -
     structure rather than a colour wash. */
  border: 1px solid var(--border);
  border-inline-start: 4px solid var(--interactive);
}
.notice p { margin: 0; font-weight: 600; }
.notice__list { margin: 0; padding-inline-start: var(--space-5); font-weight: 600; }

/* ------------------------------------------------------ form framing */
.request-form__header { margin-block-end: var(--space-6); }
.request-form__lead {
  margin: var(--space-4) 0 0;
  color: var(--text-secondary); font-size: var(--text-base); line-height: 1.7;
  max-inline-size: 60ch;
}

.request-form__actions {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  margin-block-start: var(--space-6);
  padding: var(--space-5) var(--space-6);
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.request-form__gate {
  margin: 0; color: var(--text-secondary); flex: 1 1 100%;
}

/* A disabled primary must LOOK disabled rather than merely refuse - the
   product's own component contract, and the thing that tells a reader the
   form is waiting on them rather than broken. */
.button--primary:disabled,
.button--primary[disabled] {
  opacity: .45;
  cursor: not-allowed;
}

/* --------------------------------------- index and detail, strengthened */
.requests-head__note {
  /* Was a faint grey line under the heading; it carries the product's central
     promise and should read like it. */
  color: var(--text-secondary);
  font-size: var(--text-base);
  border-inline-start: 3px solid var(--border);
  padding-inline-start: var(--space-4);
}

.request-card { border-width: 1px; }
.request-card:hover { border-color: var(--control-border); }

.request-card__status {
  /* A status pill rather than loose grey text: restrained, but a component. */
  display: inline-block;
  padding: 2px var(--space-3);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-sunken);
  color: var(--text-secondary);
  font-weight: 600;
  white-space: nowrap;
}

.request-detail__status {
  display: inline-block;
  padding: 4px var(--space-4);
  border-radius: 999px;
  background: var(--interactive); color: var(--text-inverse);
  font-size: var(--text-sm); font-weight: 600;
}

.request-detail__demand { border-width: 1px; }
.request-detail__count { font-size: var(--text-xl); }

/* The source block on a request detail is an ACTION, not a footnote: it is
   what an operator and a curious reader both reach for. */
.request-source {
  margin-block-start: var(--space-6);
  padding: var(--space-5);
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
}
.request-source__body { flex: 1 1 220px; min-inline-size: 0; }
.request-source__label { margin: 0; color: var(--text-secondary); }
.request-source__name { margin: 2px 0 0; font-weight: 600; }

@media (max-width: 559px) {
  .step { padding: var(--space-5); }
  .source-grid { grid-template-columns: 1fr; }
  .source-card { flex-wrap: wrap; }
  .source-card__actions { inline-size: 100%; }
  .source-card__actions .button { inline-size: 100%; }
  .request-form__actions { flex-direction: column; align-items: stretch; }
}

/* =========================================================================
   TEAMS
   =========================================================================
   An editorial collective, not a clan page. The visual problem is the same one
   Translation Requests had - no artwork to lead with - so identity is carried
   by a typographic mark and real component boundaries rather than by tint.

   Following the standing correction: achromatic is not faint. Sections are
   surfaces, marks are filled, and a selected craft is unmistakably selected.
   ========================================================================= */

.team-grid {
  display: grid; gap: var(--space-5);
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  align-items: stretch;
}

.team-card {
  display: flex; align-items: flex-start; gap: var(--space-4);
  padding: var(--space-6);
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: inherit; text-decoration: none;
  min-inline-size: 0;
}
.team-card:hover { border-color: var(--control-border); }
.team-card:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

/* The mark IS the artwork. Filled, so a wall of teams has visual rhythm
   instead of reading as a list of grey boxes. */
.team-card__mark {
  inline-size: 48px; block-size: 48px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--interactive); color: var(--text-inverse);
  font-size: var(--text-lg); font-weight: 700;
}

.team-card__body { flex: 1 1 auto; min-inline-size: 0; display: flex; flex-direction: column; gap: 2px; }
.team-card__name { font-size: var(--text-lg); font-weight: 600; overflow-wrap: anywhere; }
.team-card__slug { color: var(--text-secondary); }
.team-card__desc { color: var(--text-secondary); margin-block-start: var(--space-3); line-height: 1.5; }
.team-card__meta { color: var(--text-secondary); white-space: nowrap; flex: 0 0 auto; }

/* ----------------------------------------------------------------- hero */
.team-profile, .team-workspace { max-inline-size: 900px; }

.team-hero {
  display: flex; align-items: flex-start; gap: var(--space-5); flex-wrap: wrap;
  padding: var(--space-7);
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  margin-block-end: var(--space-7);
}
.team-hero--compact { padding: var(--space-6); }

.team-hero__mark {
  inline-size: 72px; block-size: 72px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-md);
  background: var(--interactive); color: var(--text-inverse);
  font-size: var(--text-2xl); font-weight: 700;
}
.team-hero--compact .team-hero__mark { inline-size: 52px; block-size: 52px; font-size: var(--text-xl); }

.team-hero__body { flex: 1 1 260px; min-inline-size: 0; }
.team-hero__name { margin: 0; font-size: var(--text-2xl); font-weight: 600; overflow-wrap: anywhere; }
.team-hero__slug { margin: 2px 0 0; color: var(--text-secondary); font-size: var(--text-sm); }
.team-hero__desc { margin: var(--space-4) 0 0; color: var(--text-secondary); line-height: 1.7; max-inline-size: 60ch; }
.team-hero__status {
  margin: var(--space-4) 0 0; display: inline-block;
  padding: 2px var(--space-3);
  border: 1px solid var(--control-border); border-radius: 999px;
  font-size: var(--text-xs); font-weight: 600;
}
.team-hero__actions { display: flex; gap: var(--space-3); flex-wrap: wrap; }

/* -------------------------------------------------------------- members */
.member-list { list-style: none; margin: var(--space-5) 0 0; padding: 0; }

.member-row {
  display: flex; align-items: center; gap: var(--space-4);
  padding-block: var(--space-4);
  border-block-start: 1px solid var(--border);
  flex-wrap: wrap;
}
.member-row:first-child { border-block-start: 0; }
.member-row__body { flex: 1 1 200px; min-inline-size: 0; display: flex; flex-direction: column; gap: 2px; }
.member-row__name { font-weight: 600; overflow-wrap: anywhere; }
.member-row__roles { color: var(--text-secondary); }

.member-row__form {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  flex: 1 1 100%;
}

/* A craft is a real toggle: filled when held, outlined when not. A 1px
   selected border beside a 1px unselected one is not a state. */
.craft-set { display: flex; gap: var(--space-2); flex-wrap: wrap; border: 0; padding: 0; margin: 0; }

.craft-chip {
  display: inline-flex; align-items: center;
  min-height: var(--control-h-sm);
  padding-inline: var(--space-4);
  border: 1px solid var(--control-border);
  border-radius: 999px;
  cursor: pointer;
  font-size: var(--text-xs);
}
.craft-chip input { position: absolute; opacity: 0; pointer-events: none; }
.craft-chip:has(input:checked) {
  background: var(--interactive); color: var(--text-inverse);
  border-color: var(--interactive); font-weight: 600;
}
.craft-chip:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ----------------------------------------------------------- invitations */
.invite-form {
  display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap;
  margin-block-start: var(--space-5);
}
.invite-form .input { flex: 1 1 220px; min-inline-size: 0; }

/* An invitation a reader must answer is a decision, so it is framed rather
   than dropped into the notification list as another grey line. */
.invite-callout {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap;
  padding: var(--space-5) var(--space-6);
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-inline-start: 4px solid var(--interactive);
  border-radius: var(--radius-md);
  margin-block-end: var(--space-5);
}
.invite-callout p { margin: 0; font-weight: 600; }
.invite-callout__actions { display: flex; gap: var(--space-3); }

/* -------------------------------------------------- opportunity cards */
.request-card--static { cursor: default; }
.request-card__actions {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  margin-block-start: var(--space-4);
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--border);
}

@media (max-width: 559px) {
  .team-grid { grid-template-columns: 1fr; }
  .team-hero { padding: var(--space-5); }
  .team-hero__actions { inline-size: 100%; }
  .team-hero__actions .button { flex: 1 1 auto; }
  .invite-form .button { inline-size: 100%; }
}

/* =========================================================================
   COLOPHON
   =========================================================================
   A design direction for the reader_platform Team and Studio surfaces.

   THESIS
   A colophon is the printer's statement at the back of a book: who set it,
   who corrected it, in what types, in what year. It is the oldest form of
   publishing credit, and it is exactly what a translation team is — the
   people whose names belong in it.

   So the team page is not a profile and not a clan card. It is an imprint
   page: ruled, typographic, indexed. Where the reader-facing catalogue is led
   by artwork, this surface has none of its own, and the honest answer to that
   is not decoration — it is typography and print structure.

   PRINCIPLES
   1. Rules, not boxes. Registers are bound by hairlines the way a masthead or
      an index is. Cards fragment a page into floating tiles; rules sequence it.
   2. The name is the artwork. Identity leads with the name set large, not with
      a letter in a square. Imagery supports identity; it never substitutes.
   3. Numbers are set, not scored. A figure over a label in a ruled row — a
      printed statement of record. Never a badge, meter, level or rank.
   4. People are credited, not listed. A member reads as a credit line, with
      the craft where a colophon would put it.
   5. Density is respect. A serious catalogue is dense. Space separates
      registers, not every element.

   WHY IT FITS THIS PRODUCT
   This product is already achromatic-first with the work supplying colour.
   Colophon extends that to a surface that has no artwork of its own, and it
   states the product's own ethic: credits are history, attribution matters.

   WHAT IS DIFFERENT
   The previous team pages were a card grid and a letter-square. This replaces
   cards with ruled registers, the square with a typographic masthead, and
   loose metadata with a printed record.
   ========================================================================= */

.colophon {
  --rule: var(--border);
  --rule-strong: var(--control-border);
  --measure: 1180px;
}

.colophon__wrap { max-inline-size: var(--measure); margin-inline: auto; padding-inline: var(--space-5); }

/* ------------------------------------------------------------- masthead */
.imprint {
  padding-block: var(--space-10) var(--space-7);
  border-block-end: 2px solid var(--rule-strong);
}

.imprint__eyebrow {
  font-size: var(--text-xs); letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-secondary); margin: 0 0 var(--space-4);
}

.imprint__name {
  margin: 0;
  font-size: clamp(var(--text-2xl), 6vw, 56px);
  line-height: 1.12;
  font-weight: 700;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

.imprint__handle {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.imprint__note {
  margin: var(--space-5) 0 0;
  max-inline-size: 62ch;
  line-height: 1.75;
  color: var(--text-secondary);
  font-size: var(--text-base);
}

.imprint__head { display: flex; gap: var(--space-7); align-items: flex-start; flex-wrap: wrap; }
.imprint__body { flex: 1 1 320px; min-inline-size: 0; }

/* The emblem is optional and deliberately restrained: a square of the team's
   own image, or nothing. It never carries the identity on its own. */
.imprint__emblem {
  flex: 0 0 auto;
  inline-size: 120px; block-size: 120px;
  border: 1px solid var(--rule-strong);
  object-fit: cover;
  background: var(--surface-elevated);
}

.imprint__actions { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-block-start: var(--space-6); }

/* A status that is not "active" is stated plainly rather than coloured. */
.imprint__state {
  display: inline-block; margin-block-start: var(--space-4);
  padding: 2px var(--space-3);
  border: 1px solid var(--rule-strong);
  font-size: var(--text-xs); font-weight: 600;
}

/* ------------------------------------------------------ record of figures */
.record {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  border-block-end: 1px solid var(--rule);
}

.record__cell {
  padding: var(--space-6) var(--space-5) var(--space-6) 0;
  border-inline-start: 1px solid var(--rule);
}
.record__cell:first-child { border-inline-start: 0; padding-inline-start: 0; }
.record__cell:not(:first-child) { padding-inline-start: var(--space-5); }

.record__figure {
  display: block;
  font-size: var(--text-2xl); font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.record__label {
  display: block; margin-block-start: var(--space-3);
  font-size: var(--text-xs); color: var(--text-secondary);
  letter-spacing: .04em;
}

/* ------------------------------------------------------------ registers */
.register { padding-block: var(--space-8); border-block-end: 1px solid var(--rule); }
.register:last-child { border-block-end: 0; }

.register__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap;
  margin-block-end: var(--space-5);
}
.register__title {
  margin: 0; font-size: var(--text-xs); font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
}
.register__aside { font-size: var(--text-xs); color: var(--text-secondary); }

/* --------------------------------------------------------- credit lines */
.credits { list-style: none; margin: 0; padding: 0; }

.credit {
  display: flex; align-items: baseline; gap: var(--space-4);
  padding-block: var(--space-4);
  border-block-start: 1px solid var(--rule);
}
.credit:first-child { border-block-start: 0; }

.credit__name { font-weight: 600; font-size: var(--text-base); overflow-wrap: anywhere; }

/* The leader is what makes this read as a colophon rather than a table: the
   eye travels the dotted run from a name to the craft it is credited for. */
.credit__leader {
  flex: 1 1 auto; min-inline-size: var(--space-6);
  border-block-end: 1px dotted var(--rule-strong);
  transform: translateY(-4px);
  opacity: .55;
}

.credit__role {
  font-size: var(--text-xs); color: var(--text-secondary);
  letter-spacing: .04em; white-space: nowrap;
}
.credit__role strong { color: var(--text); font-weight: 600; }

/* ------------------------------------------------------------- the index */
.index { list-style: none; margin: 0; padding: 0; }

.index__row {
  display: flex; align-items: center; gap: var(--space-5);
  padding-block: var(--space-4);
  border-block-start: 1px solid var(--rule);
  color: inherit; text-decoration: none;
}
.index__row:first-child { border-block-start: 0; }
.index__row:hover .index__title { text-decoration: underline; text-underline-offset: 3px; }
.index__row:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

/* Small on purpose. The work is listed here, not merchandised. */
.index__cover {
  flex: 0 0 auto; inline-size: 44px; block-size: 62px;
  object-fit: cover; background: var(--surface-elevated);
  border: 1px solid var(--rule);
}

/* An absent cover is a deliberate typographic mark, never a blank box. */
.index__cover--letter {
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-lg); font-weight: 700;
  color: var(--text-secondary);
}

.index__body { flex: 1 1 auto; min-inline-size: 0; }
.index__title { display: block; font-weight: 600; overflow-wrap: anywhere; }
.index__meta { display: block; margin-block-start: 2px; font-size: var(--text-xs); color: var(--text-secondary); }
.index__figure {
  flex: 0 0 auto; font-variant-numeric: tabular-nums;
  font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap;
}

/* ------------------------------------------------------- empty registers */
.register__empty {
  padding-block: var(--space-6);
  color: var(--text-secondary); font-size: var(--text-sm);
  border-block-start: 1px solid var(--rule);
}

/* =========================================================================
   STUDIO
   The same language, working rather than presenting. A masthead bar that says
   which team you are inside, and a register of what needs you.
   ========================================================================= */

.studio-bar {
  border-block-end: 1px solid var(--rule-strong);
  background: var(--surface-elevated);
}
.studio-bar__wrap {
  max-inline-size: var(--measure, 1180px); margin-inline: auto;
  padding: var(--space-5);
  display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap;
}
.studio-bar__team { font-weight: 700; font-size: var(--text-lg); overflow-wrap: anywhere; }
.studio-bar__role { font-size: var(--text-xs); color: var(--text-secondary); }

/* Tabs as a ruled strip, not pills: this is a working surface. */
.studio-tabs { display: flex; gap: var(--space-6); flex-wrap: wrap; margin-inline-start: auto; }
.studio-tabs__link {
  padding-block: var(--space-3);
  font-size: var(--text-sm); color: var(--text-secondary);
  text-decoration: none; border-block-end: 2px solid transparent;
  min-height: var(--control-h); display: inline-flex; align-items: center;
}
.studio-tabs__link:hover { color: var(--text); }
.studio-tabs__link[aria-current="page"] { color: var(--text); font-weight: 600; border-block-end-color: var(--interactive); }
.studio-tabs__link:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* A team you are in, on the Studio index. */
.desk { list-style: none; margin: 0; padding: 0; }
.desk__row {
  display: flex; align-items: center; gap: var(--space-5);
  padding-block: var(--space-5);
  border-block-start: 1px solid var(--rule);
  color: inherit; text-decoration: none;
}
.desk__row:first-child { border-block-start: 0; }
.desk__row:hover .desk__name { text-decoration: underline; text-underline-offset: 3px; }
.desk__row:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
.desk__name { font-weight: 700; font-size: var(--text-lg); overflow-wrap: anywhere; }
.desk__meta { font-size: var(--text-xs); color: var(--text-secondary); margin-block-start: 2px; }
.desk__body { flex: 1 1 auto; min-inline-size: 0; }
.desk__count { font-variant-numeric: tabular-nums; color: var(--text-secondary); font-size: var(--text-sm); }

/* A task register: what this team owes, first thing. */
.docket { list-style: none; margin: 0; padding: 0; }
.docket__row {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  padding-block: var(--space-4);
  border-block-start: 1px solid var(--rule);
}
.docket__row:first-child { border-block-start: 0; }
.docket__main { flex: 1 1 260px; min-inline-size: 0; color: inherit; text-decoration: none; }
.docket__main:hover .docket__title { text-decoration: underline; text-underline-offset: 3px; }
.docket__title { display: block; font-weight: 600; overflow-wrap: anywhere; }
.docket__meta { display: block; margin-block-start: 2px; font-size: var(--text-xs); color: var(--text-secondary); }

/* A decision waiting on this reader is ruled off, not tinted. */
.summons {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap;
  padding: var(--space-5) 0;
  border-block: 1px solid var(--rule-strong);
  margin-block-end: var(--space-7);
}
.summons p { margin: 0; font-weight: 600; }
.summons__actions { display: flex; gap: var(--space-3); }

@media (max-width: 739px) {
  .imprint { padding-block: var(--space-7) var(--space-6); }
  .imprint__emblem { inline-size: 76px; block-size: 76px; }
  .record__cell { border-inline-start: 0; padding-inline: 0; padding-block: var(--space-5);
                  border-block-start: 1px solid var(--rule); }
  .record__cell:first-child { border-block-start: 0; }
  .record { grid-template-columns: 1fr 1fr; }
  .record__cell:nth-child(2) { border-inline-start: 1px solid var(--rule); padding-inline-start: var(--space-5); }
  .credit__leader { display: none; }
  .credit { flex-wrap: wrap; gap: var(--space-2); }
  .studio-tabs { margin-inline-start: 0; inline-size: 100%; gap: var(--space-5); overflow-x: auto; }
}
