/* Throughline — one stylesheet, no build step, no external requests. */

/* ---------------------------------------------------------------------------
   Design tokens.

   Two accent axes carry the tool's central idea and must never be confused:
     --thread-mark  lime (dark) / teal-cyan (light) — thread relevance ONLY
     --adjacent     violet in both themes           — adjacency ONLY
   Neither is ever used for verdicts, links, borders, or chrome. The axes are
   also distinguished by *shape* (heavy numeral + chip vs. outlined bracket), so
   they survive greyscale and colour-blindness, not just hue.

   Legacy aliases at the bottom of each block keep the other screens coherent
   while only the Feed is restructured.
   --------------------------------------------------------------------------- */
:root {
  /* One width for the app shell, per page. The feed's two columns need more room
     than a prose page does, and the site header has to inset to whichever is in
     force — otherwise the wordmark hangs off to the left of the content it is
     supposed to sit above. Declaring it here and overriding it on body.feed-page
     means the header, the page container, and the feed cannot drift apart. */
  --shell: 62rem;

  --page:             #eef1f6;
  --surface:          #fbfcfe;
  --surface-sunken:   #f1f4f9;
  --surface-selected: #eef6fb;
  --surface-hover:    #f4f7fc;

  --border:        #d5dde9;
  --border-strong: #c2cddd;
  --border-soft:   #e6ebf2;

  --text-strong: #0b1526;
  --text:        #1b2b47;
  /* dim carries reading text (blurbs, method lines), so it is held at ~7:1 —
     AAA for normal text — not at the 4.5:1 AA floor. AA is what incidental text
     must clear, not what a paragraph someone actually reads should sit at.
     dimmer carries labels and timestamps and may sit lower.
     These were also the wrong way round: dim (5.62:1) was fainter than
     dimmer (6.12:1) in dark mode. */
  --text-dim:    #4a5872;   /* 7.6:1 on --surface */
  --text-dimmer: #616f8b;   /* 5.6:1 — was #8794aa, 2.99:1, below AA */

  --thread-mark:         #0b7896;
  --thread-mark-ink:     #ffffff;
  --thread-chip-line:    #b6d9e3;
  --thread-chip-fill:    #c4f4ff;
  --thread-chip-ink:     #0a3745;
  --thread-chip-fill-hi: #7bf0ff;
  --thread-chip-ink-hi:  #08303d;

  --adjacent:            #5b3fc4;
  --adjacent-dim:        #6a52c9;
  --adjacent-line:       #d5cbf7;
  --adjacent-line-sel:   #c6b9f2;
  --adjacent-wash:       #f5f2fe;
  /* A panel tint, lighter than the wash: the adjacency card was a filled
     violet block that outranked the verdict beside it. */
  --adjacent-tint:       #faf9ff;

  /* Swipe actions wear what the app already wears for the same two actions.
     The pane's Save is .btn-primary, filled with the accent; its Dismiss is
     .btn-tertiary, which has no fill at all. So: one accent and one neutral,
     not two competing hues.

     An earlier attempt gave them a green and a coral, chosen to sit as far as
     possible in hue from both accent axes. They did — and landed outside the
     palette entirely, reading as Christmas rather than as this app. Distance
     from the axes was the wrong thing to maximise; belonging was.

     Save takes the *highlight* chip fill rather than the thread mark itself,
     because that is the brightest voice the palette has and a full-height block
     of the darker teal reads as chrome. Skip takes --cool, which is already
     what a skipped card's numeral wears. */
  --swipe-save:      var(--thread-chip-fill-hi);
  --swipe-save-ink:  var(--thread-chip-ink-hi);
  --swipe-skip:      #6b7280;
  --swipe-skip-ink:  #ffffff;
  /* Star takes --hot, which the card language already uses to mean "worth
     reading" — apt for the tier that means read this next, and palette-native
     rather than a fourth invented hue.
     Not --warm, which was the first choice: in the dark theme amber sits only
     30 degrees from the acid on the Save button beside it, and two yellowish
     blocks side by side is the same confusion the green-and-coral pair had.
     --hot opens that to 54. */
  --swipe-star:      var(--hot);
  --swipe-star-ink:  #ffffff;

  --display: "Helvetica Neue", Helvetica, "Segoe UI", system-ui, sans-serif;
  --ui:      ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono:    ui-monospace, SFMono-Regular, Menlo, monospace;

  --radius: 6px;

  /* Legacy aliases — the non-Feed screens still reference these. */
  --bg:        var(--page);
  --ink:       var(--text);
  --ink-soft:  var(--text-dim);
  --ink-faint: var(--text-dimmer);
  --rule:      var(--border);
  --accent:    var(--thread-mark);
  --accent-bg: var(--surface-selected);
  --hot:       #b3462a;
  /* Amber as TEXT. --hot is a mark and a fill: at 4.23:1 it is below AA for
     words even on --surface, and "open access" is a word. Measured ~7:1. */
  --hot-ink:   #8a5209;
  --warm:      #8a6a12;
  --cool:      #6b7280;
  --danger:    #a12a2a;
}

/* ---------------------------------------------------------------------------
   Dark palette.

   The hex values live here once, as --d-* on :root, where they are inert — they
   define the palette but nothing consumes them. Two blocks below map them onto
   the real tokens, because the dark theme has to apply in two distinct
   situations that CSS cannot express in one rule: the reader's OS is dark and
   they have not overridden it, or they explicitly chose dark. A media query
   cannot be part of a selector list, so the mapping is stated twice.

   Only the mapping repeats. Change a colour in one place, above, and both paths
   follow. (Duplicating the values instead is exactly how db/schema.sql drifted
   away from its migrations.)
   --------------------------------------------------------------------------- */
:root {
  --d-page: #0a0f1a;
  --d-surface: #101a2c;
  --d-surface-sunken: #0c1422;
  --d-surface-selected: #16223a;
  --d-surface-hover: #141e31;
  --d-border: #202d46;
  --d-border-strong: #2c3c5a;
  --d-border-soft: #1a2437;
  --d-text-strong: #f4f8ff;
  --d-text: #dfe7f4;
  --d-text-dim: #98a5bd;
  --d-text-dimmer: #8b9ab4;
  --d-thread-mark: #d6f84a;
  --d-thread-mark-ink: #1a2200;
  --d-thread-chip-line: #4a5c17;
  --d-thread-chip-fill: transparent;
  --d-thread-chip-ink: #d6f84a;
  --d-thread-chip-fill-hi: #d6f84a;
  --d-thread-chip-ink-hi: #1a2200;
  --d-adjacent: #a98cff;
  --d-adjacent-dim: #a08ce0;
  --d-adjacent-line: #3b2c63;
  --d-adjacent-line-sel: #453372;
  --d-adjacent-wash: #181430;
  --d-adjacent-tint: #1b1830;
  --d-swipe-skip: #8a9098;
  --d-swipe-skip-ink: #0a0f1a;
  --d-swipe-star-ink: #2a1000;
  --d-hot: #e2764a;
  --d-hot-ink: #e0a537;
  --d-warm: #c9a44f;
  --d-cool: #8a9098;
  --d-danger: #d96a6a;
  /* Journal tints, per tone. Four sets, not four rewrites of one set. */
  /* Journal tints, light. Swapped wholesale in the dark blocks below. */
  /* A default, so a tile that somehow arrives without a tone class still
     follows the theme. The fallbacks inside the gradient below are literal
     light values, and would otherwise paint a pale card on a dark page. */
  --js: var(--t0-s); --jl: var(--t0-l); --jt: var(--t0-t);
  --t0-s: 58%; --t0-l: 44%; --t0-t: 96%;
  --t1-s: 26%; --t1-l: 40%; --t1-t: 96.5%;
  --t2-s: 72%; --t2-l: 58%; --t2-t: 95%;
  --t3-s: 42%; --t3-l: 26%; --t3-t: 97%;

  --d-t0-s: 55%; --d-t0-l: 62%; --d-t0-t: 13%;
  --d-t1-s: 24%; --d-t1-l: 55%; --d-t1-t: 12%;
  --d-t2-s: 74%; --d-t2-l: 70%; --d-t2-t: 15%;
  --d-t3-s: 40%; --d-t3-l: 44%; --d-t3-t: 11%;
}

/* Following the machine — and :not([data-theme="light"]) is what makes an
   explicit light choice stick on a dark OS. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --page: var(--d-page);
    --surface: var(--d-surface);
    --surface-sunken: var(--d-surface-sunken);
    --surface-selected: var(--d-surface-selected);
    --surface-hover: var(--d-surface-hover);
    --border: var(--d-border);
    --border-strong: var(--d-border-strong);
    --border-soft: var(--d-border-soft);
    --text-strong: var(--d-text-strong);
    --text: var(--d-text);
    --text-dim: var(--d-text-dim);
    --text-dimmer: var(--d-text-dimmer);
    --thread-mark: var(--d-thread-mark);
    --t0-s: var(--d-t0-s); --t0-l: var(--d-t0-l); --t0-t: var(--d-t0-t);
    --t1-s: var(--d-t1-s); --t1-l: var(--d-t1-l); --t1-t: var(--d-t1-t);
    --t2-s: var(--d-t2-s); --t2-l: var(--d-t2-l); --t2-t: var(--d-t2-t);
    --t3-s: var(--d-t3-s); --t3-l: var(--d-t3-l); --t3-t: var(--d-t3-t);
    --thread-mark-ink: var(--d-thread-mark-ink);
    --thread-chip-line: var(--d-thread-chip-line);
    --thread-chip-fill: var(--d-thread-chip-fill);
    --thread-chip-ink: var(--d-thread-chip-ink);
    --thread-chip-fill-hi: var(--d-thread-chip-fill-hi);
    --thread-chip-ink-hi: var(--d-thread-chip-ink-hi);
    --adjacent: var(--d-adjacent);
    --adjacent-dim: var(--d-adjacent-dim);
    --adjacent-line: var(--d-adjacent-line);
    --adjacent-line-sel: var(--d-adjacent-line-sel);
    --adjacent-wash: var(--d-adjacent-wash);
    --adjacent-tint: var(--d-adjacent-tint);
    --swipe-skip: var(--d-swipe-skip);
    --swipe-skip-ink: var(--d-swipe-skip-ink);
    --swipe-star-ink: var(--d-swipe-star-ink);
    --hot: var(--d-hot);
    --hot-ink: var(--d-hot-ink);
    --warm: var(--d-warm);
    --cool: var(--d-cool);
    --danger: var(--d-danger);
  }
}

/* Chosen deliberately, whatever the machine says. */
:root[data-theme="dark"] {
    --page: var(--d-page);
    --surface: var(--d-surface);
    --surface-sunken: var(--d-surface-sunken);
    --surface-selected: var(--d-surface-selected);
    --surface-hover: var(--d-surface-hover);
    --border: var(--d-border);
    --border-strong: var(--d-border-strong);
    --border-soft: var(--d-border-soft);
    --text-strong: var(--d-text-strong);
    --text: var(--d-text);
    --text-dim: var(--d-text-dim);
    --text-dimmer: var(--d-text-dimmer);
    --thread-mark: var(--d-thread-mark);
    --t0-s: var(--d-t0-s); --t0-l: var(--d-t0-l); --t0-t: var(--d-t0-t);
    --t1-s: var(--d-t1-s); --t1-l: var(--d-t1-l); --t1-t: var(--d-t1-t);
    --t2-s: var(--d-t2-s); --t2-l: var(--d-t2-l); --t2-t: var(--d-t2-t);
    --t3-s: var(--d-t3-s); --t3-l: var(--d-t3-l); --t3-t: var(--d-t3-t);
    --thread-mark-ink: var(--d-thread-mark-ink);
    --thread-chip-line: var(--d-thread-chip-line);
    --thread-chip-fill: var(--d-thread-chip-fill);
    --thread-chip-ink: var(--d-thread-chip-ink);
    --thread-chip-fill-hi: var(--d-thread-chip-fill-hi);
    --thread-chip-ink-hi: var(--d-thread-chip-ink-hi);
    --adjacent: var(--d-adjacent);
    --adjacent-dim: var(--d-adjacent-dim);
    --adjacent-line: var(--d-adjacent-line);
    --adjacent-line-sel: var(--d-adjacent-line-sel);
    --adjacent-wash: var(--d-adjacent-wash);
    --adjacent-tint: var(--d-adjacent-tint);
    --swipe-skip: var(--d-swipe-skip);
    --swipe-skip-ink: var(--d-swipe-skip-ink);
    --swipe-star-ink: var(--d-swipe-star-ink);
    --hot: var(--d-hot);
    --hot-ink: var(--d-hot-ink);
    --warm: var(--d-warm);
    --cool: var(--d-cool);
    --danger: var(--d-danger);
}

* { box-sizing: border-box; }

/* The hidden attribute, made to actually hide.
   [hidden] { display: none } lives in the browser's own stylesheet, so ANY
   author display declaration outranks it — and elements worth hiding are
   usually flex or grid containers, which is to say exactly the ones that
   declare display. That bit three times in one evening: a menu that was never
   closed, an action row that never went away, and the audio player beneath it.
   Stated once, with !important, because the whole point is to beat a
   declaration somewhere further down. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--page);
  color: var(--text);
  /* Sans throughout — the serif body face is dropped in the redesign. */
  font: 400 15px/1.55 var(--ui);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, .brand { font-family: var(--display); letter-spacing: -.02em; }

main { max-width: var(--shell); margin: 0 auto; padding: 1.5rem 1.25rem 5rem; }

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

h1 { font-size: 1.6rem; margin: .4rem 0 .3rem; letter-spacing: -.01em; }
h2 { font-size: 1.15rem; }

.lede { color: var(--ink-soft); max-width: 48rem; margin: 0 0 1.5rem; }
.muted { color: var(--ink-faint); }
.small { font-size: .85rem; }
.err   { color: var(--danger); font-size: .85rem; }

/* --- top bar --------------------------------------------------------------- */
.topbar {
  display: flex; align-items: baseline; gap: 1.5rem;
  /* Full-bleed background and rule, but the contents line up with the shell
     below. max() keeps the normal 1.25rem gutter on narrow screens, where the
     centring term goes negative. */
  padding: .9rem max(1.25rem, calc((100% - var(--shell)) / 2 + 1.25rem));
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
  position: sticky; top: 0; z-index: 10;
}
.brand {
  font-weight: 700; font-size: 1.05rem; letter-spacing: -.02em;
  text-decoration: none; color: var(--ink);
}
.brand span { color: var(--accent); }
.topbar nav { display: flex; gap: 1.1rem; }
.topbar nav a {
  text-decoration: none; color: var(--ink-soft); font-size: .92rem;
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.topbar nav a.on { color: var(--ink); font-weight: 600; border-bottom: 2px solid var(--accent); }

/* --- filters --------------------------------------------------------------- */
.filters { margin-bottom: 1.4rem; font-family: ui-sans-serif, system-ui, sans-serif; }
.filterset { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin-bottom: .5rem; }
.spacer { flex: 1 1 auto; }

.chip {
  display: inline-block; padding: .25rem .7rem;
  border: 1px solid var(--rule); border-radius: 100px;
  font-size: .85rem; text-decoration: none; color: var(--ink-soft);
  background: var(--surface);
}
.chip.small { font-size: .78rem; padding: .18rem .6rem; }
.chip.on { background: var(--accent-bg); border-color: var(--accent); color: var(--accent); font-weight: 600; }

.relfilter { font-size: .8rem; color: var(--ink-faint); }
.relfilter a { text-decoration: none; padding: 0 .3rem; color: var(--ink-soft); }
.relfilter a.on { color: var(--accent); font-weight: 700; }

.count { font-size: .85rem; color: var(--ink-faint); margin: 0 0 .8rem; font-family: ui-sans-serif, system-ui, sans-serif; }

/* --- article cards --------------------------------------------------------- */
.card {
  display: flex; gap: 1rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--cool);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  margin-bottom: .9rem;
}
.card.hot  { border-left-color: var(--hot); }
.card.warm { border-left-color: var(--warm); }

