/*
 * Layout and component styles.
 *
 * Consumes the §2 semantic tokens from tokens.css and holds no literal colour, size or spacing
 * values of its own, so a theme swap never touches this file either. Logical properties only --
 * no physical left/right anywhere, which is what makes RTL correct by construction rather than by
 * a pile of overrides (ADR-015).
 *
 * The handful of literal values that do appear are structural rather than stylistic: hairline
 * dividers (1px), full-round pills (50%), and the opacity/blur of the sticky header's backdrop.
 */

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

html {
  direction: var(--direction);
  /* 112.5% of the browser's own base = 18px by default, and still larger if the reader has raised
     it. See the type-scale note in tokens.css. */
  font-size: 112.5%;
  background: var(--color-canvas);
  color: var(--color-text);
  font-family: var(--font-body);
  scroll-behavior: smooth;
}

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

body {
  margin: 0;
  min-block-size: 100vh;
  line-height: var(--leading-ui);
  font-size: var(--text-base);
  background: var(--color-canvas);
}

::selection { background: var(--color-accent-strong); color: var(--color-text); }

/* ---------- focus ----------
 *
 * One rule, every interactive element, no exceptions. `outline: none` never appears in this file
 * without an equally visible replacement, because for a keyboard visitor the ring is the only
 * indication of where they are. */

/* No border-radius here. Setting one would reshape the element itself on focus -- a pill chip would
   snap to square corners the moment it was tabbed to. Browsers already curve the outline to follow
   whatever radius the element has. */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
}

/* A skip link is the one thing that must be reachable before anything else on the page. */
.skip-link {
  position: absolute;
  inset-block-start: var(--space-2);
  inset-inline-start: var(--space-2);
  z-index: 100;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-200%);
}

.skip-link:focus { transform: translateY(0); }

/* ---------- skeleton ---------- */

.site-header {
  position: sticky;
  inset-block-start: 0;
  z-index: 50;
  background: color-mix(in oklab, var(--color-canvas) 92%, transparent);
  backdrop-filter: blur(8px);
  border-block-end: 1px solid var(--color-border-subtle);
}

.site-header__inner,
.site-main,
.site-footer__inner {
  max-inline-size: var(--measure-wide);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

.site-header__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
  padding-block: var(--space-4);
}

.site-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: var(--leading-tight);
}

.site-title a { color: var(--color-text); text-decoration: none; display: inline-flex; align-items: center; }

.site-title__logo {
  block-size: 1.5rem;
  inline-size: auto;
  display: block;
  transition: transform 0.3s ease, filter 0.3s ease;
}

.site-title a:hover .site-title__logo,
.site-title a:focus-visible .site-title__logo {
  transform: scale(1.08) rotate(-4deg);
  filter: drop-shadow(0 0 10px color-mix(in oklab, var(--color-accent-bright) 55%, transparent));
}

.site-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-inline-start: auto;
  font-size: var(--text-base);
}

.site-nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  color: var(--color-text-secondary);
  text-decoration: none;
  opacity: 0;
  transform: translateY(-6px);
  animation: nav-item-in 0.5s ease forwards;
  transition: color 0.25s ease, background-color 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease;
}

.site-nav a:nth-child(1) { animation-delay: 0.03s; }
.site-nav a:nth-child(2) { animation-delay: 0.08s; }
.site-nav a:nth-child(3) { animation-delay: 0.13s; }
.site-nav a:nth-child(4) { animation-delay: 0.18s; }

@keyframes nav-item-in {
  to { opacity: 1; transform: translateY(0); }
}

.site-nav a:hover,
.site-nav a:focus-visible {
  color: var(--color-text);
  background: var(--color-surface-raised);
  transform: translateY(-2px);
  box-shadow: 0 8px 18px -10px color-mix(in oklab, var(--color-accent) 70%, transparent);
}

.site-nav__icon {
  inline-size: 1.15em;
  block-size: 1.15em;
  flex: none;
  transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.25s ease;
}

.site-nav a:hover .site-nav__icon,
.site-nav a:focus-visible .site-nav__icon {
  transform: scale(1.15) rotate(-6deg);
}

/* Mint text plus a tinted pill -- colour alone would not carry for a colour-blind reader, so
   shape and motion (the icon's pulse) carry the "you are here" signal too. */
.site-nav a[aria-current="page"] {
  color: var(--color-accent-bright);
  font-weight: 600;
  background: var(--color-accent-soft);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--color-accent-bright) 35%, transparent);
}

.site-nav a[aria-current="page"] .site-nav__icon {
  animation: nav-icon-pulse 2.4s ease-in-out infinite;
}

@keyframes nav-icon-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.12); }
}

.site-main { padding-block: var(--space-7); min-block-size: 60vh; }

/* ---------- app shell ----------
 *
 * One page is not a document: the conversation (`/chat`) is an application surface where the input
 * belongs at the bottom of the viewport and only the transcript between header and input scrolls.
 * A page opts in by setting ViewData["BodyClass"], so the shell is a documented layout mode rather
 * than a stylesheet special case, and every other page is untouched.
 *
 * The skeleton overrides live here; everything inside the shell lives in the page's own stylesheet.
 */
