/* =====================================================================
 * HANDOVER CONTRACT - read this before adding a rule to this file.
 * =====================================================================
 *
 * This stylesheet is being refreshed in two halves. The FOUNDATION half is
 * below and is finished. The COMPONENT half is marked with its own banner
 * further down and is still the pre-refresh code. This block is the seam: it
 * is the complete list of what the foundation owns and what it hands over, so
 * the two halves cannot drift.
 *
 * ---------------------------------------------------------------------
 * OWNED BY THE FOUNDATION (do not restyle these; extend them)
 * ---------------------------------------------------------------------
 *   reset/base    html body h1-h4 p ul ol a img svg form controls, [hidden]
 *   focus         a/button/input/select/textarea/summary/[tabindex] :focus-visible
 *   layout        .wrap .wrap-3 .layout .col-main .col-side
 *                 .page-home .disc-board__grid .rankings-grid
 *                 .panel .top-rule .skip-link .sr-only
 *   client state  [data-busy] .is-fresh, at the FOOT of the file - written by
 *                 js/site.js on components this file does not own, and here
 *                 rather than in features/interactions.css because the READER
 *                 page links this stylesheet and never that one. Their block
 *                 says why they sit after the component half.
 *                 (the two component selectors are the only place the
 *                  foundation reaches a component, and they set a TRACK COUNT
 *                  and nothing else - no paint, no spacing, no type.
 *                  features/discovery.css still owns what a leaderboard panel
 *                  and a ranked row look like. The .home-grid / .home-genres
 *                  pair that used to sit here is gone: both of those
 *                  components now count their own tracks off a floor token,
 *                  so the widest page needs nobody to say so.)
 *   header        .hdr .hdr-in .hdr-logo .hdr-search .hdr-tools .hdr-user
 *                 .hdr-burger .burger
 *   tool buttons  .icon-btn .icon-btn-glyph .icon-btn-label
 *   nav           .nav-primary .nav-list .nav-item .nav-sub .has-sub
 *                 .mobile-menu .mobile-list .mobile-sub
 *   breadcrumbs   .crumbs .crumbs-list .crumbs-item
 *   bands         .sect .sect-chip .sect-brand .sect-accent .sect-rank
 *                 .sect-tax .page-title .dir-intro .dir-count .more-link
 *   filter chips  .chip .chip-grid .chip-count .is-active
 *                 .filters .filters-summary .filter-group .filter-group-label
 *                 .sort-tabs
 *   covers        .cover .cover-sm .cover-md .cover-slider
 *                 .cover-wrap and the four component cover wrappers
 *   badges        .badges .badge .badge-hot .badge-new .badge-custom .badge-adult
 *   buttons       .btn .btn-brand .btn-accent .btn-ghost
 *                 .btn[disabled] .is-disabled
 *   forms         .search .search-input .search-btn .field .field-label
 *                 .field-input
 *   ads           .ad .ad-label .ad-body and every .ad-<slot> reserved height,
 *                 .walls .wall .wall-left .wall-right
 *   footer        .ftr .ftr-in .ftr-brand .ftr-mark .ftr-note .ftr-menu
 *                 .ftr-legal .to-top
 *   utilities     .empty (its .empty-icon/.empty-text/.empty-action parts are
 *                 the component half's) .prose .meta .meta-muted .scroll-x .adult-gate
 *                 .adult-gate-box .adult-gate-actions
 *                 html[data-safe="1"] / html[data-adultok="1"] behaviour
 *   breakpoints   the single @media block at the foot of the file
 *
 * ---------------------------------------------------------------------
 * HANDED OVER - the component half owns these
 * ---------------------------------------------------------------------
 *   cards         .card-grid .card-column
 *                 .card-update .card-update__cover/__title/__link/__chapters/
 *                              __chapter/__time
 *                 .card-list   .card-list__cover/__body/__title/__link/
 *                              __chapter/__meta/__date/__views/__synopsis/
 *                              __more
 *                 .card-rank   .card-rank__num .card-rank__num--top
 *                              .card-rank__cover/__body/__title/__link/
 *                              __chapter/__meta/__views
 *   genre table   .genre-grid .sidebar-genres__list/__item/__link
 *                 (.sidebar-genres is a component ROOT and carries no rule on
 *                  purpose: it always ships on .sidebar-block, which is the box,
 *                  so the modifier exists to name the block for a page that has
 *                  to reach it. Nothing else in this file is allowed to be
 *                  paint-free.)
 *   chapter table .chapter-list .chapters .chapter-row .chapter-row__name/
 *                 __views/__date .chapter-link .is-reading .vol .more-btn
 *   carousel      .slider .slider-track .slide .slide-cover .slide-title
 *                 .slide-chapter .carousel .carousel-track .carousel-item
 *                 .carousel-cover .carousel-title
 *   sidebar boxes .side-block .sidebar-block  (build these on .panel)
 *   pagination    .pager .pg .pg-gap .is-current
 *   comments      .comments .comments__heading/__thread/__count/__list/
 *                 __loading/__empty/__noscript/__more
 *                 .comment .comment__head/__author/__guest/__time/__body/
 *                 __reply/__replies .comment--reply
 *                 .comment-form .comment-form__replying/__replying-to/
 *                 __cancel/__guest/__trap/__actions .comment-note .is-bad
 *   suggest       .search-drop (the panel; search_box.html prints it empty and
 *                 js/site.js fills it) .suggest .suggest-item .suggest-link
 *                 .suggest-cover .suggest-text .suggest-title .suggest-chapter
 *                 .suggest-more
 *   series page   .series .series-head/-cover/-body/-title/-alt/-stats/-actions
 *                 .facts .tax .synopsis .synopsis-body .rate .rate-stars
 *                 .rate-star .rate-meta .is-on
 *   reader        .reader-shell .reader-body .rhead .rhead-in .rhead-mark
 *                 .rhead-series .rbar .rbar-group .rjump .rjump-wrap .rnav
 *                 .rnav-select .rnav-step .rmode .rzone .rzone-prev
 *                 .rzone-next .rstage .rpages .rpage .rtitle .rtext .rvideo
 *                 .rend  (+ the reader page's own <style> block)
 *   page odds     .notfound .notfound-code/-lead/-search/-links
 *                 .static-page .static-page-updated .search-page-form
 *                 .search-fallback
 *
 * ---------------------------------------------------------------------
 * CUSTOM PROPERTIES THE COMPONENT HALF MAY USE (tokens/<theme>.css)
 * ---------------------------------------------------------------------
 *   surfaces   --page-bg --surface --surface-alt --surface-control --surface-inset
 *   colour     --brand --brand-strong --brand-soft --accent
 *              --brand-fill --brand-fill-hover   (plates only - see rule 2)
 *              --hot --new --warn                (signal only - never as text)
 *   chips      --chip-primary --chip-accent --chip-popular --chip-genres --chip-ink
 *   badges     --badge-hot --badge-new --badge-custom --badge-adult --badge-ink
 *   ink        --text --text-2 --text-muted --on-brand --on-accent
 *              --danger-ink --success-ink --genre-ink
 *   links      --link --link-hover --link-chapter --link-visited
 *   borders    --border --border-strong
 *   ranks      --rank-10 (the plate under a ranked row), --rank-num
 *              --rank-num-ink. --rank-1 ... --rank-9 were the sidebar ladder's
 *              steps and --rank-num-hi/-ink a podium plate this file never
 *              read; all of them are gone from the token files.
 *   chrome     --nav-bg --nav-ink --nav-hover --nav-active --crumb-bg --crumb-ink
 *   type       --font-sans --fs-meta --fs-body --fs-card --fs-lead --fs-h2
 *              --fs-h1 --lh-body --lh-head --fw-head --track-chip
 *   shape      --r-card --r-panel --r-pill --shadow-1 --shadow-2 --shadow-3
 *              --shadow-cover --scrim
 *   motion     --t-fast --lift
 *   metrics    --space-1 ... --space-8, --wrap-3 --col-main --col-side
 *              --reader-shell --cover-ratio --tap
 *   tracks     --fit-card --fit-row --fit-fact --fit-genre --fit-board
 *              (the auto-fit floors - see rule 7)
 *   local      --band (set by .sect-*; the band's colour)
 *              --ring (override ONLY where the ring lands on a filled PLANE the
 *                      control sits inside - the nav strip, the reader's mode
 *                      track. A plate the size of the control is not one: the
 *                      ring is drawn at outline-offset 2px, which puts it
 *                      OUTSIDE the fill and onto the surface behind it, where a
 *                      white ring is white on white. See .nav-primary, the only
 *                      override left in this file.)
 *
 * SIX RULES THAT ARE NOT NEGOTIABLE
 *   1. No literal colours in this file. If a value cannot be expressed as a
 *      token it is a missing token, not an exception.
 *      THE BRAND HUE HAS THREE JOBS AND THREE TOKENS, because they move in
 *      opposite directions when the theme flips:
 *        --brand       borders, rules, outlines, icon graphics. NEVER as text -
 *                      in the dark theme it is 3.8:1 on a card, which passes
 *                      the 3:1 bar for a graphic and fails the 4.5:1 for a word.
 *        --link,       brand-coloured TEXT. --brand-strong is also the ink for
 *        --brand-strong  anything sitting on a --brand-soft plate, which
 *                      inverts between themes.
 *        --brand-fill  a PLATE carrying a white label - button, active chip,
 *                      current page pill. Stays deep in both themes.
 *      Likewise --chip-* and --badge-* are plates, never text, and --genre-ink
 *      / --danger-ink / --success-ink are the text stops for the teal, rose and
 *      emerald. --hot, --new and --warn are SIGNAL colours - a rule, a dot, a
 *      meter - and are never written as words: as text they are 3.7:1 and
 *      2.5:1 on white. Every pairing in this file was measured in both themes
 *      at 4.5:1, which is also why --text-muted is not slate-500.
 *   3. Anything that lifts on hover writes `transform: var(--lift)` and
 *      anything that transitions writes `var(--t-fast)`. Both are re-declared
 *      under prefers-reduced-motion at the foot of this file, which is the
 *      only place that preference is answered.
 *   4. Hover may change colour, border, shadow and --lift. It may never change
 *      a size, a margin or a font-size: this is a dense scanning layout and a
 *      row that reflows under the cursor loses the reader's place.
 *   5. Visited chapter and title links keep --link-visited, and :visited is
 *      always declared BEFORE :hover. The other order makes hover unreachable
 *      on a link the visitor has already followed, and the recolouring is a
 *      read-state cue readers rely on. There is deliberately no blanket
 *      a:visited - each component that tracks read state declares its own.
 *   6. Focus is :focus-visible, 2px, offset 2px, on every interactive element,
 *      and every tap target on the nav, the pagination and the header tools is
 *      at least --tap (44px). Neither is a component's decision to revisit.
 *   7. A COLLECTION MAY NOT NAME A COLUMN COUNT AND MAY NOT ADD A BREAKPOINT.
 *      Browser zoom is a viewport-width change, so a layout hung on a handful
 *      of @media widths snaps as the reader zooms - and it is wrong the moment
 *      the same component is rendered in a rail instead of a page. Every grid
 *      on the site is therefore
 *          repeat(auto-fit, minmax(min(<--fit-* floor>, 100%), 1fr))
 *      and every size that used to step is a clamp() in tokens/<theme>.css.
 *      A component that appears in more than one context asks the SPACE IT IS
 *      IN with @container, never the window - and the auto-fit grid above is
 *      that query's fallback, so the base rule outside every @container block
 *      has to be a layout a phone can already read. The four @media widths
 *      left at the foot of this file are the ones that are genuinely about the
 *      DEVICE: the nav becoming a burger, the wall ads appearing in a gutter
 *      wide enough to hold one, and two chrome controls on a small phone.
 *
 * TWO RULES THAT CARRY BEHAVIOUR RATHER THAN LOOKS
 *   [data-safe="1"] hides adult cards for the family-safe toggle
 *   [data-adultok="1"] drops the adult gate
 * Both are stamped on <html> before first paint, because the cached HTML is
 * identical for every visitor and only the client knows the cookie. Nothing in
 * this file may assume otherwise.
 * ===================================================================== */