.score { flex: 0 0 3.2rem; text-align: center; font-family: ui-sans-serif, system-ui, sans-serif; }
.score .num { display: block; font-size: 1.5rem; font-weight: 700; line-height: 1; }
.card.hot  .num { color: var(--hot); }
.card.warm .num { color: var(--warm); }
.card.cool .num { color: var(--cool); }
.verdict {
  display: inline-block; margin-top: .3rem;
  font-size: .65rem; text-transform: uppercase; letter-spacing: .06em;
  padding: .1rem .35rem; border-radius: 3px; border: 1px solid var(--rule);
  color: var(--ink-faint);
}
.v-read { color: var(--hot); border-color: var(--hot); }
.v-skim { color: var(--warm); border-color: var(--warm); }

/* Shown only in a thread view, when the verdict was earned on a different
   thread than the one being displayed. */
.overall {
  display: block; margin-top: .25rem;
  font-size: .65rem; color: var(--ink-faint); letter-spacing: .02em;
}

.body { flex: 1 1 auto; min-width: 0; }
.card h2 { margin: 0 0 .25rem; font-size: 1.08rem; line-height: 1.35; font-weight: 600; }
.card h2 a { color: var(--ink); text-decoration: none; }
.card h2 a:hover { color: var(--accent); text-decoration: underline; }

.meta { margin: 0 0 .6rem; font-size: .82rem; color: var(--ink-faint); font-family: ui-sans-serif, system-ui, sans-serif; }
.meta .journal { color: var(--ink-soft); font-weight: 600; }
.meta .oa { color: var(--warm); }
.authors { font-style: italic; }

.blurb { margin: 0 0 .7rem; color: var(--ink); }

.threads { list-style: none; margin: 0 0 .7rem; padding: 0; font-family: ui-sans-serif, system-ui, sans-serif; }
.threads li {
  display: flex; gap: .5rem; align-items: baseline;
  font-size: .82rem; padding: .18rem 0; border-top: 1px solid var(--rule);
}
.tname { font-weight: 600; color: var(--ink-soft); flex: 0 0 auto; }
.trel  { font-variant-numeric: tabular-nums; color: var(--accent); font-weight: 700; flex: 0 0 2rem; }
.twhy  { color: var(--ink-faint); }

.actions { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.actions form { margin: 0; }

button, .pdf, .doi {
  font: inherit; font-family: ui-sans-serif, system-ui, sans-serif; font-size: .8rem;
  padding: .25rem .7rem; border: 1px solid var(--rule); border-radius: 4px;
  background: var(--surface); color: var(--ink-soft); cursor: pointer; text-decoration: none;
}
button:hover, .pdf:hover, .doi:hover { border-color: var(--accent); color: var(--accent); }

/* The rule above assumes a button drawn ON the page background, where turning
   the text accent-coloured reads as a highlight. A button FILLED with the
   accent is the opposite case: its ink becomes its own background and the
   label vanishes. `button:hover` (0,1,1) also outranks a plain class (0,1,0),
   so the filled buttons cannot simply state their colour once and be left
   alone — they have to restate it here.

   This is what made the dial's icon "disappear", and it took six attempts to
   find because of where it hides. A mouse un-hovers when it leaves, so on a
   desktop the icon flickers accent-on-accent for as long as the pointer rests
   there and looks fine either side of that. A touch has no such exit: iOS
   applies :hover on tap and leaves it applied until you touch something else.
   So the phone showed a permanently blank button and the desktop showed
   nothing at all wrong, and every fix went to the icon, which was never hurt.

   Any new button filled with an accent belongs in this list. */
.paneFabBtn:hover, .paneFabBtn:active, .paneFabBtn:focus,
.nowplayBtn:hover, .nowplayBtn:active, .nowplayBtn:focus,
.btn-primary:hover, .btn-primary:active, .btn-primary:focus {
  color: var(--thread-mark-ink);
  border-color: transparent;
}
button.danger { color: var(--danger); border-color: transparent; }
button.danger:hover { border-color: var(--danger); }

/* --- empty state ----------------------------------------------------------- */
.empty {
  border: 1px dashed var(--rule); border-radius: var(--radius);
  padding: 2.5rem 1.5rem; text-align: center; color: var(--ink-soft);
}
.empty p { margin: .3rem 0; }

.notice {
  background: var(--accent-bg); border: 1px solid var(--accent);
  color: var(--accent); padding: .6rem .9rem; border-radius: var(--radius);
  font-size: .9rem;
}

/* --- journals: the shelf --------------------------------------------------- */
.shelfbar {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  margin: 1.5rem 0 .5rem; font-family: ui-sans-serif, system-ui, sans-serif;
}
.sortlabel { font-size: .75rem; text-transform: uppercase; letter-spacing: .07em;
             color: var(--ink-faint); margin-right: .2rem; }
.metricnote { font-size: .74rem; color: var(--ink-faint); border-bottom: 1px dotted var(--ink-faint);
              cursor: help; }

.shelf { margin: 1.6rem 0 2.2rem; }
.shelflabel {
  margin: 0 0 .7rem; font-size: .76rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--ink-faint); font-family: ui-sans-serif, system-ui, sans-serif; font-weight: 600;
  display: flex; align-items: center; gap: .5rem;
}
.shelflabel::after {
  /* the shelf board the tiles stand on */
  content: ''; flex: 1 1 auto; height: 1px;
  background: linear-gradient(90deg, var(--rule), transparent);
}
.shelflabel span {
  order: -1; background: var(--rule); color: var(--ink-soft);
  border-radius: 100px; padding: .05rem .45rem; font-size: .7rem; letter-spacing: 0;
}

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
  gap: .85rem;
}

/* A periodical standing on the shelf: portrait, spine down the left edge.
   Roughly 3:4, near enough to a trimmed journal cover to read as one. */
.tile {
  display: flex; flex-direction: column;
  min-height: 16.5rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 5px solid var(--cool);
  border-radius: 3px var(--radius) var(--radius) 3px;
  padding: .85rem .9rem .7rem;
  position: relative;
}
/* Identity, not signal: the spine and tint come from a hue hashed off the ISSN,
   so each journal is recognizable at a glance and keeps its color permanently.
   The citedness band still colors the metric below — the two carry different
   information and should not share a channel. */
/* Four tone tiers, so two journals sharing a hue still read as different.
   --js / --jl are the spine's saturation and lightness; --jt is the tint. */
/* Each tone points at tokens rather than carrying its own numbers. A literal
   here is set ON the tile, which outranks anything :root says, so the dark
   palette could never reach these and every tile stayed at light-mode
   lightness in both themes — a pale card under text that had correctly
   turned pale with it. */
.tile.tone-0 { --js: var(--t0-s); --jl: var(--t0-l); --jt: var(--t0-t); }
.tile.tone-1 { --js: var(--t1-s); --jl: var(--t1-l); --jt: var(--t1-t); }
.tile.tone-2 { --js: var(--t2-s); --jl: var(--t2-l); --jt: var(--t2-t); }
.tile.tone-3 { --js: var(--t3-s); --jl: var(--t3-l); --jt: var(--t3-t); }

.tile {
  border-left-color: hsl(var(--jhue, 220) var(--js, 50%) var(--jl, 44%));
  background:
    linear-gradient(hsl(var(--jhue, 220) 45% var(--jt, 96%)),
                    hsl(var(--jhue, 220) 45% var(--jt, 96%))),
    var(--surface);
}
.tile::before {
  /* a slim head-band, the way a cover carries a masthead rule */
  content: ''; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: hsl(var(--jhue, 220) 42% 46%); opacity: .35;
}

.tile.paused { opacity: .5; }
.tile.paused:hover { opacity: 1; }

.tilehead { flex: 1 1 auto; }
.tile h3 {
  margin: 0 0 .3rem; font-size: .93rem; line-height: 1.28; font-weight: 600;
  letter-spacing: -.01em; hyphens: auto;
}
.tile .pub {
  margin: 0; font-size: .74rem; color: var(--ink-faint);
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.tile .desc {
  margin: .5rem 0 .2rem; font-size: .78rem; line-height: 1.42;
  color: var(--ink-soft);
}
.tile .desc.none { color: var(--ink-faint); font-style: italic; }
.tile .desc.none code { font-style: normal; font-size: .72rem; }

.editlink {
  font-size: .66rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-faint); text-decoration: none;
  font-family: ui-sans-serif, system-ui, sans-serif;
  opacity: 0; transition: opacity .12s;
}
.tile:hover .editlink, .editlink:focus { opacity: 1; color: var(--accent); }

.descedit { margin: .5rem 0 .2rem; }
.descedit textarea { font-size: .78rem; line-height: 1.4; padding: .35rem .45rem; }
.descops { display: flex; gap: .5rem; align-items: center; margin-top: .35rem; }
.descops a {
  font-size: .7rem; color: var(--ink-faint);
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.tilemetrics {
  display: flex; gap: .7rem; align-items: baseline;
  margin: .7rem 0 .55rem; padding-top: .55rem;
  border-top: 1px solid var(--rule);
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.metric { display: flex; flex-direction: column; cursor: help; }
.metric .mval {
  font-size: .95rem; font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--ink-soft); line-height: 1.1;
}
.metric.primary .mval { font-size: 1.35rem; }
.tile.band-high .metric.primary .mval { color: var(--hot); }
.tile.band-mid  .metric.primary .mval { color: var(--warm); }
.metric .mlabel {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-faint);
}

.tilefoot { font-family: ui-sans-serif, system-ui, sans-serif; }
.tags { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .35rem; min-height: 1rem; }
.tag {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .05em;
  padding: .08rem .35rem; border-radius: 3px; border: 1px solid var(--rule);
  color: var(--ink-faint);
}
.tag.oa  { color: var(--warm); border-color: var(--warm); }
.tag.off { color: var(--ink-faint); }
.tag.bad { color: var(--danger); border-color: var(--danger); cursor: help; }

.tile .issn {
  margin: 0 0 .5rem; font-size: .68rem; color: var(--ink-faint);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.tileops { display: flex; gap: .35rem; }
.tileops form { margin: 0; flex: 0 0 auto; }
.tileops button { font-size: .72rem; padding: .18rem .5rem; }

.searchbar { display: flex; gap: .5rem; margin-bottom: 1rem; }
.searchbar input[type=text] { flex: 1 1 auto; }

input[type=text], input[type=password], textarea {
  font: inherit; font-size: .92rem; padding: .45rem .6rem;
  border: 1px solid var(--rule); border-radius: 4px;
  background: var(--surface); color: var(--ink); width: 100%;
}
input.tiny { width: 9rem; font-size: .8rem; padding: .25rem .4rem; }
textarea { font-family: inherit; line-height: 1.5; resize: vertical; }
textarea.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .82rem; }

.results { list-style: none; padding: 0; margin: 0 0 1.5rem; }
.results li {
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  padding: .6rem .2rem; border-bottom: 1px solid var(--rule);
}
.results form { display: flex; gap: .4rem; align-items: center; }
.results .muted { display: block; font-size: .8rem; }

/* --- threads --------------------------------------------------------------- */
.thread {
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--radius); padding: 1.1rem; margin-bottom: 1.2rem;
}
.threadhead { display: flex; align-items: center; gap: .8rem; margin-bottom: .9rem; }
.tname-input { font-size: 1.05rem; font-weight: 600; flex: 1 1 auto; }
.threadhead code { font-size: .78rem; color: var(--ink-faint); }
.toggle { font-size: .85rem; color: var(--ink-soft); white-space: nowrap;
          font-family: ui-sans-serif, system-ui, sans-serif; }
.fieldlabel {
  display: block; margin: .9rem 0 .3rem; font-size: .82rem; font-weight: 600;
  color: var(--ink-soft); font-family: ui-sans-serif, system-ui, sans-serif;
}
.fieldlabel .muted { font-weight: 400; }
.thread button { margin-top: .9rem; }

/* --- runs ------------------------------------------------------------------ */
.runs { font-family: ui-sans-serif, system-ui, sans-serif; font-size: .85rem; }
.runs th {
  text-align: left; padding: .4rem; border-bottom: 2px solid var(--rule);
  font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-faint);
}
.runs td { padding: .45rem .4rem; border-bottom: 1px solid var(--rule); white-space: nowrap; }
.runs td.notes { white-space: normal; color: var(--ink-faint); font-size: .8rem; }
.runs tr.unfinished { background: color-mix(in srgb, var(--danger) 7%, transparent); }

/* --- adjacency ------------------------------------------------------------- */
.chip.adj.on { background: var(--warm); border-color: var(--warm); color: var(--surface); }
.chip .badge {
  display: inline-block; margin-left: .25rem; padding: 0 .3rem;
  border-radius: 100px; background: var(--warm); color: var(--surface);
  font-size: .7rem; font-weight: 700;
}
.chip.adj.on .badge { background: var(--surface); color: var(--warm); }

.adjacency {
  margin: 0 0 .6rem; padding: .4rem .6rem;
  border-left: 2px solid var(--warm);
  background: color-mix(in srgb, var(--warm) 7%, transparent);
  font-size: .82rem; color: var(--ink-soft);
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.alabel {
  font-size: .64rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--warm); font-weight: 700; margin-right: .4rem;
}
.suggests { display: inline-block; margin-left: .3rem; font-weight: 600; text-decoration: none; }
.suggests:hover { text-decoration: underline; }

