/* ==========================================================================
   Hush — "Lamplight after midnight"

   Hush is listened to, not read: every story is narrated by the TTS engine,
   usually late, usually alone, usually on headphones. So the interface is a
   dim room with one warm lamp in it — a deep aubergine ground, a muted brass
   that behaves like light rather than paint, and a single pool of glow behind
   the content. Committed dark: there is no light theme, because there is no
   daylight use case.

   Signature element: THE APERTURE — a ring broken at two points, its arc-ends
   swollen into quotation-mark heads. It is the logo, the favicon, the light
   source behind the header, and the progress indicator: while a story is
   being written the two arcs grow out from their heads until the mark closes.
   ========================================================================== */

:root {
  /* --- the six brand colours ------------------------------------------- */
  --ink:        #171219;  /* ground: deep aubergine, warm, never black      */
  --ink-raised: #211A24;  /* a surface catching a little of the lamp        */
  --brass:      #C89B5A;  /* lamplight. Used as light, almost never as fill */
  --linen:      #EDE3D9;  /* warm paper-white, faintly rosy                 */
  --linen-dim:  #9C8E93;  /* mauve-grey, for anything secondary             */
  --verdigris:  #7F9C8C;  /* the calm counterpoint: settled, ready, saved   */

  /* --- functional derivations ------------------------------------------ */
  --brass-bright: #DDB075;
  --clay:         #D08A76;  /* failure. Warm, not an alarm.                 */
  --hairline:     rgba(237, 227, 217, 0.10);
  --hairline-lit: rgba(200, 155, 90, 0.28);
  --tint:         rgba(200, 155, 90, 0.09);

  /* --- type ------------------------------------------------------------- */
  --display: "Fraunces", "Iowan Old Style", Georgia, serif;
  --body: "Karla", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* Fraunces is used at LIGHT weight and wide tracking throughout — the
     opposite of the usual heavy editorial serif. A whisper, not a headline.
     Its SOFT/WONK axes are pinned in the font request in base.html; opsz is
     left to `font-optical-sizing: auto`. */

  /* --- shape ------------------------------------------------------------ */
  --r-sm: 8px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-pill: 999px;

  --measure: 46rem;
  --measure-narrow: 25rem;

  /* --- motion ----------------------------------------------------------- */
  /* One curve for everything. The metaphor here is light, not machinery:
     things arrive and settle, they never snap or bounce. This is the same
     expo-out the Aperture's progress has always used — promoted to a token
     so every transition in the product is visibly the same gesture.

     Three durations, and only three:
       micro   — colour/border on hover and focus
       element — something entering or leaving the page
       progress— the ring closing, deliberately slower than anything else */
  --ease-lamp: cubic-bezier(0.33, 1, 0.68, 1);
  --dur-micro: 0.18s;
  --dur-element: 0.32s;
  --dur-progress: 0.6s;
}

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

/* Several components below set a display value, which would otherwise win over
   the [hidden] attribute. Keep [hidden] authoritative. */
[hidden] { display: none !important; }

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

body {
  margin: 0;
  min-height: 100vh;
  background: var(--ink);
  color: var(--linen);
  font-family: var(--body);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* The lamp. One pool of warm light at the top of the page; everything below
   falls away into the dark. Fixed, so it reads as the room rather than the
   document. */
body::before {
  content: "";
  position: fixed;
  inset: 0 0 auto 0;
  height: 85vh;
  background:
    radial-gradient(58% 52% at 50% -6%, rgba(200, 155, 90, 0.15), transparent 68%),
    radial-gradient(90% 60% at 50% 0%, rgba(126, 96, 130, 0.10), transparent 75%);
  pointer-events: none;
  z-index: 0;
}

/* ----------------------------------------------------------------- type -- */

h1, h2, h3 {
  font-family: var(--display);
  font-optical-sizing: auto;
  font-weight: 300;
  margin: 0;
  color: var(--linen);
}

h1 {
  font-size: clamp(2rem, 5vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: 0.005em;
}

h2 {
  font-size: 1.375rem;
  line-height: 1.25;
  letter-spacing: 0.01em;
}

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

a {
  color: var(--brass);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 0.18s ease, border-color 0.18s ease;
}
a:hover {
  color: var(--brass-bright);
  border-bottom-color: var(--hairline-lit);
}

/* The one small-caps utility style, doing the work a third typeface would.
   Used for eyebrows, field labels, and status. */
.eyebrow,
.field > label,
.fieldset > legend {
  display: block;
  font-family: var(--body);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--linen-dim);
}

.lede {
  color: var(--linen-dim);
  font-size: 1.0625rem;
  max-width: 34rem;
}

.muted { color: var(--linen-dim); font-size: 0.9375rem; }

/* ------------------------------------------------------------ structure -- */

.shell {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* No bar, no border. The nav is separated from the page by light and air.
   Same measure as .page so the mark sits on the same left edge as the h1. */
.topnav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  width: min(100% - 2.5rem, var(--measure));
  margin: 0 auto;
  padding: 1.75rem 0 1rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  color: var(--linen);
  border-bottom: none;
}
.brand:hover { color: var(--linen); }
.brand .mark { color: var(--brass); flex: none; }
.brand .wordmark {
  font-family: var(--display);
  font-optical-sizing: auto;
  font-weight: 300;
  font-size: 1.375rem;
  letter-spacing: 0.14em;
  text-transform: lowercase;
}
.brand:hover .mark { color: var(--brass-bright); }

.navlinks {
  display: flex;
  align-items: center;
  /* Wraps rather than overflowing. Measured, not assumed: with five items
     (Library / Global / Preferences / Subscription / Sign out) this row is
     448px wide, so at 375–390px it pushed the whole document sideways —
     every page scrolled horizontally, not just the nav. Adding the
     Subscription link is what tipped it over, but the row had no wrap rule
     to begin with, so it was one nav item away from this regardless. */
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.navlinks a,
.navlinks .link-button { color: var(--linen-dim); }
.navlinks a:hover,
.navlinks .link-button:hover { color: var(--linen); }
.navlinks a[aria-current="page"] { color: var(--brass); }

.page {
  flex: 1;
  width: min(100% - 2.5rem, var(--measure));
  margin: 0 auto;
  padding: 2.5rem 0 6rem;
}
.page-head { margin-bottom: 2.5rem; }
.page-head .lede { margin-top: 0.75rem; }

/* ------------------------------------------------------------- surfaces -- */

/* Not a bordered box. A panel is a surface that has caught some of the lamp:
   slightly warmer fill, and a lit hairline along its top edge only. */
.panel {
  position: relative;
  background: linear-gradient(var(--ink-raised), rgba(33, 26, 36, 0.55));
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  padding: 1.75rem;
  overflow: hidden;
}
.panel::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--hairline-lit) 25%, var(--hairline-lit) 75%, transparent);
}