/* ================================================================ RESET === */

*,
*::before,
*::after { box-sizing: border-box; }

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

/* --page-texture is the one image the page itself carries, and it is a token
   because it is IDENTITY: two sites running this stylesheet on the same flat
   colour read as the same site, and a tiling gradient is the cheapest thing
   that breaks that - no request, no asset, no layout. Split into -color and
   -image rather than left as the `background` shorthand, because the shorthand
   would reset the image every time a theme only meant to move the colour.
   Every texture ships as a repeating-* gradient so it tiles off its own
   definition and needs no second token to size it. */
body {
  background-color: var(--page-bg);
  background-image: var(--page-texture);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  margin: 0;
  /* The two that stop a system stack from looking like a system stack. */
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, p, ul, ol, figure, dd, dl, blockquote { margin: 0; }
ul, ol { list-style: none; padding: 0; }

/* Headings sit at 650, not 900. The bands carry the hierarchy on this layout;
   a heavy heading on top of a solid chip is two shouts in the same breath. */
h1, h2, h3, h4 {
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  line-height: var(--lh-head);
}
h1 { font-size: var(--fs-h1); letter-spacing: -.01em; }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-card); }
h4 { font-size: var(--fs-body); }

/* No blanket a:visited. The visited recolouring is a READ-STATE CUE attached
   to the things a reader tracks - series titles and chapter numbers - and each
   of those declares it for itself, :visited before :hover. Declaring it on the
   element would repaint every crumb, tool and footer link violet and force
   half this file to spend a rule undoing it. */
a {
  color: var(--link);
  text-decoration: none;
  transition: color var(--t-fast);
}
a:hover { color: var(--link-hover); text-decoration: underline; }

img { border: 0; display: block; max-width: 100%; }
svg { display: block; }

/* Controls inherit type but keep their user-agent chrome. A blanket
   `button { background: none; border: 0 }` would silently un-draw every button
   the component half has not styled yet - .more-btn, the reader's steppers -
   and an invisible button is worse than an ugly one. */
button, input, select, textarea {
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
}

hr { border: 0; border-top: 1px solid var(--border); margin: var(--space-4) 0; }

::placeholder { color: var(--text-2); opacity: 1; }
::selection { background: var(--brand-soft); color: var(--text); }

/* The scripts hide things with the hidden ATTRIBUTE (the dropdown, the reply
   bar, the guest fields). Any display rule of ours would out-specify the user
   agent's [hidden], so it is won back once, here, for every one of them. */
[hidden] { display: none !important; }

/* One ring everywhere, drawn outside the box so it is never clipped. --ring
   exists so a control sitting IN an indigo plane - the nav strip - can swap the
   indigo ring for a white one without every other rule learning about it. It is
   NOT for a control that is merely filled indigo itself: offset 2px puts the
   ring on the surface behind the control, so a white ring on a white button
   draws white on white at 1:1 and the button loses its focus indicator
   entirely. The default indigo is 6.3:1 on --surface and 5.8:1 on --page-bg in
   the light theme and 3.8:1 on a dark card, all of them past the 3:1 a focus
   indicator is held to, so the default is right almost everywhere. tabindex="-1" is
   excluded: <main> carries one so the skip link actually moves focus, and a
   ring around the whole page when it lands is noise, not feedback. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
details:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible {
  outline: 2px solid var(--ring, var(--brand));
  outline-offset: 2px;
  border-radius: var(--r-card);
}
[tabindex="-1"]:focus { outline: none; }


/* =============================================================== LAYOUT === */

.wrap {
  margin: 0 auto;
  padding: 0 var(--space-4);
  width: 100%;
}
/* One width class, because one width is emitted. --wrap-1 and --wrap-2 were the
   narrow measures for pages that were never built; they are out of the token
   files, so a preset author no longer tunes two numbers nothing reads. */
.wrap-3 { max-width: var(--wrap-3); }

/* The two-column split, and it decides its own breakpoint. The rail used to
   drop under the content at a hard @media (max-width: 1033px); a flex line
   breaks when the two BASES plus the gap stop fitting, which is the same
   answer computed from the columns themselves - and it is still right after a
   theme retunes --col-main/--col-side, after a reader zooms to 175%, and on a
   phone held sideways, none of which the number 1033 knew about.
   The bases sum to 1000 with the gap, so the drop lands where it always did;
   above it the two columns share the surplus 728:428, which is the proportion
   the numbers in those token names used to state directly. Without an aside
   the main column simply takes the row. */
.layout {
  align-items: flex-start;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  padding-bottom: var(--space-7);
  padding-top: var(--space-6);
}
/* min(100%, …) on the basis is what lets a wrapped column fill its own line
   instead of sitting at 600px inside a 360px phone. */
.col-main { flex: 728 1 min(100%, var(--col-main)); min-width: 0; }
.col-side { flex: 428 1 min(100%, var(--col-side)); min-width: 0; }
/* base.html closes the <aside> ON the block, with no whitespace inside it, so a
   page that fills neither block leaves this element genuinely empty and it
   collapses - flex gap and screen-reader landmark included. */
.col-side:empty { display: none; }


/* ----------------------------------------------------------------- home --- */

/* The front page is the one page whose content is wider than the split above.
   It carries no rail at all: what used to sit beside it was a tinted top-ten
   the leaderboard now answers over three periods, and the genre table, which
   became the band at the foot of the page. Scoped rules rather than a deletion
   - the split is still exactly what any other page gets when it fills
   base.html's sidebar block.

   TWO OF THE FOUR RULES THAT WERE HERE ARE GONE, and that is the point of the
   refresh rather than an oversight. .card-grid.home-grid and
   .home-genres .sidebar-genres__list existed to raise a column count on the
   one page wide enough to carry more of them; both components now count their
   own tracks off a floor, so the front page gets the extra columns by being
   wider and needs nobody to say so. The classes stay in home.html and are
   simply no longer load-bearing - deleting a rule is cheaper than keeping two
   sources of truth for one number. */

/* .col-main already grows into a row with no <aside> in it, and this says so
   out loud: the front page's width is a decision, not a leftover of an empty
   block that a later flex-basis change could quietly undo. */
.page-home .col-main { flex: 1 1 100%; }

/* The leaderboard's three period panels, and the ONE rule this file keeps
   about a component in features/discovery.css - the same reach, for the same
   reason, as the two that were just deleted: a TRACK COUNT is a layout fact
   and layout is this file's. Three fixed columns collapsing to one at 768px
   put a 300px panel next to a 1200px one on the way past a tablet; auto-fit
   gives three panels three columns wherever three fit, two where two do, and
   one on a phone. auto-fit rather than auto-fill matters here: there are
   exactly three panels, and a fourth empty track would leave the row a quarter
   short of the container it is meant to fill. Two-class specificity so it
   still wins over discovery.css's own rule and that file's 768px variant,
   which load after this one. */
.page-home .disc-board__grid {
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-board), 100%), 1fr));
}

/* The ranking page's rows, same reach and same reason. The class is additive -
   discovery.css still owns what a ranked row IS - and it is printed by
   rankings.html beside .rankings__list, which is a flex column there and would
   otherwise leave a 24-row page one row wide on a 1280px container. */
.rankings__list.rankings-grid {
  display: grid;
  gap: var(--space-2);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-row), 100%), 1fr));
}

/* The panel is the one surface primitive: everything that is a box on the page
   is this, or is built from it. 10px, one hairline, resting elevation. */
.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-1);
  padding: var(--space-4);
}

/* The 3px signature at the very top of the document. The sweep is a TOKEN, not
   three stops written out here: features/reader.css paints the same strip as its
   progress fill, and two copies of one gradient in two stylesheets is a value
   that has to be kept in step by hand. tokens/<theme>.css owns it. */
.top-rule {
  background: var(--grad-rule);
  height: var(--rule-h);
}

/* IT IS PARKED WHERE IT LANDS, AND IT TRAVELS ON A TRANSFORM. The obvious
   spelling - top: -100px, then top: var(--space-3) on focus - animates the box's
   own position, which is a layout property: every frame of that 150ms is a
   layout pass on the first element of the document, for the one visitor who is
   navigating by keyboard and can least afford a stutter. So the link SITS at its
   final coordinates and is held off-screen by a transform, which is composited
   and costs no layout at all. -200% of its own height clears the box and the
   shadow under it whatever the type scale does; focus takes the transform away
   rather than putting a number back. */
.skip-link {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-3);
  color: var(--text);
  font-weight: 600;
  left: var(--space-3);
  padding: var(--space-3) var(--space-4);
  position: absolute;
  top: var(--space-3);
  transform: translateY(-200%);
  transition: transform var(--t-fast);
  z-index: 80;
}
.skip-link:focus { text-decoration: none; transform: none; }

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


/* =============================================================== HEADER === */

.hdr {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: relative;
  z-index: 30;
}
.hdr-in {
  align-items: center;
  display: flex;
  gap: var(--space-4);
  min-height: var(--hdr-h);
}

/* Boxed wordmark, left. The box is the logo - there is no image asset to load
   and nothing to shift when it arrives. It is a PLATE carrying a white label, so
   it is --grad-brand: the same sweep the brand button and the reader's mark
   wear, measured once in tokens/<theme>.css at 6.29:1 and 5.70:1 under white,
   rather than the same two stops spelled out again here. */
.hdr-logo { flex: 0 0 auto; font-size: 0; line-height: 0; }
.hdr-logo a {
  align-items: center;
  background: var(--grad-brand);
  border-radius: var(--logo-radius);
  box-shadow: var(--shadow-1);
  color: var(--on-brand);
  display: inline-flex;
  font-family: var(--font-head);
  font-size: var(--fs-h2);
  font-weight: 700;
  letter-spacing: -.01em;
  line-height: 1;
  min-height: 38px;
  padding: 0 var(--space-4);
  transition: box-shadow var(--t-fast), transform var(--t-fast);
}
.hdr-logo a:hover {
  box-shadow: var(--shadow-2);
  color: var(--on-brand);
  text-decoration: none;
  transform: var(--lift);
}