/* --- emergent threads ------------------------------------------------------ */
.emergent {
  background: var(--surface); border: 1px solid var(--rule);
  border-left: 3px solid var(--warm); border-radius: var(--radius);
  padding: .9rem 1rem; margin-bottom: .8rem;
}
.ehead { display: flex; align-items: baseline; gap: .7rem; flex-wrap: wrap; }
.emergent h2 { margin: 0; font-size: 1.02rem; font-weight: 600; }
.emeta {
  font-size: .76rem; color: var(--ink-faint);
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.promote {
  margin-left: auto; font-size: .76rem; text-decoration: none;
  font-family: ui-sans-serif, system-ui, sans-serif; white-space: nowrap;
  border: 1px solid var(--accent); border-radius: 4px; padding: .15rem .55rem;
}
.promote:hover { background: var(--accent-bg); }

.ework { list-style: none; margin: .6rem 0 0; padding: 0; }
.ework li {
  display: flex; gap: .5rem; align-items: baseline; padding: .25rem 0;
  border-top: 1px solid var(--rule); font-size: .82rem;
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.escore { flex: 0 0 1.8rem; font-weight: 700; color: var(--warm); font-variant-numeric: tabular-nums; }
.etitle { color: var(--ink-soft); }
.ewhy   { color: var(--ink-faint); }
.emore  { color: var(--ink-faint); font-style: italic; }

/* --- thread spinner -------------------------------------------------------- */
.threadhead-actions { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; }
.newthread {
  margin-left: auto; font-size: .82rem; text-decoration: none;
  font-family: ui-sans-serif, system-ui, sans-serif;
  border: 1px solid var(--accent); border-radius: 4px; padding: .25rem .7rem;
}
.newthread:hover { background: var(--accent-bg); }

.spinform {
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--radius); padding: 1.2rem; max-width: 46rem;
}
.spinform button { margin-top: 1rem; padding: .4rem 1rem; }
.jhead { margin: 1.6rem 0 .3rem; font-size: 1rem; }

.candidates { list-style: none; padding: 0; margin: .7rem 0 0; }
.cand {
  padding: .55rem .1rem; border-top: 1px solid var(--rule);
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.cand label { display: flex; align-items: center; gap: .45rem; cursor: pointer; }
.cname { font-weight: 600; font-size: .92rem; }
.ctag {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .05em;
  padding: .05rem .3rem; border-radius: 3px; border: 1px solid var(--rule);
  color: var(--ink-faint);
}
.ctag.renamed { color: var(--warm); border-color: var(--warm); cursor: help; }
.cwhy  { margin: .2rem 0 .15rem 1.55rem; font-size: .8rem; color: var(--ink-soft); }
.cmeta { margin: 0 0 0 1.55rem; font-size: .74rem; color: var(--ink-faint); }
.cstatus { color: var(--danger); }
.cand.s-ok .cstatus { color: var(--ink-faint); }
.cand.s-dormant, .cand.s-notfound { opacity: .6; }

/* --- citations ------------------------------------------------------------- */
.card.cite .score .num { color: var(--accent); font-size: 1.35rem; }
.card.cite .mlabel {
  display: block; font-size: .58rem; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-faint);
}
/* A citation from outside every thread is the signal worth noticing, not the
   noise — someone found your work useful for something you are not tracking. */
.card.offthread { border-left-color: var(--accent); }

.citeswhat {
  margin: 0 0 .6rem; font-size: .76rem; color: var(--ink-faint);
  font-family: ui-sans-serif, system-ui, sans-serif;
  padding-left: .6rem; border-left: 2px solid var(--rule);
}

.citescores {
  display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
  margin: 0 0 .5rem; font-size: .78rem; color: var(--ink-faint);
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.cs strong { color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.offtag {
  font-size: .64rem; text-transform: uppercase; letter-spacing: .05em;
  padding: .08rem .4rem; border-radius: 3px;
  border: 1px solid var(--accent); color: var(--accent); cursor: help;
}

.workrail { list-style: none; padding: 0; margin: .4rem 0 1.6rem; }
.workrail li { border-bottom: 1px solid var(--rule); }
.workrail a {
  display: flex; gap: .6rem; align-items: baseline; padding: .3rem .2rem;
  text-decoration: none; color: var(--ink-soft); font-size: .82rem;
  font-family: ui-sans-serif, system-ui, sans-serif;
}
.workrail a:hover { background: var(--accent-bg); }
.workrail a.on { color: var(--accent); font-weight: 600; }
.wcount {
  flex: 0 0 2rem; text-align: right; font-weight: 700;
  color: var(--accent); font-variant-numeric: tabular-nums;
}
.wtitle { flex: 1 1 auto; }
.wyear  { color: var(--ink-faint); font-size: .74rem; }

/* --- login ----------------------------------------------------------------- */
.login-body { display: grid; place-items: center; min-height: 100vh; }
.login {
  background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 2rem; width: min(22rem, 90vw); text-align: center;
}
.login h1 { margin-top: 0; font-size: 1.2rem; letter-spacing: -.01em; }
.login input { margin-bottom: .7rem; text-align: center; }
.login button { width: 100%; padding: .5rem; }
.login .error { color: var(--danger); font-size: .88rem; }

/* Wide content should scroll inside itself, never the page. */
table { display: block; overflow-x: auto; }

@media (max-width: 34rem) {
  .card { flex-direction: column; gap: .5rem; }
  .score { display: flex; align-items: baseline; gap: .5rem; text-align: left; }
  .ops { justify-content: flex-start; }

  /* The brand plus four nav links do not fit on a narrow phone, and an
     unwrapped flex row pushes the whole document sideways. Let it wrap. */
  .topbar { flex-wrap: wrap; gap: .4rem 1rem; padding: .7rem .9rem; }
  .topbar nav { flex-wrap: wrap; gap: .85rem; }

  main { padding: 1rem .9rem 4rem; }
  .searchbar { flex-wrap: wrap; }
  .results li { flex-direction: column; align-items: flex-start; gap: .5rem; }
}

/* ===========================================================================
   FEED — split rail redesign
   ===========================================================================
   A two-pane split: a scannable rail of complete triage rows on the left, one
   article at full depth on the right. The screen does triage, not reading, so
   the rail carries exactly enough to decide open-or-not: both numbers, the
   verdict, the matched thread, one clause of prose, the suggested direction,
   and abbreviated meta.

   Selection lives entirely in the querystring, so this works with JavaScript
   disabled and deep links and browser history behave correctly.
   =========================================================================== */

.feed-shell {
  display: grid;
  grid-template-columns: 440px 1fr;
  height: calc(100vh - 6.5rem);      /* below the sticky site header */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
/* The feed manages its own scrolling, so the page itself must not.
   Capped and centred rather than full-bleed: the pane's prose is held at a 46rem
   measure, so on a wide display the shell stretched to 1860px while the text
   filled 736px of it and left 673px of dead space hard against the right edge.
   Centring the text alone would only move the imbalance — it would tear a gap
   between the rail and the passage it belongs to. The whole two-column unit
   travels together instead. */
body.feed-page { --shell: 80rem; }
body.feed-page main {
  max-width: var(--shell);
  margin-inline: auto;
  padding: 1rem 1.25rem 1.25rem;
}

/* --- rail ------------------------------------------------------------------ */
.rail {
  display: flex; flex-direction: column;
  min-height: 0;                     /* load-bearing: without it the rail
                                        overflows its grid track and clips */
  border-right: 1px solid var(--border);
}

.railbar {
  flex: none;
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-sunken);
  font-family: var(--ui);
}
.railbar select {
  font: 500 12px/1 var(--ui);
  padding: 5px 9px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
}
.railbar .statefilter { display: flex; gap: 7px; }
.railbar .statefilter a {
  font: 500 12px/1 var(--ui); color: var(--text-dim); text-decoration: none;
}
.railbar .statefilter a.on { color: var(--text-strong); font-weight: 700; }
.railbar .floor { font: 500 12px/1 var(--ui); color: var(--text-dim); }
.railbar .floor b { color: var(--thread-mark); font-weight: 700; }
.railbar .railcount { font: 400 12px/1 var(--ui); color: var(--text-dimmer); }
.railbar .railcount a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 2px;
}
.railbar .railcount a:hover { color: var(--text); }

/* "12 new since Friday" — a count and a filter in one control. Weighted above
   the other railbar text because it is the one thing here that is only true
   right now; the rest of the bar says the same thing on every visit. */
.railbar .railnewline { flex: 0 0 100%; display: flex; }
.railbar .newsince {
  display: inline-flex; align-items: center; gap: 6px;
  font: 600 12px/1 var(--ui);
  color: var(--thread-mark);
  text-decoration: none;
  border: 1px solid var(--thread-chip-line);
  border-radius: 999px;
  padding: 4px 9px;
  white-space: nowrap;
}
.railbar .newsince:hover { background: var(--surface-hover); }
.railbar .newsince.on {
  background: var(--thread-chip-fill);
  color: var(--thread-chip-ink);
  border-color: transparent;
}

.railscroll { overflow-y: auto; min-height: 0; }

.railrow {
  display: grid;
  grid-template-columns: 46px 1fr;
  gap: 12px;
  padding: 14px 16px 15px;
  border-bottom: 1px solid var(--border-soft);
  text-decoration: none;
  color: inherit;
}
/* No transition — a delay makes fast scanning feel laggy. */
.railrow:hover { background: var(--surface-hover); }
.railrow:focus-visible { outline: 2px solid var(--thread-mark); outline-offset: -2px; }
.railrow.on {
  background: var(--surface-selected);
  /* An inset shadow, not a border. A border occupies layout, so it had to be
     paid for with padding-left: 13px — which only added up to the unselected
     16px on desktop. The mobile override drops base padding to 14px, so a
     selected row sat 2px right of its neighbours and the list appeared to
     twitch sideways as the selection moved. A shadow takes no space, so the
     compensation is not needed at any width and cannot fall out of step with
     it again. */
  box-shadow: inset 3px 0 0 var(--thread-mark);
}
.railrow.done { opacity: .55; }

.railgutter { text-align: right; min-width: 0; }
.railnum {
  display: block;
  font: 800 22px/1 var(--display);
  letter-spacing: -.04em;
  font-variant-numeric: tabular-nums;
  color: var(--thread-mark);
}
.railrow.on .railnum { color: var(--text-strong); }
.railverdict {
  display: block; margin-top: 5px;
  font: 700 11px/1 var(--ui); letter-spacing: .09em;
  color: var(--text-dim); text-transform: uppercase;
}
/* Adjacency is a bracket, not a numeral — a different shape from thread
   relevance so the two axes never blur together. */
.adjbracket {
  display: inline-block; margin-top: 9px;
  font: 700 11.5px/1 var(--ui);
  color: var(--adjacent);
  border: 1px solid var(--adjacent-line);
  border-radius: 3px;
  padding: 3px 4px;
  font-variant-numeric: tabular-nums;
}
.railrow.on .adjbracket { border-color: var(--adjacent-line-sel); }

.threadchip {
  display: inline-block; margin-bottom: 7px;
  font: 700 11px/1 var(--ui); letter-spacing: .07em; text-transform: uppercase;
  border-radius: 2px; padding: 3px 5px;
  color: var(--thread-chip-ink);
  background: var(--thread-chip-fill);
  border: 1px solid var(--thread-chip-line);
}
/* Access, beside the thread chip and deliberately quieter than it. What the
   article is about is the reason to read it; whether it opens without a login
   is logistics, and logistics should not outshout the subject.

   Outlined rather than filled, so it cannot be mistaken for a thread chip at a
   glance, and so the two states differ in weight as well as hue — the closed
   one recedes, which is the honest rendering of "this will cost you a step". */
.accesschip {
  display: inline-block; margin: 0 0 7px 5px;
  font: 700 10px/1 var(--ui); letter-spacing: .06em; text-transform: uppercase;
  border-radius: 2px; padding: 3px 5px;
  background: none;
  border: 1px solid currentColor;
}
.accesschip.is-open   { color: var(--warm); }
.accesschip.is-closed { color: var(--text-dimmer); }

.railrow.on .threadchip,
.pane .threadchip {
  background: var(--thread-chip-fill-hi);
  color: var(--thread-chip-ink-hi);
  border-color: transparent;
  padding: 4px 6px;
}

/* Arrival marker. A dot rather than a "NEW" pill: the row already carries a
   score, a verdict, an adjacency bracket and a thread chip, and a fifth badge
   competing for the same glance would cost more than it tells. The dot is
   noticed while scanning and ignored while reading, which is the whole job.
   The railbar count says what it means, so the dot never has to. */
.railflags {
  display: flex; align-items: center; gap: 7px;
  margin-bottom: 7px;
}
.railflags .threadchip { margin-bottom: 0; }

.newdot {
  flex: none;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--thread-mark);
}

/* In the pane there is room to say it in words. */
.newflag {
  display: inline-flex; align-items: center; gap: 6px;
  margin-right: 8px;
  font: 700 11px/1 var(--ui); letter-spacing: .07em; text-transform: uppercase;
  color: var(--thread-mark);
}

/* The row's parts are <span>s so the whole row can be one anchor (a block-level
   child of an <a> is legal, but keeping it spans avoids invalid nesting when
   the title carries a link). They must be told to stack. */
.railbody { display: block; min-width: 0; }

.railtitle {
  display: block;
  margin: 0;
  font: 700 16.5px/1.3 var(--display);
  letter-spacing: -.02em;
  color: var(--text);
  text-wrap: pretty;
}
.railrow.on .railtitle { color: var(--text-strong); }

.railhook {
  display: block;
  margin: 6px 0 0;
  font: 400 13.5px/1.5 var(--ui);
  color: var(--text-dim);
}
.raildirection {
  display: block; margin-top: 8px;
  font: 600 11.5px/1.35 var(--ui);
  color: var(--adjacent-dim);
}
.railmeta {
  display: block;
  margin: 8px 0 0;
  font: 400 12.5px/1.4 var(--ui);
  /* --text-dim, not --text-dimmer: this is the journal name and date, which is
     read, not decoration. dimmer is for chrome that only needs to be noticed. */
  color: var(--text-dim);
}
.railfoot {
  padding: 12px 16px;
  font: 400 11.5px/1 var(--ui);
  color: var(--text-dimmer);
}
.railempty {
  padding: 24px 16px;
  font: 400 13px/1.6 var(--ui);
  color: var(--text-dim);
}
.railempty a { color: var(--thread-mark); }

/* --- detail pane ------------------------------------------------------------ */
.pane { padding: 26px 30px; min-height: 0; overflow-y: auto; }

.panehead { display: flex; gap: 16px; align-items: flex-start; margin-bottom: 18px; }
.panescore { flex: none; text-align: center; }
.panenum {
  display: block;
  font: 800 42px/1 var(--display); letter-spacing: -.05em;
  font-variant-numeric: tabular-nums;
  color: var(--text-strong);
}
.panescore .railverdict { margin-top: 6px; letter-spacing: .12em; }
.panescore .adjbracket { margin-top: 12px; font-size: 13px; }

.panetitle {
  margin: 0;
  font: 800 26px/1.14 var(--display); letter-spacing: -.035em;
  color: var(--text-strong);
  text-wrap: pretty;
}
.panesource { margin: 8px 0 0; font: 400 13.5px/1.45 var(--ui); color: var(--text-dim); }
/* The publication is a name; the date beside it is not. Weight separates
   them without a second colour or a second size. */
.panevenue { font-weight: 600; color: var(--text); }
/* Room between the credits and the writing. Four pixels read as a wrapped
   line of the same block rather than the end of one. */
.paneauthors { margin: 4px 0 18px; font: 400 13.5px/1.45 var(--ui); color: var(--text-dimmer); }

.panelede {
  font: 500 17.5px/1.6 var(--ui); letter-spacing: -.006em;
  color: var(--text-strong); margin: 0 0 14px;
}
/* The longest passage on the screen, and it was the faintest thing in the pane:
   14px at 5.6:1, while the adjacency note beside it ran 13.5px at 16.9:1. A
   sidebar was louder than the body it annotates. Full --text, 16px. */
.panebody {
  font: 400 16px/1.68 var(--ui); color: var(--text);
  margin: 0 0 16px; text-wrap: pretty;
}
.panemethod {
  font: 400 13.5px/1.55 var(--ui); color: var(--text-dim);
  padding: 11px 0; margin: 0;
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}

.paneadj {
  margin-top: 14px; padding: 13px 15px;
  background: var(--adjacent-wash);
  border-left: 2px solid var(--adjacent);
  border-radius: 0 4px 4px 0;
}
.paneadj .alabel {
  display: block; margin: 0 0 6px;
  font: 800 11px/1 var(--ui); letter-spacing: .12em;
  color: var(--adjacent); text-transform: uppercase;
}
.paneadj .anote { margin: 0; font: 400 15px/1.62 var(--ui); color: var(--text); }
.paneadj .adir  { display: block; margin-top: 9px; font: 600 13.5px/1.35 var(--ui); color: var(--adjacent); }

.panethread {
  display: grid; grid-template-columns: 180px 30px 1fr; gap: 12px;
  align-items: baseline; margin-top: 13px;
}
.panethread .tn {
  font: 700 11.5px/1.35 var(--ui); letter-spacing: .03em; text-transform: uppercase;
  color: var(--text);
}
.panethread .ts {
  font: 800 14px/1 var(--display); letter-spacing: -.03em;
  color: var(--thread-mark); font-variant-numeric: tabular-nums;
}
.panethread .tj { font: 400 14px/1.6 var(--ui); color: var(--text-dim); }

.paneactions { display: flex; gap: 8px; margin-top: 22px; align-items: center; }
.paneactions form { margin: 0; }
.btn-primary, .btn-secondary, .btn-tertiary, .btn-link {
  font: 700 12px/1 var(--ui);
  border-radius: 5px; padding: 9px 13px;
  cursor: pointer; text-decoration: none; display: inline-block;
  border: 1px solid transparent; background: none;
}
.btn-primary   { background: var(--thread-mark); color: var(--thread-mark-ink); }
.btn-secondary { border-color: var(--border-strong); color: var(--text); font-weight: 500; }
.btn-tertiary  { border-color: var(--border); color: var(--text-dim); font-weight: 500; }
.btn-link      { color: var(--thread-mark); border-color: var(--thread-chip-line); }
.paneactions .pushright { margin-left: auto; }

.paneempty { padding: 40px 0; color: var(--text-dim); font: 400 14px/1.6 var(--ui); }

/* --- responsive ------------------------------------------------------------- */
@media (max-width: 1180px) {
  .feed-shell { grid-template-columns: 380px 1fr; }
  .pane { padding: 22px 24px; }
}

/* --- back link (mobile detail only) ---------------------------------------- */
.backlink {
  display: none;
  font: 600 13px/1 var(--ui);
  color: var(--thread-mark);
  text-decoration: none;
  margin-bottom: 14px;
}

/* --- bottom navigation ------------------------------------------------------ */
/* Primary navigation below 900px, where the header nav is hidden. Hidden on
   desktop, where the header carries it. */
.bottomnav { display: none; }

@media (max-width: 900px) {
  .bottomnav {
    display: flex; gap: 2px;
    justify-content: space-between; align-items: center;
    /* Lifted off the foot rather than pinned to it: 18px of air, plus whatever
       the home indicator claims, so the bar reads as floating above the page
       rather than welded to the bottom edge of the glass. */
    position: sticky;
    bottom: calc(18px + env(safe-area-inset-bottom, 0px));
    z-index: 20;
    margin: 10px 12px 0;
    padding: 5px;
    border-radius: 99px;
    background: var(--surface);
    border: 1px solid var(--border);
    box-shadow: 0 8px 22px rgba(20, 33, 58, .12);
  }
  .bottomnav a {
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    text-decoration: none;
    color: var(--text-dim);
    font: 500 10.5px/1 var(--ui);
    padding: 9px 7px 8px;
    border-radius: 99px;
  }
  .bottomnav a.on {
    background: var(--thread-mark);
    color: var(--thread-mark-ink);
    font-weight: 700;
    padding: 9px 13px 8px;
  }
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .bottomnav {
    background: #0e1729;
    border-color: #22304a;
    box-shadow: 0 8px 22px rgba(0, 0, 0, .5);
  }
}

/* --- mobile: list or detail, never both ------------------------------------- */
@media (max-width: 900px) {
  body.feed-page main { padding: 0; }

  .feed-shell {
    display: block;                  /* one column; which pane shows is decided
                                        by the URL, not by client-side routing */
    height: auto;
    border: 0;
    border-radius: 0;
    background: transparent;
  }

  .topbar nav { display: none; }     /* the floating bar takes over */
  .topbar { padding: 12px 14px; }

  .rail { border-right: 0; }
  .railscroll { overflow: visible; }

  .railbar {
    position: sticky; top: 0; z-index: 5;
    padding: 14px 16px;
    gap: 11px;
  }
  .railbar .statefilter { gap: 10px; }

  /* Reflow the row: signal line first, then title, hook, direction, footer.
     A phone shows one row at a time rather than a scannable column of them, so
     the density that helps on a 440px rail beside a reading pane only crowds
     here. Padding and the gaps between the parts open up; the type does not
     grow much, because the constraint on a phone is vertical room, not size. */
  .railrow { grid-template-columns: 1fr; gap: 0; padding: 19px 16px 20px; }
  .railgutter {
    display: flex; align-items: center; gap: 10px;
    text-align: left; margin-bottom: 11px;
  }
  .railnum { font-size: 19px; }
  .railverdict { display: none; }              /* moves to the footer line */
  .adjbracket { margin-top: 0; margin-left: auto; }
  .railflags { margin-bottom: 10px; }
  .railtitle { font-size: 16.5px; line-height: 1.34; }
  .railhook { font-size: 13.5px; line-height: 1.52; margin-top: 9px; }
  .raildirection { font-size: 11.5px; line-height: 1.4; margin-top: 11px; }
  .railmeta { display: flex; gap: 7px; align-items: baseline; margin-top: 11px; }
  .railmeta .mverdict {
    font: 700 9px/1 var(--ui); letter-spacing: .1em; text-transform: uppercase;
    color: var(--text-dimmer);
  }

  /* Detail is its own screen: the rail is hidden when an article is selected. */
  .feed-shell.show-detail .rail { display: none; }
  .feed-shell:not(.show-detail) .pane { display: none; }

  .pane { padding: 18px 16px 16px; overflow: visible; }
  .backlink { display: inline-block; }

  .panenum { font-size: 30px; letter-spacing: -.045em; }
  .panehead { gap: 12px; }
  .panescore { display: flex; align-items: center; gap: 10px; text-align: left; }
  .panescore .railverdict { margin-top: 0; }
  .panescore .adjbracket { margin-top: 0; }
  .panetitle { font-size: 22px; line-height: 1.18; letter-spacing: -.03em; }
  .panelede { font-size: 15.5px; }

  /* Thread rows restack: score + name on one baseline, justification below. */
  .panethread {
    grid-template-columns: auto 1fr;
    gap: 9px;
    margin-top: 14px; padding-top: 13px;
    border-top: 1px solid var(--border-soft);
  }
  .panethread .ts { order: 1; }
  .panethread .tn { order: 2; font-size: 11px; }
  .panethread .tj { order: 3; grid-column: 1 / -1; }

  /* Actions become a thumb-reachable bar; all targets ≥ 48px. */
  .paneactions {
    position: sticky; bottom: 0;
    margin: 22px -16px 0;
    padding: 10px 12px 12px;
    /* 7 rather than 8: four gaps between five controls, so the point recovers
       4px for the labels. */
    gap: 7px;
    border-top: 1px solid var(--border);
    background: var(--surface-sunken);
  }
  .paneactions form { flex: 1; }
  /* The star is icon-only and must not take a text button's share: with five
     controls splitting the bar evenly they fell to 63px, which breaks "Mark
     read" mid-label. Pinned narrow, the three text buttons keep the width they
     had before the tier existed. */
  .paneactions form.staraction { flex: none; width: 48px; }
  .paneactions form.staraction button { width: 48px; }
  /* min-height rather than padding arithmetic: padding alone left Dismiss and
     the DOI/PDF links at 46-47px, under the 48px touch minimum. */
  .paneactions button,
  .paneactions .btn-link {
    width: 100%;
    min-height: 48px;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 8px 0; font-size: 13px; border-radius: 6px;
  }
  .paneactions .btn-secondary, .paneactions .btn-tertiary { font-weight: 500; }
  /* DOI needs 44px for its icon and label and had 64. Handing 8 of those back
     is what takes Dismiss from 8px of slack to comfortable. */
  .paneactions .btn-link { width: 56px; flex: none; }
  .paneactions .pushright { margin-left: 0; }
}

@media (max-width: 420px) {
  .railbar { gap: 7px; }
  .railbar .railcount { width: 100%; }
}

/* ===========================================================================
   Feed corrections — readability pass
   =========================================================================== */

/* 1. Cap the measure. Prose at 14-16px wants 65-75 characters a line; the pane
      was running the full width of the display, which is unreadable at 1500px. */
.panecontent { max-width: 46rem; }

/* 2. The pane title is a link, but it should read as a heading. Inheriting the
      global link colour turned it lime and underlined it — the accent belongs
      to the thread axis alone, never to chrome. */
.panetitle a {
  color: inherit;
  text-decoration: none;
  text-decoration-color: var(--border-strong);
}
.panetitle a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* 3. Icons sit inside buttons on the text baseline. */
.btn-primary, .btn-secondary, .btn-tertiary, .btn-link {
  display: inline-flex; align-items: center; gap: 6px;
}
.bicon { flex: none; opacity: .85; }
.btn-primary .bicon { opacity: 1; }

/* 4. Label the adjacency number. A bare numeral under a bigger numeral reads as
      a second score on the same axis; it is a different axis entirely. */
.adjlabel {
  font-size: 10px; letter-spacing: .07em; text-transform: uppercase;
  opacity: .8; margin-right: 3px;
}
.adjbracket { white-space: nowrap; }

/* 5. The adjacency wash was #181430 violet on a #101a2c navy surface — two dark
      chromatic fields a few points apart, which reads as a smudge rather than a
      panel. Give it a surface that actually separates, and let the left rule and
      the label carry the violet. */
.paneadj {
  background: color-mix(in srgb, var(--adjacent) 15%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--adjacent) 34%, transparent);
  border-left: 3px solid var(--adjacent);
  border-radius: 0 5px 5px 0;
}
.paneadj .anote { color: var(--text); }
.paneadj .adir { text-decoration: none; }
.paneadj .adir:hover { text-decoration: underline; text-underline-offset: 2px; }