.panel + .panel { margin-top: 1.25rem; }

/* -------------------------------------------------------------- fields -- */

.field { margin-bottom: 1.5rem; }
.field:last-child { margin-bottom: 0; }
.field > label { margin-bottom: 0.5rem; }

.hint {
  margin: 0.5rem 0 0;
  font-size: 0.875rem;
  color: var(--linen-dim);
}

/* NOTE the `search` type. It was missing from this list, and the omission
   had a real consequence rather than a cosmetic one: the Global feed's
   search box fell back to the browser default (13.3px in Chromium), and
   iOS Safari auto-zooms the page whenever a focused input is under 16px —
   then does not zoom back out. Searching the feed on an iPhone left the
   reader stuck at 1.3x with the layout off-screen. `tel` and `url` are
   included for the same reason before anything starts using them. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="tel"],
input[type="url"],
select,
textarea {
  width: 100%;
  padding: 0.75rem 0.875rem;
  background: rgba(23, 18, 25, 0.65);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  color: var(--linen);
  font-family: var(--body);
  font-size: 1rem;
  line-height: 1.4;
  transition: border-color 0.18s ease, background-color 0.18s ease;
}
input::placeholder, textarea::placeholder { color: rgba(156, 142, 147, 0.7); }
textarea { min-height: 5.5rem; resize: vertical; }

select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--linen-dim) 50%),
                    linear-gradient(135deg, var(--linen-dim) 50%, transparent 50%);
  background-position: right 1.15rem center, right 0.85rem center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 2.5rem;
}

input:hover, select:hover, textarea:hover { border-color: rgba(237, 227, 217, 0.18); }

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--brass);
  background: rgba(23, 18, 25, 0.9);
}

/* Keyboard focus is always visible, and always the same gesture: brass. */
:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

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

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 0.9375rem;
  border: 1px solid var(--hairline);
  border-radius: var(--r-pill);
  background: rgba(23, 18, 25, 0.5);
  color: var(--linen-dim);
  font-size: 0.9375rem;
  line-height: 1.3;
  cursor: pointer;
  user-select: none;
  transition: color 0.18s ease, border-color 0.18s ease, background-color 0.18s ease;
}
.chip input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  inset: 0;
  margin: 0;
  cursor: pointer;
}
.chip:hover { color: var(--linen); border-color: rgba(237, 227, 217, 0.2); }
.chip:has(input:checked) {
  color: var(--brass-bright);
  border-color: var(--hairline-lit);
  background: var(--tint);
}
.chip:has(input:focus-visible) {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
}