.shell-app {
  /* dvh, not vh: on mobile browsers vh is the address-bar-less height, which would push the input
     below the fold. --shell-block-size is set from visualViewport where available, so the shell
     also shrinks to sit above an open on-screen keyboard. */
  block-size: var(--shell-block-size, 100dvh);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.shell-app .site-main {
  flex: 1 1 auto;
  /* Without this a flex item refuses to shrink below its content, and the transcript would push the
     input off-screen instead of scrolling. */
  min-block-size: 0;
  display: flex;
  padding-block: 0;
  padding-inline: 0;
  max-inline-size: none;
}

/* The footer is a document artefact. In an app shell it would either steal height from the
   transcript or sit unreachable below a non-scrolling body. */
.shell-app .site-footer { display: none; }

.site-footer {
  border-block-start: 1px solid var(--color-border-subtle);
  margin-block-start: var(--space-9);
  color: var(--color-text-faint);
  font-size: var(--text-sm);
  text-align: center;
}

.site-footer__inner { padding-block: var(--space-5); }

a { color: var(--color-accent); }
a:hover { color: var(--color-accent-hover); }

/* ---------- buttons ---------- */

.button {
  display: inline-block;
  font: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  line-height: var(--leading-tight);
  padding: var(--space-3) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  text-decoration: none;
  cursor: pointer;
  transition: background 0.2s ease;
}

.button:hover { background: var(--color-accent-hover); color: var(--color-text-on-accent); }

.button--quiet {
  background: var(--color-surface);
  border-color: var(--color-border);
  color: var(--color-text-secondary);
}

.button--quiet:hover {
  background: var(--color-surface-raised);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* ---------- headings ---------- */

.page-heading {
  font-size: var(--text-2xl);
  line-height: var(--leading-heading);
  font-weight: 700;
  margin-block: 0 var(--space-3);
}

.page-intro {
  color: var(--color-text-secondary);
  max-inline-size: 70ch;
  margin-block: 0 var(--space-6);
}

.section-heading {
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  font-weight: 600;
  margin-block: 0 var(--space-4);
}

/* ---------- story page ---------- */

.story { max-inline-size: var(--measure-reading); margin-inline: auto; }

.story__number {
  color: var(--color-text-faint);
  font-size: var(--text-sm);
  margin-block: 0 var(--space-3);
}

.story__title {
  font-size: var(--text-display);
  line-height: var(--leading-heading);
  font-weight: 700;
  margin-block: 0 var(--space-4);
}

.story__summary {
  color: var(--color-text-secondary);
  font-size: var(--text-base);
  line-height: var(--leading-ui);
  margin-block: 0 var(--space-6);
}

.story__chips { margin-block-end: var(--space-7); }

.story__body {
  font-size: var(--text-prose);
  line-height: var(--leading-prose);
  color: var(--color-text-secondary);
}

.story__body > p { margin-block: 0 var(--space-6); text-align: justify; }

/*
 * Several stories enumerate their points as a Markdown list. The browser's default `decimal` marker
 * puts Western digits at the head of a Persian line, which is both visually wrong next to the
 * Persian numerals used everywhere else on the page and bidirectionally fragile. `persian` is a
 * predefined CSS counter style and needs no @counter-style rule of its own.
 */
.story__body ol { list-style: persian; }

.story__body ol,
.story__body ul { padding-inline-start: var(--space-5); margin-block: 0 var(--space-6); }

.story__body li { margin-block-end: var(--space-3); }

.story__body li::marker { color: var(--color-text-muted); }

.story__body strong,
.story__body em { color: var(--color-text); }

.story-divider {
  border: 0;
  border-block-start: 1px solid var(--color-border-subtle);
  margin-block: var(--space-7);
}

/* ---------- verse (بیت / مصراع) ---------- */

/*
 * A couplet is one typographic unit. The two hemistichs sit side by side on wide screens and stack
 * on narrow ones, but they never separate across a break -- splitting a بیت changes how it reads.
 * Four verses in the corpus carry a single hemistich, so this must also lay out correctly with one
 * child rather than two.
 */
/*
 * Poetry breaks out of the reading column. The widest couplet in the corpus needs 843px to sit on
 * one line, and the prose measure is 608px -- inside it, a majority of couplets would fold. Widening
 * the block is what lets the form survive, and setting verse apart from prose is the conventional
 * treatment anyway. The negative inline margin goes to zero once the viewport is narrower than the
 * target, so it never causes overflow.
 */
.verse {
  --verse-width: min(46rem, calc(100vw - 2 * var(--space-5)));

  inline-size: var(--verse-width);
  margin-inline: calc((100% - var(--verse-width)) / 2);
  background: var(--color-verse-bg);
  border-radius: var(--radius-lg);
  padding: var(--space-6) var(--space-5);
  margin-block: var(--space-6);
  text-align: center;
}

/*
 * Each hemistich is a column that wraps within itself; the pair never wraps apart. That distinction
 * is the whole layout: with the pair allowed to wrap, a long couplet put its second مصراع on its own
 * line and left the divider hanging off the end of the first, which read as a rule pointing at
 * nothing. Wrapping inside a fixed column instead keeps the divider between the two, always.
 */
.bayt {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: stretch;
  column-gap: var(--space-6);
  margin: 0 0 var(--space-4);
  font-size: var(--text-lg);
  line-height: var(--leading-prose);
  color: var(--color-text);
  break-inside: avoid;
}

.bayt:last-child { margin-block-end: 0; }

/* min-inline-size:0 is what permits the shrink -- a flex item will not go below its content width
   without it, and the column would push past the block instead of wrapping. */
.hemistich { flex: 0 1 auto; min-inline-size: 0; text-align: center; }

/* A hairline between the two hemistichs, drawn only when there are two -- the four single-hemistich
   verses in the corpus must not grow a stray rule. */
.hemistich + .hemistich { position: relative; }

.hemistich + .hemistich::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--space-6) / -2);
  inline-size: 1px;
  background: var(--color-border-strong);
}

/* Below this width two columns are too narrow to read as verse, so the hemistichs stack -- and the
   divider goes, because it no longer divides anything. */
@media (max-width: 34rem) {
  .bayt { flex-wrap: wrap; row-gap: var(--space-2); }
  .hemistich { flex-basis: 100%; }
  .hemistich + .hemistich::before { display: none; }
}

.verse__attribution {
  display: block;
  text-align: center;
  color: var(--color-accent-bright);
  font-size: var(--text-sm);
  margin-block-start: var(--space-3);
}

/* ---------- Arabic quotation ---------- */

/*
 * Quran and hadith text, always verbatim. A naskh face at a larger size with generous leading,
 * because harakat need vertical room above and below the baseline without colliding with the
 * adjacent line.
 *
 * Turquoise rather than green, and that is the whole point: the reader learns that a turquoise
 * edge means "this is quoted scripture, not the author's voice".
 *
 * The accent bar is border-inline-start, and the corners next to it stay square via logical radius
 * properties -- both flip correctly for an LTR variant, which a hardcoded border-left would not.
 */