/* 6. Thread rows read as data, so give them a rule and room. */
.panethread {
  padding-top: 12px;
  border-top: 1px solid var(--border-soft);
}

/* 7. The rail's selected row used a lime left edge AND a lime numeral; the edge
      alone is enough, and the doubled accent fought the thread chip. */
.railrow.on .railnum { color: var(--text-strong); }

/* 8. Give the pane's own scrollbar room so text does not sit against it. */
.pane { scrollbar-gutter: stable; }

/* --- theme switcher --------------------------------------------------------- */
/* Pushed to the far end of the header; the nav keeps its natural position. */
/* One icon showing the theme in force; the three choices live in a disclosure
   under it. Three permanent buttons cost more header than a setting changed
   twice a year deserves, and on a phone that row also carries the wordmark. */
.thememenu { margin-left: auto; position: relative; }
.thememenu summary {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 30px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-sunken);
  color: var(--text-dim);
  cursor: pointer;
  list-style: none;                 /* Firefox */
}
.thememenu summary::-webkit-details-marker { display: none; }   /* Safari */
.thememenu summary:hover { color: var(--text-strong); background: var(--surface-hover); }
.thememenu[open] summary { color: var(--text-strong); background: var(--surface); }
.thememenu summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.thememenupop {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 30;
  min-width: 168px;
  padding: 5px;
  display: flex; flex-direction: column; gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 22px rgb(0 0 0 / .16);
}
.thememenupop a {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 10px;
  border-radius: 6px;
  font: 500 13px/1 var(--ui);
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}
.thememenupop a:hover { background: var(--surface-hover); }
.thememenupop a.on { background: var(--surface-selected); color: var(--text-strong); font-weight: 700; }
.thememenupop a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Visually hidden, still read aloud: the control is icon-only, so each link
   needs a name that is not the tooltip. */
.vh {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* On a phone the header is tight; drop the switcher to a 44px touch target and
   let the nav wrap around it rather than squeezing everything onto one line. */
@media (max-width: 767px) {
  .themeswitch a { width: 44px; height: 44px; }
}
/* The pane leads with the exact date; the relative form trails it so the two
   readings of "when" sit together instead of competing. */
.paneage { color: var(--text-dimmer); }

/* ---------------------------------------------------------------------------
   Runs summary. Three figures that were previously only derivable by adding up
   a table by eye — and for bulk runs, not derivable at all.
   --------------------------------------------------------------------------- */
.runsummary {
  display: flex; flex-wrap: wrap; gap: 10px;
  margin: 0 0 14px;
}
.runsummary .stat {
  flex: 1 1 190px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}
.runsummary .statlabel {
  display: block;
  font: 700 10.5px/1 var(--ui); letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-dimmer);
}
.runsummary .statvalue {
  display: block; margin: 7px 0 5px;
  font: 700 24px/1 var(--display); letter-spacing: -.02em;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}
.runsummary .statnote {
  display: block;
  font: 400 12px/1.4 var(--ui);
  color: var(--text-dim);
}

/* ---------------------------------------------------------------------------
   Swipe-to-triage. The wrapper exists because buttons cannot live inside an
   anchor, so the action layer has to be a sibling of the row rather than a
   child. Everything here is inert until assets/swipe.js adds a class — with no
   JavaScript the wrapper is a plain block and the row behaves exactly as before.
   --------------------------------------------------------------------------- */
.railitem {
  position: relative;
  overflow: hidden;
  transition: height .22s ease, opacity .22s ease;

  /* Who owns a horizontal drag that starts inside a vertical scroller.
     Without this the answer is Safari, and the swipe is unreliable in a way
     that reads as the row fighting back: iOS claims the gesture, fires
     pointercancel, the drag is abandoned partway, and the row springs home.
     Whether that happens depends on the angle of the first few pixels, which
     is why it fails intermittently rather than always.

     The listeners are all passive, so preventDefault is not available to argue
     the point — this declaration is the argument. pan-y keeps vertical
     scrolling native and gives horizontal movement to the script.

     pinch-zoom is spelled out because pan-y alone would withdraw it, and
     removing zoom from a page of small type is not a trade worth making for a
     gesture. */
  touch-action: pan-y pinch-zoom;
}
.railitem .railrow { position: relative; z-index: 1; background: var(--surface); }
.railitem.is-dragging .railrow { user-select: none; }