/* Centred, and centred by auto margins rather than by order: the tools keep
   their own margin-left:auto, so the field stays optically central whatever
   the wordmark's width turns out to be. */
.hdr-search {
  flex: 1 1 320px;
  margin-inline: auto;
  max-width: 460px;
  min-width: 0;
}

/* THE CLUSTER WRAPS, BECAUSE NOTHING INSIDE IT CAN GIVE. It was flex: 0 0 auto
   around a ~118px segmented control, a fixed 44px toggle and two nowrap
   buttons - ~337px of content with no shrink anywhere in it - so on a 320px
   phone the header pushed the DOCUMENT sideways, which is the one failure a
   layout is never allowed: every line of every page then scrolls away from the
   thumb. `0 1 auto` with min-width:0 lets the cluster be narrower than its
   content wants, and flex-wrap is what it does with that: the auth pair drops
   onto a second line under the two toggles rather than off the end of the
   screen. justify-content keeps both lines hard against the right edge, so the
   cluster still reads as one right-hand group and not as two stray rows.
   Nothing here needs a width to know when to do it - it happens exactly when
   the row runs out, at whatever type scale and zoom level the reader is on. */
.hdr-tools {
  align-items: center;
  display: flex;
  flex: 0 1 auto;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: flex-end;
  margin-left: auto;
  min-width: 0;
}
.hdr-user { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: flex-end; }
/* Log in / Sign up are header tools, so they are held to the header's target
   and not to the default button height - rule 6. The two toggles beside them
   are already --tap, and a 38px button in a row of 44px ones is both a mis-tap
   and a visibly ragged row. */
.hdr-user .btn { min-height: var(--tap); }

.hdr-burger {
  align-items: center;
  background: transparent;
  border: 0;
  cursor: pointer;
  display: none;
  height: var(--tap);
  justify-content: center;
  padding: 0;
  width: var(--tap);
}
.burger,
.burger::before,
.burger::after {
  background: var(--text-2);
  border-radius: var(--r-pill);
  content: "";
  display: block;
  height: 2px;
  position: relative;
  transition: background var(--t-fast);
  width: 20px;
}
.burger::before { top: -6px; }
.burger::after { top: 4px; }
.hdr-burger:hover .burger,
.hdr-burger:hover .burger::before,
.hdr-burger:hover .burger::after { background: var(--brand); }

/* The tool buttons: a 44px target, a circle when bare, a pill when labelled. */
.icon-btn {
  align-items: center;
  background: var(--surface-control);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  color: var(--text-2);
  cursor: pointer;
  display: inline-flex;
  height: var(--tap);
  justify-content: center;
  min-width: var(--tap);
  transition: background var(--t-fast), border-color var(--t-fast),
              color var(--t-fast), box-shadow var(--t-fast);
  width: var(--tap);
}
/* Ink on a --brand-soft plate is --brand-strong, never --brand: the soft plate
   inverts between themes (a pale wash, then a deep well) and only the strong
   stop clears 4.5:1 against both. Same pairing on .chip:hover and .pg:hover. */
.icon-btn:hover {
  background: var(--brand-soft);
  border-color: var(--brand);
  color: var(--brand-strong);
}
.icon-btn[aria-pressed="true"] {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  box-shadow: var(--shadow-1);
  color: var(--on-brand);
}
.icon-btn-glyph { font-size: var(--fs-lead); line-height: 1; }

/* A labelled toggle stops being a circle: it grows into a pill so the word has
   somewhere to sit. Below the mobile breakpoint the label is dropped and the
   circle returns, since the header has no room for it there. */
.icon-btn-label {
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1;
}
.icon-btn:has(.icon-btn-label) {
  gap: var(--space-2);
  padding: 0 var(--space-4);
  width: auto;
}


/* ================================================================== NAV === */

/* The full-width strip under the header. --ring is swapped here because the
   default indigo focus outline would land on an indigo plane. */
.nav-primary {
  --ring: var(--nav-ink);
  background: var(--nav-bg);
  position: relative;
  z-index: 20;
}
.nav-list { display: flex; flex-wrap: wrap; }
.nav-item { position: relative; }
.nav-item > a {
  align-items: center;
  color: var(--nav-ink);
  display: flex;
  font-size: var(--fs-meta);
  font-weight: 650;
  letter-spacing: var(--track-chip);
  min-height: var(--tap);
  padding: 0 var(--space-4);
  position: relative;
  text-transform: var(--nav-case);
  transition: background var(--t-fast);
}
.nav-item > a:hover {
  background: var(--nav-hover);
  color: var(--nav-ink);
  text-decoration: none;
}
/* The current page is a plate plus a 3px underline drawn INSIDE the item, so
   marking it costs no height and the strip never reflows. */
.nav-item > a[aria-current="page"] {
  background: var(--nav-active);
  box-shadow: inset 0 calc(var(--nav-marker) * -1) 0 var(--nav-ink);
  color: var(--nav-ink);
}
.nav-item.has-sub > a::after {
  border: 4px solid transparent;
  border-top-color: currentColor;
  content: "";
  margin-left: var(--space-2);
  margin-top: 3px;
}

.nav-sub {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0 0 var(--r-panel) var(--r-panel);
  box-shadow: var(--shadow-3);
  display: none;
  left: 0;
  min-width: 200px;
  overflow: hidden;
  padding: var(--space-1) 0;
  position: absolute;
  top: 100%;
  z-index: 40;
}
.nav-item:hover > .nav-sub,
.nav-item:focus-within > .nav-sub { display: block; }
/* Same target as .mobile-sub a: a dropdown row is a nav row, and rule 6 holds
   the nav to --tap everywhere. Flex rather than block, so the min-height is
   filled by the link and centres its word instead of leaving 7px of dead
   plate under it. */
.nav-sub a {
  align-items: center;
  color: var(--text-2);
  display: flex;
  min-height: var(--tap);
  padding: var(--space-2) var(--space-4);
  transition: background var(--t-fast), color var(--t-fast);
}
.nav-sub a:hover {
  background: var(--surface-alt);
  color: var(--link-hover);
  text-decoration: none;
}

.mobile-menu {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-2);
  padding: var(--space-4) 0;
  position: relative;
  z-index: 25;
}
.mobile-list > li > a {
  align-items: center;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  display: flex;
  font-weight: 600;
  min-height: var(--tap);
}
.mobile-list > li > a[aria-current="page"] { color: var(--link); }
.mobile-sub a {
  align-items: center;
  color: var(--text-2);
  display: flex;
  min-height: var(--tap);
  padding-left: var(--space-4);
}


/* ========================================================== BREADCRUMBS === */

/* Quiet, not coloured. The reference painted this bar in the brand hue, which
   put the loudest strip on the page above its least important line of text. */
.crumbs {
  background: var(--crumb-bg);
  border-bottom: 1px solid var(--border);
}
.crumbs-list {
  align-items: center;
  color: var(--crumb-ink);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--fs-meta);
  gap: var(--space-1) var(--space-2);
  min-height: 38px;
  padding: var(--space-2) 0;
}
.crumbs-item + .crumbs-item::before {
  color: var(--text-muted);
  content: "\203A";
  margin-right: var(--space-2);
}
.crumbs-item a { color: var(--crumb-ink); }
.crumbs-item a:hover { color: var(--link-hover); }
.crumbs-item [aria-current="page"] { color: var(--text); font-weight: 600; }


/* ================================================== BANDS AND BAND CHIPS === */

/* The signature of this layout: a small solid chip sitting ON a rule, with the
   rule running the full width of the column. Four members of one family, set
   through --band so a modifier is one line and the component half can read the
   band's colour without knowing which modifier produced it.
   THE CHIP'S SHAPE IS THE THEME'S, NOT THIS FILE'S, and that is the single
   biggest reason two sites running this stylesheet do not read as siblings.
   Four tokens carry the whole geometry and between them they spell all three
   shapes the presets use, with no selector here learning which one it got:
     --sect-chip-radius  `R R 0 0` is a TAB standing on its rule; a full pill
                         radius plus a hairline rule is a chip FLOATING above
                         one; 0 is a slab.
     --sect-chip-bg/-ink the fill and its label. A theme that wants the chip to
                         be quiet points -bg at a surface and -ink at the text
                         ramp, which is what turns the chip into a heading; one
                         that wants no plate at all points -bg at transparent
                         and -ink at the band.

                         THE FALLBACK IS WHERE IT IS FOR A REASON AND MUST STAY
                         HERE. --band is declared on .sect, and a custom
                         property is substituted where it is DECLARED, not
                         where it is used - so a token file writing
                         `--sect-chip-bg: var(--band)` at :root resolves it at
                         :root, where --band does not exist, and computes to
                         the guaranteed-invalid value that inherits down as
                         invalid. The chip's background then falls back to
                         transparent and every band heading on the site becomes
                         white 12px text on the page, silently, on that site
                         only. Written as a var() FALLBACK it is resolved on
                         .sect-chip instead, where --band is in scope. A theme
                         that wants the band's own colour therefore declares
                         `initial` - the guaranteed-invalid value, which is
                         exactly what selects the fallback - rather than naming
                         --band, and stays self-contained.
     --sect-chip-bar     a left rule in the band's colour, 0 by default. With a
                         surface -bg it produces the LEFT-BAR heading; with the
                         solid fill it does nothing visible, so the two are
                         independent rather than a mode flag.
   --band-* is which colour ROLE each band takes. The four modifiers used to
   name --chip-* directly, which meant every site's "latest" band was the same
   sky blue; pointing them at four indirections lets a theme rotate the family
   without touching a modifier. */
.sect {
  --band: var(--band-accent);
  align-items: flex-end;
  border-bottom: var(--band-rule) solid var(--band);
  display: flex;
  margin-bottom: var(--space-4);
  margin-top: var(--space-6);
}
.sect:first-child { margin-top: 0; }

.sect-accent { --band: var(--band-accent); }
.sect-brand { --band: var(--band-brand); }
.sect-rank { --band: var(--band-rank); }
.sect-tax { --band: var(--band-tax); }

.sect-chip {
  background: var(--sect-chip-bg, var(--band));
  border-left: var(--sect-chip-bar) solid var(--band);
  border-radius: var(--sect-chip-radius);
  color: var(--sect-chip-ink, var(--band));
  display: inline-flex;
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: var(--track-chip);
  line-height: 1;
  padding: var(--space-3) var(--space-4);
  text-transform: var(--sect-chip-case);
}
.sect-chip a { color: var(--sect-chip-ink, var(--band)); }
.sect-chip a:hover { color: var(--sect-chip-ink, var(--band)); text-decoration: underline; }

/* The one visible h1 a page carries. It sits above the first band, so it stays
   quiet: the bands do the shouting. */
.page-title { margin-bottom: var(--space-2); }
.dir-intro { color: var(--text-2); margin-bottom: var(--space-4); }
.dir-count { color: var(--text-muted); font-size: var(--fs-meta); margin-bottom: var(--space-3); }
.more-link { padding: var(--space-4) 0; text-align: center; }


/* ========================================================= FILTER CHIPS === */

/* The directory's filters and sort tabs: plain links, so every combination is
   a shareable URL and the panel works with JavaScript off. */