.quotation {
  font-family: var(--font-quote);
  font-size: var(--text-quote);
  line-height: var(--leading-quote);
  color: var(--color-text);
  background: var(--color-quote-bg);
  border-inline-start: 3px solid var(--color-citation-bar);
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  border-start-end-radius: var(--radius-lg);
  border-end-end-radius: var(--radius-lg);
  margin-block: var(--space-6);
  margin-inline: 0;
  padding: var(--space-5) var(--space-5);
}

.quotation p { margin: 0; }

.quotation__translation {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-ui);
  color: var(--color-text-secondary);
  margin-block-start: var(--space-4);
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--color-border-subtle);
}

/* ---------- citations ---------- */

/*
 * Turquoise, not mint. Mint is marginally the higher contrast of the two (12.5:1 against the canvas
 * versus 10.0:1) but it is also the colour of the active nav item, the focus ring and the poet
 * credit -- a marker in it reads as "part of the site's chrome" rather than as a reference. Turquoise
 * already means "sourced material" everywhere else: the quotation bar, the chat citation chips, the
 * back-to-text links. Markers now join that family, which is both easier to pick out mid-sentence
 * and one fewer thing the colour system has to mean.
 */
.citation-ref {
  font-size: var(--text-xs);
  font-weight: 700;
  vertical-align: super;
  line-height: 0;
  margin-inline-start: 2px;
}

.citation-ref a {
  color: var(--color-citation);
  text-decoration: none;
  padding-inline: 3px;
  border-radius: var(--radius-sm);
  transition: background 0.15s ease, color 0.15s ease;
}

.citation-ref a:hover,
.citation-ref a[data-tip-open] {
  background: var(--color-citation-chip-bg);
  color: var(--color-citation);
}

.sources {
  margin-block-start: var(--space-7);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-border-subtle);
  font-size: var(--text-base);
  color: var(--color-text-secondary);
}

.sources__heading {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-block: 0 var(--space-4);
}

.sources__list {
  padding: 0;
  margin: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.sources__item {
  line-height: var(--leading-ui);
  padding-inline-start: var(--space-6);
  position: relative;
}

/*
 * The marker is printed by the page, not generated by a CSS counter: it has to be the story's own
 * footnote number so the ⟦N⟧ superscript in the prose lands on the right entry, and a counter would
 * renumber from 1 wherever the manuscript's markers are not contiguous.
 */
/* Same turquoise as the marker in the prose, so a reader following one down the page lands on a
   numeral that looks like the one they left. */
.sources__marker {
  position: absolute;
  inset-inline-start: 0;
  color: var(--color-citation);
  font-weight: 700;
}

.sources__back {
  color: var(--color-citation);
  margin-inline-start: var(--space-2);
  font-size: var(--text-sm);
}

/* Highlights the target citation when a reader follows a superscript down to it. */
.sources__item:target {
  background: var(--color-surface-raised);
  border-radius: var(--radius-sm);
  padding-block: var(--space-1);
  padding-inline-end: var(--space-2);
}

/* ---------- footnote preview ----------
 *
 * Hovering or focusing a marker shows the reference where the reader already is, instead of sending
 * them to the foot of the page and back. Built by footnotes.js from the sources list already in the
 * document -- it fetches nothing, and it adds nothing a reader without JavaScript loses, because the
 * marker underneath is still a plain anchor to the same footnote.
 */
.footnote-tip {
  position: fixed;
  z-index: 60;
  max-inline-size: min(24rem, calc(100vw - 2 * var(--space-4)));
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-citation-bar);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-ui);
  text-align: start;
  box-shadow: 0 18px 44px -14px rgb(0 0 0 / 0.75);
  pointer-events: none;
  opacity: 0;
  /* Scaled from the edge nearest the marker, so it reads as unfolding out of the numeral rather
     than as a box fading in over the text. transform-origin is set by the script, which is the only
     thing that knows whether the tip ended up above or below. */
  transform: translateY(var(--tip-rise, 4px)) scale(0.96);
  transition: opacity 0.16s ease, transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.footnote-tip[data-open] { opacity: 1; transform: translateY(0) scale(1); }

/* The arrow is a rotated square sharing the tip's border, so only two of its edges show. */
.footnote-tip::after {
  content: "";
  position: absolute;
  inline-size: 10px;
  block-size: 10px;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-citation-bar);
  transform: rotate(45deg);
  /* Physical left, not inset-inline-start: the script computes this as a pixel offset from the
     tip's own left edge in viewport coordinates, which have no reading direction. */
  left: var(--tip-arrow, 50%);
  margin-left: -5px;
}

.footnote-tip[data-placement="above"]::after {
  inset-block-end: -6px;
  border-block-start: 0;
  border-inline-end: 0;
}

.footnote-tip[data-placement="below"]::after {
  inset-block-start: -6px;
  border-block-end: 0;
  border-inline-start: 0;
}

.footnote-tip__number {
  color: var(--color-citation);
  font-weight: 700;
  margin-inline-end: var(--space-2);
}

@media (prefers-reduced-motion: reduce) {
  .footnote-tip { transition: opacity 0.16s ease; transform: none; }
  .footnote-tip[data-open] { transform: none; }
}

/*
 * ~20 stories have an unresolved footnote in the source manuscript. An amber pill states that
 * plainly rather than leaving a blank line: the gap is real, and styling it with intent is more
 * honest than hiding it.
 */
.source-missing {
  display: inline-block;
  background: var(--color-warning);
  color: var(--color-warning-text);
  font-size: var(--text-xs);
  font-weight: 600;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  line-height: var(--leading-tight);
}

/* ---------- reserved media slot ---------- */

/*
 * Rendered rather than hidden once media exists, and dashed rather than solid so an empty player
 * reads as "reserved", not "broken". See _MediaBlock.cshtml -- nothing renders at MVP.
 */
.story__media {
  margin-block-start: var(--space-7);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-border-subtle);
}

.story__media figure {
  margin: 0 0 var(--space-4);
  background: var(--color-surface-sunken);
  border: 1.5px dashed var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}

.story__media audio,
.story__media video { inline-size: 100%; }

.story__media figcaption { font-size: var(--text-sm); margin-block-start: var(--space-2); }

/* ---------- chips ---------- */

.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: 0; margin: 0; list-style: none; }