/* Behind the row, revealed as it is pulled left. */
.railactions {
  position: absolute;
  inset-block: 0;
  right: 0;
  display: flex;
  align-items: stretch;
  z-index: 0;
}
.railaction {
  border: 0;
  /* Icons rather than words, so each action needs less room: 64px against the
     76 the labels wanted. Three at 64 is a 192px strip, which leaves 198px of
     row visible at 390px — you can still read what you are acting on while the
     actions are open, which you could not at 228.
     The glyphs are the same three the pane uses for these actions, so nothing
     here has to be learned; the colour carries the rest. */
  width: 64px;
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* 15px is sized to sit beside 12px label text. Alone in a 64px block it needs
   to be the thing you see. */
.railaction .bicon { width: 24px; height: 24px; }
.railaction.is-star { background: var(--swipe-star); color: var(--swipe-star-ink); }
.railaction.is-save { background: var(--swipe-save); color: var(--swipe-save-ink); }
.railaction.is-skip { background: var(--swipe-skip); color: var(--swipe-skip-ink); }

/* Committed: collapse rather than vanish, so the list does not jump under a
   thumb that is still on the screen. */
.railitem.is-going { opacity: .55; }
.railitem.is-gone  { opacity: 0; }

/* A failed action has to look failed. Silently doing nothing is worse. */
.railitem.is-failed .railrow {
  box-shadow: inset 3px 0 0 var(--danger);
}

@media (prefers-reduced-motion: reduce) {
  .railitem, .railitem .railrow { transition: none !important; }
}

/* ---------------------------------------------------------------------------
   The action dial.

   One button at the lower right that raises the article's actions above it.
   With no permanent row at the foot of the article there is nothing for a
   floating control to collide with, nothing to lift clear of, and no width
   arithmetic keeping five labels on one line — three problems that each took a
   fix of their own before this replaced the shape that caused them.

   .panelisten holds the native <audio> and is the no-JavaScript path; actions.js
   hides it and the row together only once the dial is built.
   --------------------------------------------------------------------------- */
.panelisten { display: flex; justify-content: flex-end; margin: 20px 0 4px; }
.panelisten audio { height: 32px; max-width: 240px; }

.paneFab { position: relative; }

.paneFabBtn {
  position: fixed;
  right: 16px;
  bottom: calc(104px + env(safe-area-inset-bottom, 0px));
  z-index: 30;
  display: inline-flex; align-items: center; justify-content: center;
  width: 54px; height: 54px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  background: var(--thread-mark);
  color: var(--thread-mark-ink);
  box-shadow: 0 4px 14px rgba(20, 33, 58, .28);
}
.paneFabBtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* Dots closed, a cross open, stacked and cross-faded.

   Five attempts were made to fix this icon before anyone measured the button it
   sits in. The cross looked broken because it is only ever shown while the dial
   is open, and until the note beside button:hover was written, an open dial was
   a button whose ink had been repainted in its own background. The icon was
   never at fault; neither stroke, nor the hidden attribute, nor display, nor
   opacity, nor a transform was ever the variable.

   Opacity rather than display because the two rules are exact mirrors, so the
   pair cannot land in a state where neither is showing. If the toggle ever
   stops working the button shows dots — the wrong icon, never no icon. */
.paneFabBtn .i-open,
.paneFabBtn .i-close {
  position: absolute;
  top: 50%; left: 50%;
  width: 24px; height: 24px;
  margin: -12px 0 0 -12px;
  transition: opacity .14s ease;
}
.paneFabBtn .i-close { opacity: 0; }
.paneFab.is-open .paneFabBtn .i-open  { opacity: 0; }
.paneFab.is-open .paneFabBtn .i-close { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .paneFabBtn .i-open,
  .paneFabBtn .i-close { transition: none; }
}

/* The column sits above the button, right-aligned to it. column-reverse so the
   first item built ends up nearest the thumb — which is Listen, taking back
   what can be taken back from an action that is now two taps. */
.paneFabMenu {
  position: fixed;
  right: 16px;
  bottom: calc(168px + env(safe-area-inset-bottom, 0px));
  z-index: 30;
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-end;
  gap: 8px;
}
.paneFabMenu form { margin: 0; }


.paneFabItem {
  display: inline-flex; align-items: center; gap: 9px;
  /* 48, matching the row this replaced — the same touch minimum the action
     buttons were held to. */
  min-height: 48px;
  padding: 0 17px 0 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font: 600 14px/1 var(--ui);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 3px 10px rgba(20, 33, 58, .16);
}
.paneFabItem:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.paneFabIcon { display: inline-flex; color: var(--text-dim); }
.paneFabItem.is-play .paneFabIcon { color: var(--thread-mark); }

/* Opening reads as one movement: the column rises and fades in together. */
.paneFab.is-open .paneFabMenu { animation: fabrise .16s ease both; }
@keyframes fabrise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .paneFab.is-open .paneFabMenu { animation: none; }
}

/* ---------------------------------------------------------------------------
   The now-playing pill.

   Beside the dial, not instead of it: playback must not cost you the way to
   every other action. Bottom-aligned with the button and stopping 12px short of
   it, so the two read as one row rather than a collision.

   Three glyphs, one shown at a time by a state class that is always set to one
   of exactly three values — the same construction as the dial's icon, for the
   same reason.
   --------------------------------------------------------------------------- */
.nowplaying {
  position: fixed;
  left: 16px;
  right: 82px;                 /* 16 + the button's 54 + 12 of air */
  bottom: calc(107px + env(safe-area-inset-bottom, 0px));
  z-index: 30;
  display: flex; align-items: center; gap: 4px;
  height: 48px;
  padding: 0 4px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  box-shadow: 0 3px 12px rgba(20, 33, 58, .18);
}

.nowplayBtn {
  position: relative;
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--thread-mark);
  color: var(--thread-mark-ink);
  cursor: pointer;
}
.nowplayBtn:disabled { cursor: default; }

.nowplayBtn .i-spin,
.nowplayBtn .i-play,
.nowplayBtn .i-pause {
  position: absolute;
  top: 50%; left: 50%;
  width: 20px; height: 20px;
  margin: -10px 0 0 -10px;
  opacity: 0;
  transition: opacity .12s ease;
}
.nowplaying.is-loading .nowplayBtn .i-spin  { opacity: 1; }
.nowplaying.is-paused  .nowplayBtn .i-play  { opacity: 1; }
.nowplaying.is-playing .nowplayBtn .i-pause { opacity: 1; }

.nowplaying.is-loading .i-spin { animation: nowplayspin .9s linear infinite; }
@keyframes nowplayspin { to { transform: rotate(360deg); } }

/* Unfilled, so the generic button:hover rule can tint these freely — it only
   does harm to a button whose background is already the accent. */
.nowplaySkip {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
}
.nowplaySkip:disabled { opacity: .35; cursor: default; }

.nowplayText {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: baseline; gap: 6px;
  margin-left: 4px;
}
/* Silent in the states an icon already covers, and taking no room when silent. */
.nowplayState:empty { display: none; }
.nowplayState {
  font: 600 13px/1 var(--ui);
  color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nowplayTime {
  font: 500 12px/1 var(--ui);
  color: var(--text-dimmer);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.nowplayStop {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--text-dimmer);
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .nowplayBtn .i-spin,
  .nowplayBtn .i-play,
  .nowplayBtn .i-pause { transition: none; }
  .nowplaying.is-loading .i-spin { animation-duration: 2.4s; }
}

/* ---------------------------------------------------------------------------
   The second tier of saving.

   Save means "do not lose this". Star means "read this next" — it sorts to the
   top of every list, and it is a much stronger statement about what this reader
   finds worth their time, which makes it the more useful signal of the two.

   Amber rather than either accent axis: starring is a verdict, and app.css
   reserves teal for thread relevance and violet for adjacency. --warm is
   already the app's third voice, worn by open-access tags and skim verdicts.
   --------------------------------------------------------------------------- */
.btn-star {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none;
  width: 40px; height: 36px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: none;
  color: var(--text-dimmer);
  cursor: pointer;
}
.btn-star:hover { color: var(--hot); border-color: var(--hot); }
.btn-star.on {
  color: var(--hot);
  border-color: var(--hot);
  background: color-mix(in srgb, var(--hot) 12%, transparent);
}
.btn-star:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* In the rail, so a starred article is identifiable on sight rather than only
   by having floated to the top. */
.railstar { display: inline-flex; color: var(--hot); }
.railstar .bicon { width: 13px; height: 13px; }

/* Add PDF: the escape hatch for publishers that refuse this server.
   Styled as one of the link buttons rather than as a form, because to the
   reader it is another way of getting the article, not an upload. The file
   input is hidden inside the label and submits on change — a file picker
   followed by a separate Submit is two decisions where there is only one. */
.addpdf { margin: 0; }
.addpdfLabel {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 36px; padding: 0 12px;
  border: 1px dashed var(--border-strong);
  border-radius: 5px;
  color: var(--text-dim);
  font: 600 13px/1 var(--ui);
  cursor: pointer;
}
.addpdfLabel input[type="file"] {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}
.addpdfLabel:hover { color: var(--accent); border-color: var(--accent); }

/* Whatever add-pdf.php came back to say. */
.pdfmsg {
  margin: 10px 0 0;
  padding: 8px 11px;
  border-radius: 5px;
  background: var(--surface-hover);
  color: var(--text-dim);
  font: 500 13px/1.4 var(--ui);
}

/* ---------------------------------------------------------------------------
   Notes.

   Below the actions rather than above them: triage first, thinking second. The
   box is quiet until it has something in it, because most articles will never
   get a note and an empty prompt on every one of them is noise.
   --------------------------------------------------------------------------- */
.notebox { margin: 22px 0 4px; }

.notehead { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.notehead h4 {
  margin: 0; flex: none;
  font: 700 11px/1 var(--ui); letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-dimmer);
}

.notemic {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
}
.notemic:hover { color: var(--accent); border-color: var(--accent); }
.notemic:disabled { opacity: .5; cursor: default; }

/* Recording is a state worth being unmistakable about: it is costing battery,
   holding the screen awake, and listening. --danger is the app's only alarm
   colour and this is the one control that should wear it. */
.notemic.is-recording {
  color: var(--danger);
  border-color: var(--danger);
  animation: notepulse 1.4s ease-in-out infinite;
}
@keyframes notepulse { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
  .notemic.is-recording { animation: none; }
}

.notestatus {
  font: 500 12px/1.3 var(--ui);
  color: var(--text-dimmer);
  font-variant-numeric: tabular-nums;
}

.noteform { margin: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.notetext {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  font: 400 15px/1.55 var(--ui);
  resize: vertical;
  min-height: 84px;
}
.notetext:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.notetext::placeholder { color: var(--text-dimmer); }

/* With the script running, autosave has already done this; the button stays
   for the path where it has not. */
.notesave { align-self: flex-end; }

/* The voice picker, sharing the theme popover. Divided from it rather than
   merged: they are both settings, but nothing about a colour scheme should
   look like it might change what a voice does. */
.thememenusplit {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
  display: flex; flex-direction: column;
}
.thememenulabel,
.thememenunote {
  padding: 4px 10px;
  color: var(--text-dimmer);
}
.thememenulabel {
  font: 700 10px/1 var(--ui); letter-spacing: .08em; text-transform: uppercase;
}
.thememenunote {
  font: 400 11px/1.4 var(--ui);
  max-width: 190px;
}

/* Standing on its own now rather than in the actions row, so it needs the
   breathing room the row was giving it. */
.addpdf { margin: 20px 0 0; }

/* ---------------------------------------------------------------------------
   Spend.

   Numbers first and large, because the question that brings anyone here is
   "how much" and everything else is the follow-up.
   --------------------------------------------------------------------------- */
.spendtotals {
  display: flex; flex-wrap: wrap; gap: 10px;
  margin: 0 0 18px;
}
.spendtotal {
  flex: 1 1 130px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}
.spendtotalnum {
  display: block;
  font: 700 22px/1.1 var(--ui);
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}
.spendtotallabel {
  display: block; margin-top: 3px;
  font: 600 11px/1 var(--ui); letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-dimmer);
}

.spendwindow { display: flex; flex-wrap: wrap; gap: 4px; margin: 0 0 18px; }
.spendwindow a {
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font: 600 12px/1 var(--ui);
  color: var(--text-dim);
  text-decoration: none;
}
.spendwindow a.on {
  background: var(--thread-mark);
  border-color: var(--thread-mark);
  color: var(--thread-mark-ink);
}

.spendtable { width: 100%; border-collapse: collapse; margin: 0 0 10px; }
.spendtable th {
  text-align: left;
  padding: 6px 8px;
  border-bottom: 1px solid var(--border);
  font: 700 10.5px/1 var(--ui); letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-dimmer);
}
.spendtable td {
  padding: 9px 8px;
  border-bottom: 1px solid var(--border-soft);
  font: 400 14px/1.4 var(--ui);
  vertical-align: top;
}
.spendtable .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.spendtable tfoot td { border-bottom: 0; font-weight: 700; }
.spendwhat { font-size: 12px; color: var(--text-dimmer); }

/* A bar rather than a chart: one number per row, drawn in the width it
   already has. No library, no axes, nothing to mislabel. */
.spendbar {
  display: inline-block;
  width: var(--share); min-width: 2px; height: 7px;
  border-radius: 4px;
  background: var(--thread-mark);
  vertical-align: middle;
  margin-right: 6px;
}
.spenddays .spendbarcell { width: 55%; }
.spenddays .spendbar { margin: 0; }

.spendnote {
  margin: 0 0 22px;
  font: 400 12.5px/1.5 var(--ui);
  color: var(--text-dimmer);
  max-width: 62ch;
}

/* The allowance: what is left, and the control that sets it. Above the
   breakdown, because a limit you are close to matters more than where last
   week's money went. */
.creditbox {
  margin: 0 0 20px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}
.creditline { margin: 0 0 8px; font: 400 15px/1.5 var(--ui); color: var(--text); }
.creditline b { font-variant-numeric: tabular-nums; }
.creditwarn { display: block; margin-top: 3px; color: var(--hot); font-size: 13px; }

.creditbar {
  display: block; height: 8px;
  border-radius: 5px;
  background: var(--surface-hover);
  overflow: hidden;
}
.creditbarfill {
  display: block; height: 100%;
  width: var(--used);
  background: var(--thread-mark);
}

.creditform {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 12px 0 0;
  font: 500 13px/1 var(--ui); color: var(--text-dim);
}
.creditform label { display: inline-flex; align-items: center; gap: 6px; }
.creditcur { color: var(--text-dimmer); }
.creditform input {
  width: 92px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--surface);
  color: var(--text);
  font: 500 14px/1 var(--ui);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
   Desk.

   Deliberately unlike the feed. No score in the margin, no relevance floor, no
   sorting by anything but intention — the judging is over by the time an
   article arrives here, and what is worth showing is where you are with it.
   --------------------------------------------------------------------------- */
.deskbar { margin: 0 0 18px; }

.deskgroup { margin: 0 0 30px; }
.deskhead {
  display: flex; align-items: baseline; gap: 9px;
  margin: 0 0 3px;
  font: 700 15px/1.2 var(--ui);
  color: var(--text-strong);
}
.deskcount {
  font: 700 11px/1 var(--ui);
  color: var(--text-dimmer);
  font-variant-numeric: tabular-nums;
}
.deskblurb {
  margin: 0 0 12px;
  font: 400 13px/1.45 var(--ui);
  color: var(--text-dimmer);
  max-width: 60ch;
}

.desklist { list-style: none; margin: 0; padding: 0; }
.deskrow {
  padding: 12px 0 13px;
  border-top: 1px solid var(--border-soft);
}
.deskrow:last-child { border-bottom: 1px solid var(--border-soft); }