.filters {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-1);
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
}
.filters-summary {
  color: var(--text-2);
  cursor: pointer;
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: var(--track-chip);
  list-style: none;
  min-height: var(--space-6);
  text-transform: uppercase;
}
.filters-summary::-webkit-details-marker { display: none; }
.filters-summary::after { content: " \25BE"; }
.filters[open] .filters-summary::after { content: " \25B4"; }

.filter-group { margin-top: var(--space-4); }
.filter-group-label {
  color: var(--text-muted);
  font-size: var(--fs-meta);
  letter-spacing: var(--track-chip);
  margin-bottom: var(--space-2);
  text-transform: uppercase;
}

.sort-tabs { margin-bottom: var(--space-4); }

.chip-grid { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip {
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--chip-radius);
  color: var(--text-2);
  display: inline-flex;
  font-size: var(--fs-meta);
  font-weight: 600;
  gap: var(--space-1);
  min-height: var(--chip-h);
  padding: 0 var(--space-3);
  transition: background var(--t-fast), border-color var(--t-fast),
              color var(--t-fast), box-shadow var(--t-fast);
}
.chip:hover {
  background: var(--brand-soft);
  border-color: var(--brand);
  color: var(--brand-strong);
  text-decoration: none;
}
.chip.is-active {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  box-shadow: var(--shadow-1);
  color: var(--on-brand);
}
.chip.is-active:hover { color: var(--on-brand); }
.chip-count { color: var(--text-muted); font-size: var(--fs-meta); font-weight: 400; }
.chip:hover .chip-count { color: inherit; }
.chip.is-active .chip-count { color: inherit; opacity: .8; }


/* =============================================================== COVERS === */

/* width/height on the element are the ratio; the box is sized here. */
.cover {
  aspect-ratio: var(--cover-ratio);
  background: var(--surface-alt);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-card);
  display: block;
  height: auto;
  object-fit: cover;
  width: 100%;
}
/* Three sizes, because components/cover.html is only ever included with sm, md
   or slider. A fourth (orig) was declared and never asked for. */
.cover-sm { max-width: 69px; }
.cover-md { max-width: 138px; }
.cover-slider { max-width: 300px; }

/* Every anchor that wraps a cover is the positioning context for its badges,
   and carries the inner hairline. It has to be the WRAPPER and not the image:
   an inset shadow on a replaced element is painted under its content and never
   seen. This is what stops pale artwork from bleeding into a white card. */
.cover-wrap,
.card-update__cover,
.card-list__cover,
.card-rank__cover,
.slide-cover,
.carousel-cover {
  display: block;
  position: relative;
}
.cover-wrap::after,
.card-update__cover::after,
.card-list__cover::after,
.card-rank__cover::after,
.slide-cover::after,
.carousel-cover::after {
  border-radius: var(--r-card);
  box-shadow: var(--shadow-cover);
  content: "";
  inset: 0;
  pointer-events: none;
  position: absolute;
}


/* =============================================================== BADGES === */

.badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  left: var(--space-1);
  position: absolute;
  top: var(--space-1);
  /* Above the cover's inner hairline, which is a later-painted ::after. */
  z-index: 1;
}
/* Badges sit on artwork, never on a surface, so their fills are the deep
   theme-independent set: white text has to clear whatever the cover is. */
.badge {
  border-radius: var(--badge-radius);
  box-shadow: var(--shadow-1);
  color: var(--badge-ink);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .02em;
  line-height: 1;
  padding: 4px var(--space-2);
  text-transform: uppercase;
}
.badge-hot { background: var(--badge-hot); }
.badge-new { background: var(--badge-new); }
.badge-custom { background: var(--badge-custom); }
.badge-adult { background: var(--badge-adult); }


/* ============================================================== BUTTONS === */

.btn {
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-card);
  color: var(--text-2);
  cursor: pointer;
  display: inline-flex;
  font-size: var(--fs-body);
  font-weight: 600;
  gap: var(--space-2);
  justify-content: center;
  line-height: 1;
  min-height: 38px;
  padding: 0 var(--space-4);
  text-align: center;
  transition: background var(--t-fast), border-color var(--t-fast),
              color var(--t-fast), box-shadow var(--t-fast),
              transform var(--t-fast);
  white-space: nowrap;
}
.btn:hover {
  background: var(--surface-alt);
  border-color: var(--brand);
  box-shadow: var(--shadow-1);
  color: var(--link);
  text-decoration: none;
}
.btn:active { transform: none; }

/* The primary action, under ONE name: .btn-primary was a second spelling of
   this rule that no template ever printed. The plate is --brand-fill and not
   --brand: --brand lightens in the dark theme so it still works as ink, which
   would drop this button's white label to 4.47:1. --brand-fill-hover moves
   darker in light and lighter in dark, so one hover rule is correct in both.
   No --ring here: the ring is drawn at offset 2px, outside this plate and on
   the surface behind it, so the default indigo is what has to clear - white
   would be white on white. */
.btn-brand {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  box-shadow: var(--shadow-1);
  color: var(--on-brand);
}
.btn-brand:hover {
  background: var(--brand-fill-hover);
  border-color: var(--brand-fill-hover);
  box-shadow: var(--shadow-2);
  color: var(--on-brand);
  transform: var(--lift);
}

/* Accent. The fill is the CHIP sky rather than --accent: white on #0ea5e9 is
   2.8:1, and a button carries a word. Hover mixes toward --text, which darkens
   it in the light theme and lightens it in the dark one - the same gesture,
   correct in both, with no second rule. */
.btn-accent {
  background: var(--chip-accent);
  border-color: var(--chip-accent);
  box-shadow: var(--shadow-1);
  color: var(--on-accent);
}
.btn-accent:hover {
  background: color-mix(in srgb, var(--chip-accent) 84%, var(--text));
  border-color: color-mix(in srgb, var(--chip-accent) 84%, var(--text));
  box-shadow: var(--shadow-2);
  color: var(--on-accent);
  transform: var(--lift);
}

.btn-ghost {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
.btn-ghost:hover {
  background: var(--surface-control);
  border-color: var(--border-strong);
  box-shadow: none;
  color: var(--text);
}

.btn[disabled],
.btn.is-disabled {
  cursor: default;
  opacity: .5;
  pointer-events: none;
}


/* ==================================================== FORMS AND CONTROLS === */

/* The header search. The magnifier is a masked SVG on the form, not markup and
   not a webfont glyph: it inherits --text-muted, it costs no request, and it
   needs no change to search_box.html, which is included in three places. */
.search {
  display: flex;
  gap: var(--space-2);
  position: relative;
}
.search::before {
  background-color: var(--text-muted);
  content: "";
  height: 16px;
  left: var(--space-3);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-4.4-4.4'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-4.4-4.4'/%3E%3C/svg%3E") center / contain no-repeat;
  pointer-events: none;
  position: absolute;
  /* Centred without a transform, so the reduced-motion block can null every
     transform on the page without knocking the icon off its axis. */
  top: calc(50% - 8px);
  width: 16px;
  z-index: 1;
}
/* --tap, not 38px: the field and its button are header tools, they are the
   pair a phone reader aims at first, and rule 6 holds the header row to 44. */
.search-input {
  background: var(--surface-control);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  color: var(--text);
  flex: 1 1 auto;
  min-height: var(--tap);
  min-width: 0;
  padding: 0 var(--space-4) 0 var(--space-8);
  transition: background var(--t-fast), border-color var(--t-fast),
              box-shadow var(--t-fast);
}
.search-input:hover { border-color: var(--brand); }
.search-input:focus {
  background: var(--surface);
  border-color: var(--brand);
  box-shadow: var(--shadow-1);
}
/* The UA's own clear button in a type=search field, which no token reaches. */
.search-input::-webkit-search-cancel-button { cursor: pointer; }

.search-btn {
  align-items: center;
  background: var(--brand-fill);
  border: 1px solid var(--brand-fill);
  border-radius: var(--r-pill);
  color: var(--on-brand);
  cursor: pointer;
  display: inline-flex;
  flex: 0 0 auto;
  font-size: var(--fs-meta);
  font-weight: 650;
  justify-content: center;
  min-height: var(--tap);
  padding: 0 var(--space-4);
  transition: background var(--t-fast), border-color var(--t-fast),
              box-shadow var(--t-fast);
}
.search-btn:hover {
  background: var(--brand-fill-hover);
  border-color: var(--brand-fill-hover);
  box-shadow: var(--shadow-2);
  color: var(--on-brand);
  text-decoration: none;
}

.field { display: block; margin-bottom: var(--space-4); }
.field-label {
  color: var(--text-2);
  display: block;
  font-size: var(--fs-meta);
  font-weight: 600;
  margin-bottom: var(--space-1);
}
.field-input {
  background: var(--surface-control);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-card);
  color: var(--text);
  min-height: 40px;
  padding: var(--space-2) var(--space-3);
  transition: background var(--t-fast), border-color var(--t-fast),
              box-shadow var(--t-fast);
  width: 100%;
}
textarea.field-input { line-height: var(--lh-body); min-height: 96px; padding: var(--space-3); }
.field-input:hover { border-color: var(--border-strong); }
.field-input:focus {
  background: var(--surface);
  border-color: var(--brand);
  box-shadow: var(--shadow-1);
}
select.field-input,
select.btn { cursor: pointer; }


/* ================================================================== ADS === */

/* EVERY PLACEMENT RESERVES ITS BOX. A creative arrives asynchronously and
   expands the slot after paint; without a reserved height that expansion
   pushes the page down under the reader's cursor, which is the single worst
   layout shift a content site can ship. The heights below are the standard
   unit each placement is sold as - a filled slot never grows past what was
   reserved, and an unfilled slot renders nothing at all (see ad_slot.html), so
   an unsold placement still leaves no hole. THE HEIGHTS ARE NOT DECORATION.
   Restyling this block may not change one of them. */
.ad {
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  margin: var(--space-4) auto;
  max-width: var(--wrap-3);
  padding: var(--space-2);
  text-align: center;
}
.ad-label {
  color: var(--text-muted);
  display: block;
  font-size: 10px;
  letter-spacing: .08em;
  margin-bottom: var(--space-1);
  text-transform: uppercase;
}
/* Block, not flex or grid: the creative is staff-authored markup that often
   sizes itself to 100% width, and a centring container would shrink it. The
   box reserves a floor and is never capped - a creative that comes back taller
   grows the slot, which is the one direction the layout can absorb. */
.ad-body { display: block; min-height: 90px; }
.ad-body img { height: auto; margin: 0 auto; }

/* 728x90 leaderboards. */
.ad-header .ad-body,
.ad-footer .ad-body { min-height: 90px; }
/* 300x250 rectangles, in the column and between the pages. */
.ad-before_content .ad-body,
.ad-after_content .ad-body,
.ad-sidebar .ad-body,
.ad-reader_top .ad-body,
.ad-reader_inline .ad-body,
.ad-reader_bottom .ad-body { min-height: 250px; }
/* 160x600 skyscrapers, outside the container. */
.ad-wall_left .ad-body,
.ad-wall_right .ad-body { min-height: 600px; }

/* Wallpapers only exist where there is room beside the 1170px container. */
.walls { display: none; }