/*
 * One chip shape for filters, story-card tags and browse clouds, so it always means the same thing.
 * Rest is outlined; hover strengthens the border and brightens the text; selected fills. Nothing
 * moves on hover -- these appear in long lists, and a transform would ripple the whole grid.
 */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
  text-decoration: none;
  transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
}

a.chip:hover {
  background: var(--color-surface-raised);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.chip[aria-pressed="true"],
.chip--selected {
  background: var(--color-accent-strong);
  border-color: var(--color-accent);
  color: var(--color-text);
  font-weight: 600;
}

/* The soft variant marks a story's own attributes -- read-only labels, not controls. */
.chip--soft {
  background: var(--color-accent-soft);
  border-color: transparent;
  color: var(--color-accent-bright);
}

/* Turquoise, because it points at sourced material -- same language as the quotation block. */
.chip--citation {
  background: var(--color-citation-chip-bg);
  border-color: transparent;
  color: var(--color-citation);
}

a.chip--citation:hover { background: var(--color-citation-chip-bg); color: var(--color-citation); border-color: var(--color-citation-bar); }

.chip__count { color: var(--color-text-faint); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }

.chip[aria-pressed="true"] .chip__count { color: var(--color-text-secondary); }

/* ---------- listings ---------- */

.story-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
}

/* Search results read as a single column: they are ranked, and a grid would imply they are not. */
.story-list--ranked { grid-template-columns: 1fr; }

.story-card {
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  padding: var(--space-5);
  transition: border-color 0.2s ease, background 0.2s ease;
}

/* Hover lifts the surface and tints the title; it does not move the card. On a read-heavy list,
   movement reads as jank and costs more than it adds. */
.story-card:hover {
  background: var(--color-surface-raised);
  border-color: var(--color-accent-strong);
}

.story-card__number {
  font-size: var(--text-xs);
  color: var(--color-text-faint);
  margin-block: 0 var(--space-1);
}

.story-card__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: var(--leading-tight);
}

.story-card__title a { color: var(--color-text); text-decoration: none; }

.story-card:hover .story-card__title a { color: var(--color-accent-bright); }

/* Where a result opens in the reading modal, the whole card is the target rather than the title
   alone, and it has to say so under the pointer. The attribute is set by story-modal.js on the
   results it has taken charge of: nothing is added to the markup, the title stays the one real
   link, and a card without the script is exactly what it was. */
[data-open-cards] .story-card { cursor: pointer; }

.story-card__summary {
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-ui);
}