.desktitle {
  display: block;
  font: 600 16px/1.35 var(--ui);
  color: var(--text-strong);
  text-decoration: none;
}
.desktitle:hover { color: var(--accent); }

.deskmeta {
  margin: 4px 0 0;
  font: 400 13px/1.4 var(--ui);
  color: var(--text-dimmer);
}
.deskoa { color: var(--warm); }

.deskmarks { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0; }
/* Outlined and small: these report a state rather than offering an action, and
   should not read as buttons. */
.deskmark {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font: 600 11.5px/1.35 var(--ui);
  color: var(--text-dim);
}
.deskmark.is-ready { color: var(--thread-mark); border-color: var(--thread-chip-line); }
.deskmark.is-done  { color: var(--text-dimmer); }
.deskmark.is-none  { color: var(--text-dimmer); border-style: dashed; }
.deskmark.is-note  { color: var(--adjacent); border-color: var(--adjacent-line); }

/* Progress shown as a filled arc of the pill's own left edge — the number is
   already there in words, so this only has to give it a shape. */
.deskprog {
  display: inline-block;
  width: 26px; height: 4px;
  border-radius: 3px;
  background: var(--border);
  position: relative;
  overflow: hidden;
}
.deskprog::after {
  content: ''; position: absolute; inset: 0 auto 0 0;
  width: var(--at); background: var(--thread-mark);
}

@media (max-width: 900px) {
  /* Six items rather than five, so each gets less room. */
  .bottomnav a { padding: 9px 4px 8px; }
  .bottomnav a span { font-size: 9.5px; letter-spacing: 0; }
  .bottomnav a.on { padding: 9px 9px 8px; }
}

/* The star and the thread, above the title on a desk row. The thread is why
   the article is on the list at all, and the first version of this page left it
   out — leaving four dozen titles with nothing to group them by eye. */
.deskchips { display: flex; align-items: center; gap: 6px; margin: 0 0 5px; }
.deskstar { display: inline-flex; color: var(--hot); }
.deskstar .bicon { width: 13px; height: 13px; }
.deskchips .threadchip { margin-bottom: 0; }

/* A link out of the feed's filter row, not one of its states. Set apart so it
   does not read as a sixth thing to filter by. */
.tothedesk {
  margin-left: 6px;
  padding-left: 9px;
  border-left: 1px solid var(--border);
  color: var(--text-dimmer) !important;
}
.tothedesk:hover { color: var(--accent) !important; }

/* A thread the article barely scored against, kept because the reader kept the
   article. Outlined rather than filled: it belongs here, but it should not
   claim the same confidence as a chip that earned its place on score alone. */
.threadchip.is-weak {
  background: none;
  color: var(--text-dim);
  border-style: dashed;
  cursor: help;
}

/* What a new thread leaves behind. Roomier than an ordinary notice because it
   is asking two questions with money attached, not reporting a fact. */
.threadmade p { margin: 0 0 10px; }
.threadmade p:last-child { margin-bottom: 0; }
.threadmadeform {
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.threadmadeform p { margin: 0 0 9px; }

/* ---------------------------------------------------------------------------
   Desk lanes.

   One lane per thread, swiped between on a phone. The swipe is scroll-snap, so
   it is the browser's own gesture rather than a reimplementation of one; the
   script only keeps the tab strip honest about where the finger stopped.
   --------------------------------------------------------------------------- */
.desktabs {
  display: flex; gap: 4px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin: 0 0 14px;
  padding-bottom: 2px;
}
.desktabs::-webkit-scrollbar { display: none; }

.desktab {
  flex: none;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 13px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font: 600 13px/1 var(--ui);
  color: var(--text-dim);
  text-decoration: none;
  white-space: nowrap;
}
/* The current lane, unmissable: filled, not merely underlined. On a phone this
   strip is often the only thing naming what is on screen. */
.desktab.on {
  background: var(--thread-mark);
  border-color: var(--thread-mark);
  color: var(--thread-mark-ink);
}
.desktabn { font-variant-numeric: tabular-nums; opacity: .7; font-size: 11.5px; }

.desklanes { display: block; }
.desklane { display: none; }
.desklane.is-active { display: block; }

/* The lane's own name. Redundant with the tabs on purpose — mid-swipe the strip
   is the last thing being looked at. */
.desklanehead {
  display: flex; align-items: baseline; gap: 9px;
  margin: 0 0 16px;
  font: 700 19px/1.2 var(--ui);
  color: var(--text-strong);
}
.desklanen {
  font: 600 12px/1 var(--ui);
  color: var(--text-dimmer);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 900px) {
  /* Below 900px every lane is present and the container scrolls between them.
     Above it, only the chosen lane is shown and the tabs behave as links —
     a horizontal swipe is not a gesture anyone makes on a desktop. */
  .desklanes {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Full-bleed: a lane that stops short of the edge does not read as a
       separate surface, it reads as a column that has been nudged. */
    margin: 0 -16px;
    padding: 0;
  }
  .desklanes::-webkit-scrollbar { display: none; }

  /* Measured by desk.js against the real viewport, because the space left
     over depends on the header, the heading, the tab strip and the floating
     nav — none of which CSS can subtract from a viewport unit. The fallback
     is deliberately conservative: too short is a lane that scrolls, too tall
     is a lane hidden behind the nav bar. */
  .desklanes { height: var(--lane-h, 62vh); }

  /* Each lane scrolls on its own, and the container is a fixed window onto
     them. Flex items stretch to the tallest sibling, so with the page doing
     the scrolling a 44-row lane inherited the height of the 176-row one and
     kept scrolling long after its last result — hundreds of pixels of
     nothing, on every lane but the longest.

     overscroll-behavior stops a lane that has reached its end from handing
     the gesture to the page behind it, which is the other half of the same
     complaint. */
  .desklane,
  .desklane.is-active {
    display: block;
    flex: 0 0 100%;
    scroll-snap-align: start;
    scroll-snap-stop: always;
    padding: 0 16px 8px;
    box-sizing: border-box;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-y: contain;
  }

  .desktabs { position: sticky; top: 0; z-index: 15;
              background: var(--page); padding-top: 4px; }
  .desklanehead { font-size: 17px; }
}

/* The balance in the header. Quiet until it is low, then not. */
.creditchip {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font: 700 13px/1 var(--ui);
  color: var(--text-dim);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.creditchip:hover { border-color: var(--accent); color: var(--accent); }
.creditchipunit { font-weight: 500; font-size: 11px; color: var(--text-dimmer); }
.creditchip.is-low {
  color: var(--hot);
  border-color: var(--hot);
}
.creditchip.is-low .creditchipunit { color: var(--hot); }

/* The balance on its own page: the one number worth setting in large type. */
.creditbig { font-size: 30px; line-height: 1.1; font-variant-numeric: tabular-nums; }
.creditof { color: var(--text-dimmer); font-weight: 400; font-size: 13px; }
.creditreason {
  flex: 1 1 160px; min-width: 120px;
  padding: 6px 9px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--surface);
  color: var(--text);
  font: 400 13px/1 var(--ui);
}

@media (max-width: 900px) {
  /* The header drops its nav below 900px; the balance is small enough to stay,
     and it is the thing most worth glancing at. */
  .creditchip { padding: 4px 9px; font-size: 12px; }
  .creditchipunit { display: none; }
}

/* A button that is working. The spinner is a rotating arc rather than a
   pulsing opacity, because a pulse reads as decoration and a rotation reads as
   progress — and this is the one moment the reader is asking whether anything
   is progressing at all. */
button.is-busy,
button.is-busy:hover {
  display: inline-flex; align-items: center; gap: 8px;
  cursor: default;
  opacity: .85;
}
.busyspin {
  display: inline-block;
  width: 13px; height: 13px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: busyturn .7s linear infinite;
  flex: none;
}
@keyframes busyturn { to { transform: rotate(360deg); } }

/* Reduced motion still needs to distinguish working from idle, so the arc
   stays and only the spinning goes. */
@media (prefers-reduced-motion: reduce) {
  .busyspin { animation: none; opacity: .6; }
}

/* What a thread cannot see. Attached to the thread it belongs to and left on
   the page, because the gap it describes is not a moment — it opens whenever a
   thread is added or its keywords change, and closes only when something is
   rescored. */
.threadblind {
  margin: -6px 0 26px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-top: 0;
  border-radius: 0 0 8px 8px;
  background: var(--surface-hover);
}
.threadblind p { margin: 0 0 9px; font: 400 13.5px/1.5 var(--ui); color: var(--text-dim); }
.threadblind p:last-child { margin-bottom: 0; }
.threadblindform { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.threadblindform p { flex: 1 1 100%; }

/* ===========================================================================
   Desk — desktop (>=901px), from the design handoff.

   One thread at a time: a rail of threads, tabs for the three groups, sort and
   search, and a grid of self-contained tiles. The mobile lanes are emitted too
   and hidden here; the server cannot know the viewport, and swiping between
   threads is right on a phone and wrong with a mouse.

   Reserved axes hold: --thread-mark appears once, on the relevance value under
   sort=Relevance; --hot appears only on "open access"; --adjacent is untouched.
   Nothing here uses --text-dimmer, which is decorative only.
   =========================================================================== */
.dk { display: none; }

@media (min-width: 901px) {
  /* Below this the lanes have it; above, they do not. */
  .desk-page .desktabs,
  .desk-page .desklanes,
  /* The phone's page heading. Above 900px the card header is the h1, so
     hiding these keeps exactly one in the accessibility tree. */
  .desk-page .deskh1,
  .desk-page .deskh1sub { display: none; }

  /* The same shell as the article page these tiles link to: editorial column on
     --surface, rail on --surface-sunken, divided by --border rather than by the
     hairline. Two pages a reader moves between all day should not disagree
     about what a rail looks like. 292 → 320 to match its proportion. */
  .dk {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    background: var(--surface);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius);
    overflow: hidden;
  }

  .dkmain { background: var(--surface); min-width: 0; }

  /* --- header ------------------------------------------------------------ */
  .dkhead { display: flex; align-items: center; gap: 16px; padding: 20px 26px 16px; }
  .dkheadtext { min-width: 0; }
  .dkthread {
    margin: 0;
    font: 700 22px/1.2 var(--display);
    letter-spacing: -.012em;
    color: var(--text-strong);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .dksub { margin: 3px 0 0; font: 400 12.5px/1.4 var(--ui); color: var(--text); }

  .dksearch {
    margin: 0 0 0 auto; flex: none;
    width: 250px;
    display: flex; align-items: center; gap: 8px;
    padding: 8px 11px;
    border: 1px solid var(--border);
    border-radius: 7px;
    background: var(--surface);
  }
  .dksearch:focus-within { border-color: var(--text-strong); }
  .dksearchglyph { display: inline-flex; color: var(--text); }
  .dksearch input[type="search"] {
    flex: 1 1 auto; min-width: 0;
    border: 0; background: none; padding: 0;
    font: 400 13.5px/1 var(--ui);
    color: var(--text-strong);
  }
  .dksearch input[type="search"]::placeholder { color: var(--text-dim); }
  .dksearchclear { font-size: 13px; color: var(--text-dim); text-decoration: none; }

  /* --- tabs and sort ----------------------------------------------------- */
  .dkbar {
    display: flex; align-items: flex-end; gap: 16px;
    padding: 0 26px;
    border-bottom: 1px solid var(--border-soft);
  }
  .dktabs { display: flex; gap: 2px; }
  .dktab {
    display: inline-flex; align-items: baseline; gap: 7px;
    padding: 9px 13px 10px;
    margin-bottom: -1px;
    font: 600 14px/1.2 var(--ui);
    color: var(--text-dim);
    text-decoration: none;
    border-bottom: 2px solid transparent;
  }
  .dktab:hover { background: var(--surface-hover); }
  /* Weight, colour and an underline: three cues, so selection never rests on
     hue alone. */
  .dktab.on { color: var(--text-strong); border-bottom-color: var(--text-strong); }
  .dktabn { font: 500 12px/1 var(--mono); opacity: .8; }

  .dksort { margin-left: auto; display: flex; align-items: center; gap: 8px; padding-bottom: 8px; }
  .dksortlabel {
    font: 500 10.5px/1 var(--mono);
    letter-spacing: .1em; text-transform: uppercase;
    color: var(--text-dim);
  }
  .dksortset {
    display: flex; gap: 1px;
    padding: 2px;
    background: var(--surface-sunken);
    border-radius: 6px;
  }
  .dksortbtn {
    padding: 5px 9px;
    border-radius: 4px;
    font: 500 12px/1 var(--ui);
    color: var(--text-dim);
    text-decoration: none;
  }
  .dksortbtn.on {
    background: var(--surface);
    color: var(--text-strong);
    box-shadow: 0 1px 1px rgba(27, 33, 48, .12);
  }

  /* --- tiles -------------------------------------------------------------- */
  .dkgrid {
    display: grid;
    /* auto-fill rather than a fixed two, so a very wide screen gets three
       columns without another breakpoint. */
    grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
    gap: 12px;
    padding: 14px 26px 26px;
    align-content: start;
    /* Without this, grid stretches every tile to the tallest in its row, and a
       one-line title sits in a box with 90px of nothing under it. The rag that
       results is what makes a tile read as an object rather than as a cell. */
    align-items: start;
  }
  .dktile {
    position: relative;
    display: flex; flex-direction: column; gap: 7px;
    padding: 14px 16px 13px;
    border: 1px solid var(--border-soft);
    border-radius: var(--radius);
    background: var(--surface);
  }
  .dktile:hover { border-color: var(--border-strong); background: var(--surface-hover); }
  .dktitle { margin: 0; font: 600 16.5px/1.34 var(--display); letter-spacing: -.004em; }
  .dktitle a { color: var(--text-strong); text-decoration: none; }
  /* The whole tile is the target; the accessible name stays the title. */
  .dktitle a::after { content: ""; position: absolute; inset: 0; }
  .dktitle a:focus-visible { outline: 2px solid var(--text-strong); outline-offset: 3px; }

  .dkmeta { margin: 0; font: 400 12.5px/1.4 var(--ui); color: var(--text); }

  /* One row, wrapping, at the foot of the tile. No fixed columns and no
     reserved slots — a mark is here or it is not, so a bare row means the item
     is untouched and has no long read. */
  .dkmarks {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 12px;
    margin: 0;
    font: 400 11.5px/1 var(--mono);
    color: var(--text);
  }
  .dkmark { display: inline-flex; align-items: center; gap: 5px; }
  /* Each glyph keeps the box it was drawn on — the partial already sets 11×12
     for the note and 10×12 for the unlock. Only the headphones is resized: it
     is drawn at 15 for a button on the article page and wants 13 in a row of
     marks. */
  .dkmark .ic { flex: none; }
  .dklr .ic { width: 13px; height: 13px; }
  .dkoa  { color: var(--hot-ink); }
  .dkrel { color: var(--thread-mark); font-weight: 500; }

  .dkempty { padding: 36px 0 8px; max-width: 52ch; }
  .dkemptytitle { margin: 0 0 6px; font: 600 15px/1.4 var(--ui); color: var(--text-strong); }
  .dkemptybody  { margin: 0; font: 400 13.5px/1.55 var(--ui); color: var(--text); }

  /* --- rail --------------------------------------------------------------- */
  .dkrail {
    background: var(--surface-sunken);
    border-left: 1px solid var(--border);
    padding: 22px 20px 36px;
    display: flex; flex-direction: column; gap: 24px;
  }
  /* --text, not --text-dim. Measured, --text-dim is 4.18:1 on --surface-sunken
     and fails AA; on --surface it passes at 4.6:1, which is why the same token
     was fine everywhere else and wrong here. The rule for the whole rail: if
     the ground is sunken or tinted, meaning-bearing text uses --text. */
  .dkraillabel {
    margin: 0 0 8px;
    font: 500 10.5px/1 var(--mono);
    letter-spacing: .12em; text-transform: uppercase;
    color: var(--text);
  }
  /* A grid, not a flex row: "Environmental crisis & information integrity"
     wraps to two lines and a vertically-centred count then sits half a line
     lower than its neighbours, so the column of numbers wanders. Baseline
     alignment pins every count to the first line of its name. */
  .dkrailitem {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 26px;
    gap: 8px;
    align-items: baseline;
    padding: 8px 10px;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--text);
    text-decoration: none;
    text-align: left;
    font: 400 13px/1.35 var(--ui);
  }
  .dkrailitem:hover { border-color: var(--border-strong); }
  /* Weight, an inset marker and a background: three cues again. */
  .dkrailitem.on {
    background: var(--surface);
    border-color: var(--border);
    box-shadow: inset 2px 0 0 var(--text-strong);
    font-weight: 600;
    color: var(--text-strong);
  }
  .dkrailname { min-width: 0; }
  .dkrailn {
    text-align: right;
    font: 500 12px/1.35 var(--mono);
    font-variant-numeric: tabular-nums;
    color: var(--text);
  }
  .dkrailitem.on .dkrailn { color: var(--text-strong); }

  .dkcount { display: flex; align-items: baseline; gap: 9px; margin: 0 0 6px; }
  .dkcountn {
    min-width: 22px; text-align: right;
    font: 500 12.5px/1 var(--mono);
    font-variant-numeric: tabular-nums;
    color: var(--text-strong);
  }
  .dkcountlabel { font: 400 12.5px/1.4 var(--ui); color: var(--text); }
  .dkfoot { margin: 10px 0 0; font: 400 11.5px/1.45 var(--ui); color: var(--text); }
  /* Standing alone rather than as a note under a list of counts. */
  .dkfootonly { margin: 0; font-size: 12.5px; line-height: 1.5; }

  .dkrailnotes { border-top: 1px solid var(--border-soft); padding-top: 16px; }
  .dknoteitem {
    display: block;
    padding: 6px 0;
    border-bottom: 1px solid var(--border-soft);
    text-decoration: none;
  }
  .dknotetitle {
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
    font: 500 12.5px/1.4 var(--ui);
    color: var(--text);
  }
  .dknotechars { display: block; margin-top: 2px; font: 400 11px/1 var(--mono); color: var(--text); }

  .dk a:focus-visible { outline: 2px solid var(--text-strong); outline-offset: 2px; }
}

/* The sort control's lift needs a darker shadow on a dark ground; the light
   value disappears into it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .dksortbtn.on { box-shadow: 0 1px 1px rgba(0, 0, 0, .4); }
}
:root[data-theme="dark"] .dksortbtn.on { box-shadow: 0 1px 1px rgba(0, 0, 0, .4); }

/* ---------------------------------------------------------------------------
   How wide the app gets.

   --shell was a single 62rem for every page and every screen, which on a 2,000px
   display left more margin than content. It now steps up with the viewport.

   Stepped rather than fluid, and capped rather than open-ended: a shell that
   simply tracks the window keeps growing past the point where a layout is
   helped by it, and every step here is a width at which something actually
   gains a column — journal tiles, desk tiles, the feed's rail beside its pane.

   Prose is unaffected by design. The measures that carry sentences are capped
   on their own components — .panecontent at 46rem, .lede at 48rem — so widening
   the shell gives more room to grids, tables and rails without letting a line of
   text run to a width nobody can read.
   --------------------------------------------------------------------------- */
@media (min-width: 1200px) { :root { --shell: 76rem; } }
@media (min-width: 1600px) { :root { --shell: 90rem; } }
@media (min-width: 2000px) { :root { --shell: 100rem; } }

/* The feed used to run ahead of everything else on the theory that two
   columns want more room. In practice it just made one page wider than the
   app around it, so moving between them felt like the window had changed
   size.

   inherit, not a number. The 80rem above is set on <body>, which outranks
   anything :root says however wide the screen gets — so simply deleting the
   steps here would have left the feed stuck at 80rem while every other page
   grew past it, and the page that was too wide would have become the only
   one too narrow. Deferring to the inherited value keeps it identical to the
   rest, and stays identical if those steps are ever changed. It keeps the
   head start below 1200, where the split view genuinely needs it. */
@media (min-width: 1200px) { body.feed-page { --shell: inherit; } }

/* A thread is a form full of prose to be edited. The shell can grow around it,
   but a textarea 1,400px wide is worse to write in than one at a readable
   measure, so this one opts out. */
.thread { max-width: 62rem; }

/* The pane's layout wrappers. Inert by design: the feed lays out the flat
   sequence it always did, and the wrappers exist so the markup does not have to
   be written twice. The article page no longer uses them — it has its own view
   and its own grid, further down. */
.panemain, .paneside, .panenotes { display: contents; }

/* The score column starts level with the headline, not with the chips.
   --------------------------------------------------------------------
   The number was aligned to the top of the title block, and the top of the
   title block is the chip row — so the numeral sat a whole row above the words
   it belongs to. It now clears that row.

   One variable holds both halves so they cannot drift: the row reserves
   --chiprow of height, and the score column is pushed down by exactly the same
   amount. Change the chip size and the alignment follows.

   The last pixel is beyond reach on purpose. --display resolves to Helvetica
   Neue, Segoe UI or whatever system-ui means on the machine, and those faces
   put their cap heights in slightly different places inside the same line box.
   This aligns the boxes; a fixed nudge to align the glyphs would be right on
   one platform and wrong on the others. */
.panehead { --chiprow: 27px; }

.panechips {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 6px;
  min-height: 19px;
  margin: 0 0 8px;
  line-height: 1;
}
/* Their own bottom margins were spacing a row that did not exist yet. */
.panechips .threadchip,
.panechips .accesschip { margin: 0; }

.panescore { margin-top: var(--chiprow); }

/* In the rail the same block is a triage row, where the score leads and there
   is no headline to line up with. */
.railrow .panescore { margin-top: 0; }

/* ===========================================================================
   THE ARTICLE PAGE
   ===========================================================================

   Its own design, not the feed's pane widened. The feed shows forty candidates
   and every one of them has to answer "is this for me" in a glance, so the
   pane leads with chips and a score. Here that question is already settled —
   the article was chosen — and the page's job is the reading. So the reading
   takes the column and the judgements move to a rail, where they are available
   without being the first thing seen.

   Colour is spent on the two reserved axes and nothing else: teal is thread
   relevance, violet is adjacency, amber says how the full text can be reached.
   The primary action is deliberately neutral — "play" is not an axis, and an
   accent that means "press this" would make the other two mean less.
   =========================================================================== */

.ap { display: block; }

.apmain { display: flex; flex-direction: column; gap: 22px; min-width: 0; }

.apback {
  font: 400 12.5px/1 var(--ui);
  color: var(--text-dim);
  text-decoration: none;
  display: inline-block;
}
.apback:hover { color: var(--accent); }

/* --- kicker ---------------------------------------------------------------
   One row of small caps where there used to be three sizes of chip. Three chip
   styles at the top of the page were three ways of saying "context", and the
   loudest of them was the least important — a filled pill for access outranked
   the thread the article actually belongs to. Now nothing is filled, and only
   the two axes carry colour. */
.apkicker {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 10px;
  margin: 12px 0 0;
  font: 500 10.5px/1 var(--mono);
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.apthread { color: var(--thread-mark); }
.apaccess { display: inline-flex; align-items: center; gap: 5px; color: var(--hot-ink); }
.apnew    { color: var(--text-dim); }
.apsep {
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--border-strong);
  flex: none;
}

/* --- head ---------------------------------------------------------------- */
.aptitle {
  font: 700 34px/1.16 var(--display);
  letter-spacing: -.018em;
  color: var(--text-strong);
  /* A measure, not a width. Past about thirty characters a display-size line
     stops being scanned and starts being read twice. */
  max-width: 30ch;
  margin: 0;
}
.apsource { margin: 0; font: 400 13.5px/1.4 var(--ui); color: var(--text-dim); }
.apvenue  { font-weight: 600; color: var(--text); }
.apauthors { margin: 5px 0 0; font: 400 13.5px/1.5 var(--ui); color: var(--text-dim); }

/* The head is one group. The column gap would otherwise space four lines of
   one masthead as though they were four sections. */
.apkicker + .aptitle { margin-top: -8px; }
.aptitle + .apsource { margin-top: -12px; }
.apsource + .apauthors { margin-top: -17px; }

/* --- actions --------------------------------------------------------------
   Labelled buttons in the flow of the page, in place of the floating dial. The
   dial exists to gather actions where there is no room for them; on a page
   this wide there is room, and a control that covers the text to show a menu
   is a cost paid for nothing. */
.apactions { display: flex; flex-wrap: wrap; gap: 8px; padding-top: 4px; margin: 0; }

.apbtn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 13px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  font: 600 13px/1 var(--ui);
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
}
.apbtn:hover { border-color: var(--border-strong); color: var(--text); background: var(--surface-hover); }
.apbtnicon { padding: 8px 12px; }