/* =============================================================== FOOTER === */

.ftr {
  background: var(--surface);
  border-top: 1px solid var(--border);
  margin-top: var(--space-8);
}
.ftr-in {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  justify-content: space-between;
  padding-bottom: var(--space-6);
  padding-top: var(--space-6);
}
.ftr-brand { flex: 1 1 320px; }
.ftr-mark { font-size: var(--fs-h2); font-weight: 700; margin-bottom: var(--space-2); }
.ftr-mark a { color: var(--link); }
.ftr-note { color: var(--text-muted); font-size: var(--fs-meta); max-width: 460px; }
.ftr-menu ul { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-5); }
.ftr-menu a { color: var(--text-2); font-size: var(--fs-meta); }
.ftr-menu a:hover { color: var(--link-hover); }
.ftr-legal {
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: var(--fs-meta);
  padding: var(--space-4);
  text-align: center;
}

.to-top {
  align-items: center;
  background: var(--brand-fill);
  border: 0;
  border-radius: var(--r-pill);
  bottom: var(--space-6);
  box-shadow: var(--shadow-3);
  color: var(--on-brand);
  cursor: pointer;
  display: flex;
  height: var(--tap);
  justify-content: center;
  position: fixed;
  right: var(--space-6);
  transition: background var(--t-fast), transform var(--t-fast);
  width: var(--tap);
  z-index: 45;
}
.to-top:hover { background: var(--brand-fill-hover); transform: var(--lift); }


/* ============================================================ UTILITIES === */

.meta { color: var(--text-2); font-size: var(--fs-meta); }
.meta-muted { color: var(--text-muted); font-size: var(--fs-meta); }

/* A SCROLLER MUST BE THE CONTAINING BLOCK FOR ITS OWN CONTENT, and position
   here is what says so. Overflow clips a descendant only when the descendant's
   containing block is inside the scroller: an absolutely positioned one whose
   nearest positioned ancestor is OUTSIDE - which, with no position on this box,
   is every one of them - is laid out at its static position, ~1200px along a
   rail of cards, and is not clipped by anything. Every card in these four rails
   carries an .sr-only, which is exactly that: absolute, and 1px, and therefore
   invisible - so the symptom was a home page whose document scrolled ~290px
   sideways with nothing painted out there, for a returning reader on a 360px
   phone. Measured in Chrome: documentElement.scrollWidth 647 against a 360
   client width, and 360 with this line. Nothing that is deliberately positioned
   against an ancestor is affected - the badges, the rating pill and the reading
   meter all sit on .cover-wrap, which is positioned itself - and the rails' own
   arrows are outside the scroller by design (see .crail__rail), because an
   absolute child of a scroller scrolls away with the content. */
.scroll-x {
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  padding-bottom: var(--space-2);
  position: relative;
  scroll-snap-type: x proximity;
}

.empty {
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-panel);
  color: var(--text-muted);
  padding: var(--space-7) var(--space-4);
  text-align: center;
}

/* Staff-authored HTML - the about page, a custom 404 body. It is the only
   place on the site where markup we did not write has to look composed, so
   the elements are styled rather than the classes. */
.prose { color: var(--text-2); font-size: var(--fs-lead); }
.prose > * + * { margin-top: var(--space-4); }
.prose h2 { color: var(--text); font-size: var(--fs-h2); margin-top: var(--space-6); }
.prose h3 { color: var(--text); font-size: var(--fs-card); margin-top: var(--space-5); }
.prose ul { list-style: disc; padding-left: var(--space-5); }
.prose ol { list-style: decimal; padding-left: var(--space-5); }
.prose li + li { margin-top: var(--space-1); }
.prose img { border-radius: var(--r-card); }
.prose blockquote {
  border-left: 3px solid var(--brand);
  color: var(--text-2);
  padding-left: var(--space-4);
}
.prose code {
  background: var(--surface-inset);
  border-radius: var(--r-card);
  font-size: var(--fs-body);
  padding: 2px var(--space-1);
}

/* The 18+ gate. It is markup every visitor receives - the cached HTML cannot
   know who consented - and is dismissed client-side by the pre-paint
   attribute, so it must be styled as though it will always be shown. */
.adult-gate {
  align-items: center;
  background: var(--scrim);
  display: flex;
  inset: 0;
  justify-content: center;
  padding: var(--space-4);
  position: fixed;
  z-index: 70;
}
.adult-gate-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-3);
  color: var(--text-2);
  max-width: 420px;
  padding: var(--space-6);
  text-align: center;
  width: 100%;
}
.adult-gate-box h2 { color: var(--text); margin-bottom: var(--space-3); }
.adult-gate-actions {
  display: flex;
  gap: var(--space-3);
  justify-content: center;
  margin-top: var(--space-5);
}

/* Family-safe and the adult gate: both driven by the pre-paint attributes. */
html[data-safe="1"] [data-adult="1"] { display: none !important; }
html[data-adultok="1"] .adult-gate { display: none; }


/* =====================================================================
 * COMPONENTS - the second half of the refresh.
 *
 * REFRESHED and written against the contract at the top of this file:
 *   cards, the chapter table, the rails, the sidebar blocks, the rank
 *   ladder, the genre table, pagination, the empty state's inner parts,
 *   comments and suggest.
 * The compatibility aliases this half leaned on (--accent-hi, --accent-tax,
 * --border-img, --r-sm, --r-md) have been deleted from the token file: the
 * page-scoped <style> blocks in manga.html, reader.html and static_page.html
 * name only tokens on the list above. Nothing may reintroduce one.
 * ===================================================================== */

/* ---------------------------------------------------------------- cards --- */

/* Three containers and nothing else decides the shape of a listing: the page
   picks one, the card inside it decides everything else.

   BOTH OF THEM COUNT THEIR OWN COLUMNS (rule 7). .card-grid was two fixed
   tracks with a breakpoint dropping it to one, which meant the front page had
   to override it to get three and the series page had to override it again -
   three places to change one number, and a snap at 680px on the way through.
   The floor is the width the card stops being readable at, the count follows
   from it, and every page that renders one gets the right answer by being the
   width it is: two in a 728px column, three across a 1248px container, one on
   a phone. min(<floor>, 100%) is the guard - a bare 320px track overflows a
   296px content box, which is a horizontal scrollbar on a 360px phone.
   .card-column was a flex column, which on the directory left a 1248px row
   carrying a 96px cover and three lines of synopsis. It is the same grid with
   a wider floor: two rows abreast where there is room for two. */
.card-grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-card), 100%), 1fr));
}
.card-column {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-row), 100%), 1fr));
}

/* Every card title is clamped rather than truncated with an ellipsis on one
   line: a manga title is the thing a reader scans for, and two lines of it is
   the difference between recognising a series and guessing at it. `anywhere`
   is the guard for the untranslated single-word titles the catalogue carries -
   without it one of them widens the grid column it sits in. */
.card-update__title,
.card-list__title,
.card-rank__title,
.slide-title,
.carousel-title {
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  display: -webkit-box;
  font-size: var(--fs-card);
  font-weight: 600;
  line-height: var(--lh-head);
  overflow: hidden;
  overflow-wrap: anywhere;
}
.card-rank__title,
.slide-title,
.carousel-title { font-size: var(--fs-body); }
.card-list__title { font-size: var(--fs-lead); }

/* :visited before :hover - the other order makes hover unreachable on a link
   the visitor has already followed, and the visited colour is a read cue here. */
.card-update__link,
.card-list__link,
.card-rank__link,
.slide-title a,
.carousel-title a { color: var(--text); }
.card-update__link:visited,
.card-list__link:visited,
.card-rank__link:visited,
.slide-title a:visited,
.carousel-title a:visited { color: var(--link-visited); }
.card-update__link:hover,
.card-list__link:hover,
.card-rank__link:hover,
.slide-title a:hover,
.carousel-title a:hover { color: var(--link-hover); }

/* The whole card is the hover target. A card is one destination wearing two
   anchors - the cover (aria-hidden, out of the tab order) and the title - so
   pointing anywhere on it has to give the same answer as pointing at the
   words. Last, so it also wins over :visited, which is what rule 5 asks for. */
.card-update:hover .card-update__link,
.card-list:hover .card-list__link,
.card-rank:hover .card-rank__link,
.slide:hover .slide-title a,
.carousel-item:hover .carousel-title a { color: var(--link-hover); }

/* Every chapter link on the site, wherever it is rendered. */
.chapter-link {
  color: var(--link-chapter);
  font-size: var(--fs-meta);
  overflow-wrap: anywhere;
}
.chapter-link:visited { color: var(--link-visited); }
.chapter-link:hover { color: var(--link-hover); }

/* The same link INSIDE a card's meta line, which is a wrapping flex row with a
   view count pushed to its far end. Both cards print this class beside
   .chapter-link and neither had a rule for it. Chapter names are staff text and
   run long - "Chapter 141: The Battle at the Gate" is ordinary - so on the row
   it is held to one line and clipped, which keeps the count on the first line
   instead of being pushed onto a second and changing the card's height. min-width
   is what lets a flex item shrink below its content at all. */
.card-list__chapter,
.card-rank__chapter {
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  overflow-wrap: normal;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The view counter's eye, masked from an inline SVG rather than printed as a
   glyph: it inherits currentColor, costs no request, and the three templates
   that show a view count keep the word "views" for a screen reader while the
   page shows the number alone. 12px so it sits on the meta line's cap height. */
.card-list__views::before,
.card-rank__views::before,
.chapter-row__views::before {
  background-color: currentColor;
  content: "";
  flex: 0 0 auto;
  height: 12px;
  margin-right: var(--space-1);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 12s3.6-6.6 10-6.6S22 12 22 12s-3.6 6.6-10 6.6S2 12 2 12z'/%3E%3Ccircle cx='12' cy='12' r='2.75'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 12s3.6-6.6 10-6.6S22 12 22 12s-3.6 6.6-10 6.6S2 12 2 12z'/%3E%3Ccircle cx='12' cy='12' r='2.75'/%3E%3C/svg%3E") center / contain no-repeat;
  width: 12px;
}

/* Home grid cell: cover, title, and up to three chapters with a relative time.
   The component prints those as three siblings, so the cover spans both rows
   of a two-column grid rather than needing a wrapper of its own.

   THE CARD IS A QUERY CONTAINER, because it is rendered at four widths that
   have nothing to do with the window: ~300px two-across in a rail-less phone
   column, ~360px two-across in the main column, ~400px three-across on the
   front page, and the full 1248px when a site has one series to show. The
   cover width is what answers, and the track is `auto` so the column follows
   the cover rather than the cover being cropped by a number declared here.
   84px is the BASE - the size this card shipped at on a desktop - so a browser
   with no container query support gets today's card at every width, and the
   queries below only spend width that is actually there. */
.card-update {
  align-content: start;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-1);
  column-gap: var(--space-3);
  container: card-update / inline-size;
  display: grid;
  grid-template-areas:
    "cover title"
    "cover chapters";
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  padding: var(--space-3);
  transition: border-color var(--t-fast), box-shadow var(--t-fast),
              transform var(--t-fast);
}
.card-update:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-2);
  transform: var(--lift);
}
.card-update__cover { grid-area: cover; transition: transform var(--t-fast); width: 84px; }
.card-update:hover .card-update__cover { transform: var(--lift); }
.card-update__title { grid-area: title; margin-bottom: var(--space-2); }
.card-update__chapters {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  grid-area: chapters;
}
/* The row tints on its own so a three-chapter card still gives the pointer an
   answer about WHICH chapter it is over. Pulled out by its own padding, so the
   tint reaches the card's gutter without moving the text off the title's edge. */