.story-card__meta {
  margin-block: var(--space-3) 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ---------- search & facets ---------- */

/* 17rem rather than the kit's 240px: the tree indents three levels deep under نوع منبع, and at
   240px a poet's name wrapped to three lines. */
.search-layout {
  display: grid;
  grid-template-columns: 17rem minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 52rem) {
  .search-layout { grid-template-columns: 1fr; }
}

/*
 * Two rails, each scrolling in its own right.
 *
 * The tree is a thousand rows and the results are unbounded, and sharing the page's one scrollbar
 * they fight: scrolling to the bottom of برچسب carries the results off the top of the screen, and
 * reading down to the twentieth story leaves the filters somewhere far above. So the search page
 * stops being a document and becomes a surface -- `shell-search`, the same opt-in the conversation
 * makes -- with the heading, the box and the narrowed-facet row fixed at the top and the two rails
 * filling what is left. Each is then scrolled without disturbing the other, and the filters and the
 * results they produced are always on screen together. `overscroll-behavior: contain` is what makes
 * them independent rather than merely tall: without it, reaching the end of one hands the wheel to
 * whatever is behind it.
 *
 * Only where there are two columns to hold side by side. Stacked on a phone this would be two small
 * windows onto two long lists, which is worse than the page scroll it replaced -- so below the
 * breakpoint the body class does nothing at all and the page is an ordinary document again.
 */
@media (min-width: 52.0625rem) {
  .shell-search {
    block-size: 100dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  .shell-search .site-main {
    flex: 1 1 auto;
    /* Without this a flex item refuses to shrink below its content, and the rails would push
       themselves off the bottom of the viewport instead of scrolling inside it. */
    min-block-size: 0;
    display: flex;
    padding-block: 0;
    padding-inline: 0;
    max-inline-size: none;
    /* The measure and gutters move inside, to .search-shell. This has to go with them: an auto
       inline margin stops a flex item stretching across its container, so the shell would
       shrink-to-fit its content and a search with no results would draw itself narrow. */
    margin-inline: 0;
  }

  /* A document artefact, and there is no page scroll left to reach it by. */
  .shell-search .site-footer { display: none; }

  /* The shell takes over the measure and gutters that .site-main gives up above. */
  .search-shell {
    flex: 1 1 auto;
    min-block-size: 0;
    inline-size: 100%;
    max-inline-size: var(--measure-wide);
    margin-inline: auto;
    padding-inline: var(--space-5);
    padding-block: var(--space-6);
    display: flex;
    flex-direction: column;
  }

  .search-shell__head { flex: 0 0 auto; }

  .search-layout { flex: 1 1 auto; min-block-size: 0; }

  /* Percentages against the grid row, which the shell has given a definite height. The panel keeps
     `align-items: start` so a tree with every branch closed stays a short card rather than a tall
     empty one; it only scrolls once it is taller than the rail. */
  .facet-panel,
  .search-scroll {
    max-block-size: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* The results rail has no border of its own, so its cards need to clear its scrollbar rather than
     sit under it. Inline-end is the left in Farsi, which is the side the scrollbar is on. */
  .search-scroll { padding-inline-end: var(--space-3); }
}

.search-form {
  display: flex;
  gap: var(--space-3);
  margin-block-end: var(--space-4);
}

.search-form__input {
  flex: 1 1 auto;
  min-inline-size: 0;
  font: inherit;
  font-size: var(--text-base);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

.search-form__input::placeholder { color: var(--color-text-faint); }

/* Mint border plus a soft ring: the input is the one control on the page that must be obviously
   focused, including for a visitor who arrived by clicking rather than tabbing. */
.search-form__input:focus {
  outline: none;
  border-color: var(--color-accent-hover);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-focus) 30%, transparent);
}

.search-form__submit {
  flex: 0 0 auto;
  font: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  padding: var(--space-3) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  cursor: pointer;
  transition: background 0.2s ease;
}

.search-form__submit:hover:not(:disabled) { background: var(--color-accent-hover); }

.search-form__submit:disabled { opacity: 0.6; cursor: default; }

.facet-panel {
  background: var(--color-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Carries the rule the hint paragraph used to draw, so the panel keeps its head/tree division after
   the words came out. */
.facet-panel__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding-block-end: var(--space-4);
  border-block-end: 1px solid var(--color-border-subtle);
}

.facet-panel__title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  transition: color 0.15s ease;
}

/*
 * The whole "you have filters on" indicator: one word, brightened and bolded, directly above the
 * tree it refers to and beside the «انتخاب همه» that undoes it. It replaced a row of chips over the
 * results that named each narrowed branch and its counts -- more words than the state was worth, and
 * far enough from the tree to read as a fact about the book rather than as something the visitor had
 * done. This says only that something is narrowed; the tree says what.
 *
 * Weight as well as colour, so it does not depend on colour being seen, and the heading also carries
 * the state in words for a screen reader.
 */
.facet-panel__title--on {
  color: var(--color-accent-bright);
  font-weight: 700;
}

.facet-panel__bulk { display: flex; gap: var(--space-2); align-items: center; }

/* Two shortcuts to either end of "everything is ticked": one restores it, the other clears it. */
.facet-bulk {
  font-size: var(--text-xs);
  color: var(--color-citation);
  text-decoration: none;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}

.facet-bulk:hover { border-color: var(--color-border-strong); color: var(--color-citation); }

/* The state the page is already in is shown as reached, not as an action. */
.facet-bulk--current { color: var(--color-text-faint); border-color: transparent; pointer-events: none; }


/* ---------- facet tree ---------- */

/*
 * The panel is a tree because the corpus is: نوع منبع owns the surahs, the cited works, the named
 * narrators and the poets, and a poet only exists underneath a poem. Built from <details>, so every
 * branch opens, closes and is keyboard-reachable with no script.
 *
 * Everything opens ticked. A tick means "in the results"; unticking takes it away. That is why the
 * rows are drawn from the whole vocabulary of the book rather than from the current result counts --
 * a tree that shrank as you filtered would leave nothing to untick, and nothing to put back.
 */
.facet-tree { --tree-indent: 20px; --tree-row: 15px; }

.facet-tree__list { list-style: none; margin: 0; padding: 0; }

/*
 * Guide lines. Each row draws the vertical run of its own branch plus the elbow into its checkbox,
 * so the shape reads as a continuous spine down a branch and closes into a corner at its end.
 * Logical properties throughout: the indent and the elbow are on the inline-start side, which is
 * the right in Farsi and would be the left in an LTR locale without a second rule.
 */
.facet-tree__item {
  position: relative;
  padding-inline-start: var(--tree-indent);
}

.facet-tree__item::before,
.facet-tree__item::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  border-color: var(--color-border);
  border-style: solid;
  border-width: 0;
}

/* The vertical run, from the top of the row to the bottom of everything nested under it. */
.facet-tree__item::before {
  inset-block: 0;
  border-inline-start-width: 1px;
}

/* The last row of a branch stops its run at the elbow, which is what closes the spine into a corner. */
.facet-tree__item--last::before { inset-block-end: auto; block-size: var(--tree-row); }

/* The elbow, run all the way up to the checkbox so the branch reads as one connected line rather
   than as a stub and a gap. Where a row has a twisty it sits on top of this, punching a hole in the
   line with its own background -- which is what makes the disclosure read as part of the tree. */
.facet-tree__item::after {
  inset-block-start: var(--tree-row);
  inline-size: calc(var(--tree-indent) + 14px);
  border-block-start-width: 1px;
}

/* The five headings are roots, not branches -- nothing is above them to draw a line from. */
.facet-tree__list--root > .facet-tree__item { padding-inline-start: 0; }

.facet-tree__list--root > .facet-tree__item::before,
.facet-tree__list--root > .facet-tree__item::after { content: none; }

.facet-tree__list--root > .facet-tree__item + .facet-tree__item { margin-block-start: var(--space-3); }

/* ---------- the twisty ---------- */

/*
 * <details> wraps only a row's children, so collapsing a branch hides what is under it rather than
 * the row itself. Its <summary> is the twisty alone, lifted out of the flow and back onto the row
 * above -- which is why this is absolute rather than a flex child of the row.
 */
.facet-tree__twisty {
  position: absolute;
  inset-inline-start: var(--tree-indent);
  inset-block-start: 7px;
  inline-size: 16px;
  block-size: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  list-style: none;
  color: var(--color-text-faint);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}

.facet-tree__list--root > .facet-tree__item > .facet-tree__sub > .facet-tree__twisty {
  inset-inline-start: 0;
}

.facet-tree__twisty::-webkit-details-marker { display: none; }

.facet-tree__twisty:hover { color: var(--color-text); background: var(--color-surface-raised); }

/*
 * A chevron drawn from one rotated square -- no icon font, no SVG. Its two borders are logical, so
 * closed points along the reading direction (leftwards in Farsi, rightwards if an LTR locale is ever
 * added) without a second rule; open points down in both.
 */
.facet-tree__twisty::after {
  content: "";
  inline-size: 0.36em;
  block-size: 0.36em;
  border-inline-end: 2px solid currentColor;
  border-block-end: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}

.facet-tree__sub[open] > .facet-tree__twisty::after { transform: rotate(-45deg); }

/* ---------- the long tail ---------- */

/*
 * برچسب's 627 rows cannot all be on screen, but the ones past the first fifteen are still part of
 * the same branch: they are rendered into the same list, at the same indent, in the same order, and
 * merely hidden. Unfolding therefore continues the branch downwards -- a row never changes its depth
 * or its neighbours by being revealed, which is what made the old nested <details> disorienting.
 *
 * A checkbox rather than a <details> for two reasons: it costs no request, and once it is checked
 * the "N مورد دیگر" row can be taken away entirely. A <summary> has to stay behind after it is
 * opened, leaving a dead control in the middle of the list it just expanded.
 */
.facet-more { position: relative; }

.facet-more__toggle:not(:checked) ~ .facet-tree__list > .facet-tree__item--folded { display: none; }

.facet-more__toggle:checked ~ .facet-tree__list > .facet-more__row { display: none; }

.facet-more__button {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  padding-inline-start: 18px;
  cursor: pointer;
  color: var(--color-text-faint);
  font-size: var(--text-xs);
  border-radius: var(--radius-sm);
}

.facet-more__button:hover { background: var(--color-surface-raised); color: var(--color-text); }

/* The checkbox is visually hidden but still tabbed to, so its focus ring has to be drawn on the
   label that stands in for it. */
.facet-more__toggle:focus-visible ~ .facet-tree__list .facet-more__button {
  outline: 2px solid var(--color-focus);
  outline-offset: -2px;
  color: var(--color-text);
}

/* ---------- facet checkbox ---------- */

/*
 * An anchor, not an <input>, because the href is the search it produces -- with scripting off,
 * clicking it simply loads that search. Every row leaves an 18px gutter for a twisty whether or not
 * it has one, so leaves and branches line up down the tree.
 */
.facet-check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  padding-inline-start: 18px;
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
  text-decoration: none;
  transition: background 0.15s ease, color 0.15s ease;
}

