/* ==========================================================================
   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');
}
/* ==========================================================================
   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;

  /* 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;

  /* 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;

  --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;
}

/* 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;
    --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;
    --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; --border: #26262D; --control-border: #8B8B95;
  --text-primary: #F2F2F4; --text-secondary: #A6A6B0; --text-tertiary: #9A9AA4;
  --text-inverse: #0E0E11; --interactive: #F2F2F4; --focus: #F2F2F4;
  --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;
}

.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%; }

.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 { display: inline-flex; border: 1px solid var(--control-border);
  border-radius: var(--radius-sm); overflow: hidden; }
.segmented a, .segmented button {
  min-height: 44px; display: flex; align-items: center;
  padding-inline: 16px; font-size: var(--text-sm);
}
.segmented [aria-current='true'], .segmented [aria-pressed='true'] {
  background: var(--interactive); color: var(--text-inverse);
}

/* --------------------------------------------------------- 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; }
  .identity__cover { width: 180px; margin-bottom: var(--space-5); }
  .identity__title { font-size: var(--text-2xl); line-height: 1.35;
    height: calc(var(--text-2xl) * 1.35 * 2); }
}
.title-layout > * { min-width: 0; }

/* --------------------------------------------------------- spotlight */
.spotlight {
  background: var(--surface-elevated); border-radius: var(--radius-lg);
  padding: var(--space-6); display: grid; grid-template-columns: 1fr; gap: var(--space-6);
}
@media (min-width: 940px) {
  .spotlight { grid-template-columns: 1fr 55%; gap: var(--space-10);
    align-items: center; padding: var(--space-8); }
}
.spotlight__art { width: 100%; aspect-ratio: 16 / 9; background: var(--art-1);
  border-radius: var(--radius-md); }
.spotlight h2 { font-size: var(--text-xl); line-height: 1.25; font-weight: 600; }
@media (min-width: 940px) { .spotlight h2 { font-size: var(--text-3xl); } }

/* ------------------------------------------------------------ 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,
.reading-surface[data-warm='true'] { background: #141210; }

.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); }

.progress-bar { position: sticky; top: 52px; height: 2px;
  background: var(--surface-sunken); z-index: 29; }
.progress-bar__fill { height: 100%; width: 0; background: var(--interactive); }
.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. */
[data-personal] { display: none; }
[data-personal][data-ready='true'] { display: block; }

/* ======================================================================
   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); }
@media (min-width: 940px) {
  .account-layout { grid-template-columns: 240px 1fr; gap: var(--space-10); align-items: start; }
  .account-tabs { display: flex; flex-direction: column; gap: 2px; overflow: visible; }
  .account-tabs a { border-radius: var(--radius-sm); box-shadow: none; }
  .account-tabs a[aria-current='page'] { background: var(--surface-elevated); }
}
.account-tabs {
  display: flex; gap: var(--space-6); overflow-x: auto;
  border-bottom: 1px solid var(--border);
}
.account-tabs a {
  min-height: 48px; display: flex; align-items: center; white-space: nowrap;
  padding-inline: 14px; font-size: var(--text-base); color: var(--text-secondary);
}
.account-tabs a[aria-current='page'] {
  color: var(--text-primary); font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--interactive);
}

.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--reply { margin-inline-start: var(--space-8); }
.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; }