.card-update__chapter {
  align-items: baseline;
  border-radius: var(--r-card);
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
  margin-inline: calc(var(--space-1) * -1);
  padding: var(--space-1);
  transition: background var(--t-fast);
}
.card-update__chapter:hover { background: var(--surface-alt); }
.card-update__time {
  color: var(--text-muted);
  flex: 0 0 auto;
  font-size: var(--fs-meta);
  white-space: nowrap;
}
/* The card answering the space it is in. The 479px @media that used to shrink
   this cover is gone: a card alone in a one-column grid on a 360px phone is
   ~300px wide and the card knows it, which is also the right answer for a
   300px card on a 1440px monitor. Queries LAST in the block, because a
   container query adds no specificity - a later plain rule would win. */
@container card-update (max-width: 17rem) {
  .card-update__cover { width: 68px; }
}
@container card-update (min-width: 25rem) {
  .card-update__cover { width: 104px; }
}
@container card-update (min-width: 34rem) {
  .card-update__cover { width: 128px; }
}

/* Wide directory / search row: cover, title, latest chapter, views, a clamped
   synopsis and the read-more that repeats the title's destination.

   A query container for the same reason the update card is: this row is now
   rendered two-across on a wide directory and one-across in a narrow column,
   and the two want different covers. The base is the 96px row that shipped -
   readable at every width down to a 296px phone content box, where it leaves
   the body ~184px - so the queries are enhancement in both directions and
   nothing here depends on them landing. */
.card-list {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-1);
  container: card-list / inline-size;
  display: flex;
  gap: var(--space-4);
  padding: var(--space-4);
  transition: border-color var(--t-fast), box-shadow var(--t-fast),
              transform var(--t-fast);
}
.card-list:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-2);
  transform: var(--lift);
}
.card-list__cover { flex: 0 0 96px; transition: transform var(--t-fast); }
.card-list:hover .card-list__cover { transform: var(--lift); }
.card-list__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}
.card-list__meta {
  align-items: baseline;
  color: var(--text-muted);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--fs-meta);
  gap: var(--space-1) var(--space-3);
}
.card-list__date { color: var(--text-muted); }
.card-list__views {
  align-items: center;
  color: var(--text-muted);
  display: inline-flex;
  margin-left: auto;
}
.card-list__synopsis {
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  color: var(--text-2);
  display: -webkit-box;
  font-size: var(--fs-meta);
  line-height: var(--lh-body);
  overflow: hidden;
}
/* align-self, so the target is the width of the words and not of the column -
   a full-width invisible hit area over a synopsis swallows text selection. */
.card-list__more {
  align-self: flex-start;
  font-size: var(--fs-meta);
  font-weight: 600;
}
.card-list__more::after { content: "\00a0\2192"; }
/* The row answering the space it is in, and the two @media widths that used to
   do it - 768px for the cover, 479px for the restack - are gone. A row two
   abreast on a 1248px directory has 560px to spend and gets a cover worth
   looking at; the same component in a 296px phone column keeps a 76px one and
   the words. LAST in the block: a container query adds no specificity, so a
   plain rule below it would win. */
@container card-list (min-width: 34rem) {
  .card-list__cover { flex-basis: 128px; }
  .card-list__synopsis { -webkit-line-clamp: 4; }
}
@container card-list (max-width: 19rem) {
  .card-list__cover { flex-basis: 76px; }
}

/* ---------------------------------------------------------- ranked rows --- */

/* THE RANK LADDER IS GONE WITH THE LIST THAT CARRIED IT. It tinted
   .sidebar-popular__list row by row, --rank-1 down to --rank-10, and that list
   was only ever printed by components/sidebar_popular.html - which is deleted,
   because every page overrides base's sidebar block empty and the front page
   says the same thing in the leaderboard. Nothing emits the class any more, so
   the nth-child ladder went with it, and --rank-1 ... --rank-9 are out of the
   token files with it; --rank-10 is still the plate every ranked row sits on,
   below.
   THE INK ON A RANKED ROW IS NOT THE INK ON A CARD. Measured on --rank-1, the
   strongest step the ladder used: --text-muted lands at 3.84:1, --link-chapter
   at 3.67:1 and --link-visited at 3.82:1, all of which pass on plain --surface
   and none of which passed there. The rows are darkened rather than the plate
   lightened, so the muted layer steps up to --text-2 (7.4:1) and the two link
   inks mix toward --text - which darkens them in the light theme and LIGHTENS
   them in the dark one. One gesture, correct in both, and it keeps the visited
   recolouring rather than trading it away. */
/* A query container: the ranking page's rows are a two-track grid now, so the
   same row is a ~600px cell on a desktop and a ~330px one on a phone, and one
   of those wants a bigger cover than the other. The dense row is the BASE, so
   a browser with no container query support gets exactly today's row. */
.card-rank {
  align-items: center;
  background: var(--rank-10);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  container: card-rank / inline-size;
  display: flex;
  gap: var(--space-3);
  padding: var(--space-2);
  transition: border-color var(--t-fast), box-shadow var(--t-fast),
              transform var(--t-fast);
}
.card-rank:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-2);
  transform: var(--lift);
}
.card-rank .chapter-link { color: color-mix(in srgb, var(--link-chapter) 80%, var(--text)); }
.card-rank .chapter-link:visited,
.card-rank__link:visited { color: color-mix(in srgb, var(--link-visited) 82%, var(--text)); }
/* :visited above, :hover here - rule 5, restated locally because the two rules
   that carry it for every other card are further up the file and the visited
   override below them would otherwise make hover unreachable on a read row. */
.card-rank .chapter-link:hover,
.card-rank__link:hover { color: var(--link-hover); }

/* The counter. The top three are a solid plate and the rest are a quiet one -
   that is the whole podium signal and it costs no second hue. The plate is
   --brand-fill, which is the token that exists precisely to carry a white label
   in both themes (6.3:1). The token files used to also carry a --rank-num-hi
   pair for this plate; nothing ever read it, and it resolved to --brand, which
   lightens in dark and would have dropped this white numeral to 4.47:1 - so it
   is deleted rather than left as a trap. The quiet plate steps toward
   --surface rather than using --rank-num, because --rank-num is 12% brand and
   rows 4-7 are 13-7% of the same brand - a counter tinted with the hue it sits
   on disappears into it. Ink stays --brand-strong either way. */
.card-rank__num {
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-card);
  color: var(--rank-num-ink);
  display: inline-flex;
  flex: 0 0 auto;
  font-size: var(--fs-meta);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  justify-content: center;
  line-height: 1;
  min-height: 24px;
  min-width: 24px;
  padding: 0 var(--space-1);
}
.card-rank__num--top {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  color: var(--on-brand);
}
.card-rank__cover { flex: 0 0 48px; transition: transform var(--t-fast); }
.card-rank:hover .card-rank__cover { transform: var(--lift); }
.card-rank__body { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.card-rank__meta {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
}
.card-rank__views {
  align-items: center;
  color: var(--text-2);
  display: inline-flex;
  font-size: var(--fs-meta);
  margin-left: auto;
}
/* The row answering the space it is in. 69px is where the sm cover variant
   stops having pixels to give (the file is 75px wide), so the wide row takes
   64 and no more rather than resampling a thumbnail up a page. LAST in the
   block: a container query adds no specificity, so the plain .card-rank__cover
   and .card-rank__num rules above would otherwise win. */
@container card-rank (min-width: 26rem) {
  .card-rank__cover { flex-basis: 64px; }
  .card-rank__title { font-size: var(--fs-card); }
  .card-rank__num { font-size: var(--fs-body); min-height: 30px; min-width: 30px; }
}

/* -------------------------------------------------------- chapter table --- */

/* Three columns - name / views / uploaded - on hairline dividers and with no
   zebra: the read-state colouring is the only thing on this table allowed to
   paint a row, and stripes underneath it would make a read chapter and an even
   row look like the same signal. The rows are pulled out to the panel's gutter
   so the hover tint reads as a table row rather than as a floating pill, and
   the two meta columns hold a floor width so the numbers stay in a column. */
/* The list is the query container for its own rows: the table is printed in a
   panel that is 1206px wide on a desktop series page and 310px on a phone, and
   a restack decided by the WINDOW would be wrong the moment the same table is
   rendered anywhere narrower than the page.

   AND IT COUNTS ITS OWN COLUMNS, exactly like every other collection (rule 7).
   As a flex column it printed one row per line, so the series page - which
   carries no rail and hands this list ~1206px - spent a full line on a chapter
   name, a view count and a date, and left more than half of it empty, forty
   times in a row. The floor is --fit-row, the same one the directory's rows and
   the ranked rows count off, and it is the right floor here rather than a
   borrowed one: 440px is WIDER than the 26rem (416px) at which the row below
   gives up its third column, so a list that has taken two tracks can never have
   handed either of them a row too narrow to keep its three columns. Two abreast
   in the series panel, three from ~1384px, one on a phone - and the container
   query at the foot of this block still restacks the ROW when the list itself is
   narrow, which after this is the only case it can fire in.
   THE GAP IS COLUMN-ONLY. A row gap would push apart rows that are held
   together by a shared hairline, which is the whole look of a table. The rows
   are also pulled --space-3 out of the list on each side so their hover tint
   reaches the panel's gutter, and --space-7 is the first step wide enough to
   swallow both of those pulls and still leave air between two tints that are
   now neighbours. */
.chapter-list {
  column-gap: var(--space-7);
  container: chapters / inline-size;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-row), 100%), 1fr));
}
.chapter-row {
  align-items: center;
  border-bottom: 1px solid var(--border);
  column-gap: var(--space-3);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  margin-inline: calc(var(--space-3) * -1);
  min-height: 40px;
  padding: var(--space-1) var(--space-3);
  transition: background var(--t-fast);
}
/* EVERY ROW IS RULED, INCLUDING THE LAST, for the same reason .genre-grid rules
   every one of its own: the column count is computed and no selector can know
   it, so :last-child is the bottom of the LAST TRACK and not the bottom of the
   table - it took the hairline off the right-hand column and left the left-hand
   one with a rule hanging under it. Ruling all of them makes the closing
   hairline read as the table's own bottom edge in one column and in three. */