.facet-check:hover { background: var(--color-surface-raised); color: var(--color-text); }

.facet-tree__list--root > .facet-tree__item > .facet-check {
  font-weight: 600;
  color: var(--color-text-muted);
}

/* 17px, matching the design kit: these are the smallest targets on the page and the audience is not
   a technical one. */
.facet-check__box {
  flex: 0 0 auto;
  inline-size: 17px;
  block-size: 17px;
  border: 1.5px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  line-height: 1;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.facet-check:hover .facet-check__box { border-color: var(--color-accent); }

.facet-check__label { flex: 1 1 auto; overflow-wrap: anywhere; }

.facet-check__count {
  flex: 0 0 auto;
  color: var(--color-text-faint);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

/* Ticked: the mint fill used for every other selected state on the site. It is also the state the
   page opens in, so it has to be quiet -- nine hundred filled boxes should read as "all of it",
   not as nine hundred alerts. */
.facet-check--on .facet-check__box {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
}

.facet-check--on .facet-check__box::after { content: "✓"; }

/*
 * Unticked: the row recedes. An empty box and dimmed text, and nothing more -- no rule through the
 * label. A strike-through reads as "deleted", which is the wrong idea for a row whose whole purpose
 * is to be clicked again, and across hundreds of dimmed rows it turned the panel into a page of
 * crossed-out words. The empty box carries the state for a colour-blind reading on its own.
 */
.facet-check--off { color: var(--color-text-faint); }

.facet-check--off .facet-check__box {
  background: transparent;
  border-color: var(--color-border);
}

.facet-check--off .facet-check__count { opacity: 0.6; }

/* Part of the branch is ticked. A dash, the convention every file tree and mail client uses. */
.facet-check--partial .facet-check__box {
  background: color-mix(in oklab, var(--color-accent) 30%, transparent);
  border-color: var(--color-accent);
  color: var(--color-accent-bright);
}

.facet-check--partial .facet-check__box::after {
  content: "";
  inline-size: 9px;
  block-size: 2px;
  border-radius: 1px;
  background: currentColor;
}

/* ---------- while a filter is on its way ---------- */

/* The list dims rather than disappears: the previous results stay readable and nothing below them
   moves. Also stops a second click landing on a card that is about to be replaced. */
#search-results[aria-busy="true"] {
  opacity: 0.55;
  transition: opacity 0.15s ease 0.1s;
  pointer-events: none;
}

/*
 * A bar at the top of the viewport, the one place that is on screen however far the tree has been
 * scrolled. Both this and the pill below fade in on a delay: a filter served from the output cache
 * comes back in a few dozen milliseconds, and a loading state that flashes for one frame reads as a
 * glitch rather than as progress.
 */
.search-progress {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 3px;
  z-index: 60;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.search-progress[data-loading] { opacity: 1; transition-delay: 0.12s; }

.search-progress__bar {
  display: block;
  block-size: 100%;
  inline-size: 45%;
  background: linear-gradient(90deg, transparent, var(--color-accent), transparent);
  animation: search-progress 1.1s ease-in-out infinite;
}

@keyframes search-progress {
  from { transform: translateX(-110%); }
  to { transform: translateX(330%); }
}

/* ---------- the next twenty ---------- */

/*
 * The results rail loads the next page as the reader reaches the end of the one they are on, so
 * story twenty-one arrives without a click. The pager underneath is not deleted -- it is what the
 * page does without scripting, and what the reader is handed back if a batch ever fails to arrive --
 * it is only hidden while the script is doing the fetching.
 */
.search-layout[data-infinite] .pagination { display: none; }

/* One pixel of nothing at the end of the rail. When it comes within reach of the bottom edge, the
   next twenty are on their way -- so the trigger is "the reader is nearly at the end", measured
   against whichever box is actually scrolling. */
.search-sentinel { block-size: 1px; }

.search-more {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding-block: var(--space-5);
  font-size: var(--text-sm);
  color: var(--color-text-faint);
}

.search-more[hidden] { display: none; }

.search-more__spinner {
  inline-size: 14px;
  block-size: 14px;
  border: 2px solid color-mix(in oklab, var(--color-accent) 30%, transparent);
  border-block-start-color: var(--color-accent);
  border-radius: 50%;
  animation: search-spin 0.7s linear infinite;
}

.search-more__loading { display: flex; align-items: center; gap: var(--space-2); }

/* Two messages, one element: fetching, and nothing left to fetch. */
.search-more:not([data-state="loading"]) .search-more__loading,
.search-more:not([data-state="done"]) .search-more__done { display: none; }

.search-more__done { font-size: var(--text-xs); }

/* Says in words what the bar says in motion, for a request slow enough that the bar alone starts to
   look like decoration. The script only unhides it after a beat. */
.search-busy {
  position: fixed;
  inset-block-start: var(--space-4);
  inset-inline: 0;
  z-index: 60;
  margin-inline: auto;
  inline-size: fit-content;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-pill);
  background: var(--color-surface-raised);
  box-shadow: 0 6px 20px rgb(0 0 0 / 35%);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  pointer-events: none;
}

.search-busy[hidden] { display: none; }

.search-busy__spinner {
  inline-size: 12px;
  block-size: 12px;
  border: 2px solid color-mix(in oklab, var(--color-accent) 30%, transparent);
  border-block-start-color: var(--color-accent);
  border-radius: 50%;
  animation: search-spin 0.7s linear infinite;
}

@keyframes search-spin {
  to { transform: rotate(360deg); }
}

/* Motion is the whole point of both, so with it turned off they have to say so some other way: a
   steady bar and a steady dot, present rather than moving. */
@media (prefers-reduced-motion: reduce) {
  .search-progress__bar,
  .search-busy__spinner,
  .search-more__spinner { animation: none; }

  .search-progress__bar { inline-size: 100%; }

  .search-busy__spinner,
  .search-more__spinner { border-color: var(--color-accent); }
}

.result-count {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  margin-block: 0 var(--space-4);
}

/* ---------- the reading modal ----------
 *
 * A search result opens over the search rather than instead of it (story-modal.js). What is inside
 * is the reading page's own <article class="story">, so every rule above -- prose, verse, quotation,
 * citation, sources -- applies here unchanged and nothing is restyled for the modal. This section is
 * only the box around it: a fixed bar the story's number and the way out sit in, and one scrolling
 * well beneath it.
 *
 * The dialog is a flex column, so the bar stays put while the prose scrolls under it -- which the
 * bar earns, because a story is long and past the first screen it is the only thing still saying
 * which one this is.
 */
.story-modal {
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  color: var(--color-text);
  /* Wider than the reading measure: the column inside keeps its own 60ch, and the extra width is the
     margin that makes this read as a surface over the page rather than as the page. */
  inline-size: min(52rem, calc(100vw - 2 * var(--space-5)));
  max-inline-size: none;
  block-size: min(46rem, calc(100dvh - 2 * var(--space-6)));
  max-block-size: calc(100dvh - 2 * var(--space-5));
  overflow: hidden;
  box-shadow: 0 40px 120px -30px rgb(0 0 0 / 80%);
  /* The spine. Physical right rather than an inline property, and deliberately: this is where the
     binding of a Persian book is, not where the start of a line happens to be. */
  transform-origin: right center;
}

/* The sheet catching the light as it falls -- a narrow highlight that crosses the page once, in the
   direction it is turning, and is gone. It is what separates "a box appeared" from "a page turned".
   Absolutely positioned so it does not become a flex item of the column below. */
.story-modal::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  background-image: linear-gradient(
    105deg,
    transparent 41%,
    rgb(255 255 255 / 5%) 47%,
    rgb(210 255 235 / 11%) 50%,
    rgb(255 255 255 / 5%) 53%,
    transparent 59%
  );
  background-size: 300% 100%;
  opacity: 0;
}