/* Length chips carry a number, so they get the display face and more room. */
.chips-length .chip {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
  padding: 0.6875rem 1.125rem;
  border-radius: var(--r-md);
  min-width: 5.5rem;
}
.chips-length .chip .n {
  font-family: var(--display);
  font-optical-sizing: auto;
  font-weight: 300;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--linen);
  font-variant-numeric: tabular-nums;
  transition: color 0.18s ease;
}
.chips-length .chip .unit {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.chips-length .chip:has(input:checked) .n { color: var(--brass-bright); }

/* ------------------------------------------------------------- controls -- */

.checkbox-row {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin: 1rem 0;
  cursor: pointer;
}
.checkbox-row input {
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  margin-top: 0.25rem;
  accent-color: var(--brass);
}
.checkbox-row span { font-size: 0.9375rem; color: var(--linen-dim); }

.slider-row {
  display: flex;
  align-items: center;
  gap: 1.125rem;
}
.slider-row input[type="range"] {
  flex: 1;
  accent-color: var(--brass);
  height: 1.5rem;
}
.slider-value {
  min-width: 9.5rem;
  text-align: right;
  font-size: 0.875rem;
  color: var(--brass);
  font-weight: 500;
}

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

button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.75rem 1.5rem;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  background: var(--brass);
  color: var(--ink);
  font-family: var(--body);
  font-size: 0.9375rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
button:hover { background: var(--brass-bright); color: var(--ink); }
button:disabled { opacity: 0.45; cursor: not-allowed; }
button:disabled:hover { background: var(--brass); }

/* A button mid-request: distinct from plain :disabled (which reads as
   "unavailable") — busy reads as "working", so it stays brighter and gets a
   spinner rather than dimming like an inert control. The combined selector
   (rather than [aria-busy] alone) beats the plain :disabled rule above on
   specificity regardless of source order, since setPending() always sets
   both attributes together. */
button:disabled[aria-busy="true"] {
  opacity: 0.75;
  cursor: wait;
}
/* Only buttons whose pending state swaps in a text label get the spinner —
   an icon-only button (e.g. "like") just dims, so its icon doesn't jump. */
button:disabled[aria-busy="true"][data-pending-spinner="1"] {
  position: relative;
  padding-left: 2.75rem;
}
button:disabled[aria-busy="true"][data-pending-spinner="1"]::before {
  content: "";
  position: absolute;
  left: 1.1rem;
  top: 50%;
  width: 1rem;
  height: 1rem;
  margin-top: -0.5rem;
  border: 2px solid rgba(23, 18, 25, 0.35);
  border-top-color: var(--ink);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* A destructive PRIMARY action (whole-account deletion) — distinct from
   both the default brass-fill button (wrong signal for something this
   consequential) and .link-danger (too quiet for it, and that class is
   scoped to .story-actions besides). Uses --clay, the failure token,
   outlined rather than filled until hovered: "warm, not an alarm" applies
   here too — this is a serious action, not a red-alert one. */
.button-danger {
  background: transparent;
  border-color: var(--clay);
  color: var(--clay);
}
.button-danger:hover {
  background: var(--clay);
  color: var(--ink);
}

.link-button {
  padding: 0;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--linen-dim);
  font-size: inherit;
  font-weight: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}
.link-button:hover { background: none; color: var(--linen); }

.inline-form { display: inline-flex; }

/* --------------------------------------------------------------- flash -- */

.flash {
  display: flex;
  gap: 0.75rem;
  padding: 0.875rem 1.125rem;
  margin-bottom: 1.75rem;
  border: 1px solid var(--hairline);
  border-left-width: 2px;
  border-radius: var(--r-md);
  font-size: 0.9375rem;
  background: var(--ink-raised);
}
.flash-error { border-left-color: var(--clay); color: var(--clay); }
.flash-ok { border-left-color: var(--verdigris); color: var(--verdigris); }

/* The underlying reason, when there is one: present but subordinate. */
.flash-detail {
  display: block;
  margin-top: 0.375rem;
  color: var(--linen-dim);
  font-size: 0.875rem;
}

/* ------------------------------------------------------------- library -- */

.compose { margin-bottom: 3rem; }
.compose .compose-row {
  display: flex;
  gap: 0.75rem;
  align-items: stretch;
}
.compose .compose-row input { flex: 1; }
.compose .compose-row button { flex: none; white-space: nowrap; }

.story-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.story-item { border-top: 1px solid var(--hairline); }
.story-item:last-child { border-bottom: 1px solid var(--hairline); }

.story-link {
  display: flex;
  align-items: baseline;
  gap: 1.5rem;
  padding: 1.25rem 0.25rem;
  color: var(--linen);
  border-bottom: none;
  transition: background-color 0.18s ease, padding-left 0.18s ease;
}
.story-link:hover {
  color: var(--linen);
  background: linear-gradient(90deg, var(--tint), transparent 70%);
}

.story-text { flex: 1; min-width: 0; }
.story-title {
  font-size: 1.0625rem;
  font-weight: 500;
  line-height: 1.35;
  /* A story's title is derived from its own first sentence, so it is
     arbitrary generated text — a long unbroken run of characters is not a
     hypothetical. Measured at 476px inside a 375px viewport before this.
     `min-width: 0` on the flex parent is necessary but not sufficient: a
     single unbreakable word still overflows without an explicit break. */
  overflow-wrap: anywhere;
}
.story-link:hover .story-title { color: var(--brass-bright); }

.story-meta {
  margin-top: 0.3125rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--linen-dim);
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
}
.story-meta .sep { opacity: 0.4; }
.story-meta .pending { color: var(--brass); }
.story-meta .broke { color: var(--clay); }
.story-meta .author { color: var(--brass); font-weight: 600; }

/* Duration is the anchor of every row: it is the unit the reader now chooses
   their stories in, so it is set in the display face and given the space. */
.story-length {
  flex: none;
  font-family: var(--display);
  font-optical-sizing: auto;
  font-weight: 300;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--linen-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  transition: color 0.18s ease;
}
.story-length .unit {
  font-family: var(--body);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin-left: 0.25rem;
}
.story-link:hover .story-length { color: var(--brass); }

.empty {
  padding: 3.5rem 0;
  text-align: center;
  border-top: 1px solid var(--hairline);
}
.empty p { color: var(--linen-dim); margin-bottom: 0.25rem; }

/* Publish/unpublish/delete controls under a library row. */
.story-actions {
  display: flex;
  align-items: baseline;
  gap: 0.625rem;
  padding: 0 0.25rem 0.875rem;
  font-size: 0.8125rem;
}
.story-actions .link-button { color: var(--brass); }
.story-actions .link-button:hover { color: var(--brass-bright); }
.story-actions .hint { margin: 0; }
.story-actions .sep { opacity: 0.4; }
.story-actions .link-danger { color: var(--linen-dim); }
.story-actions .link-danger:hover { color: var(--clay); }

/* -------------------------------------------------------------- global -- */

.global-item {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  padding: 1.25rem 0.25rem;
}
.global-item .story-link { flex: 1; padding: 0; }
.global-item .story-title { color: var(--linen); }

.like-form { flex: none; }
.like-button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  background: none;
  border: 1px solid var(--hairline);
  color: var(--linen-dim);
  padding: 0.625rem 1rem;
  font-weight: 600;
}
.like-button:hover { background: var(--tint); color: var(--linen); border-color: rgba(237, 227, 217, 0.2); }
.like-button.liked { background: var(--brass); color: var(--ink); border-color: var(--brass); }
.like-button.liked:hover { background: var(--brass-bright); border-color: var(--brass-bright); }
.like-count { font-variant-numeric: tabular-nums; }

.story-actions {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  margin-top: 1.5rem;
}
.story-actions .download-link {
  color: var(--linen-dim);
  font-size: 0.875rem;
  font-weight: 600;
  align-self: center;
}
.story-actions .download-link:hover { color: var(--linen); }