.chapter-row:hover { background: var(--surface-alt); }
/* Flex, so the link fills the row's height and the whole cell is the target. */
.chapter-row__name {
  align-items: center;
  display: flex;
  font-size: var(--fs-body);
  min-height: 32px;
  min-width: 0;
}
.chapter-row__views,
.chapter-row__date {
  color: var(--text-muted);
  font-size: var(--fs-meta);
  justify-self: end;
  white-space: nowrap;
}
.chapter-row__views { align-items: center; display: inline-flex; min-width: 64px; }
.chapter-row__date { min-width: 92px; text-align: right; }
/* A chapter table may be introduced by a header row, and the series page prints
   one with the same classes as a data row (marked aria-hidden, because the
   cells already name their own columns for a screen reader). The eye is
   switched off there: an icon in front of the word "Views" is the icon
   captioning itself. Matched on the attribute as well as on the class, so the
   guard holds whatever that row ends up being called. */
.chapter-row[aria-hidden="true"] .chapter-row__views::before,
.chapters-head .chapter-row__views::before { content: none; }
/* Where the reader left off, painted from /api/me/manga-state/ after load. */
.chapter-row.is-reading { box-shadow: inset 3px 0 0 var(--brand); }
/* The restack, moved off the 479px @media and onto the table's own width: the
   name takes the whole first row and the two meta columns share the second,
   which keeps a 40px target on the link instead of squeezing three columns
   into 280px. 26rem rather than 479px because 64 + 92 + two gaps of meta is
   ~180px, and below ~416px what is left of the row cannot hold a chapter
   name. The @media that did this is deliberately NOT kept as a fallback: a
   browser without container queries gets the three-column row at every width,
   which is cramped on a small phone but never unreadable - the name cell
   simply wraps. */
@container chapters (max-width: 26rem) {
  .chapter-row {
    align-items: start;
    grid-template-columns: minmax(0, 1fr) auto;
    padding-bottom: var(--space-2);
    padding-top: var(--space-2);
  }
  .chapter-row__name { grid-column: 1 / -1; min-height: 24px; }
  .chapter-row__views { justify-self: start; min-width: 0; }
}

/* ---------------------------------------------------------------- rails --- */

/* The featured slider and the popular carousel are the same object at two
   sizes: a horizontal scroller of fixed-width cells. Scrolling is native, so
   they work with no JavaScript and with a keyboard (the region is focusable).
   The track carries the vertical padding rather than the rail, so a lifted
   cover is not clipped by the scroller's own overflow. */
.slider,
.carousel { margin-bottom: var(--space-4); }

.slider-track,
.carousel-track {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-1) 0 var(--space-2);
}

.slide,
.carousel-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  scroll-snap-align: start;
}
/* The cell width is a clamp rather than a number with a breakpoint under it:
   the rail's cells shrank at 680px and jumped back at 681px, which on a zoom
   step is the whole rail moving at once. 26vw puts three-and-a-bit cells on a
   360px phone and holds the old 150px from ~577px up. */
.slide { flex: 0 0 clamp(126px, 26vw, 150px); }
.carousel-item { flex: 0 0 clamp(96px, 19vw, 110px); }

.slide .cover,
.carousel-item .cover { max-width: 100%; }

.slide-cover,
.carousel-cover { transition: transform var(--t-fast); }
.slide:hover .slide-cover,
.carousel-item:hover .carousel-cover { transform: var(--lift); }

.slide-chapter { align-self: flex-start; }

/* -------------------------------------------------------------- sidebar --- */

/* The page wraps a rail block in .side-block; the component prints its own
   .sidebar-block. Nested, only the outer one keeps the box - two borders
   around one list is the tell that a component was dropped into a card. */
.side-block,
.sidebar-block {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-1);
  margin-bottom: var(--space-4);
  padding: var(--space-4);
}
.side-block > .sidebar-block {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  margin: 0;
  padding: 0;
}

/* The genre table: as many columns as the space holds, a hairline between ROWS
   only, and no rule between the columns - a full grid of borders on forty
   two-word links is more ink than the links themselves.

   THE FIRST-ROW GUARD IS GONE, and it had to be. It hid the divider on the
   first row by counting cells (:nth-child(-n + 3)), which is only true while
   the column count is a constant - so the count and the guard had to move
   together at every breakpoint, and the front page, which wanted six columns,
   needed a third rule to switch both off again. The count is now computed from
   the floor, so no selector can know it: every row is ruled, including the
   first, and that top hairline reads as the table's own edge under the section
   band. Three rules and two breakpoints collapse into one declaration that is
   right in a 376px rail (two columns), in a 1248px front-page band (seven) and
   on a 360px phone (one). */
.genre-grid,
.sidebar-genres__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-genre), 100%), 1fr));
}
.genre-grid > li,
.sidebar-genres__item { border-top: 1px solid var(--border); min-width: 0; }
.genre-grid a,
.sidebar-genres__link {
  border-radius: var(--r-card);
  color: var(--genre-ink);
  display: block;
  font-size: var(--fs-meta);
  overflow: hidden;
  padding: var(--space-2);
  text-overflow: ellipsis;
  transition: background var(--t-fast), color var(--t-fast);
  white-space: nowrap;
}
.genre-grid a:hover,
.sidebar-genres__link:hover {
  background: var(--surface-alt);
  color: var(--link-hover);
  text-decoration: none;
}

/* -------------------------------------------------------- empty states --- */

/* .empty itself is the foundation's dashed panel; these are the parts inside
   it. An empty state that only apologises is a dead end, so the block is an
   icon, one sentence and - where the caller has somewhere to send the visitor -
   a button that goes there. The action is optional: the callers that have no
   better destination than the page they are already on render without it. */
.empty-icon { color: var(--brand); margin: 0 auto var(--space-3); }
.empty-text {
  color: var(--text-2);
  font-size: var(--fs-body);
  margin: 0 auto;
  max-width: 46ch;
}
.empty-action { margin-top: var(--space-4); }

/* ------------------------------------------------------------- comments --- */

/* Everything below the heading is PAINTED BY js/site.js after load - see the
   cache rule - so this block is styling markup it has never seen. The class
   names are exactly the ones that file emits and the shapes are exactly the
   ones it builds: no rule here may assume a wrapper, an order or an attribute
   the script does not print, because there is no template to add one to.
   #comments is a link target from the series actions, so it keeps a little air
   above it when a jump lands here. */
.comments { scroll-margin-top: var(--space-5); }
.comments__heading {
  align-items: center;
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.comments__count {
  background: var(--brand-soft);
  border-radius: var(--r-pill);
  color: var(--brand-strong);
  font-size: var(--fs-meta);
  font-weight: 700;
  min-width: 24px;
  padding: 2px var(--space-2);
  text-align: center;
}
/* The thread is fetched after load: reserving its box keeps the page from
   jumping when it lands, for the same reason every ad slot reserves one.
   THE FORM IS PART OF WHAT LANDS, which is what 120px did not know. js/site.js
   appends the whole post form INTO this element - and appends it before the
   first request, so it is there in every state the thread can be in - and a
   form is not a line of text: a guest pair, a 96px textarea, both their labels
   and a 38px button, under a rule with --space-5 above and below it. 120px
   reserved less than the "Loading comments" line it was standing in front of.
   MEASURED in Chrome at these tokens, anonymous and with an empty thread: 384px
   in a desktop panel and 408px in a 330px one, where the guest name/email pair
   stacks instead of sitting two across. The taller of the two is what is
   reserved, on purpose - over-reserving costs whitespace at the foot of a panel
   that nobody can see, and under-reserving costs a shift on the page a reader is
   already inside. A thread with comments in it is taller than either number and
   never reads this rule at all. */
.comments__thread { min-height: 408px; }
/* The three states the thread can be in before it has rows - loading, empty,
   and no JavaScript at all - are one quiet dashed well, so an empty thread
   reads as a state rather than as a hole in the page. */
.comments__loading,
.comments__empty,
.comments__noscript {
  background: var(--surface-alt);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-panel);
  color: var(--text-muted);
  font-size: var(--fs-meta);
  padding: var(--space-5) var(--space-4);
  text-align: center;
}
.comments__list { display: flex; flex-direction: column; }
.comments__more { margin-top: var(--space-4); width: 100%; }

/* One comment: a 36px avatar in the first column, everything else in the
   second, so the head, the body and the reply button share one left edge.
   THE AVATAR IS DRAWN, NOT FETCHED. The endpoint returns a display name and a
   body and nothing else, so a picture here would be one request per row for a
   placeholder. ::before is the plate and ::after is the glyph laid over it -
   one element cannot be both, because a mask clips the plate it should sit on.
   --avatar-top keeps the absolutely positioned glyph level with the plate when
   the row carries a top padding; a reply, which carries none, resets it. */
.comment {
  --avatar: 36px;
  --avatar-top: 0px;
  column-gap: var(--space-3);
  display: grid;
  grid-template-columns: var(--avatar) minmax(0, 1fr);
  padding-bottom: var(--space-4);
  position: relative;
}
.comment + .comment {
  --avatar-top: var(--space-4);
  border-top: 1px solid var(--border);
  padding-top: var(--space-4);
}
/* Every child the script appends belongs in the second column; without this,
   auto placement would drop the body underneath the avatar. */
.comment > * { grid-column: 2; min-width: 0; }
.comment::before {
  align-self: start;
  background: var(--brand-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  content: "";
  grid-column: 1;
  grid-row: 1;
  height: var(--avatar);
  width: var(--avatar);
}
.comment::after {
  background-color: var(--brand-strong);
  content: "";
  height: var(--avatar);
  left: 0;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Ccircle cx='12' cy='9' r='3.6'/%3E%3Cpath d='M12 13.4c-3.9 0-7 2.4-7 5.4v.6h14v-.6c0-3-3.1-5.4-7-5.4z'/%3E%3C/svg%3E") center / 58% no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Ccircle cx='12' cy='9' r='3.6'/%3E%3Cpath d='M12 13.4c-3.9 0-7 2.4-7 5.4v.6h14v-.6c0-3-3.1-5.4-7-5.4z'/%3E%3C/svg%3E") center / 58% no-repeat;
  pointer-events: none;
  position: absolute;
  top: var(--avatar-top);
  width: var(--avatar);
}
.comment__head {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  font-size: var(--fs-meta);
  gap: var(--space-1) var(--space-2);
  /* Half the avatar, so a one-line head still centres the plate optically
     against the name rather than sitting above it. */
  min-height: calc(var(--avatar) / 2);
}
.comment__author { color: var(--text); font-size: var(--fs-body); font-weight: 650; }
.comment__guest {
  background: var(--surface-control);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--track-chip);
  padding: 1px var(--space-2);
  text-transform: uppercase;
}
.comment__time { color: var(--text-muted); }
/* pre-wrap and anywhere: the body is plain text, and a pasted URL must wrap
   rather than push the column wider than the page. */
.comment__body {
  color: var(--text-2);
  margin: var(--space-1) 0 var(--space-2);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}
/* A grid item stretches, and a full-column-wide "Reply" is a target the size
   of the comment. justify-self is what keeps it the size of its word. */