/* On the attribute, not the element: the UA rule that hides a closed <dialog> is in a weaker origin
   than this file, so an unqualified `display` here would leave the modal on screen when shut. */
.story-modal[open] { display: flex; flex-direction: column; }

.story-modal::backdrop {
  background: rgb(3 5 10 / 72%);
  backdrop-filter: blur(8px);
}

/*
 * A page falling open on its spine.
 *
 * It starts past edge-on, so the first frame is a sliver at the right-hand binding rather than a
 * small copy of the finished box -- the difference between a page turning and a card scaling up.
 * It swings a few degrees beyond flat and settles back, which is what a sheet of paper does when it
 * lands, and the highlight above crosses it on the way down.
 *
 * The same gesture as the home page's summary panel, at the weight this surface needs: slower,
 * because it is ten times the area, and with the overshoot a small card does not earn.
 */
.story-modal[open] { animation: story-modal-open 0.58s cubic-bezier(0.22, 0.72, 0.24, 1); }

.story-modal[open]::before { animation: story-modal-sheen 0.72s ease-out; }

.story-modal[open]::backdrop { animation: story-modal-fade 0.4s ease; }

@keyframes story-modal-open {
  0% { transform: perspective(2600px) rotateY(98deg); opacity: 0.15; }
  40% { opacity: 1; }
  74% { transform: perspective(2600px) rotateY(-5deg); }
  100% { transform: perspective(2600px) rotateY(0deg); opacity: 1; }
}

@keyframes story-modal-sheen {
  0% { opacity: 0; background-position: 100% 0; }
  30% { opacity: 1; }
  100% { opacity: 0; background-position: -10% 0; }
}

@keyframes story-modal-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/*
 * And closing it folds the page back onto the binding.
 *
 * A <dialog> would otherwise vanish between two frames, which after an opening like the one above
 * reads as the story having been snatched away. The script holds the close until this has run
 * (`data-closing` in story-modal.js) -- and skips it entirely where motion is unwelcome, because
 * there the wait would buy nothing.
 */
.story-modal[open][data-closing] {
  animation: story-modal-close 0.26s cubic-bezier(0.55, 0, 0.85, 0.35) forwards;
}

.story-modal[open][data-closing]::before { animation: none; }

.story-modal[open][data-closing]::backdrop {
  animation: story-modal-fade 0.26s ease reverse forwards;
}

@keyframes story-modal-close {
  from { transform: perspective(2600px) rotateY(0deg); opacity: 1; }
  to { transform: perspective(2600px) rotateY(72deg); opacity: 0; }
}

/*
 * The story settling into the page it was opened on, a beat behind the fold. Written against the
 * well's children rather than against the article, because it has to cover the two things that can
 * arrive there -- the story, and the message that it could not be fetched -- and because a CSS
 * animation runs on insertion, which is exactly when each of them appears.
 */
.story-modal__body > * {
  animation: story-modal-settle 0.42s 0.05s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

@keyframes story-modal-settle {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .story-modal[open],
  .story-modal[open]::before,
  .story-modal[open]::backdrop,
  .story-modal__body > * { animation: none; }
}

.story-modal__bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
  border-block-end: 1px solid var(--color-border-subtle);
  background: var(--color-surface-raised);
}

/* The story's own number line, lifted out of the article by the script so it is still on screen at
   the fortieth paragraph. One line, clipped rather than wrapped: the bar must not change height as
   stories with and without a printed page number follow one another. */