/* ---------------------------------------------------------------- share -- */
/* Quiet by design. These sit under the player on a page whose subject is the
   story, so they read as the same weight as the metadata line above them,
   not as a call to action. */

.share {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
}
.share .link-button,
.share .download-link {
  color: var(--linen-dim);
  font-size: 0.875rem;
  font-weight: 600;
}
.share .link-button:hover,
.share .download-link:hover { color: var(--brass); }

/* Only ever shown when the clipboard write was refused, so it has to be
   immediately usable — wide enough to read a URL in, and pre-selected. */
.share-fallback { flex: 1 1 14rem; min-width: 0; }
.share-fallback input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  font-size: 0.8125rem;
}

/* ------------------------------------------------------ the aperture -- */
/* The mark closing as the story is written. Two arcs grow from their
   quotation-mark heads until the ring completes. */

.aperture {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.125rem;
  padding: 2.5rem 0;
  text-align: center;
}
.aperture svg { display: block; overflow: visible; }

/* Scoped to the mark itself, not to .aperture, because the player's play
   button uses the same geometry outside this block. */
[data-aperture] .track { stroke: var(--hairline); }
[data-aperture] .arc {
  stroke: var(--brass);
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset 0.6s cubic-bezier(0.33, 1, 0.68, 1);
}
[data-aperture] .head { fill: var(--brass); }

/* The only ambient motion in the product: while work is happening the lamp
   behind the mark breathes. Nothing else on the page moves. */
[data-aperture] .glow {
  fill: var(--brass);
  opacity: 0.14;
  filter: blur(14px);
  transform-origin: center;
  animation: breathe 4.5s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { opacity: 0.10; transform: scale(0.94); }
  50%      { opacity: 0.20; transform: scale(1.06); }
}

.aperture-status {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--linen-dim);
  min-height: 1.25rem;
}

/* A second, unambiguous reading of the same fraction driving the ring —
   the ring is the product's mark, but a filling bar is the one shape
   everyone already reads as "progress" at a glance. */
.progress-bar {
  width: 100%;
  max-width: 220px;
  height: 4px;
  border-radius: 999px;
  background: var(--hairline);
  overflow: hidden;
}
.progress-bar-fill {
  height: 100%;
  width: 0%;
  background: var(--brass);
  border-radius: inherit;
  transition: width 0.6s cubic-bezier(0.33, 1, 0.68, 1);
}

/* The "how much longer" readout: the exact fraction driving the ring above,
   spelled out. Tabular figures so "9%" -> "10%" doesn't shift the layout. */
.aperture-percent {
  margin: -0.75rem 0 0;
  font-family: var(--display);
  font-optical-sizing: auto;
  font-weight: 300;
  font-size: 0.9375rem;
  color: var(--linen-dim);
  font-variant-numeric: tabular-nums;
  min-height: 1.125rem;
}

/* The way out of a progress view that has stopped moving. Quiet by design —
   it should be findable when wanted, not a suggestion that something is
   probably wrong. */
.aperture-escape {
  margin: 0.5rem 0 0;
  font-size: 0.8125rem;
  color: var(--linen-dim);
}
.aperture-escape .inline-form-wrap { display: inline-flex; }
.aperture-escape .link-button {
  font-size: inherit;
  color: var(--linen-dim);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.aperture-escape .link-button:hover { color: var(--clay); }

/* The technical reason a story failed: available, subordinate. Leading with
   it (as this page used to) puts an internal diagnostic in front of someone
   who just wanted a story. */
.failure-detail { margin-top: 1.75rem; }
.failure-detail summary {
  cursor: pointer;
  list-style: none;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--linen-dim);
  transition: color var(--dur-micro) var(--ease-lamp);
}
.failure-detail summary::-webkit-details-marker { display: none; }
.failure-detail summary:hover { color: var(--linen); }
.failure-detail p {
  margin-top: 0.75rem;
  font-size: 0.875rem;
  word-break: break-word;
}

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

.backlink {
  display: inline-block;
  margin-bottom: 1.75rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--linen-dim);
}
.backlink:hover { color: var(--brass); }

.story-head { margin-bottom: 2rem; }
.story-head .story-meta { margin-top: 0.875rem; }

audio {
  width: 100%;
  margin-top: 0.5rem;
  color-scheme: dark;
}

/* The player. The Aperture is the play button, and its ring is the playhead. */
.player {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  margin-top: 0.75rem;
}

.player-toggle {
  position: relative;
  flex: none;
  width: 64px;
  height: 64px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 50%;
  color: var(--linen);
}
.player-toggle:hover { background: transparent; color: var(--brass-bright); }
.player-toggle [data-aperture] {
  position: absolute;
  inset: 0;
  overflow: visible;
}
/* No easing here: the ring is a playhead, and a playhead that lags is wrong. */
.player-toggle .arc { transition: none; }
.player-toggle:hover .arc,
.player-toggle:hover .head { stroke: var(--brass-bright); fill: var(--brass-bright); }
.player-toggle .head { stroke: none; }

.player-icon {
  position: relative;
  width: 24px;
  height: 24px;
  fill: currentColor;
}

.player-body { flex: 1; min-width: 0; }

/* The scrub bar. This is the product's core interaction — someone listening
   in bed, one-handed, wanting to go back thirty seconds — and as a default
   `input[type=range]` it measured **205x16**: a mouse target, not a thumb.
   Apple's HIG minimum is 44x44.

   The track stays visually thin (4px) while the control itself is 44px
   tall, with the extra height as transparent padding. That is the whole
   trick: the hit area a finger needs and the line a reader sees are not the
   same rectangle, and making the visible bar 44px thick to solve this would
   have been ugly and wrong. Styling the track/thumb explicitly means
   `accent-color` no longer applies, so the brass has to be restated. */