/* Pressed state is the fill and the word together — "Add to Read next" becomes
   "In Read next". Neither is carried by hue, so it survives a colourblind
   reader and a monochrome screenshot. */
.apbtn.on {
  background: var(--surface-sunken);
  border-color: var(--border-strong);
  color: var(--text-strong);
}
.apbtn.on:hover { background: var(--surface-sunken); }
.apbtn.on .apstar { fill: currentColor; }

.apbtn .ic { flex: none; }

/* --- more menu ------------------------------------------------------------ */
.apmore { position: relative; }
.apmore > summary { list-style: none; }
.apmore > summary::-webkit-details-marker { display: none; }
.apmore > summary::marker { content: none; }
.apmore[open] > summary { background: var(--surface-sunken); border-color: var(--border-strong); }

.apmenu {
  position: absolute; top: calc(100% + 6px); left: 0;
  z-index: 40;
  min-width: 220px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 7px;
  box-shadow: 0 6px 20px rgba(11, 21, 38, .12);
}
.apmenu form { display: block; }
.apmenulabel {
  margin: 6px 8px 4px;
  font: 500 10px/1 var(--mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.apmenuitem {
  display: block; width: 100%;
  padding: 7px 8px;
  border: 0; border-radius: 5px;
  background: none;
  font: 400 13px/1.3 var(--ui);
  color: var(--text);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.apmenuitem:hover { background: var(--surface-hover); color: var(--text-strong); border-color: transparent; }
.apmenuitem.on { color: var(--accent); font-weight: 600; }

/* The citation itself: a thing to be selected and pasted, so it is set in the
   text face at reading size rather than as interface chrome. */
.apcite {
  padding: 14px 16px;
  background: var(--surface-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
}
.apcitetext {
  margin: 6px 0 0;
  font: 400 14px/1.6 var(--ui);
  color: var(--text-strong);
  /* A reference is judged on its hanging indent as much as on its content. */
  padding-left: 22px;
  text-indent: -22px;
}
.apcitefail { margin: 6px 0 0; font-size: 13px; color: var(--text-dim); }

.aprule { height: 1px; border: 0; margin: 0; background: var(--border); }

/* --- prose ----------------------------------------------------------------
   Two registers, and they must never be mistaken for one another: one is what
   the authors argue, the other is Throughline explaining itself. The second is
   a step smaller and set behind a rule, which is how a pull-quote has always
   been distinguished from the article around it. */
.approse { display: flex; flex-direction: column; gap: 9px; max-width: 66ch; }
.aplabel {
  margin: 0;
  font: 500 10.5px/1 var(--mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.apsays { margin: 0; font: 400 16.5px/1.62 var(--ui); color: var(--text-strong); }
.apwhy {
  margin: 0;
  padding-left: 18px;
  border-left: 2px solid var(--border);
  font: 400 15.5px/1.62 var(--ui);
  color: var(--text);
}

/* --- attribute strip ------------------------------------------------------ */
.apstrip {
  display: flex; flex-wrap: wrap;
  max-width: 66ch;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  overflow: hidden;
}
.apcell {
  flex: 1; min-width: 150px;
  padding: 11px 16px;
  border-left: 1px solid var(--border-soft);
}
/* The method statement is a sentence — "Systematic review with grounded-theory
   coding of assessments, 2011–2020" — while the long read is three words. Equal
   cells wrap the first to four lines and leave the second nearly empty. */
.apcell:first-child { border-left: 0; flex: 2; }
.apcelllabel {
  margin: 0 0 3px;
  font: 500 10px/1 var(--mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  /* --text, not --text-dim: on the sunken fill --text-dim measures 4.2:1 and
     fails AA. Every rule of this kind in this block is the same rule. */
  color: var(--text);
}
.apcellvalue { margin: 0; font: 400 13.5px/1.45 var(--ui); color: var(--text-strong); }

/* --- notes ---------------------------------------------------------------- */
.apnotes { display: flex; flex-direction: column; gap: 10px; max-width: 66ch; }
.apnoteshead { display: flex; align-items: center; gap: 12px; }
.aphair { flex: 1; height: 1px; background: var(--border-soft); }

/* A bare microphone is a guess. It says "record" to some people and "mute" to
   others, and one of those is the opposite of what it does. */
.apdictate {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  font: 500 12px/1 var(--ui);
  color: var(--text);
  cursor: pointer;
}
.apdictate:hover { border-color: var(--border-strong); color: var(--text); }
.apdictate.is-recording { border-color: var(--danger); color: var(--danger); }

/* The note block keeps the feed's class names so notes.js finds it unchanged,
   which means it also inherits the feed's shapes. Three of them do not survive
   the move: the mic was a 34px circle holding one glyph and now holds a word,
   the box carried its own top margin into a column that already has a gap, and
   the form was a shrink-to-fit column, which leaves a space-between footer
   with nothing to space. */
.apnotes.notebox { margin: 0; }
.apdictate.notemic { width: auto; height: auto; padding: 5px 10px; border-radius: 6px; }
.apnotes .noteform { display: flex; flex-direction: column; align-items: stretch; gap: 10px; width: 100%; }

.apnotetext {
  width: 100%; box-sizing: border-box;
  min-height: 150px;
  resize: vertical;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  font: 400 15px/1.6 var(--ui);
  color: var(--text-strong);
}
.apnotetext:focus-visible { border-color: var(--border-strong); outline: 2px solid var(--text-strong); outline-offset: 1px; }

.apnotesfoot { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.apcount { font: 400 11.5px/1 var(--mono); color: var(--text-dim); }
.apsave {
  padding: 9px 18px;
  border: 1px solid var(--text-strong);
  border-radius: 7px;
  background: var(--text-strong);
  color: var(--surface);
  font: 600 13px/1 var(--ui);
  cursor: pointer;
}
.apsave:hover { background: var(--text); border-color: var(--text); color: var(--surface); }

/* --- rail ----------------------------------------------------------------- */
.aprail { display: flex; flex-direction: column; gap: 22px; min-width: 0; }

.apverdict {
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  background: var(--surface);
}
.apscore { display: flex; align-items: baseline; gap: 12px; }
.apscoren {
  font: 500 34px/1 var(--mono);
  letter-spacing: -.02em;
  color: var(--text-strong);
}
.apscoretext { display: flex; flex-direction: column; gap: 3px; }
.apscoretext b { font: 700 14px/1 var(--display); color: var(--text-strong); }
.apscoretext span { font-size: 12px; color: var(--text-dim); }

/* The long read is the reason this page has a rail rather than a footnote, so
   it sits at the top of it and takes the only filled button on the page. */
.aplong {
  display: flex; flex-direction: column; gap: 10px;
  padding-top: 12px;
  border-top: 1px solid var(--border-soft);
}
.applace {
  display: flex; justify-content: space-between; gap: 8px;
  margin: 0;
  font: 400 11.5px/1 var(--mono);
  color: var(--text-dim);
}
.apprimary {
  position: relative;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 14px;
  border: 1px solid var(--text-strong);
  border-radius: 7px;
  background: var(--text-strong);
  color: var(--surface);
  font: 600 13px/1 var(--ui);
  text-decoration: none;
  cursor: pointer;
}
.apprimary:hover { background: var(--text); border-color: var(--text); color: var(--surface); }
/* The upload variant is a label wrapping a file input: the input has to be
   reachable by the pointer and invisible to the eye. */
.apprimary input[type="file"] {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0;
  cursor: pointer;
}
.apsecondary {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  font: 400 12.5px/1 var(--ui);
  color: var(--text-dim);
  text-decoration: none;
}
.apsecondary:hover { color: var(--accent); }
.apnote { margin: 0; font: 400 11.5px/1.45 var(--ui); color: var(--text-dim); }

/* --- adjacency ------------------------------------------------------------
   Tinted with an edge, not filled. Filled violet, this card outranked the
   verdict above it — and adjacency is by definition the thing that is NOT what
   was asked for. The edge is enough to say which axis is speaking. */
.apadj {
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 16px;
  background: var(--adjacent-tint);
  border: 1px solid var(--adjacent-line);
  border-left: 3px solid var(--adjacent);
  border-radius: var(--radius);
}
.apadjlabel {
  display: flex; justify-content: space-between; gap: 10px;
  margin: 0;
  font: 500 10.5px/12px var(--mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--adjacent);
}
.apadjbody { margin: 0; font: 400 13px/1.55 var(--ui); color: var(--text); }
.apadjlink { font-size: 12.5px; color: var(--adjacent); text-decoration: none; }
.apadjlink:hover { text-decoration: underline; }

/* --- thread relevance ----------------------------------------------------- */
.apthreads { display: flex; flex-direction: column; }
.aplabelink { color: var(--text); }
.apthreadrow {
  display: flex; flex-direction: column; gap: 5px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
.aplabelink + .apthreadrow { border-top: 0; padding-top: 8px; }
.apthreadhead { display: flex; align-items: baseline; gap: 10px; margin: 0; }
.apthreadname { flex: 1; font: 600 12.5px/1.35 var(--ui); color: var(--text-strong); }
.apthreadscore { font: 500 13px/1 var(--mono); color: var(--thread-mark); }
/* --text, not --text-dim: this sits on the sunken rail. */
.apthreadwhy { margin: 0; font: 400 12.5px/1.5 var(--ui); color: var(--text); }

/* Every focusable thing on the page, one ring. */
.ap a:focus-visible, .ap button:focus-visible,
.ap summary:focus-visible, .ap label:focus-visible, .ap [tabindex]:focus-visible {
  outline: 2px solid var(--text-strong);
  outline-offset: 2px;
}

/* --- the two columns ------------------------------------------------------
   Below this the page is one column in source order, which is the order it
   should be read in: what it is, then what to do about it, then what the
   machine made of it. */
@media (min-width: 901px) {
  .ap {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    align-items: stretch;
    background: var(--surface);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius);
  }
  .apmain { padding: 26px 40px 44px; gap: 26px; }
  .aprail {
    padding: 26px 24px 40px;
    background: var(--surface-sunken);
    border-left: 1px solid var(--border);
    border-radius: 0 var(--radius) var(--radius) 0;
  }
}

@media (max-width: 900px) {
  .aptitle { font-size: 26px; max-width: none; }
  .apsays  { font-size: 16px; }
  .aprail {
    margin-top: 30px;
    padding-top: 22px;
    border-top: 1px solid var(--border);
  }
}

/* The finished prompt.
   -------------------
   Sits under the long read in the rail and only when the recording was heard to
   the end. It is the one place this app asks the reader for anything, so it
   gets a rule above it and no colour at all — an accent here would make it read
   as an alert, and it is a question. */
.apask {
  display: flex; flex-direction: column; gap: 8px;
  margin: 2px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border-soft);
}
.apaskq { margin: 0; font: 400 12.5px/1.45 var(--ui); color: var(--text); }
.apaskbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 8px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--surface);
  font: 600 12.5px/1 var(--ui);
  color: var(--text-strong);
  cursor: pointer;
}
.apaskbtn:hover {
  background: var(--surface-hover);
  border-color: var(--text-strong);
  color: var(--text-strong);
}
/* "Was it right?" — the verdict-against-behaviour table on Runs.
   -------------------------------------------------------------
   Deliberately plain. Every instinct says to colour the cells where the model
   was wrong, and that would be the third accent axis this app does not have —
   and worse, it would decide for the reader which disagreements are failures.
   Some of them are the reader changing their mind, which is not an error. */
.judgetable { margin: 30px 0 8px; }
.judgetable h2 { margin: 0 0 4px; }
.judgewrap {
  margin: 14px 0 0;
  overflow-x: auto;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  background: var(--surface);
}
.judgewrap table { width: 100%; border-collapse: collapse; font-size: 14px; }
.judgewrap thead th {
  padding: 10px 14px;
  text-align: left;
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--border);
  font: 500 10px/1.3 var(--mono);
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--text);
  white-space: nowrap;
}
.judgewrap tbody th {
  padding: 11px 14px;
  text-align: left;
  font: 600 13.5px/1.3 var(--ui);
  color: var(--text-strong);
  border-bottom: 1px solid var(--border-soft);
}
.judgewrap tbody td {
  padding: 11px 14px;
  border-bottom: 1px solid var(--border-soft);
  font: 500 13.5px/1.3 var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-strong);
}
.judgewrap thead th.n, .judgewrap tbody td.n { text-align: right; }
/* A zero is not news; it should not read with the same weight as a count. */
.judgewrap td.zero { color: var(--text-dimmer); font-weight: 400; }
.judgewrap tbody tr:last-child th,
.judgewrap tbody tr:last-child td { border-bottom: 0; }
.judgenote {
  max-width: 60ch;
  margin: 12px 0 0;
  font: 400 13px/1.6 var(--ui);
  color: var(--text-dim);
}
/* The upstream account is empty.
   -----------------------------
   --danger, not --hot: this is the one alarm state in the app, where nothing
   works at all until a person does something outside it. Everything else that
   looks urgent here is merely expensive. */
.creditchip.is-blocked {
  border-color: var(--danger);
  color: var(--danger);
  font-weight: 600;
}
.creditchip.is-blocked:hover { border-color: var(--danger); color: var(--danger); }
.creditchip.is-blocked .creditchipunit { color: var(--danger); }

.upstreamdown {
  margin: 22px 0 6px;
  padding: 18px 20px;
  max-width: 68ch;
  border: 1px solid var(--danger);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--surface);
}
.upstreamhead {
  margin: 0 0 8px;
  font: 600 15px/1.4 var(--ui);
  color: var(--danger);
}
.upstreamdown p { margin: 8px 0 0; font: 400 13.5px/1.6 var(--ui); color: var(--text); }
.upstreamdown p:first-of-type { margin-top: 0; }
/* Long-read outlook on a Desk tile.
   --------------------------------
   Three states that used to be one. "Queued" and "coming" are the app's
   promises and read as ordinary marks; "needs your PDF" is the one that asks
   something of the reader, so it is the only one that carries the access
   colour — the same amber that means "how you get at this" everywhere else. */
.deskmark.is-queued,
.deskmark.is-looking,
.deskmark.is-coming { color: var(--text-dim); }
.deskmark.is-needs-pdf {
  color: var(--hot-ink);
  border-color: var(--hot-ink);
}
/* The upload, demoted. It is a real action and still reachable, but it is not
   what somebody standing on a pavement wants offered first. */
.apupload {
  position: relative;
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}
.apupload:hover { border-color: var(--border-strong); }
.apupload input[type="file"] {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0; cursor: pointer;
}
/* Asking a summary a question.
   ---------------------------
   Sits between the prose and the notes, because it is about understanding the
   thing and notes are about what you made of it.

   The unanswered case is styled as an ordinary outcome rather than an error.
   It is what happens thirty times in thirty-one, since the app holds only an
   abstract for almost everything — and an interface that treats the common
   case as a failure teaches people the feature is broken when it is being
   honest. No --danger, no warning colour; a lighter ground and a quieter rule. */
.apask { display: flex; flex-direction: column; gap: 12px; max-width: 66ch; }

.apqa {
  display: flex; flex-direction: column; gap: 7px;
  padding: 14px 16px;
  background: var(--surface-sunken);
  border: 1px solid var(--border-soft);
  border-left: 2px solid var(--text-dim);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.apqa.is-unanswered { border-left-color: var(--border-strong); }

.apq {
  margin: 0;
  font: 600 13.5px/1.45 var(--ui);
  color: var(--text-strong);
}
.apa { margin: 0; font: 400 14.5px/1.6 var(--ui); color: var(--text); }

/* The verbatim sentence. Set apart and in the reading face, because its whole
   job is to be compared against the paper. */
.apquote {
  margin: 2px 0 0;
  padding: 8px 0 8px 14px;
  border-left: 2px solid var(--border-strong);
  font: 400 13.5px/1.55 var(--display);
  color: var(--text);
}
.apsrc {
  margin: 0;
  font: 500 10px/1 var(--mono);
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--text);
}

.apaskform { display: flex; flex-direction: column; gap: 8px; }
.apasktext {
  width: 100%; box-sizing: border-box;
  min-height: 62px; resize: vertical;
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  font: 400 14.5px/1.5 var(--ui);
  color: var(--text-strong);
}
.apasktext::placeholder { color: var(--text-dimmer); }
.apasktext:focus-visible {
  border-color: var(--border-strong);
  outline: 2px solid var(--text-strong); outline-offset: 1px;
}

.apasksend { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
/* Said before the question is asked rather than after it fails. */
.apasknote { font: 400 11.5px/1.45 var(--ui); color: var(--text-dim); }
.apaskbtn {
  flex: none;
  padding: 8px 16px;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--surface);
  font: 600 12.5px/1 var(--ui);
  color: var(--text-strong);
  cursor: pointer;
}
.apaskbtn:hover { background: var(--surface-hover); border-color: var(--text-strong); color: var(--text-strong); }

.apquotelabel {
  margin: 2px 0 -4px;
  font: 500 10px/1 var(--mono);
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-dim);
}
/* Adding a paper by hand. */
.deskadd { margin: 0 0 4px; font-size: 13px; }
.deskadd a, .dkadd a { color: var(--accent); text-decoration: none; }
.deskadd a:hover, .dkadd a:hover { text-decoration: underline; }
.dkadd { margin: 0 0 6px; font: 500 12px/1 var(--ui); }