.story-modal__eyebrow {
  margin: 0;
  min-inline-size: 0;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.story-modal__actions { display: flex; align-items: center; gap: var(--space-3); flex: 0 0 auto; }

.story-modal__permalink {
  color: var(--color-citation);
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
}

.story-modal__permalink:hover { text-decoration: underline; }

.story-modal__permalink[hidden] { display: none; }

.story-modal__close {
  inline-size: 2rem;
  block-size: 2rem;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background 0.2s, transform 0.2s;
}

.story-modal__close:hover { background: var(--color-surface-sunken); transform: rotate(90deg); }

@media (prefers-reduced-motion: reduce) {
  .story-modal__close:hover { transform: none; }
}

/* The well the story scrolls in. `overscroll-behavior` for the same reason the two search rails have
   it: reaching the end of the story must not start scrolling whatever is behind the dialog. */
.story-modal__body {
  flex: 1 1 auto;
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-6) var(--space-5) var(--space-7);
}

/* Focus is moved here when a story lands, so the keyboard scrolls the prose. That is a placement,
   not an interaction, and a ring around the whole well would say otherwise. */
.story-modal__body:focus { outline: none; }

/* The number line has moved to the bar, so the title is the first thing in the well and does not
   need the space the page leaves above it. */
.story-modal__body .story__title { margin-block-start: 0; }

/* Standing in for :target, which the script cannot use without writing a fragment onto the search
   URL. Same treatment as the story page's, so following a citation looks the same in both. */
.story-modal__body .sources__item[data-cite-target] {
  background: var(--color-surface-raised);
  border-radius: var(--radius-sm);
  padding-block: var(--space-1);
  padding-inline-end: var(--space-2);
}

/* Waiting for a story, and failing to get one. Centred in the well rather than laid out as prose:
   neither is the story, and neither should look like the beginning of one. */
.story-modal__status {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-block-size: 60%;
  text-align: center;
}

.story-modal__status-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: var(--leading-tight);
  color: var(--color-text);
  max-inline-size: var(--measure-reading);
}

.story-modal__status-text {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.story-modal__spinner {
  inline-size: 12px;
  block-size: 12px;
  border: 2px solid color-mix(in oklab, var(--color-accent) 30%, transparent);
  border-block-start-color: var(--color-accent);
  border-radius: 50%;
  animation: search-spin 0.7s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .story-modal__spinner { animation: none; border-color: var(--color-accent); }
}

/* On a phone the modal is the screen: a 32px inset around a full page of prose is width the reading
   column cannot spare. */
@media (max-width: 40rem) {
  .story-modal {
    inline-size: 100vw;
    max-inline-size: 100vw;
    block-size: 100dvh;
    max-block-size: 100dvh;
    border: 0;
    border-radius: 0;
  }

  .story-modal__body { padding-inline: var(--space-4); }
}

/* ---------- browse ---------- */

.browse-group { margin-block-end: var(--space-7); }

.browse-group__heading {
  font-size: var(--text-xl);
  font-weight: 600;
  line-height: var(--leading-tight);
  margin-block: 0 var(--space-4);
}

.browse-group__count { color: var(--color-text-faint); font-size: var(--text-sm); font-weight: 400; }

/* A facet landing page opens with a tinted strip so it reads as a destination, not a filtered list. */
.facet-hero {
  background: var(--color-accent-soft);
  border-radius: var(--radius-lg);
  padding: var(--space-5) var(--space-6);
  margin-block-end: var(--space-6);
}

.facet-hero__title {
  font-size: var(--text-2xl);
  line-height: var(--leading-heading);
  font-weight: 700;
  margin: 0;
  color: var(--color-text);
}

.facet-hero__count { color: var(--color-accent-bright); font-size: var(--text-base); font-weight: 400; }

.facet-hero__blurb { color: var(--color-text-secondary); font-size: var(--text-sm); margin-block: var(--space-2) 0; }

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

/*
 * RTL pitfall: "next" points logically forward, which is visually leftward here, so the arrow
 * glyphs are ← for next and → for previous. An unmirrored LTR icon would point a reader backwards.
 */
.pagination {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: center;
  align-items: center;
  margin-block-start: var(--space-7);
  font-size: var(--text-sm);
}

.pagination a,
.pagination span {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  line-height: var(--leading-tight);
}

.pagination a {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  text-decoration: none;
}

.pagination a:hover {
  background: var(--color-surface-raised);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.pagination__current { color: var(--color-text-muted); }

/* Previous/next story links at the foot of a reading page. */
.story-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-4);
  margin-block-start: var(--space-7);
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--color-border-subtle);
  font-size: var(--text-sm);
}

.story-nav a { color: var(--color-text-secondary); text-decoration: none; }

.story-nav a:hover { color: var(--color-accent-bright); }

/* The conversation is a page-specific surface with a shell layout of its own: see chat.css, loaded
   only by /chat, the same way cylinder.css is loaded only by the home page. */

/* ---------- empty & error states ---------- */

/*
 * Never accusatory -- "no results" reads to a visitor as their own fault -- and always paired with
 * one clear next action. The same pattern serves zero-result search and the 404 page.
 */
.empty-state {
  text-align: center;
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  padding: var(--space-7) var(--space-5);
  margin-block: var(--space-6);
}

.empty-state__mark {
  inline-size: var(--space-7);
  block-size: var(--space-7);
  border-radius: 50%;
  background: var(--color-surface-raised);
  border: 1.5px solid var(--color-border);
  margin: 0 auto var(--space-4);
}

.empty-state__code {
  font-family: var(--font-mono);
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--color-accent-strong);
  margin-block: 0 var(--space-3);
}

.empty-state__title {
  font-size: var(--text-lg);
  font-weight: 600;
  margin-block: 0 var(--space-2);
  color: var(--color-text);
}

.empty-state__text {
  color: var(--color-text-muted);
  font-size: var(--text-base);
  margin-block: 0 var(--space-5);
}

.empty-state__detail {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-faint);
  margin-block-start: var(--space-5);
}

/* ---------- utility ---------- */

.muted { color: var(--color-text-muted); }

.faint { color: var(--color-text-faint); }

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