.player-seek {
  display: block;
  width: 100%;
  height: 44px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  /* The thumb sits outside the 4px track, so without this the top and
     bottom of a 16px thumb are clipped on WebKit. */
  overflow: visible;
}
.player-seek::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: var(--hairline);
}
.player-seek::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--hairline);
}
.player-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  border: none;
  border-radius: 50%;
  background: var(--brass);
  /* Centres a 16px thumb on a 4px track: (4 - 16) / 2. */
  margin-top: -6px;
}
.player-seek::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border: none;
  border-radius: 50%;
  background: var(--brass);
}
.player-seek:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--hairline-lit); }
.player-seek:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px var(--hairline-lit); }

.player-times {
  display: flex;
  justify-content: space-between;
  margin: 0.375rem 0 0;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  color: var(--linen-dim);
  font-variant-numeric: tabular-nums;
}

/* Buffering. Previously invisible: the playhead simply stopped advancing,
   which is pixel-identical to being paused — the reader can't tell "loading"
   from "you pressed pause". The ring dims and breathes, borrowing the same
   "the lamp breathes while work is happening" vocabulary the generation
   Aperture already uses, so waiting looks the same everywhere in the product.

   The static dim is deliberate and load-bearing: reduced-motion users get no
   animation, so without it they'd be back to no signal at all. */
.player.is-buffering .arc { opacity: 0.5; }
.player.is-buffering .arc { animation: playhead-wait 1.6s ease-in-out infinite; }
@keyframes playhead-wait {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.3; }
}
.player.is-buffering .player-times { color: var(--brass); }

/* Audio that failed to load or decode. Previously silent — a 404 or a
   corrupt file left a dead play button and no explanation. */
.player-error {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.875rem;
  margin-top: 0.75rem;
  padding: 0.875rem 1.125rem;
  border: 1px solid var(--hairline);
  border-left: 2px solid var(--clay);
  border-radius: var(--r-md);
  background: var(--ink-raised);
  font-size: 0.9375rem;
  color: var(--clay);
}
.player-error button {
  padding: 0.5rem 1rem;
  font-size: 0.8125rem;
}

/* -------------------------------------------------------------- reader -- */
/* The story, in words, tracking the narration. Long-form like the legal
   pages, but unlike them it is read *while* something else is moving, so the
   rhythm is looser and the active line has to be findable at a glance after
   looking away. */

/* The transport follows the reader down the page — scrolling half an hour of
   prose must never mean scrolling back up to pause. Solid rather than the
   panel's usual translucent gradient, because prose passes underneath it.
   Held back on short viewports, where a pinned player would take a third of
   the screen away from the thing it is there to accompany. */
@media (min-height: 34rem) {
  .player-panel {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--ink-raised);
    box-shadow: 0 0.75rem 1.25rem rgba(23, 18, 25, 0.55);
  }
}

.reader { padding-top: 1.5rem; }
.reader-hint { margin: -0.25rem 0 0.5rem; }

.reader-body {
  max-width: 40rem;
  font-size: 1.0625rem;
  line-height: 1.8;
  color: var(--linen);
}

.reader-para {
  margin: 0;
  padding: 0.4rem 0.9rem;
  border-left: 2px solid transparent;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  transition: background-color var(--dur-element) var(--ease-lamp),
              border-color var(--dur-element) var(--ease-lamp);
}
.reader-para + .reader-para { margin-top: 0.85rem; }

/* Only a pointer affordance, and only once app.js has actually wired the
   click — see _reader.html for why these are not focusable. */
[data-reader].is-seekable .reader-para { cursor: pointer; }
[data-reader].is-seekable .reader-para:hover { background: rgba(200, 155, 90, 0.05); }

/* The lamp falling on one line. Tint alone is too faint to find after
   looking away, so the brass rule carries it; together they read as lit
   rather than selected. */
.reader-para.is-reading {
  background: var(--tint);
  border-left-color: var(--brass);
}

/* ----------------------------------------------------------------- auth -- */

.auth {
  width: min(100% - 2.5rem, var(--measure-narrow));
  margin: 0 auto;
  padding: 4rem 0 6rem;
}
.auth .auth-head { text-align: center; margin-bottom: 2rem; }
.auth .auth-head .mark { color: var(--brass); margin-bottom: 1.25rem; }
.auth .auth-alt { margin-top: 1.75rem; text-align: center; font-size: 0.9375rem; color: var(--linen-dim); }
.auth button[type="submit"] { width: 100%; margin-top: 1.75rem; }

/* ------------------------------------------------------------ sections -- */

.section + .section { margin-top: 2.5rem; }
.section > .eyebrow {
  padding-bottom: 0.875rem;
  margin-bottom: 1.5rem;
  border-bottom: 1px solid var(--hairline);
  color: var(--brass);
}

.form-actions {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  margin-top: 2.5rem;
  padding-top: 2rem;
  border-top: 1px solid var(--hairline);
}

.grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
}

/* ------------------------------------------------------- touch targets -- */
/* Apple's HIG minimum is 44x44pt. This product is used overwhelmingly on a
   phone, in the dark, one-handed — so this is not box-ticking, it is the
   main interaction quality problem the desktop-first CSS left behind.
   Everything below was measured in a real browser at 375/390/768/1024 and
   the numbers in the comments are what it actually was.

   The pattern throughout is negative margin plus padding: grow the hit area
   without moving anything visually or opening gaps in the layout. Where a
   control sits in a text flow, `align-items: center` on the parent keeps the
   larger box from pushing lines apart. */

.navlinks a,
.navlinks .link-button {
  /* Were 21px tall (36px for the brand). The nav is the most-tapped thing
     in the app and every item was under half the minimum. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

button {
  /* Every button in the app came out at exactly 34–43px from its padding
     plus line-height — one to ten pixels under, which is the least
     excusable kind of miss. One rule fixes all of them because they all
     inherit from here. */
  min-height: 44px;
}