.addpaper {
  display: flex; flex-direction: column; gap: 14px;
  align-items: flex-start;
  max-width: 52ch;
  margin: 22px 0 0;
}
.addpaperdrop {
  position: relative;
  display: flex; flex-direction: column; gap: 6px;
  width: 100%;
  padding: 22px 24px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
}
.addpaperdrop:hover { border-color: var(--text-strong); background: var(--surface-hover); }
.addpapercta { font: 600 15px/1.3 var(--ui); color: var(--text-strong); }
.addpaperhint { font: 400 13px/1.55 var(--ui); color: var(--text-dim); }
.addpaperdrop input[type="file"] {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0; cursor: pointer;
}

.addpapernote {
  max-width: 56ch;
  margin: 30px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--border-soft);
}
.addpapernote p { margin: 0 0 10px; font: 400 13.5px/1.6 var(--ui); color: var(--text); }
.addpapernote p:last-child { margin-bottom: 0; }
/* Asking the corpus.
   -----------------
   The one surface that runs the other way, and the only place the app writes
   more than three sentences at a stretch — so it gets a reading measure and the
   display face, unlike every other list in here. */
.cqform { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; max-width: 62ch; margin: 20px 0 0; }
.cqtext {
  width: 100%; box-sizing: border-box;
  min-height: 62px; resize: vertical;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  font: 400 15px/1.5 var(--ui);
  color: var(--text-strong);
}
.cqtext::placeholder { color: var(--text-dimmer); }
.cqtext:focus-visible { border-color: var(--border-strong); outline: 2px solid var(--text-strong); outline-offset: 1px; }

.cqanswer { max-width: 66ch; margin: 34px 0 0; }
.cqq {
  margin: 0 0 16px;
  font: 600 21px/1.3 var(--display);
  letter-spacing: -.012em;
  color: var(--text-strong);
}
.cqpara { margin: 0 0 14px; font: 400 16px/1.65 var(--ui); color: var(--text); }

/* The gaps get their own block and a rule. It is the part most worth reading
   and the part every reader skims past, so it does not get to be a closing
   sentence in the prose. */
.cqgaps {
  margin: 22px 0 0;
  padding: 16px 18px;
  background: var(--surface-sunken);
  border-left: 2px solid var(--border-strong);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.cqgaps p:last-child { margin: 6px 0 0; font: 400 14.5px/1.6 var(--ui); color: var(--text); }

.cqsources { margin: 30px 0 0; padding-top: 18px; border-top: 1px solid var(--border-soft); }
.cqlist { margin: 12px 0 0; padding-left: 22px; }
.cqlist li { margin-bottom: 12px; font-size: 14.5px; line-height: 1.5; }
.cqlist a { color: var(--text-strong); font-weight: 600; text-decoration: none; }
.cqlist a:hover { color: var(--accent); }
.cqmeta { display: block; margin-top: 2px; font: 400 12.5px/1.4 var(--ui); color: var(--text-dim); }
.cqwhy { display: block; margin-top: 3px; font: 400 13px/1.5 var(--ui); color: var(--text); }

.cqrecent { margin: 40px 0 0; padding-top: 18px; border-top: 1px solid var(--border-soft); max-width: 66ch; }
.cqrecent ul { margin: 10px 0 0; padding-left: 0; list-style: none; }
.cqrecent li { margin-bottom: 6px; }
.cqrecent a { font-size: 13.5px; color: var(--text); text-decoration: none; }
.cqrecent a:hover { color: var(--accent); text-decoration: underline; }