.comment__reply {
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  color: var(--link);
  cursor: pointer;
  font-size: var(--fs-meta);
  font-weight: 600;
  justify-self: start;
  padding: 3px var(--space-3);
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.comment__reply:hover {
  background: var(--brand-soft);
  border-color: var(--brand);
  color: var(--brand-strong);
}
/* ONE level of indent, which is all the API renders: the model collapses a
   reply-to-a-reply onto its root on insert. The replies already sit in the
   second column, so the rule is the cue rather than the distance. */
.comment__replies {
  border-left: 2px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: var(--space-4) 0 0;
  padding-left: var(--space-4);
}
.comment--reply {
  --avatar: 28px;
  --avatar-top: 0px;
  border-top: 0;
  padding: 0;
}

.comment-form { border-top: 1px solid var(--border); margin-top: var(--space-5); padding-top: var(--space-5); }
.comment-form__replying {
  align-items: center;
  background: var(--brand-soft);
  border-radius: var(--r-card);
  color: var(--brand-strong);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--fs-meta);
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  padding: var(--space-2) var(--space-3);
}
.comment-form__replying-to { font-weight: 600; }
.comment-form__cancel {
  background: none;
  border: 0;
  color: var(--link);
  cursor: pointer;
  font-weight: 600;
  margin-left: auto;
  padding: 0;
  text-decoration: underline;
}
/* Name and email side by side while two fields fit, stacked when they do not -
   counted from the form's own width rather than from the 768px @media that
   used to say so, which was wrong for a comment form in any narrow column. */
.comment-form__guest {
  display: grid;
  gap: 0 var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}
/* Off-screen, not display:none - a bot reads a hidden field as easily as a
   visible one, but never sees this. */
.comment-form__trap { left: -9999px; position: absolute; top: 0; }
.comment-form__actions { display: flex; gap: var(--space-2); }
/* The moderation notice. "Held for moderation" is the one answer a poster must
   not miss - their comment will not appear and the thread does not reload - so
   it is a plate with a marker rather than a line of small green text. Both
   states are one shape and differ only in the signal colour on the edge; the
   words themselves are always an ink token, never --new or --hot. */
.comment-note {
  align-items: center;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-left: 3px solid var(--new);
  border-radius: var(--r-card);
  color: var(--success-ink);
  display: flex;
  font-size: var(--fs-meta);
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-3);
}
.comment-note::before {
  background: currentColor;
  border-radius: var(--r-pill);
  content: "";
  flex: 0 0 auto;
  height: 8px;
  width: 8px;
}
.comment-note.is-bad { border-left-color: var(--hot); color: var(--danger-ink); }

/* -------------------------------------------------------------- suggest --- */

/* The live-search panel, also painted by js/site.js. It floats over the header
   and the nav strip, so it carries the leaving-the-page elevation and a z-index
   above both. */
.search-drop {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-3);
  left: 0;
  margin-top: var(--space-2);
  max-height: 66vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  position: absolute;
  right: 0;
  top: 100%;
  z-index: 50;
}
.suggest { padding: var(--space-1); }
.suggest-item + .suggest-item { border-top: 1px solid var(--border); }
.suggest-link {
  align-items: center;
  border: 1px solid transparent;
  border-radius: var(--r-card);
  color: var(--text);
  display: flex;
  gap: var(--space-3);
  padding: var(--space-2);
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.suggest-link:hover { background: var(--surface-alt); color: var(--text); text-decoration: none; }
/* The keyboard cursor is NOT the hover state. Arrow keys move a highlight the
   pointer cannot see the position of, so it gets its own plate and border -
   otherwise a reader typing with one hand and hovering with the other sees two
   identical rows and no way to tell which Enter will open. */
.suggest-link.is-active {
  background: var(--brand-soft);
  border-color: var(--brand);
  color: var(--brand-strong);
  text-decoration: none;
}
.suggest-cover {
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-cover);
  flex: 0 0 34px;
  height: 49px;
  object-fit: cover;
  width: 34px;
}
.suggest-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.suggest-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.suggest-chapter { color: var(--link-chapter); font-size: var(--fs-meta); }
.suggest-more {
  color: var(--link);
  font-size: var(--fs-meta);
  font-weight: 650;
  justify-content: center;
  min-height: var(--tap);
}
.suggest-more:hover,
.suggest-more.is-active { color: var(--brand-strong); }

/* ----------------------------------------------------------- pagination --- */

/* First / Prev / numbers / Next / Last, every one of them a pill at the full
   44px target in both axes - this is the control most often used on a phone,
   one-handed, and a 32px number is a mis-tap. Ink on the --brand-soft plate is
   --brand-strong and never --brand, for the reason given on .icon-btn:hover:
   the soft plate inverts between themes and only the strong stop clears both. */
.pager {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: center;
  padding: var(--space-6) 0;
}
.pg {
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  color: var(--text-2);
  display: inline-flex;
  font-size: var(--fs-body);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  justify-content: center;
  min-height: var(--tap);
  min-width: var(--tap);
  padding: 0 var(--space-4);
  transition: background var(--t-fast), border-color var(--t-fast),
              box-shadow var(--t-fast), color var(--t-fast);
}
.pg:hover {
  background: var(--brand-soft);
  border-color: var(--brand);
  box-shadow: var(--shadow-1);
  color: var(--brand-strong);
  text-decoration: none;
}
/* A <span>, never a link: the current page is not somewhere to navigate to. It
   therefore takes no focus and needs no ring - it had one, in white, which is
   the giveaway that the override was copied from plate to plate rather than
   measured where the ring actually lands. */
.pg.is-current {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  box-shadow: var(--shadow-1);
  color: var(--on-brand);
}
.pg-gap {
  align-items: center;
  color: var(--text-muted);
  display: inline-flex;
  min-height: var(--tap);
  padding: 0 var(--space-1);
}


/* ======================================== SHARED CLIENT-WRITTEN STATE === */

/* TWO RULES THAT DRESS MARKUP NO TEMPLATE PRINTS. Both are written by
   js/site.js, and they live in THIS file rather than in
   features/interactions.css because site.js runs on every page and that file is
   not loaded on every page: the reader has no header, so it links this
   stylesheet and features/reader.css and nothing else. A busy comment button and
   an arriving comment thread were therefore unstyled on the one page where a
   reader spends the whole visit.
   THEY ARE AT THE FOOT OF THE FILE ON PURPOSE. [data-busy] is a single-attribute
   selector, so it ties on specificity with .btn, .rate-star and every other
   control it is written on to - and a tie is settled by document order. Declared
   up with the utilities it would lose its cursor to every one of them; here it
   wins, and it still loses to anything more specific, which is right.

   BUSY IS NOT DISABLED. It marks the moment between an optimistic click and the
   answer coming back: the control has already changed to what the visitor asked
   for and the write is still in the air. It is an ATTRIBUTE, so it can sit on
   the bookmark button, the star row or the comment form's submit without any of
   them being restyled, and it disappears with the attribute. `disabled` would
   drop focus from the element the visitor is standing on and hand it back to
   <body>, which is the top of the page; js/site.js refuses the second click
   instead, and aria-busy beside this says so out loud. */
[data-busy] {
  cursor: progress;
  opacity: .62;
  transition: opacity var(--t-fast);
}

/* .is-fresh is what arriving content does when it replaces a skeleton. site.js
   puts it on the LIST, not on every row: thirty rows animating together are one
   composited layer, thirty staggered rows are a slot machine. Opacity and a 4px
   transform only - the box it arrives into has already been sized by the
   handover in site.js, so nothing here can move the page.
   backwards, never both: a forwards fill would go on applying this animation's
   own `transform: none` for as long as the class is on the element, and a filled
   animation outranks every normal declaration - so a row that had just arrived
   would be the one row on the page that could not answer a press. */
.is-fresh { animation: fresh-in var(--t-fast) backwards; }
@keyframes fresh-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}


/* ========================================================== BREAKPOINTS === */

/* WHAT IS LEFT HERE IS ABOUT THE DEVICE, NOT ABOUT THE LAYOUT (rule 7). Nine
   rules moved out of this block in the intrinsic pass and none of them were
   replaced by another width:
     .layout / .col-*      a flex line that breaks on its own bases
     .card-grid            auto-fit off --fit-card
     .genre-grid + guards  auto-fit off --fit-genre, every row ruled
     .wrap padding         a fluid --space-4
     .sect margin          a fluid --space-6
     .slide                a clamp()
     .card-list__cover     a container query on the card
     .card-update          a container query on the card
     .chapter-row          a container query on the table
     .comment-form__guest  auto-fit off the field's own floor
     .ftr-in               already flex-wrap: wrap, so it wrapped anyway
   Zoom is a viewport-width change, and every one of those was a step the page
   took while a reader zoomed through it. */

/* The wall ads live in the gutter beside the container, so the guard is the
   width at which that gutter can hold one: --wrap-3 grew to 1280, and at
   1560px each side is 124px - the narrowest a wall creative is worth serving.
   Below it the slots stay display:none and the markup costs nothing. */
@media (min-width: 1560px) {
  .walls { display: block; }
  .wall {
    position: fixed;
    top: 120px;
    width: calc((100vw - var(--wrap-3)) / 2 - var(--space-4));
  }
  .wall-left { left: var(--space-2); }
  .wall-right { right: var(--space-2); }
}

@media (max-width: 992px) {
  .hdr-search { max-width: none; }
}

/* The one real device switch on the site: the hover-driven primary nav becomes
   a burger and a tap-driven drawer. That is an input-mode decision, not a
   space decision, and no amount of container query makes it one. */
@media (max-width: 768px) {
  .hdr-burger { display: flex; }
  .nav-primary { display: none; }
  .hdr-in {
    flex-wrap: wrap;
    min-height: 0;
    padding-bottom: var(--space-3);
    padding-top: var(--space-3);
  }
  .hdr-search { flex: 1 1 100%; margin-inline: 0; order: 3; }
  /* No room for the word here: the glyph and the accessible name carry it. */
  .icon-btn-label { display: none; }
  .icon-btn:has(.icon-btn-label) { gap: 0; padding: 0; width: var(--tap); }
}

/* Two pieces of chrome that are about the HANDSET rather than about the space
   they are in: the floating back-to-top button, which sits over content and has
   to move out of a thumb's way, and the wordmark, which is the one thing on the
   page allowed to give up size so the header still fits on one line. */
@media (max-width: 479px) {
  .to-top { bottom: var(--space-3); right: var(--space-3); }
  .hdr-logo a { font-size: var(--fs-lead); padding: 0 var(--space-3); }
  /* The auth pair, reduced to what a 296px content box actually holds. The
     cluster above already wraps rather than overflowing, so this is not what
     stops the sideways scroll - it is what buys the wrap back: measured at
     320px the two toggles are 170px and the pair was 127, nine pixels over the
     line, so the header spent a whole fourth row on two words. At 115 the
     tools are one row again (293 of 296) and the header is three rows instead
     of four. Type, padding and the gap between them only - the target stays
     --tap in both axes (rule 6), because these are the two buttons a phone
     reader aims at first. */
  .hdr-user { gap: var(--space-1); }
  .hdr-user .btn { font-size: var(--fs-meta); padding: 0 var(--space-2); }
}


/* =============================================================== MOTION === */

/* The preference is answered ONCE, by re-declaring the two motion tokens every
   animated rule in this file is written against, plus a belt-and-braces sweep
   for anything a component ships with a literal duration. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --lift: none;
    --t-fast: 0s;
  }
  *,
  *::before,
  *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: .01ms !important;
  }
}