.brand {
  /* Was 97x36. It is the home link on every page. */
  min-height: 44px;
}

.sitefoot a {
  /* Footer legal links measured 19–21px tall. These are the links a payment
     processor's underwriter and a regulator will click, so "small but
     nobody uses them" is exactly wrong about this row. min-width too:
     "Terms" is a short enough word to come in at 40px wide even once the
     height is right. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  min-width: 44px;
}

.link-button {
  /* "Delete" was 39px wide after the height fix — short label, same
     problem as "Terms" above. */
  min-width: 44px;
}

/* Deliberately NOT enlarged: the "Terms of Service", "End User Agreement",
   and "Privacy Policy" links inside the signup attestation sentence. They
   are inline links in a running paragraph, and forcing them to 44px would
   space the sentence's own lines apart to no benefit — the surrounding text
   would look broken to everyone in order to help nobody, since the whole
   label is already a 44px-tall tap target that toggles the checkbox.
   WCAG 2.2 SC 2.5.8 exempts inline targets in a sentence for exactly this
   reason. Recorded here so a later measurement pass doesn't "fix" it. */

.checkbox-row {
  /* The age and ToS attestations. The native box is 18x18 and cannot
     usefully be made bigger without looking wrong, so the *label* is the
     target — which it already is via `for=`; it just needed the height. */
  min-height: 44px;
}
.checkbox-row input {
  width: 20px;
  height: 20px;
  flex: none;
}

.backlink,
.download-link,
.auth-alt a,
.hint a,
.legal-links a,
.form-actions a {
  /* 18–27px tall: "← Library", "Download", "Forgot your password?",
     "Terms of Service". Negative margin so a 44px box inside a paragraph
     doesn't space the paragraph out. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: -0.6875rem;
  margin-bottom: -0.6875rem;
}

.like-button {
  /* Was 67x40 — four pixels short, which is the least excusable kind of
     miss because it costs nothing to fix. */
  min-height: 44px;
}

.link-button {
  /* "Delete" measured 39x16 and "Remove from Global" 122x16. Both are
     destructive-ish actions sitting close to other controls, which is the
     worst combination of small and consequential. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.chip,
.filter-menu-body a,
.filter-active,
.sort-toggle a,
.pagination a {
  /* Filter chips measured 194x37, the sort toggle 121x34, and the pagination
     links similarly. All are newly-added discovery controls, which is why
     they cluster: they were built at desktop width. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.chip input[type="checkbox"],
.chip input[type="radio"],
.check input[type="checkbox"] {
  /* 18x18 native boxes. The label around them is the real target, so the
     fix is to make sure that label is tall enough and the box is easy to
     hit rather than to inflate the box itself into something ugly. */
  width: 20px;
  height: 20px;
  flex: none;
}

.check {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 44px;
}

/* --------------------------------------------------------- safe areas -- */
/* iPhone notch and home indicator. Without these the nav tucks under the
   status bar and the footer under the home bar in the installed PWA, which
   is exactly the context this app is being pointed at (see B-05: the app
   stores are not an option, so the installed web app IS the mobile client). */
.shell { padding-top: calc(1rem + env(safe-area-inset-top, 0px)); }
.sitefoot { padding-bottom: calc(3rem + env(safe-area-inset-bottom, 0px)); }
body {
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

/* ------------------------------------------------------------- billing -- */
/* No new visual language here — the paywall is a panel, the button is the
   primary button, the small print is .hint. A paywall that announces itself
   with a different palette reads as an ad someone bolted on; this product's
   whole tone is that it is one considered thing. */

.paywall {
  text-align: center;
  padding: 0.5rem 0 0.25rem;
}

.paywall h2 {
  font-size: 1.375rem;
  margin-bottom: 0.75rem;
}

.paywall .hint {
  /* Wider than the default hint measure: this one is two sentences of
     reassurance about what a reader keeps, and setting it narrow makes it
     look like a disclaimer. */
  max-width: 34rem;
  margin-left: auto;
  margin-right: auto;
}

.paywall form {
  margin: 1.5rem 0 1rem;
}

.free-remaining {
  /* Brass because it is lamplight, not a warning — "you have one left" is
     information, and --clay would make it read as an error. */
  color: var(--brass);
  white-space: nowrap;
}

.status-line {
  font-size: 1.0625rem;
  line-height: 1.5;
  margin-bottom: 0.75rem;
}

.status-line strong { color: var(--linen); }
.status-active strong { color: var(--verdigris); }
.status-problem strong { color: var(--clay); }

/* --------------------------------------------------------------- small -- */

@media (max-width: 34rem) {
  .topnav {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.875rem;
    padding-top: 1.25rem;
  }
  .navlinks { gap: 1.125rem; font-size: 0.75rem; }
  .page { padding-top: 1.75rem; }
  .panel { padding: 1.25rem; border-radius: var(--r-md); }
  .compose .compose-row { flex-direction: column; }
  .compose .compose-row button { width: 100%; }
  .story-link { gap: 1rem; }
  .story-length { font-size: 1.25rem; }
  .grid-2 { grid-template-columns: minmax(0, 1fr); gap: 1.25rem; }
  .slider-row { flex-wrap: wrap; }
  .slider-value { min-width: 0; text-align: left; width: 100%; }
  .form-actions { flex-direction: column; align-items: stretch; }
  .form-actions button { width: 100%; }
}

/* --------------------------------------------------- transitional swap -- */
/* Used for exactly one thing: the compose form giving way to the Aperture
   when a story starts being written. That is the product's defining moment,
   and it used to happen as an instant `hidden` toggle — the form blinked
   out and the ring blinked in, which read as a page glitch rather than as
   an action taking effect. These classes are driven from app.js because a
   `display: none` element cannot be transitioned directly. */

.swap-enter {
  opacity: 0;
  transform: translateY(8px) scale(0.985);
}
.swap-enter-active {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-element) var(--ease-lamp),
              transform var(--dur-element) var(--ease-lamp);
}
.swap-exit {
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;
  transition: opacity 0.2s var(--ease-lamp), transform 0.2s var(--ease-lamp);
}

/* --------------------------------------------------------------- toast -- */
/* For feedback on an action that does NOT navigate — the flash banner still
   owns anything that arrives with a page load. Bottom-centre so it never
   covers the nav or the thing just acted on. */

.toast-stack {
  position: fixed;
  left: 50%;
  bottom: 2rem;
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  transform: translateX(-50%);
  pointer-events: none;  /* the stack never blocks; individual toasts opt in */
}

.toast {
  pointer-events: auto;
  max-width: min(90vw, 26rem);
  padding: 0.75rem 1.25rem;
  border: 1px solid var(--hairline-lit);
  border-radius: var(--r-pill);
  background: var(--ink-raised);
  color: var(--linen);
  font-size: 0.9375rem;
  line-height: 1.4;
  text-align: center;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.5);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity var(--dur-element) var(--ease-lamp),
              transform var(--dur-element) var(--ease-lamp);
}
.toast.is-visible { opacity: 1; transform: none; }
.toast-error { border-color: var(--clay); color: var(--clay); }

/* ------------------------------------------------------ confirm dialog -- */
/* The styled replacement for window.confirm() (UX plan 0.2). Built on a
   native <dialog> + showModal(), so the focus trap, Escape-to-close and
   top-layer stacking come from the browser rather than from JS — see
   base.html and showConfirm() in app.js.

   Styled as a panel that has been lifted off the page: same raised surface
   and lit hairline, a heavier shadow, and the ground dimmed behind it. */

.confirm {
  max-width: min(92vw, 26rem);
  padding: 0;                          /* the inner form owns the padding */
  border: 1px solid var(--hairline-lit);
  border-radius: var(--r-lg);
  background: linear-gradient(var(--ink-raised), rgba(33, 26, 36, 0.94));
  color: var(--linen);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.62);
}
/* Deep enough to read as "the room behind this is out of reach", not so
   opaque that the page vanishes — the reader should still see what they're
   acting on. */
.confirm::backdrop {
  background: rgba(11, 8, 13, 0.68);
}

.confirm-form { padding: 1.75rem; }

.confirm-message {
  margin: 0 0 1.5rem;
  font-size: 1rem;
  line-height: 1.55;
  color: var(--linen);
}

.confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
}

/* Cancel is the quiet one and holds initial focus (autofocus in base.html).
   Outlined rather than filled so the accept button is unambiguously the
   weightier of the two, even though it is the more dangerous. */
.confirm-cancel {
  background: none;
  border-color: var(--hairline-lit);
  color: var(--linen-dim);
}
.confirm-cancel:hover {
  background: none;
  border-color: var(--brass);
  color: var(--linen);
}

/* Same treatment as .button-danger — outlined --clay, filling on hover.
   Every action that reaches this dialog is destructive, so it is always the
   danger styling, never the default brass fill. */
.confirm-accept {
  background: transparent;
  border-color: var(--clay);
  color: var(--clay);
}
.confirm-accept:hover {
  background: var(--clay);
  color: var(--ink);
}

/* At 375px two full-width buttons side by side get tight enough that the
   labels start wrapping mid-word. Stacking keeps both at a comfortable
   touch size, with the destructive one last. */
@media (max-width: 24rem) {
  .confirm-actions { flex-direction: column-reverse; }
  .confirm-actions button { width: 100%; }
}

/* ------------------------------------------------- the compose dialog -- */
/* The same lifted surface as .confirm, but this one holds a whole form
   rather than a sentence: wider, and scrolling inside itself so the two
   buttons stay reachable however tall the adjust section gets. */

.compose-dialog { max-width: min(94vw, 40rem); }

.compose-dialog-form {
  padding: 1.75rem;
  max-height: min(82vh, 46rem);
  overflow-y: auto;
}

.compose-dialog-title {
  margin: 0 0 0.5rem;
  font-size: 1.375rem;
  font-weight: 300;
}
.compose-dialog .hint { margin-bottom: 1.5rem; }

/* Closed by default, and its open state is what marks the request as an
   override — so it reads as a decision, not as a disclosure widget. */
.compose-adjust > summary {
  cursor: pointer;
  padding: 0.75rem 0;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--linen-dim);
  list-style: none;
}
.compose-adjust > summary::-webkit-details-marker { display: none; }
.compose-adjust > summary:hover { color: var(--brass); }
.compose-adjust[open] > summary { border-bottom-color: transparent; color: var(--brass); }

.compose-adjust-body { padding-top: 1.5rem; }
/* Tighter than the preferences page, where these sections are the whole
   screen rather than the middle of a dialog. */
.compose-adjust-body .section + .section { margin-top: 1.75rem; }

.compose-dialog .confirm-actions {
  position: sticky;
  bottom: -1.75rem;
  margin-top: 1.75rem;
  padding: 1.25rem 0 0;
  background: linear-gradient(rgba(33, 26, 36, 0), var(--ink-raised) 35%);
}

/* Nothing here is destructive, so the accept button keeps the ordinary
   brass fill rather than .confirm-accept's danger styling. */
.compose-write { min-width: 8rem; }

/* ----------------------------------------------------------- sitefoot -- */

.sitefoot {
  width: min(100% - 2.5rem, var(--measure));
  margin: 0 auto;
  padding: 2rem 0 3rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  border-top: 1px solid var(--hairline);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.sitefoot a { color: var(--linen-dim); }
.sitefoot a:hover { color: var(--brass); }

/* -------------------------------------------------------------- legal -- */
/* Long-form prose, unlike anything else in the product — these are the one
   place a reader is expected to actually read paragraphs rather than scan. */

.legal { padding-bottom: 2rem; }
.legal .section > .eyebrow { color: var(--brass); }
.legal p { max-width: 40rem; }
.legal ul {
  max-width: 40rem;
  margin: 0 0 1rem;
  padding-left: 1.25rem;
  color: var(--linen);
}
.legal li { margin-bottom: 0.5rem; }
.legal code {
  font-size: 0.875em;
  padding: 0.1em 0.35em;
  border-radius: var(--r-sm);
  background: rgba(23, 18, 25, 0.65);
  color: var(--brass);
}
.legal table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 1rem;
  font-size: 0.9375rem;
}
.legal th,
.legal td {
  text-align: left;
  vertical-align: top;
  padding: 0.625rem 0.75rem 0.625rem 0;
  border-bottom: 1px solid var(--hairline);
}
.legal th {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--linen-dim);
}

/* The signup attestations: given a bordered surface of their own so they
   don't read as visually equivalent to a "remember me" checkbox. */
.attestations {
  margin: 1.75rem 0 0;
  padding: 0.25rem 1.125rem 1rem;
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  background: rgba(23, 18, 25, 0.4);
}

/* --------------------------------------------------------- utilities -- */

/* Text for screen readers only. Not `display: none` — that would remove it
   from the accessibility tree, which is the opposite of the intent. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

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

.pagination {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--hairline);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.pagination a { color: var(--linen-dim); }
.pagination a:hover { color: var(--brass); }

/* ---------------------------------------------------------- discovery -- */

.discovery { margin-bottom: 2.5rem; }

.discovery-row {
  display: flex;
  gap: 0.75rem;
  align-items: stretch;
  margin-bottom: 1.125rem;
}
.discovery-row input { flex: 1; }
.discovery-row button { flex: none; white-space: nowrap; }

/* Sort is a choice between two states, not a menu — a segmented pair reads
   as "which one of these", where a <select> would read as "pick from many". */
.sort-toggle {
  display: inline-flex;
  border: 1px solid var(--hairline);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.sort-toggle a {
  padding: 0.4375rem 1rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--linen-dim);
  border-bottom: none;
  transition: background-color var(--dur-micro) var(--ease-lamp),
              color var(--dur-micro) var(--ease-lamp);
}
.sort-toggle a:hover { color: var(--linen); }
.sort-toggle a[aria-current="true"] {
  background: var(--tint);
  color: var(--brass-bright);
}

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

/* An applied filter is a link that removes itself — the × is the affordance,
   so there's no separate "clear" control to hunt for. */
.filter-active {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4375rem 0.875rem;
  border: 1px solid var(--hairline-lit);
  border-radius: var(--r-pill);
  background: var(--tint);
  color: var(--brass-bright);
  font-size: 0.875rem;
  border-bottom: none;
}
.filter-active:hover { color: var(--linen); border-color: rgba(237, 227, 217, 0.28); }
.filter-active .x { font-size: 1rem; line-height: 1; opacity: 0.7; }

.filter-menu { position: relative; }
.filter-menu summary {
  cursor: pointer;
  list-style: none;
  padding: 0.4375rem 0.875rem;
  border: 1px solid var(--hairline);
  border-radius: var(--r-pill);
  color: var(--linen-dim);
  font-size: 0.875rem;
  transition: color var(--dur-micro) var(--ease-lamp),
              border-color var(--dur-micro) var(--ease-lamp);
}
.filter-menu summary::-webkit-details-marker { display: none; }
.filter-menu summary:hover { color: var(--linen); border-color: rgba(237, 227, 217, 0.2); }
.filter-menu[open] summary { color: var(--brass); border-color: var(--hairline-lit); }
.filter-menu-body {
  position: absolute;
  z-index: 20;
  top: calc(100% + 0.5rem);
  left: 0;
  min-width: 14rem;
  /* Measured overflowing to 471px inside a 375px viewport: a 14rem menu
     opening from a chip that already sits well to the right has nowhere to
     go. Capping to the viewport keeps it on screen wherever it opens from,
     without needing to know which chip it hangs off. */
  max-width: calc(100vw - 2.5rem);
  padding: 0.875rem;
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  background: var(--ink-raised);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.5);
}
.filter-menu-body a {
  display: block;
  padding: 0.375rem 0;
  color: var(--linen-dim);
  font-size: 0.9375rem;
  border-bottom: none;
}
.filter-menu-body a:hover { color: var(--brass-bright); }

/* --------------------------------------------------------- skip link -- */
/* Visually hidden until focused: the first thing a keyboard or screen-reader
   user reaches, so they can jump the nav on every page. */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--brass);
  color: var(--ink);
  font-weight: 700;
  border-radius: 0 0 var(--r-md) 0;
  transform: translateY(-120%);
  transition: transform var(--dur-micro) var(--ease-lamp);
}
.skip-link:focus {
  transform: none;
  color: var(--ink);
}

/* --------------------------------------------------------- moderation -- */

.report-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.9375rem;
}
.report-list li {
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--hairline);
}
.report-list li:last-child { border-bottom: none; }

/* Reporting is deliberately understated — present and findable, but not
   competing with the player for attention on a page someone came to enjoy. */
.report-panel summary {
  cursor: pointer;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--linen-dim);
  list-style: none;
}
.report-panel summary::-webkit-details-marker { display: none; }
.report-panel summary:hover { color: var(--clay); }
.report-panel[open] summary { margin-bottom: 1rem; }

/* ------------------------------------------------------ reduced motion -- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  [data-aperture] .glow { opacity: 0.14; transform: none; }
}
