/* PPRT design system v2 — "warm earth + LCD", night mode. Day mode values
   live in the [data-theme="light"] block near the end of this file.
   Both are selected by the data-theme attribute alone: public/boot-theme.js
   sets it before first paint and ThemeContext.jsx maintains it, so this block
   is what a document explicitly marked dark gets, not merely a no-attribute
   fallback.

   v1's "warm paper + teal" is gone. The metaphor now is a machine you own:
   a warm espresso chassis, moulded keys with real bevels, and teal LCD
   windows for anything a machine produced. Held back from pastiche — no
   chrome gradients, no faux screws, no scanlines.

   Two rules do most of the work and are easy to break by accident:
   1. Depth comes from tone plus one light edge, NOT from drawn borders.
      There is deliberately no general-purpose 1px border token; --hairline
      exists for genuine structural divisions (table rows, card footers, hr)
      and nothing else. If a shape needs outlining to be legible, it is the
      wrong shape.
   2. Teal is not the accent any more. It belongs to the LCD alone, so the
      only glowing thing on screen is machine output. Amber accents, orange
      acts, red destroys.

   Scales are in rem, not the handoff's literal px. The proportions and the
   token names are v2's — so a rule copied out of the design system's
   components.css drops in unchanged — but expressing them in rem keeps the
   whole interface scaling with the reader's own browser font size, which px
   would silently take away. Body is 0.875rem (14px at the default root),
   which is v2's specified body size. */
:root {
  /* ---- base ramps ---- */
  /* clay — warm espresso through sand. The whole neutral axis: every surface
     and every text colour comes from here. The base is brown, not black; the
     chassis should read as an object under a lamp. */
  --clay-1000: #150e08;
  --clay-950: #1d140c;
  --clay-900: #241a12;
  --clay-850: #2e2116;
  --clay-800: #33241a;
  --clay-700: #4a3524;
  --clay-600: #63482f;
  --clay-500: #8a6c4c;
  --clay-400: #b79a78;
  --clay-300: #cfb593;
  --clay-200: #e3d1b2;
  --clay-100: #f0e2c9;
  --clay-50: #f9edd8;

  /* teal — the LCD, and nothing else. */
  --teal-900: #0b2a22;
  --teal-800: #12332a;
  --teal-700: #17614f;
  --teal-600: #1f7a63;
  --teal-500: #269d7d;
  --teal-400: #2bc79b;
  --teal-300: #55e0b6;
  --teal-200: #7cf0cb;

  /* ember — the sunset band, amber at the top through plum at the bottom.
     This is what carries hierarchy now. */
  --ember-300: #f9911c;
  --ember-400: #f4601c;
  --ember-500: #e23b30;
  --ember-600: #bd2242;
  --ember-700: #7a1f3c;

  /* olive + amber — secondary indicators. Warning is amber, never yellow. */
  --olive-600: #4c7522;
  --olive-500: #5f8f2b;
  --olive-400: #7dbe3c;
  --amber-600: #b07d10;
  --amber-500: #d99b18;
  --amber-400: #ffc13d;
  --amber-300: #ffd67a;

  /* dusk — the cool counterweight; informational only. */
  --dusk-600: #0b6f95;
  --dusk-500: #1189b8;
  --dusk-400: #0e9cd0;
  --dusk-300: #63c6e8;

  /* spectrum — five categorical channels from record-sleeve printing,
     identical in both themes so a topic keeps its colour across a theme
     switch. The ONLY place in the system where a hue means "which", not
     "how bad". Status never uses a channel. Each has an ink twin for text
     on cream; the raw fills are for bands, dots and swatches. */
  --ch-cyan: #0e9cd0;
  --ch-green: #7dbe3c;
  --ch-yellow: #f2ce00;
  --ch-orange: #e8871e;
  --ch-magenta: #e5117f;
  --ch-1: var(--ch-cyan);
  --ch-2: var(--ch-green);
  --ch-3: var(--ch-yellow);
  --ch-4: var(--ch-orange);
  --ch-5: var(--ch-magenta);
  /* The spectrum band — the five channels as one contiguous line, the same
     graphic the transactional emails carry under their header. Hard stops
     rather than a blend: these are five categorical colours sitting next to
     each other, not a gradient between two of them, and a blend would invent
     four hues that mean nothing. Identical in both themes, like every other
     use of the channels. */
  --spectrum-band: linear-gradient(
    90deg,
    var(--ch-1) 0 20%,
    var(--ch-2) 20% 40%,
    var(--ch-3) 40% 60%,
    var(--ch-4) 60% 80%,
    var(--ch-5) 80% 100%
  );

  --ch-cyan-ink: #0b6f95;
  --ch-green-ink: #46701d;
  --ch-yellow-ink: #8a6f00;
  --ch-orange-ink: #b2620d;
  --ch-magenta-ink: #b80d66;

  /* ---- semantic: dark is the default theme ---- */
  --bg: var(--clay-900);
  --bg-sunk: var(--clay-950);
  --surface: var(--clay-850);
  --surface-raised: var(--clay-800);
  --surface-inset: var(--clay-950);
  --lcd: var(--teal-900);
  --lcd-text: var(--teal-200);
  --lcd-text-dim: var(--teal-500);

  --text: var(--clay-50);
  --text-muted: var(--clay-400);
  /* Engraved labels only — 9–10px uppercase mono. The handoff's own value
     here was --clay-500, which measures 3.2:1 on --surface: under 4.5:1, and
     this token carries small text (section labels, field labels, LCD legends,
     table headers), which needs more contrast than body copy, not less.
     Raised one step. */
  --text-faint: var(--clay-400);
  --text-on-accent: var(--clay-1000);
  /* Ink on a filled --action/--danger key. The handoff specified a near-white
     (#fff4ea) here, which measures 2.96:1 on the ember action colour — the
     primary button's own label failing contrast. Dark ink measures 5.95:1,
     and it is what the handoff's email kit already puts on that exact button,
     so this aligns the token with the design rather than departing from it. */
  --text-on-action: var(--clay-1000);

  --accent: var(--amber-400);
  --accent-strong: var(--amber-300);
  --accent-quiet: rgba(255, 193, 61, 0.16);
  --action: var(--ember-400);
  --action-hover: var(--ember-300);
  --danger: var(--ember-500);
  --danger-quiet: rgba(226, 59, 48, 0.18);
  /* --danger as *text* measures 3.6:1 on --surface, so it is a fill, a lamp
     and a key — not a sentence. Small danger copy uses this lighter twin
     (6.1:1), mirroring the --warning/--warning-ink split the system already
     ships for exactly this reason. */
  --danger-ink: #ff7a6e;
  /* A destructive KEY is a third case again. --danger under the near-black
     ink every other key uses measures 4.46:1 — a hair under, on a 12px
     label — so the key drops one step down the ember ramp to crimson and
     takes light ink instead, which measures 5.2:1. */
  --danger-key: var(--ember-600);
  --text-on-danger: var(--clay-50);
  --warning: var(--amber-400);
  --warning-quiet: rgba(255, 193, 61, 0.18);
  --warning-ink: var(--amber-400);
  --success: var(--olive-400);
  --success-quiet: rgba(125, 190, 60, 0.16);
  --info: var(--dusk-400);
  --info-quiet: rgba(14, 156, 208, 0.16);

  /* The <select> chevron. Chromium pins its own arrow to the border edge and
     ignores padding-right, so the control takes appearance:none and draws
     this instead — the only way to inset the arrow from the field's edge the
     way the text is inset from the other one. Stroke colour is a literal
     because a data URI cannot reference a custom property; it tracks
     --text-muted and has to be changed with it. Round caps and the same
     open-chevron shape as the app's other hand-written inline SVGs. */
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23b79a78' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  /* The light that travels across a skeleton bone. Kept low-contrast on
     purpose: it should read as something passing over the well, not as a
     glossy highlight sliding along a plastic bar. */
  --skeleton-sheen: rgba(255, 226, 182, 0.14);

  --hairline: rgba(255, 222, 176, 0.12);
  --bevel-light: rgba(255, 226, 182, 0.11);
  --bevel-dark: rgba(0, 0, 0, 0.45);
  --focus-ring: var(--amber-400);
  --scrim: rgba(14, 8, 4, 0.72);

  /* Two families, one job each: sans is language, mono is machine output —
     counts, domains, timestamps, credentials, engraved labels. That single
     rule does most of the visual work. Both self-hosted via @fontsource,
     imported in main.jsx. */
  --font-sans: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-family: var(--font-sans);
  color-scheme: dark;

  /* Everything below is theme-independent — it lives in this block rather
     than a second :root only because there is no reason to split them, and
     none of it is repeated in the day-mode block at the end of the file.

     Corner radius. Nothing is fully square and nothing is soft: 3px on
     controls, 5px on plates, 7px on a card floating over a scrim, pill for
     subtopic chips alone. These are a third of v1's values — the softness
     went with the paper. A true circle (.status-detail-icon) uses 50%
     directly, since that is a shape rather than a step on this scale. */
  --radius-sm: 3px;
  --radius: 5px;
  --radius-lg: 7px;
  --radius-pill: 999px;

  /* Depth, not borders. A surface separates from what is under it by tone
     plus one light edge. Raised = a plate on the chassis; panel = the main
     content plate, with more shadow under it; pressed = the light moves to
     the bottom edge and the face takes a shadow; sunk = cut into the chassis
     (inputs, wells, toggle tracks); lcd = glass, dark and slightly
     vignetted. */
  --bevel-raised: inset 0 1px 0 var(--bevel-light), 0 1px 2px var(--bevel-dark);
  --bevel-panel: inset 0 1px 0 var(--bevel-light), 0 2px 6px var(--bevel-dark);
  --bevel-pressed: inset 0 2px 4px var(--bevel-dark), inset 0 -1px 0 var(--bevel-light);
  --bevel-sunk: inset 0 2px 4px var(--bevel-dark);
  --bevel-lcd: inset 0 2px 10px rgba(0, 0, 0, 0.6);

  /* The two transient layers that detach from the page behind them. Adding
     a third elevation should be a deliberate decision, not a new literal. */
  --lift-menu: 0 8px 24px rgba(0, 0, 0, 0.45);
  --lift-modal: 0 18px 48px rgba(0, 0, 0, 0.5);
  /* Derived from the semantic colours so a palette change carries through. */
  --glow-accent: 0 0 10px color-mix(in oklab, var(--accent) 38%, transparent);
  --glow-action: 0 0 8px color-mix(in oklab, var(--action) 45%, transparent);

  /* Stacking order, named by what stacks. The numbers are unevenly spaced
     on purpose: the gaps are where a future layer goes without renumbering
     its neighbours, and the names are what should appear in a rule, so that
     "is this above the nav?" is answerable without grepping for integers. */
  --z-jump-nav: 5;
  --z-tooltip: 10;
  --z-nav: 20;
  --z-nav-backdrop: 25;
  --z-panel: 40;
  --z-modal: 60;

  /* Motion is mechanical: short, no overshoot. A press moves 1px down and
     inverts its bevel; nothing scales, bounces or grows. The only continuous
     animations in the system are the dream-job spinner and the LCD cursor
     blink — both disabled under prefers-reduced-motion at the end of this
     file. */
  --ease-hardware: cubic-bezier(0.2, 0, 0.2, 1);
  --dur-press: 80ms;
  --dur-fast: 120ms;
  --dur: 180ms;
  --dur-slow: 260ms;
  --transition-control: background-color var(--dur-fast) var(--ease-hardware),
    box-shadow var(--dur-fast) var(--ease-hardware), color var(--dur-fast) var(--ease-hardware),
    transform var(--dur-press) var(--ease-hardware);

  /* Two weights per family, and only two: every weight is a real font file
     the browser downloads (~14KB each, latin subset), and this interface had
     been pulling seven — 96KB — to render text that is overwhelmingly regular
     with some emphasis. Regular and semibold cover both.

     --weight-medium and --weight-bold are deliberately absent rather than
     defined-but-unused: naming a weight with no face behind it invites a rule
     that asks for it, and the browser answers by synthesising a smeared
     approximation. If a third weight is ever genuinely needed, add the
     @fontsource import in main.jsx in the same commit as the token. */
  --weight-regular: 400;
  --weight-semibold: 600;

  /* Spacing — v2's twelve-step ramp, in rem. The old nine-step
     --space-3xs…3xl scale is gone; five of its steps were exactly these
     values under another name.

     Roughly fifty declarations in this file still carry bare rem literals
     sitting between steps. Those are drift, not intent: nobody chose 0.55rem
     over 0.5rem for a reason that was written down. Snapping them onto this
     scale moves each affected rule by a pixel or two, which is a real visual
     change and wants something rendered to look at, so a bare rem value in a
     spacing property remains the marker for "this hasn't been through that
     pass yet". */
  --space-1: 0.25rem;
  --space-2: 0.375rem;
  --space-3: 0.5rem;
  --space-4: 0.625rem;
  --space-5: 0.75rem;
  --space-6: 0.875rem;
  --space-7: 1rem;
  --space-8: 1.125rem;
  --space-9: 1.375rem;
  --space-10: 1.75rem;
  --space-11: 2.25rem;
  --space-12: 3rem;

  /* Panels are padded 18–22px with a 12–14px internal gap; naming those two
     means a card's shape is stated once instead of guessed per rule. */
  --panel-pad: var(--space-9);
  --panel-gap: var(--space-6);
  --stack-gap: var(--space-7);

  /* Two widths, decided by route and never by the page: a reading column,
     and a two-column width for the screens with a sidebar. */
  --measure-read: 780px;
  --measure-wide: 1040px;
  --measure-prose: 62ch;

  /* Type scale — a 1.25 ratio, named --text-* as the design system names it.
     This collides by sight with --text and --text-muted, the two foreground
     *colours*, which is exactly why the previous scale was called --type-*.
     The trade was made deliberately: matching v2's names means a rule lifted
     from the design system's components.css drops in unchanged, on this pass
     and on every future re-drop, which is worth more than the ambiguity
     costs. A --text-* value is a size; --text and --text-muted are colours.

     em values are left alone: .code-reveal and the two 0.9em rules size
     against their own container on purpose. */
  --text-2xs: 0.5625rem;
  --text-xs: 0.625rem;
  --text-sm: 0.75rem;
  --text-base: 0.875rem;
  --text-md: 1rem;
  --text-lg: 1.25rem;
  --text-xl: 1.5625rem;
  --text-2xl: 1.9375rem;
  --text-3xl: 2.4375rem;

  --leading-tight: 1.2;
  --leading-snug: 1.35;
  --leading-body: 1.55;
  --leading-loose: 1.8;

  /* Engraved label: uppercase mono, wide tracking, tiny. Used for every panel
     legend, meta label and status word in the product. */
  --tracking-engraved: 0.18em;
  --tracking-mono: 0.1em;
  --tracking-tight: -0.015em;
}

* {
  box-sizing: border-box;
}

html,
body {
  /* Defensive: something (a dropdown panel, an inline element's computed
     width, safe-area padding interacting with a flex row — no single
     100vw-style culprit found) was creating a sliver of horizontal
     overflow on mobile. This is the standard guard against it regardless
     of the exact source, since nothing in this app's layout is meant to
     scroll sideways.
     `clip`, not `hidden` — confirmed directly this was silently breaking
     `.nav`'s `position: sticky` on every page, not just Library's new
     jump bar: per the CSS Overflow spec, setting overflow-x to anything
     other than `visible` while overflow-y is left unset forces the
     computed overflow-y to `auto` too (you can't mix `visible` with a
     non-visible value on the same box). That silently turned html/body
     into their own scroll containers instead of the normal viewport
     scroll, so `.nav`'s sticky positioning had nothing to actually stick
     within — measured directly: nav's own top tracked scrollY 1:1 (fully
     scrolled away) instead of clamping at 0. `overflow-x: clip` gives the
     same horizontal-overflow guard without that overflow-y side effect —
     confirmed overflow-y computes back to `visible` and sticky nav works
     again on every page tested. */
  overflow-x: clip;
}

/* The design system's display type is semibold with tight tracking. Nothing
   in this file ever said so, so every heading rendered at the user agent's
   default bold — heavier than specified, and the sole reason a 700 face was
   being downloaded at all. */
h1,
h2,
h3,
h4 {
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* v2's body size, and the reference every --text-* step is a ratio of.
     Set here rather than on :root so the rem tokens keep measuring against
     the reader's own root size instead of compounding against this one. */
  font-size: var(--text-base);
  line-height: var(--leading-body);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  /* Keeps page content clear of the home indicator (iOS) / gesture nav bar
     (Android) so it doesn't crowd whatever's at the bottom of a page —
     max() so devices/browsers that report 0 for the inset (non-notched
     hardware, or plain browser tabs) still get a sane minimum margin. */
  padding-bottom: max(1rem, env(safe-area-inset-bottom));
}

a {
  color: inherit;
  text-decoration: none;
}

/* Inline text links that should read as links even inside a .muted
   paragraph (where `a { color: inherit }` above would otherwise blend
   them into the surrounding gray text) — e.g. Save.jsx's "Use Bulk
   Import" hint. */
.text-link {
  color: var(--accent);
  text-decoration: underline;
}

/* The bare <button> IS the primary: a moulded transport key in ember. It
   carries --action rather than --accent because v2 splits the two — amber
   marks what matters, orange is the one thing on the screen you press. The
   label is mono uppercase like every other machine-facing string.

   The press is mechanical: 1px down and the bevel inverts. Nothing scales,
   bounces or overshoots anywhere in this system. */
button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  min-height: 38px;
  background: var(--action);
  color: var(--text-on-action);
  border: none;
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-8);
  cursor: pointer;
  font-weight: var(--weight-semibold);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), inset 0 -2px 0 rgba(0, 0, 0, 0.28),
    0 1px 3px var(--bevel-dark);
  transition: var(--transition-control);
}

button:hover:not(:disabled) {
  background: var(--action-hover);
}

button:active:not(:disabled) {
  transform: translateY(1px);
  box-shadow: var(--bevel-pressed);
}

button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.link-button {
  background: none;
  box-shadow: none;
  min-height: 0;
  color: var(--text-muted);
  padding: 0;
  font-family: var(--font-sans);
  font-size: inherit;
  letter-spacing: normal;
  text-transform: none;
  font-weight: var(--weight-regular);
  text-decoration: underline;
}

.link-button:hover:not(:disabled) {
  background: none;
  color: var(--text);
}

.link-button:active:not(:disabled) {
  transform: none;
  box-shadow: none;
}

/* Icon+label button in the secondary style — e.g. the Library page's
   Change/Remove/Delete actions, and ConfirmModal's Cancel button. It used to
   be an accent-coloured outline; v2 has no outlined controls, so it is now a
   moulded key in a quieter tone, which is the same hierarchy said with depth
   instead of a line. Same padding/font-size as the default `button` rule by
   design — Delete sits directly next to Reprocess (a primary button) and
   needs to match its size to read as the same hierarchy, just a different
   (secondary) treatment. */
.btn-secondary {
  /* display/align-items/gap inherited from the base `button` rule above. */
  background: var(--surface-raised);
  color: var(--text);
  box-shadow: var(--bevel-raised);
  border: 0;
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-8);
  font-weight: var(--weight-semibold);
}

/* A label or anchor carrying a button class gets none of the bare `button`
   element rules above, so the key's typography has to be restated. The
   file-input label and the extension download links both rely on this. */
a.btn-secondary,
label.btn-secondary,
span.btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: 38px;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  cursor: pointer;
  text-decoration: none;
}

.btn-secondary svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
}

.btn-secondary:hover:not(:disabled) {
  background: var(--surface);
}

/* Icon symbols keep their own semantic color regardless of the button's
   text color (which stays accent/mint) — Change is informational (blue),
   Remove/Delete lean destructive (red), Reprocess's bolt is white against
   its filled accent background. Matches the Digest page's own
   statusIndicator() convention of coloring just the symbol. */
.icon-change,
.icon-edit {
  color: var(--info);
}

.icon-remove,
.icon-delete {
  color: var(--danger-ink);
}

.icon-bolt {
  color: #fff;
  /* Unlike the other icons, this one lives inside a plain `button` rather
     than .btn-secondary/.btn-secondary-sm, which are what size the other
     icons — without this it falls back to the SVG's huge browser-default
     intrinsic box (confirmed directly: rendered ~300px, blowing up the
     whole Reprocess button). */
  width: 15px;
  height: 15px;
  flex-shrink: 0;
}

/* Compact variant — same secondary-mint coloring, smaller footprint for
   inline per-item actions like Library's Change/Remove (which sit next to
   a topic name/link, not paired with a primary button the way
   Delete/Reprocess are). Combine with .btn-secondary, e.g.
   className="btn-secondary btn-secondary-sm". */
.btn-secondary-sm {
  gap: 0.3rem;
  border-radius: var(--radius-sm);
  padding: 0.3rem 0.55rem;
  font-size: var(--text-xs);
  /* Same weight as full-size .btn-secondary — the compact variant differs in
     size, not in emphasis. */
  font-weight: var(--weight-semibold);
}

.btn-secondary-sm svg {
  width: 13px;
  height: 13px;
}

/* Destructive confirm action (ConfirmModal's Confirm button when danger
   is set, e.g. deleting an item) — same shape as the default button, in the
   ember band's crimson rather than its orange. It names its own fill and ink
   instead of reusing --danger/--text-on-action: that pairing measures 4.46:1,
   which is a hair under the 4.5 a 12px label needs. */
.btn-danger {
  background: var(--danger-key);
  color: var(--text-on-danger);
}

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: var(--z-modal);
  padding: var(--space-7);
}

.modal-card {
  background: var(--surface-raised);
  box-shadow: var(--lift-modal);
  border-radius: var(--radius-lg);
  padding: var(--space-9);
  max-width: 360px;
  width: 100%;
}

.modal-card p {
  margin: 0 0 var(--space-8);
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-5);
}

input,
textarea,
select {
  font: inherit;
  /* Sunk, not outlined: an input reads as cut into the chassis. */
  background: var(--surface-inset);
  border: 0;
  box-shadow: var(--bevel-sunk);
  border-radius: var(--radius);
  padding: 0.55rem var(--space-5);
  color: var(--text);
}

textarea {
  resize: vertical;
}

/* The chevron sits the same distance from the right edge as the label sits
   from the left, which is what the native arrow would not do at any padding.
   The right padding is that inset plus the arrow's own width, so a long
   option never runs underneath it. */
select {
  appearance: none;
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right var(--space-5) center;
  background-size: 10px 6px;
  padding-right: calc(var(--space-5) * 2 + 10px);
}

input:focus,
textarea:focus,
select:focus {
  outline: 2px solid var(--accent);
}

/* Only `button` had a disabled rule, so a disabled input or select was
   drawn exactly like a live one — it simply ignored you. That matters more
   now that two controls ship deliberately inert (Settings' digest schedule,
   Login's SSO row): "visibly off" is the entire point of shipping them, and
   without this they are only off to whoever tries them.

   Same 0.6 opacity and default cursor as button:disabled, so all four
   control types agree on what unavailable looks like. */
input:disabled,
textarea:disabled,
select:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Settings' inert schedule picker and its "coming soon" badge, kept on one
   line so the badge reads as a note on the control rather than as a
   separate thing under it. */
/* Title and status badge on one line, badge pushed to the right edge so a
   column of cards shows its statuses in a single scannable column rather
   than at whatever x-position each title happens to end at. */
.import-batch-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.import-batch-card-head h3 {
  margin: 0;
}

.settings-schedule-row {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

.settings-schedule-select {
  width: auto;
  min-width: 14rem;
}

/* Form vocabulary — the classes behind components/ui/Field.jsx and
   Input.jsx. Until these existed the three form elements above were the
   whole story: styled bare, with every screen inventing its own label
   markup and none for help or error text, so an invalid control looked
   exactly like a valid one and said nothing to a screen reader either. */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-5);
}

.field-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-engraved);
  text-transform: uppercase;
  color: var(--text-faint);
}

.field-help,
.field-error {
  margin: 0;
}

.field-error {
  color: var(--danger-ink);
}

/* ui/Field.jsx has emitted this class on an errored field since it was
   written, and nothing matched it — the visible state came entirely from
   .is-invalid on the input itself, which only appears if the caller also
   passes invalid. Marking the wrapper means the label goes with it. */
.field-invalid .field-label {
  color: var(--danger-ink);
}

/* An inset ring drawn inside the field's own sunk moulding, rather than a
   fill or an icon: the moulding is doing work already (it is what separates
   a field from the page), and the ring is the one part free to carry state
   now that v2 leaves inputs unbordered. The focus ring still wins while the
   field is focused, which is the right priority — you are being told where
   you are before you are told what is wrong, and the message below the field
   does not go anywhere. */
.is-invalid {
  box-shadow: var(--bevel-sunk), inset 0 0 0 1px var(--danger);
}

.is-invalid:focus {
  outline-color: var(--danger);
}

/* Plain <a> tags (topic titles, item titles, nav links, "Sources:" links,
   etc.) had no focus style of their own anywhere in this file — only
   this browser default, which doesn't render visibly against either
   theme's background (confirmed directly via screenshot: tabbing to a
   topic link showed no ring at all). WCAG 2.4.7 requires a visible focus
   indicator; this is the one place that covers every link in the app at
   once. :focus-visible (not :focus) so a mouse click still doesn't show
   a ring — only keyboard/programmatic focus does. */
a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Buttons had no rule of their own and fell back to the UA ring. Chrome
   renders that acceptably in both themes, but it's the one interactive
   element type left relying on browser defaults — matching the link rule
   above makes the indicator ours and consistent across browsers. */
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Visually hidden but still reachable by screen readers/keyboard — e.g.
   an unlabeled table column's accessible name, or the native file input
   behind ImportBatches.jsx's styled label. display:none would drop it
   from the accessibility tree and tab order entirely, which is not what
   we want here. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

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

.small {
  font-size: var(--text-sm);
}

/* Danger as small TEXT uses the lighter ink twin: --danger itself is a fill,
   a lamp and a key, and only reaches 3.6:1 on --surface. */
.error {
  color: var(--danger-ink);
}

.badge {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-mono);
  background: var(--accent-quiet);
  color: var(--accent);
  padding: 0.15rem var(--space-3);
  border-radius: var(--radius-sm);
  margin-left: var(--space-3);
}

/* "Coming soon" on a control that is present and switched off. The tint is
   not decoration: warning-coloured 10px text with nothing behind it is
   unreadable on cream, so the chip is what makes the label legible rather
   than what makes it pretty. */
.badge-warning {
  background: var(--warning-quiet);
  color: var(--warning-ink);
}

.center-page {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

/* Auth screen */
.auth-screen {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-7);
}

.auth-card {
  width: 100%;
  max-width: 380px;
  background: var(--surface);
  box-shadow: var(--bevel-panel);
  border-radius: var(--radius-lg);
  padding: var(--space-10);
  text-align: center;
}

.auth-brand-mark {
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-5);
}

.auth-card h1 {
  margin: 0 0 var(--space-1);
}

/* Sits between the brand lockup and whatever the card is asking for, which
   is where the email puts it too. Thicker than the nav's: the band under the
   nav runs the full width of the viewport, where 3px reads as a line, and on
   a 380px card the same 3px reads as a hairline instead. This is roughly the
   email's own 8px-on-600px proportion. */
.auth-card .spectrum-band {
  height: 5px;
  margin-top: var(--space-8);
}

.auth-card form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin-top: var(--space-9);
}

.auth-card input {
  text-align: left;
}

/* App shell */
.app-shell {
  min-height: 100vh;
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--space-9);
  /* Installed iOS PWAs (viewport-fit=cover + status-bar-style=
     black-translucent, see index.html) extend content under the notch/
     status bar instead of pushing it down automatically. Padding rather
     than margin, so .nav's own --surface background still fills that
     area (reads as one piece with the status bar, via the matching
     theme-color meta tag) while the actual content sits below it. Left/
     right insets cover the landscape-notch case the same way. */
  padding: calc(1rem + env(safe-area-inset-top)) calc(1.5rem + env(safe-area-inset-right)) 1rem
    calc(1.5rem + env(safe-area-inset-left));
  box-shadow: var(--bevel-raised);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
}

/* The spectrum band under the app's header, matching the one the emails
   carry under theirs. Drawn as a pseudo-element because .nav is a flex row —
   a real child would become a flex item and land between the brand and the
   links — and it spans the full width including the safe-area padding, since
   it is the header's own bottom edge. Sticky positioning already makes .nav
   a containing block for it. */
.nav::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: var(--spectrum-band);
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.nav-brand-mark {
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}

.nav-brand-text {
  display: flex;
  flex-direction: column;
  line-height: var(--leading-tight);
}

/* Not translated, in either language: a model number is machine output, the
   same way a domain or a timestamp is. */
.nav-brand-model {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-engraved);
  text-transform: uppercase;
  color: var(--text-faint);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex: 1;
  /* Flex items default to min-width:auto, refusing to shrink below their
     text's natural width — below that width the text wraps mid-word
     instead (confirmed directly between 320-410px), which also threw off
     the sticky header's box height enough to leave part of its background
     unpainted. min-width:0 plus nowrap text below forces a real overflow
     instead, which the scroll fallback then handles cleanly. */
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.nav-links::-webkit-scrollbar {
  display: none;
}

/* Engraved labels, not keys. These were moulded like the rest of the
   hardware for a while, which put seven boxes across the top of every
   screen and made the nav the loudest thing in the interface — the row is a
   place list, and a place list is read rather than pressed. The padding
   stays: it is the touch target (see the 768px query below), it just isn't
   drawn any more. */
.nav-links a {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  /* Tighter than it was: with the key faces gone, the old padding sat
     between two invisible boxes and read as a scattering of words rather
     than one row. */
  padding: var(--space-2) var(--space-3);
  white-space: nowrap;
  flex-shrink: 0;
  transition: color var(--dur-fast) var(--ease-hardware);
}

.nav-links a:hover {
  color: var(--text);
}

/* No press to animate: there is no key face left to move. */
.nav-links a:active {
  color: var(--accent);
}

/* Tablet/mobile: real 44px touch targets on the primary nav links and the
   hamburger toggle (WCAG 2.5.5) — desktop stays dense since a mouse
   pointer doesn't need the same tolerance. Grown via padding/min-height,
   not font-size, so the visible label/icon stays the same size. */
@media (max-width: 768px) {
  .nav-links a {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0 0.15rem;
  }

  .nav-menu-toggle {
    min-width: 44px;
    min-height: 44px;
  }
}

/* The current route is said with colour and weight instead of a pressed
   key face. */
.nav-links a.active {
  color: var(--accent);
  font-weight: var(--weight-semibold);
}

.nav-account {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  font-size: var(--text-sm);
}

/* Hamburger menu: smartphone-only, holds Theme/Language/User options only
   now (the one nav link that used to collapse into it, Unconsolidated,
   was retired — Digest/Library/Save/Imports all stay inline at every
   width instead). Hidden by default (desktop/tablet show .nav-account
   inline instead) — see the max-width query near the bottom of this file
   for the swap. */
.nav-menu {
  display: none;
  position: relative;
}

/* The phone's stand-in for the three triggers it replaces, so it is a ghost
   for the same reason they are — see .icon-menu-toggle. */
.nav-menu-toggle {
  background: none;
  color: var(--text-muted);
  border: 0;
  box-shadow: none;
  border-radius: var(--radius-sm);
  padding: var(--space-2) 0.7rem;
  font-size: var(--text-lg);
  line-height: 1;
}

.nav-menu-toggle:hover:not(:disabled),
.nav-menu-toggle:focus-visible {
  background: var(--surface-raised);
  box-shadow: var(--bevel-raised);
  color: var(--text);
}

.nav-menu-toggle[aria-expanded="true"] {
  background: var(--surface-raised);
  box-shadow: var(--bevel-pressed);
  color: var(--accent);
}

.nav-menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--z-nav-backdrop);
  background: rgba(0, 0, 0, 0.5);
}

.nav-menu-panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: var(--z-panel);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-5);
  min-width: 200px;
  background: var(--surface-raised);
  border-radius: var(--radius);
  padding: 0.9rem var(--space-7);
  box-shadow: var(--lift-menu);
}

/* Smartphone-only: hide the inline account controls and show the
   hamburger toggle instead. Now that theme/language/user collapse into
   single icons instead of an always-expanded text row + raw email
   address, the inline nav no longer needs ~1000px to avoid squeezing —
   660px (raised from an initial 480px guess) is a phone-vs-tablet cutoff,
   not the existing 860px breakpoint used elsewhere for the Digest sidebar
   (a tablet is still wide enough for the full inline nav). 480px let the
   longer Portuguese nav labels wrap to two lines before the hamburger
   kicked in; 660px gives them room. Digest/Library/Save/Imports stay
   inline even here (never move into the hamburger) — the brand wordmark
   hides instead, freeing up the room those four always-visible links
   need; the icon mark alone still carries the branding. */
@media (max-width: 660px) {
  .nav-account {
    display: none;
  }

  .nav-menu {
    display: block;
  }

  .nav-brand-text {
    display: none;
  }

  /* v2's h1 is half again the size v1's was, which is right on a reading
     column and too wide to share a row with a page action on a phone. The
     header wraps here rather than squeezing the heading into a column two
     words wide. */
  .page-header {
    flex-wrap: wrap;
  }

  .page-header h1 {
    font-size: var(--text-xl);
  }
}

/* Narrow phones (320-410px reported directly): even with the brand
   wordmark already hidden (see the 660px query above), four always-inline
   links (Digest/Library/Save/Imports) plus the hamburger toggle can still
   get tight, especially with the longer Portuguese labels. Tighter
   padding/gaps/font-size first, with .nav-links' own scroll fallback
   above as the last resort if a device is narrower still. */
@media (max-width: 420px) {
  .nav {
    gap: var(--space-5);
    /* Same safe-area handling as the base rule above — just tighter base
       spacing to match this breakpoint's other tightened values. */
    padding: calc(0.75rem + env(safe-area-inset-top)) calc(1rem + env(safe-area-inset-right)) 0.75rem
      calc(1rem + env(safe-area-inset-left));
  }

  /* Tight enough that the longest Portuguese label still fits whole on a
     390px phone — "Importar" was clipping to "Importa" with 13px to spare
     needed, and the links have no key faces to keep apart any more, so the
     gap is the cheapest 18px in the row. Narrower than that and .nav-links'
     own scroll fallback takes over, as before. */
  .nav-links {
    gap: var(--space-2);
  }

  .nav-links a {
    font-size: var(--text-sm);
  }
}

/* Reprocess ("bolt" icon + label) and Delete must stay on one line
   (nowrap, see .item-card-footer) — below this width the two labels
   combined no longer fit the card without clipping against its edge.
   Raised from an initial 310px (a synthetic Chromium-at-1x estimate) to
   340px after a real-device report that it was still touching the card
   edge up to that width — real font rendering runs wider than the
   estimate. This is a tighter/unrelated threshold from the 420px nav
   breakpoint above, not reused from it. Icon alone carries the button
   below this breakpoint, same fallback already used for pagination's
   Previous/Next labels; aria-label keeps the full word for screen
   readers regardless. */
@media (max-width: 340px) {
  .item-card-footer .footer-btn-label {
    display: none;
  }
}

/* Icon-trigger dropdowns (theme, language, user — ThemeSwitcher.jsx,
   LanguageSwitcher.jsx, UserMenu.jsx via Dropdown.jsx). Collapsing what
   used to be always-visible text/email into a single icon each is what
   makes a low smartphone-only nav breakpoint viable at all — the old
   always-expanded row (Auto/Day/Night + EN/PT + email + Sign out) is what
   forced the inline nav to squeeze until ~1000px. */
.dropdown {
  position: relative;
}

/* Ghost triggers: no face at rest, a face on hover and focus.
   This is the one place the system's "a control has to look operable" rule
   is bent, and deliberately — moulding all three of these put boxes across
   the header beside the nav's own, and the header is the one row where the
   product should be quiet. The affordance moves to interaction instead of
   sitting there permanently: hover and keyboard focus both raise a real key
   face, and the hit area is unchanged either way. */
.icon-menu-toggle {
  background: none;
  box-shadow: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0.35rem 0.55rem;
  min-height: 30px;
  min-width: 32px;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-mono);
  line-height: 1;
}

.icon-menu-toggle:hover:not(:disabled),
.icon-menu-toggle:focus-visible,
.icon-menu-toggle[aria-expanded="true"] {
  background: var(--surface-raised);
  box-shadow: var(--bevel-raised);
  color: var(--text);
}

/* While its panel is open the trigger stays pressed, which is the one bit
   of state a ghost control would otherwise lose. */
.icon-menu-toggle[aria-expanded="true"] {
  box-shadow: var(--bevel-pressed);
  color: var(--accent);
}

.dropdown-panel {
  position: absolute;
  z-index: var(--z-panel);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-1);
  min-width: 170px;
  background: var(--surface-raised);
  border-radius: var(--radius);
  padding: var(--space-3);
  box-shadow: var(--lift-menu);
}

.dropdown-panel-right {
  right: 0;
}

.dropdown-panel-left {
  left: 0;
}

/* Centered on its trigger — for Login.jsx's centered card footer, where
   anchoring an edge would throw the panel off to one side. */
.dropdown-panel-center {
  left: 50%;
  transform: translateX(-50%);
}

.dropdown-panel-below {
  top: calc(100% + 0.5rem);
}

/* Opens upward instead (Login.jsx's footer, see Dropdown.jsx) — the
   shadow stays cast downward, which reads correctly either way since the
   panel still floats above the card. */
.dropdown-panel-above {
  bottom: calc(100% + 0.5rem);
}

.dropdown-user-email {
  padding: var(--space-2) 0.6rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
  word-break: break-all;
}

/* .icon-menu-option is also used flat (no Dropdown wrapper) inside
   .nav-menu-panel on smartphones — same look either way.

   This class is worn by a real <button> (Sign out, the theme and language
   picks) and by a plain <a> (Settings), and an anchor inherits none of the
   element rules a <button> gets. So everything the row's appearance depends
   on is stated here rather than left to the element: the moulding, the mono
   face, the uppercase, the height and the left alignment. Leave any of them
   out and one row in the menu quietly renders as something else — which is
   exactly what Settings was doing, sitting unboxed in Space Grotesk, in
   sentence case and four pixels shorter than everything around it. */
.icon-menu-option {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.55rem;
  min-height: 38px;
  background: none;
  box-shadow: var(--bevel-raised);
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius);
  padding: 0.45rem 0.6rem;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  font-weight: var(--weight-regular);
  text-align: left;
  text-decoration: none;
  width: 100%;
  transition: var(--transition-control);
}

/* Both of these carry :not(:disabled) purely for specificity, matching
   .btn-secondary's own hover: the base `button:hover:not(:disabled)` rule
   outranks a plain .icon-menu-option:hover, so without it every row in this
   menu lit up in ember on hover — it had done since the transport key got
   its fill, and only showed up when someone hovered a menu in a screenshot.

   The background is a tone step DOWN, not up: the panel behind these rows is
   --surface-raised, so hovering to that same value was invisible anyway. */
.icon-menu-option:hover:not(:disabled) {
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
}

.icon-menu-option:active:not(:disabled) {
  transform: translateY(1px);
  box-shadow: var(--bevel-pressed);
}

/* The current pick reads as an engaged key, the same way the nav's current
   route does. */
.icon-menu-option.active {
  color: var(--accent);
  box-shadow: var(--bevel-pressed);
}

/* Fixed-width column rather than sizing to each glyph's own natural width.
   That started as a workaround for emoji rendering at inconsistent widths;
   it still earns its place now that the icons are a mix of geometry (☼ ☾ ⚙)
   and two-letter codes (EN, PT), which are wider still. */
.icon-menu-icon {
  display: inline-flex;
  justify-content: center;
  flex-shrink: 0;
  width: 1.25rem;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1;
}

/* Groups ThemeOptionsList/LanguageOptionsList's rows under a heading so
   they read as separate nested sections rather than one flat list once
   consolidated into the smartphone hamburger panel (they already show as
   distinct dropdowns on desktop, where this reads as a small dropdown
   title instead). */
.icon-menu-title {
  font-family: var(--font-mono);
  color: var(--text-faint);
  text-transform: uppercase;
  font-size: var(--text-2xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-engraved);
  padding: 0.35rem 0.6rem 0.15rem;
}

/* Sits at the foot of the sign-in card (see Login.jsx for why). The rule
   itself only has to flip the spacing from below to above — the hairline
   is what stops two lone icons under a form from reading as orphaned
   content rather than a footer. */
.auth-switchers {
  display: flex;
  justify-content: center;
  gap: var(--space-3);
  margin-top: 1.75rem;
  padding-top: var(--space-7);
  border-top: 1px solid var(--hairline);
}

/* Third-party sign-in, currently disabled (see Login.jsx). The two buttons
   share the row equally rather than sizing to their labels — "Google" and
   "Apple" are different lengths and neither is the more important of the
   two, so matching them is the honest arrangement. */
.auth-sso {
  margin-top: var(--space-9);
}

.auth-sso-label,
.auth-sso-note {
  margin: 0 0 var(--space-3);
}

/* The chip sits on its own line under the two buttons, centred by the card,
   so it drops .badge's left margin — that margin exists for a badge trailing
   a heading, and here it would shift the chip off centre. */
.auth-sso-note {
  margin: var(--space-5) 0 0;
}

.auth-sso-row {
  display: flex;
  gap: var(--space-3);
}

.auth-sso-row button {
  flex: 1;
  justify-content: center;
}

.app-main {
  max-width: var(--measure-read);
  margin: 0 auto;
  padding: var(--space-10) var(--space-9) 4rem;
}

.app-main-wide {
  max-width: var(--measure-wide);
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-7);
  margin-bottom: var(--space-9);
}

/* Emitted by ui/PageHeader.jsx since it was written, styled by nothing until
   now. A page's subtitle is language, not machine output, so it stays in the
   sans face — the engraved treatment belongs to labels. */
.page-header-subtitle {
  margin-top: var(--space-2);
  max-width: var(--measure-prose);
  line-height: var(--leading-body);
}

/* Same story: ui/ItemCard.jsx emits both of these and neither had a rule.
   The title row is what holds an item's lamp beside its heading. */
.item-card-title-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

.item-card-summary {
  color: var(--text-muted);
  line-height: var(--leading-body);
  max-width: var(--measure-prose);
}

/* The same band as an element, for a header that is not the nav bar. The
   sign-in card uses it: that screen has no nav, so its own brand lockup is
   the header, and the band sits under it exactly as it does in the email. */
.spectrum-band {
  height: 3px;
  border-radius: 1px;
  background: var(--spectrum-band);
}

/* An eyebrow is the engraved word above a heading — same treatment as
   .section-label, but inline in a stack rather than opening a section. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-engraved);
  text-transform: uppercase;
  color: var(--text-faint);
}

/* A content plate. .topic-card, .item-card and friends each state their own
   version of this; .panel is for the one-off blocks that would otherwise
   invent a fourth. */
.panel {
  padding: var(--panel-pad);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--bevel-panel);
}

.panel-raised {
  background: var(--surface-raised);
  box-shadow: var(--bevel-raised);
}

/* The salience readout riding in a topic card's headline row: a number the
   machine computed, so it sits behind glass like every other one. */
.topic-card-slot {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--lcd);
  box-shadow: var(--bevel-lcd);
  color: var(--lcd-text);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
}

.page-header h1 {
  /* Browser default h1 top margin pushed it below the top of its flex
     siblings (the "Run Dream Job now" button) and the Recently Saved
     column's own heading, which has no top margin. */
  margin-top: 0;
}

/* Every other page's own <h1> (no .page-header wrapper, unlike Digest's
   and TopicDetail's) — Library's directly inside the shared .digest-main
   column, Save/Imports/Settings directly inside their own root <div>
   (one level under .app-main). Both `>` selectors keep this from also
   matching Digest's/TopicDetail's own h1, which sit one level deeper
   inside .page-header and are already zeroed by the rule above. Same
   fix, same reason each time: the browser default h1 top margin left
   these out of step with Digest's heading — the last visible
   inconsistency across pages once Library also got it (confirmed
   directly, page by page). */
.digest-main > h1,
.app-main > div > h1 {
  margin-top: 0;
}

/* Button + its last-run result message on one line (desktop). flex-wrap
   handles mobile without a breakpoint: the message wraps under the button
   with only the small row-gap above it, and the row's own margin-bottom
   keeps it visually attached to the button rather than the topic list. */
/* The run button lives in .page-header's action slot now (see Digest.jsx),
   so it needs the header's own no-wrap treatment rather than the row that
   used to hold it. Without flex: 0 0 auto it shrinks to fit whatever the
   heading beside it leaves over, and "Consolidar Aprendizados" — the
   Portuguese label, half again as long as the English — wraps to two lines
   and takes the header's height with it. */
.digest-run-button {
  flex: 0 0 auto;
  white-space: nowrap;
}

.digest-run-button svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

/* flex-basis 220px keeps the message beside the button whenever at least
   that much room remains (wrapping its own text internally if needed) and
   drops it under the button only on genuinely narrow (mobile) layouts. */
/* Now a block of its own under the header rather than a flex sibling of the
   button, so the sizing that kept the two on one line is gone. The negative
   top margin pulls it up under .page-header's own bottom margin — it is the
   result of the button directly above it and should read as attached to the
   header, not as the first thing in the topic list. */
/* The dream job's own report of what it just did — counts a machine
   produced, so it is read off a panel rather than written as a sentence. */
.digest-run-result {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-6);
  margin: calc(var(--space-9) * -1) 0 var(--space-9);
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-sm);
  background: var(--lcd);
  box-shadow: var(--bevel-lcd);
  color: var(--lcd-text);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
}

.icon-spin {
  animation: icon-spin 0.9s linear infinite;
}

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

/* Engraved label — the panel legend stamped into the chassis. Uppercase is a
   typographic effect applied here, not in the string: write the label in
   sentence case in the JSX and let this rule shout it. */
.section-label {
  font-family: var(--font-mono);
  font-weight: var(--weight-regular);
  color: var(--text-faint);
  text-transform: uppercase;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-engraved);
  margin: var(--space-10) 0 var(--space-5);
}

/* .section-label is shared with h3 (TopicDetail/Digest) — scoped to h2
   only (Save/Settings) so those aren't affected. */
h2.section-label {
  font-size: var(--text-sm);
}

/* Separates Save/Bulk Import/Bookmarklet/Browser Extensions on the merged
   Save page — .section-label's own top margin already adds space above
   each heading, so this only needs a small top margin of its own to
   avoid doubling up. */
.save-page hr {
  border: none;
  border-top: 1px solid var(--hairline);
  margin: var(--space-9) 0 0;
}

.back-link {
  display: inline-block;
  color: var(--text-muted);
  margin: 0 var(--space-7) var(--space-7) 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
}

.back-link:hover {
  color: var(--accent);
}

/* Topic digest — Techmeme-style dense river: stacked stories separated by a
   thin divider, no card boxes, byline-style meta above the headline and a
   "More:" line of source links below the summary. */
.topic-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* Filter + density toggle above the ranked river. Wraps to two rows on a
   narrow viewport without a breakpoint: the input grows, the toggle keeps
   its intrinsic width and drops below. */
.digest-controls {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
  margin: var(--space-3) 0 var(--space-8);
}

/* Left of the row, with everything else pushed right by the filter's
   auto margin below. Sized at the type scale's large step rather than as
   an h2 default, so it reads as a list heading and not as a second page
   title competing with the h1 above it. */
.digest-controls-heading {
  margin: 0 var(--space-9) 0 0;
  flex: 0 0 auto;
  font-size: var(--text-lg);
}

/* `1 1 12rem` let the filter absorb every spare pixel of a 1040px row, so
   a control holding a dozen characters ran the full width of the page.
   `0 1 14rem` gives it a size it can shrink from but not grow past, and
   the auto margin is what separates the two groups — heading at the left,
   filter and density at the right — without a spacer element. */
.digest-filter {
  flex: 0 1 14rem;
  margin-left: auto;
  min-width: 0;
}

/* The density label and its switch, as one unit — see Digest.jsx. */
.digest-density {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex: 0 0 auto;
}

/* Phone: below this width the heading, the filter and the density switch
   stop fitting on one row, and the default wrap stranded the filter beside
   the heading with the switch alone underneath — two half-empty rows. Giving
   the heading the whole row instead puts the two controls together on the
   next one, which is also the order they are read in: what this list is,
   then how to narrow it and how to show it.

   560px rather than the 660px the nav collapses at: between the two the row
   genuinely fits, and forcing the break there would cost a row for nothing. */
@media (max-width: 560px) {
  .digest-controls-heading {
    flex: 1 0 100%;
    margin-right: 0;
  }

  /* A small basis rather than `auto`: flexbox decides whether to wrap from
     base sizes, before any shrinking, so an auto-sized input took its full
     intrinsic width into that decision and pushed the density switch onto a
     third row on a 360px phone. Starting narrow and growing into whatever is
     left keeps the two on one row down to 320px. */
  .digest-filter {
    flex: 1 1 8rem;
    margin-left: 0;
  }
}

/* Segmented control: a track cut into the chassis with the two keys sitting
   in it, so it reads as a single either/or rather than two independent
   buttons. The outline and the divider rule it used to carry are both gone —
   the pressed key's own moulding says which half is selected. */
.density-toggle {
  display: inline-flex;
  flex: 0 0 auto;
  background: var(--surface-inset);
  box-shadow: var(--bevel-sunk);
  border-radius: var(--radius);
  padding: 2px;
  gap: 2px;
  overflow: hidden;
}

.density-toggle button {
  appearance: none;
  display: flex;
  align-items: center;
  border: 0;
  background: transparent;
  box-shadow: none;
  min-height: 0;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font: inherit;
  font-size: var(--text-sm);
  /* Tighter horizontally than the old worded halves, which needed room for
     "Confortável": the content is now a fixed 15px graphic in every
     language, so the padding is free to be the same everywhere too. */
  padding: 0.45rem 0.6rem;
  cursor: pointer;
}

/* :not(:disabled) for specificity, the same as .btn-secondary and the menu
   rows: the base `button:hover:not(:disabled)` sets the ember fill, and a
   plain .density-toggle button:hover only restated the colour, so hovering
   the unselected half of this switch lit it up like a primary action. */
.density-toggle button:hover:not(:disabled) {
  background: var(--surface-raised);
  color: var(--text);
}

.density-toggle button[aria-pressed="true"] {
  background: var(--accent);
  color: var(--text-on-accent);
  box-shadow: var(--bevel-raised);
  font-weight: var(--weight-semibold);
}

/* Beats the hover rule above, so the selected half does not lose its fill
   when the pointer crosses it. */
.density-toggle button[aria-pressed="true"]:hover:not(:disabled) {
  background: var(--accent-strong);
  color: var(--text-on-accent);
}

.density-toggle button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The bars that replaced the words in each half of the toggle — three
   spaced ones for comfortable, five tight ones for compact. Count and
   thickness come from DensityToggle.jsx, since they are what the two
   options differ by; everything shared lives here.

   background: currentColor is the whole trick. The bars inherit whatever
   color the surrounding button resolves to — muted when unselected, --text
   on hover, --text-on-accent against the accent fill when pressed — so they
   invert with the button without a single rule of their own, in both
   themes, and without appearing in the light-mode ink question at all. */
.density-bars {
  display: flex;
  flex-direction: column;
  width: 15px;
}

.density-bars > span {
  background: currentColor;
  border-radius: 1px;
}

/* ------------------------------------------------------------------ *
   Instruments — status and magnitude as hardware, not as coloured text.

   The rule these three share: anything a machine produced is read off a
   readout, never described in a sentence. A status is a lamp, a magnitude
   is a segmented meter, and a machine's own output sits behind glass. That
   replaces the coloured symbols (✓ ✕ ⋯), the coloured status words and the
   progress bars this app used to draw.
 * ------------------------------------------------------------------ */

/* A moulded LED. Never carries the meaning alone — every .lamp in the app
   ships with a visually-hidden label beside it (see ui/Lamp.jsx), because a
   coloured dot announces nothing to a screen reader and the symbols it
   replaced did. */
.lamp {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
  background: var(--text-faint);
  box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.35);
}

.lamp-on {
  background: var(--success);
  box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.3),
    0 0 7px color-mix(in oklab, var(--success) 70%, transparent);
}

.lamp-wait {
  background: var(--warning);
  box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.3),
    0 0 7px color-mix(in oklab, var(--warning) 65%, transparent);
}

.lamp-fail {
  background: var(--danger);
  box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.3),
    0 0 7px color-mix(in oklab, var(--danger) 65%, transparent);
}

/* Segmented level meter — salience, import progress, run history. Segments,
   never a continuous bar: a bar reads as "loading", and none of these are. */
.meter {
  display: inline-flex;
  gap: 2px;
  align-items: flex-end;
  flex: none;
}

.meter > span {
  width: 9px;
  height: 6px;
  border-radius: 1px;
  background: var(--surface-inset);
}

.meter > span.on {
  background: var(--action);
}

.meter-accent > span.on {
  background: var(--accent);
}

.meter-tall > span {
  width: 6px;
  height: 10px;
}

/* LCD — a glass window over machine output. Stays dark in both themes,
   because a screen is a screen in daylight too. */
.lcd {
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-sm);
  background: var(--lcd);
  box-shadow: var(--bevel-lcd);
  color: var(--lcd-text);
  font-family: var(--font-mono);
  letter-spacing: var(--tracking-mono);
}

.lcd-label {
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-engraved);
  text-transform: uppercase;
  color: var(--lcd-text-dim);
}

.lcd-value {
  font-size: var(--text-md);
}

.lcd-dim {
  color: var(--lcd-text-dim);
}

.lcd-cursor {
  display: inline-block;
  width: 8px;
  height: 1em;
  margin-left: 4px;
  vertical-align: -2px;
  background: var(--lcd-text);
  animation: lcd-cursor 1.1s step-end infinite;
}

@keyframes lcd-cursor {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}

/* Placeholder for content that has not arrived — an empty slot in the
   machine, in the same sunk moulding an input or an unlit meter segment
   uses. The heights are the line boxes of the things they stand in for, so a
   skeleton reserves the space its content will take rather than collapsing
   the layout and jolting it back.

   The sheen is a third continuous animation in a system that specifies two,
   added deliberately after the static version read as a stalled page rather
   than a loading one. It is held as close to the language as a moving
   highlight can be: low contrast, linear, no scaling or bouncing, and it
   travels rather than pulsing — a light passing over the wells, not a
   plastic shine. It stops entirely under prefers-reduced-motion, where the
   static treatment is what remains. */
.skeleton {
  display: block;
  position: relative;
  overflow: hidden;
  height: 0.7rem;
  border-radius: var(--radius-sm);
  background: var(--surface-inset);
  box-shadow: var(--bevel-sunk);
}

.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--skeleton-sheen), transparent);
  animation: skeleton-sweep 1.6s linear infinite;
}

@keyframes skeleton-sweep {
  to {
    transform: translateX(100%);
  }
}

/* Staggered down each record so the light reads as one pass across the card
   rather than every line flashing in lockstep. */
.skeleton-card .skeleton:nth-child(2)::after {
  animation-delay: 120ms;
}

.skeleton-card .skeleton:nth-child(3)::after {
  animation-delay: 240ms;
}

.skeleton-lines .skeleton:nth-child(2)::after {
  animation-delay: 300ms;
}

.skeleton-lines .skeleton:nth-child(3)::after {
  animation-delay: 420ms;
}

.skeleton-heading {
  height: 1.4rem;
  border-radius: var(--radius);
}

.skeleton-byline {
  height: 0.6rem;
}

/* Stands in for a whole stat tile: the plate's own height (measured, 64px —
   12px padding, a 20px/1.1 value, a 4px gap, a 9px label, 12px padding), so
   the row reserves exactly what the tiles will take. */
.skeleton-tile {
  height: 4rem;
  border-radius: var(--radius);
}

/* Stands in for a control rather than a line of text, so it is the height of
   one — inputs, selects and the density switch all settle at 39px. */
.skeleton-control {
  height: 39px;
  border-radius: var(--radius);
}

/* A record made of placeholders has to supply its own rhythm: the real card
   gets its spacing from the margins on the headings and paragraphs inside
   it, and a stack of bare <span>s has none. */
.skeleton-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* The stack of placeholder lines standing in for one block of prose. */
.skeleton-lines {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Categorical channel dot — says WHICH topic this is, never how good or how
   broken it is. Identical values in both themes so a topic keeps its colour
   across a theme switch; status never uses one. */
.ch-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  flex: none;
}

/* A card is a tone step plus one bevel — no outline, no coloured left
   border, no drop shadow beyond the bevel's own. The river used to be
   separated by rules instead; a plate does the same job without drawing a
   line for it. */
.topic-card {
  padding: var(--panel-pad);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--bevel-panel);
  transition: background-color var(--dur) var(--ease-hardware);
}

/* Hover is one tone step, never opacity. */
.topic-card:hover {
  background: var(--surface-raised);
}

/* Compact view: the card loses its bullets and sources line in TopicList, so
   the only thing left to do here is close up the vertical rhythm — at full
   padding a stripped card is mostly whitespace. */
.topic-list-compact .topic-card {
  padding: var(--space-5) var(--space-8);
}

.topic-list-compact .topic-card h2 {
  font-size: var(--text-md);
}

.topic-list-compact .topic-card-byline {
  margin-bottom: 0;
}

.topic-list-compact .subtopics-row {
  margin-top: var(--space-1);
}

.topic-card-byline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 0.35rem;
}

.topic-card-headline {
  display: block;
}

.topic-card-head {
  display: flex;
  align-items: center;
}

.topic-card h2 {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--text);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-snug);
  transition: color var(--dur-fast) var(--ease-hardware);
}

/* Hover puts the accent on the heading rather than underlining it — the
   whole card is the hit area, so an underline on one line inside it reads
   as a second, smaller target. */
.topic-card-headline:hover h2 {
  color: var(--accent);
}

.topic-tldr {
  color: var(--text);
  margin: var(--space-2) 0 0.6rem;
  line-height: var(--leading-body);
  max-width: var(--measure-prose);
  text-wrap: pretty;
}

.topic-tldr.large {
  color: var(--text);
  font-size: var(--text-md);
}

/* Bullet-list summaries (key learnings/facts) — composes with whatever
   color/size class the caller already passes (.topic-tldr, .muted.small,
   ...), just adds list-specific spacing.

   The marker is drawn rather than listed: a disc is the browser's shape, and
   v2's is a small accent triangle pointing at the line it introduces. */
.bullet-list {
  list-style: none;
  padding-left: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.bullet-list li {
  margin: 0;
  padding-left: 1.15rem;
  position: relative;
}

.bullet-list li::before {
  content: "\25B8";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--accent);
  font-size: var(--text-sm);
}

.subtopics-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
}

.subtopics-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-engraved);
  text-transform: uppercase;
  color: var(--text-faint);
  font-weight: var(--weight-regular);
}

/* The only fully-rounded shape in the system — a chip is the one thing here
   that is neither a plate nor a key. */
.subtopic-chip {
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  background: var(--surface-raised);
  color: var(--text-muted);
  box-shadow: var(--bevel-raised);
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius-pill);
  line-height: 1.2;
  transition: var(--transition-control);
}

.subtopic-chip:hover {
  background: var(--surface);
  color: var(--text);
}

.topic-sources-line {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  line-height: var(--leading-loose);
  color: var(--text-muted);
}

.topic-sources-line .sources-label {
  color: var(--text-faint);
  letter-spacing: var(--tracking-engraved);
  margin-right: var(--space-3);
}

.topic-sources-line a {
  color: var(--text-muted);
}

.topic-sources-line a:hover {
  text-decoration: underline;
}

/* Custom tooltip (the article's full title), a real element rather than a
   ::after/title attribute so its own title text can be a clickable link.
   Visibility is driven by a JS-managed .source-tooltip-open class (see
   TopicList.jsx's SourcesLine), not CSS :hover/:focus-within — that let a
   tooltip get stuck open (hover state doesn't reliably clear after a link
   opens a new tab and the user tabs back) with nothing to force-close it,
   and let multiple tooltips in the same line be open/overlapping at once.
   .source-tooltip is still the hit-tested/positioned box, offset via
   padding-top (not margin/gap) so that invisible padding strip is still
   part of its own hoverable area — the visible white card is a separate
   inner element (.source-tooltip-inner). Without this, there'd be a literal
   gap between the link and the card where the pointer is over nothing
   belonging to .source-item, which would drop the mouseenter/mouseleave
   tracking mid-transit. Fixed white/dark-text/shadow regardless of app
   theme — a deliberately elevated "popover" look, distinct from the rest of
   this flat, shadowless design, so it reads clearly as a floating overlay. */
.topic-sources-line .source-item {
  position: relative;
}

.topic-sources-line .source-tooltip {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  padding-top: var(--space-3);
  z-index: var(--z-panel);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease;
}

.topic-sources-line .source-tooltip-open {
  opacity: 1;
  visibility: visible;
}

.topic-sources-line .source-tooltip-inner {
  display: block;
  background: #ffffff;
  color: #1a1c22;
  border-radius: var(--radius);
  padding: 0.65rem 0.9rem;
  font-size: var(--text-md);
  line-height: 1.4;
  white-space: normal;
  width: max-content;
  max-width: 320px;
  box-shadow: var(--lift-menu);
}

.topic-sources-line .source-tooltip-link {
  color: #0d9488;
  text-decoration: none;
}

.topic-sources-line .source-tooltip-link:hover {
  text-decoration: underline;
}

.topic-meta {
  margin-top: var(--space-7);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Digest two-column layout */
.digest-layout {
  display: flex;
  align-items: flex-start;
  gap: var(--space-11);
}

.digest-main {
  flex: 1;
  min-width: 0;
}

/* The rail is a plate of its own, like the cards it sits beside. */
.digest-sidebar {
  width: 260px;
  flex-shrink: 0;
  font-size: var(--text-sm);
  padding: var(--panel-pad);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--bevel-panel);
}

.digest-sidebar .section-label {
  margin-top: 0;
  font-size: var(--text-xs);
}

.recent-item-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

/* Full width rather than sitting at its natural size against the left edge
   of a 260px column: it closes the list rather than starting a new thing
   under it, and an <a> styled as a button needs the flex centering spelled
   out since it gets none of the base `button` rule. */
.digest-sidebar-more {
  display: flex;
  justify-content: center;
  width: 100%;
  margin-top: var(--space-7);
}

.recent-item-title-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.recent-item a {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--accent);
  /* Flex items don't shrink below their content's width by default, which
     would silently defeat the ellipsis above and push the row wider than
     the sidebar. */
  min-width: 0;
  flex: 1;
}

.recent-item a:hover {
  text-decoration: underline;
}

/* Holds the lamp in the title row. The colour rules that used to live here
   (.ok/.fail/.pending, tinting a ✓/✕/⋯ glyph) are gone — the lamp classes
   carry the state now. */
.recent-item-status {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  line-height: 1;
  cursor: default;
}

/* flex-wrap so this line breaks onto a second line rather than forcing
   its items to overflow or squeeze at narrow widths (320px and up — the
   smallest mobile viewport this app supports) — domain/date/status
   themselves stay unbroken (see the flex-shrink: 0 block below), it's
   just the row as a whole that wraps when they no longer all fit. */
.recent-item-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  margin-top: 0.15rem;
}

/* Domain is the one part of this flex row allowed to wrap/shrink (a long
   domain like "steve-yegge.medium.com" already breaks at the hyphen on
   its own, no special CSS needed for that). Everything after it —
   separators, date, status text+icon (Library page only) — gets
   flex-shrink: 0 + nowrap so a long domain can never squeeze *them* onto
   a second line instead: confirmed directly, without this the status
   item (e.g. "Failed" + its detail icon, see .status-detail-item below)
   would get squeezed narrower than its own content, wrapping the icon
   onto its own line and stretching the whole span to the row's full
   height — which then LOOKED top-aligned since the shrunk item, now
   exactly as tall as the row, had nowhere left to visually center within. */
.recent-item-date,
.recent-item-status-text {
  flex-shrink: 0;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
}

.recent-item-sep {
  opacity: 0.6;
  flex-shrink: 0;
}

.recent-item-tags {
  color: var(--text-muted);
  font-size: 0.9em;
  margin-top: 0.15rem;
}

/* Library page's Manage Topics sidebar — same .digest-layout/.digest-
   sidebar/.section-label shell as Digest.jsx's Recently Saved list, and
   now the same no-divider, gap-only spacing between rows too (0.9rem,
   matching .recent-item-list) rather than a border-bottom on each item. */
.topic-manage-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

/* Title left, Rename/Delete icon-only buttons right, on one line; the
   item count sits on its own line below (.topic-manage-count) rather
   than sharing the title's line — freed up by dropping the buttons'
   text labels, which no longer need the room. */
.topic-manage-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.topic-manage-title {
  color: var(--accent);
  font-weight: var(--weight-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.topic-manage-title:hover {
  text-decoration: underline;
}

.topic-manage-count {
  display: block;
  margin-top: 0.15rem;
  color: var(--text-muted);
}

.topic-manage-actions {
  display: flex;
  flex-shrink: 0;
  gap: var(--space-2);
}

/* Icon-only variant of .btn-secondary.btn-secondary-sm (Rename/Delete in
   the Manage Topics sidebar) — equal padding on all sides instead of the
   wider left/right padding sized for a text label, and no gap since
   there's no label to space from the icon. aria-label/title on the
   button itself carry what the visible text used to. */
.btn-icon-only {
  padding: 0.35rem;
  gap: 0;
}

/* Library page's mobile-only "↓ Items / ↓ Topics" jump bar — desktop
   already shows items and topics side by side (see .digest-layout below
   860px), so there's nothing to jump between there. Sticks just under
   the app's own top .nav (top offset set inline from Library.jsx's
   measured nav height, not hardcoded — .nav's own height moves with
   env(safe-area-inset-top) on notched phones). Its buttons use the
   existing .link-button class (not the boxed default `button` styling)
   so it reads as a secondary in-page nav strip, and — just as
   importantly — .link-button is already excluded from the light-theme
   white-button-text override below (see that rule's own comment), which
   a bespoke class here would have needed adding to itself. */
/* The phone-only jump bar. It takes the same plate treatment as the stat
   tiles below it — a tone step up from the page plus --bevel-panel — rather
   than being a bare strip of page colour, and it is inset from the content
   on both sides so it reads as an object sitting on the page rather than a
   band welded across it. It stays opaque because it is sticky: the item
   cards run underneath it at exactly the same width, so nothing shows past
   its edges. */
.library-jump-nav {
  display: none;
  position: sticky;
  z-index: var(--z-jump-nav);
  gap: var(--space-9);
  padding: var(--space-4) var(--space-7);
  margin: var(--space-3) 0 var(--space-8);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--bevel-panel);
}

/* The only interactive thing on that plate, so it carries the accent. */
.library-jump-nav .link-button {
  color: var(--accent);
  text-decoration: none;
  font-weight: var(--weight-semibold);
}

.library-jump-nav .link-button:hover:not(:disabled) {
  color: var(--accent-strong);
  text-decoration: underline;
}

@media (max-width: 860px) {
  .digest-layout {
    flex-direction: column;
    /* .digest-layout's align-items: flex-start (set above, for the
       desktop row) controls the CROSS axis — height there, but width
       here once flex-direction flips to column. flex-start on that axis
       means "size to content" rather than "fill the container," so
       .digest-main sized itself to whatever its widest unshrinkable
       content wanted (confirmed directly: Library's status/sort filter
       row, whose selects have flex-shrink: 0, pushed it to 1227px on a
       375px screen — sending the sort dropdown off-screen entirely).
       stretch restores normal full-width stacking. */
    align-items: stretch;
  }

  .digest-sidebar {
    width: 100%;
  }

  .library-jump-nav {
    display: flex;
  }
}

/* Item lists */
.item-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.item-card {
  background: var(--surface);
  box-shadow: var(--bevel-panel);
  border-radius: var(--radius);
  padding: var(--space-7) var(--space-8);
  /* Outer safety net — even if some inner content resists wrapping, the
     card itself can never visually exceed its box. */
  overflow: hidden;
}

.item-card h4 {
  margin: 0 0 var(--space-2);
  /* break-all/anywhere would break mid-word for any title, not just the
     rare case where a single word is wider than the card — break-word
     only kicks in when a word genuinely doesn't fit on its own line. */
  overflow-wrap: break-word;
}

/* Signals the title link opens in a new tab (target="_blank"), unlike
   every other link on this page. No explicit width/height on the <svg>
   itself falls back to the browser's ~300x150px intrinsic box (bit this
   page before, see .icon-bolt) — sized here instead. */
.icon-external-link {
  width: 13px;
  height: 13px;
  margin-left: 0.3rem;
  vertical-align: middle;
  color: var(--text-muted);
  flex-shrink: 0;
}

.item-url {
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  word-break: break-all;
  overflow-wrap: anywhere;
}

/* No longer flex — the leading status glyph (.recent-item-status, still
   used by Digest.jsx's Recently Saved sidebar) was dropped from Library
   cards specifically so the title's left edge lines up with the
   domain/date/status line, "Not in a topic", and summary text below it,
   none of which had that icon's indent to begin with. Just a single <a>
   child now, no layout rules needed. */

/* The Change/Remove buttons' assign/create-topic sub-form — renders
   inline in .item-card-topic-actions in place of those buttons (see
   Library.jsx), not as a separate block further down the card, so no
   margin-top of its own is needed here. */
.item-card-topic-editor {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.item-card-topic-editor select {
  max-width: 220px;
}

/* The Reprocess/Delete row at the bottom of a Library card. Natural
   content width for both buttons (flex:1 equal-split made Reprocess
   balloon to fill half the card once it also carried an icon) — nowrap
   plus right-alignment keeps them on one line at the trailing edge
   without stretching either one. */
.item-card-footer {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-5);
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--hairline);
}

/* Every control in this row is pinned to the same height, because nothing
   else makes them agree. .btn-secondary carries a 1px border and the plain
   accent button does not, so identical padding still produces a 2px
   difference; a bare <select> is sized by the platform and lands somewhere
   else again. Left alone the row is three controls at three heights sharing
   one baseline, which reads as a rendering fault rather than a design.

   Height, not min-height: min-height would let a select with a longer
   option grow past its neighbours and put the row back where it started. */
.item-card-footer > button,
.item-card-footer > select,
.item-card-footer .item-card-topic-editor > button,
.item-card-footer .item-card-topic-editor > select {
  height: 32px;
  padding-top: 0;
  padding-bottom: 0;
}

/* Library page */
.library-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-9);
  margin: var(--space-7) 0 0;
}

/* Phone: four tiles at the desktop gap and padding wrap onto a second line,
   which turns a row you read at a glance into a block you have to parse.
   Tightening both keeps them on one line down to 320px — the numbers are
   short and the engraved labels are already small, so the tiles lose nothing
   by sitting closer together. */
@media (max-width: 560px) {
  .library-stats {
    gap: var(--space-2);
  }

  .library-stat {
    padding: var(--space-3) var(--space-4);
  }
}

/* Narrowest supported phones, and Portuguese, where the labels run longer
   ("Agrupados" against "Sorted"). The engraved tracking is what costs the
   most width at this size, so it gives way before the padding does — the
   labels are still unmistakably engraved at 0.1em.

   labels are still unmistakably engraved at 0.1em. */
@media (max-width: 400px) {
  .library-stats {
    gap: var(--space-1);
  }

  .library-stat {
    padding: var(--space-2) var(--space-3);
  }

  .library-stat-label {
    letter-spacing: var(--tracking-mono);
  }
}

/* Below this the four tiles genuinely do not fit, and the choice is between
   wrapping, shrinking the label past the type scale's smallest step, and
   scrolling. This scrolls, the same last resort .nav-links takes a few
   hundred lines up: the row stays one row and nothing is read at 8px.

   It is scoped this tightly on purpose. Setting overflow-x to anything but
   `visible` while overflow-y is left unset forces overflow-y to compute to
   `auto` as well — the same CSS Overflow rule documented on html/body at the
   top of this file — which makes the row a scroll container and clips the
   tiles' own drop shadow along the bottom. So the row only becomes one at
   the width where it has to be, and it carries padding to keep the shadow
   inside the clip box when it does. */
@media (max-width: 360px) {
  .library-stats {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    /* --bevel-panel's outer shadow reaches ~8px below the tile. */
    padding-bottom: var(--space-3);
  }

  .library-stats::-webkit-scrollbar {
    display: none;
  }

  .library-stat {
    flex: 0 0 auto;
  }
}

/* Manage Topics sidebar's own stat row — same tiles, tighter gap/margin
   to fit the 260px .digest-sidebar column instead of the wide main one. */
.library-stats-sidebar {
  gap: var(--space-7);
  margin: var(--space-3) 0 var(--space-7);
}

.library-stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-5) var(--space-7);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--bevel-panel);
}

.library-stat-value {
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: 1.1;
}

/* Processed is the one "good news" number worth a glance of color — Total
   and Unprocessed stay neutral text (Unprocessed is just in-progress
   "pending"/"indexed" items now that Failed has its own tile below, so it
   isn't bad news either — no --danger tint here). */
.library-stat-value-accent {
  color: var(--accent);
}

.library-stat-value-danger {
  color: var(--danger-ink);
}

.library-stat-label {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: var(--tracking-engraved);
}

.library-controls {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin: var(--space-7) 0;
}

.library-controls input[type="text"] {
  width: 100%;
}

/* Direct-child-only (>) so this doesn't reach the item list's own status/
   sort selects, which sit one level deeper inside .library-controls-row
   and stay their natural width there, side by side. The Manage Topics
   sidebar's sort select has no such row — it's a lone direct child,
   stacked under its search input instead. */
.library-controls > select {
  width: 100%;
}

.library-controls-row {
  display: flex;
  gap: var(--space-5);
}

/* ~46% of the row (search field's own width) for the status filter,
   which needs far less room than its options list suggests — the sort
   filter takes the remaining space beside it. */
.library-status-filter {
  flex: 0 0 46%;
  min-width: 0;
}

/* Two selects side by side stop holding their longest option on a narrow
   phone — a <select> clips rather than ellipsizing, so "Recentes primeiro"
   simply loses its last letters, and Portuguese hits it first. They stack
   instead, which is what .library-controls does with everything else at this
   width anyway. */
@media (max-width: 380px) {
  .library-controls-row {
    flex-direction: column;
  }

  .library-status-filter {
    flex: 1 1 auto;
  }
}

.library-sort-filter {
  flex: 1;
  min-width: 0;
}

/* Right-aligned to match the item cards' own action rows
   (.item-card-footer, .item-card-topic-actions) elsewhere on this page. */
.library-pagination {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-5);
  margin-top: var(--space-7);
}

/* Below 346px there isn't room for "← Previous"/"Next →" without the
   buttons crowding or wrapping — the arrow alone is still a clear enough
   affordance (aria-label keeps the full word for screen readers). */
@media (max-width: 346px) {
  .library-pagination .pagination-label {
    display: none;
  }
}

/* Topic name/link always above its Change/Remove actions (never sharing a
   row with them) — consistent whether there's a real topic name or the
   "Not in a topic" placeholder, rather than only wrapping to two lines
   once the label happens to be long enough to force it. */
.item-card-topic {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: var(--space-2);
}

/* Wraps the topic name, not a label — it is the truncation box. The name's
   own treatment lives on .item-card-topic-link below. */
.item-card-topic-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Matches the Digest page's own topic-title treatment (.topic-card h2:
   semibold in --text, taking the accent on hover) so a Library card's topic
   link reads the same way it
   does everywhere else in the app. */
.item-card-topic-link {
  font-weight: var(--weight-semibold);
  color: var(--text);
  font-size: var(--text-md);
  letter-spacing: var(--tracking-tight);
  transition: color var(--dur-fast) var(--ease-hardware);
}

.item-card-topic-link:hover {
  color: var(--accent);
}

.item-card-topic-actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  /* Its own bottom margin rather than relying on the next element's
     top margin to create space — .item-card-topic-actions is a flex item
     inside .item-card-topic (itself display:flex), and flex-item margins
     never collapse with anything outside their container, so without
     this the button row's bottom edge sat flush against whatever margin
     the next element brought, reading as adjacent regardless of that
     margin's size (confirmed directly). */
  margin-bottom: var(--space-5);
}

/* Same size/weight as .item-card-topic-link below (--warning, not its
   --accent color — this isn't a real, clickable topic) so "Not in a
   topic" reads at the same visual weight as an actual topic name, while
   its --warning color (matching "Indexed" below) flags it as a state the
   user could act on — assign it to a topic — unlike a merely
   informational fact. */
.item-card-topic-empty {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  color: var(--warning-ink);
}

/* Per-card status — folded into the domain/date meta line (Library page
   only) rather than its own line below. Sorted/Pending read as plain
   muted text, same as the domain and date beside them, since there's
   nothing to do about either. Failed/Blocked (libraryStatusLabel()'s
   "failed" cssClass) stand out in --danger — a real problem. Indexed
   (its "indexed" cssClass) stands out in --warning, matching
   .item-card-topic-empty above — not wrong, but not fully done either
   (still needs Dream Job to sort it into a topic). */
.item-status-inline-error {
  color: var(--danger-ink);
  font-weight: var(--weight-semibold);
}

.item-status-inline-warning {
  color: var(--warning-ink);
  font-weight: var(--weight-semibold);
}

/* Fail-reason detail icon/tooltip (libraryStatusLabel()'s `detail` field) —
   inline parenthetical text used to sit right after the status ("Failed
   (Blocked, retryable)"), which was long enough to wrap the whole
   domain/date/status meta line onto extra lines on narrow viewports.
   Hover and tap are deliberately two independent triggers rather than one
   JS-managed open state driving both (confirmed directly: mouseenter
   setting the same state a click then toggles causes a plain hover-then-
   click — the ordinary way to interact with a mouse — to open and
   immediately re-close it in one motion). Hover is pure CSS below, with
   no JS involved at all; tap toggles openStatusDetailId in Library.jsx,
   which OR's into the same visibility via .status-detail-tooltip-open,
   plus an outside-click handler to close it (a tap has nothing to blur to
   on mobile). currentColor on the icon picks up --danger from the
   surrounding .item-status-inline-error text. */
.status-detail-item:hover .status-detail-tooltip {
  opacity: 1;
  visibility: visible;
}
.status-detail-item {
  position: relative;
  display: inline-flex;
  align-items: center;
  margin-left: 0.3rem;
}

.status-detail-icon {
  all: unset;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  background: var(--surface-raised);
  box-shadow: var(--bevel-raised);
  color: currentColor;
  font-size: var(--text-2xs);
  line-height: 1;
}

.status-detail-tooltip {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  padding-bottom: var(--space-2);
  z-index: var(--z-panel);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease;
}

.status-detail-tooltip-open {
  opacity: 1;
  visibility: visible;
}

.status-detail-tooltip-inner {
  display: block;
  background: #ffffff;
  color: #1a1c22;
  border-radius: var(--radius);
  padding: var(--space-3) 0.7rem;
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  line-height: 1.4;
  white-space: normal;
  width: max-content;
  max-width: 200px;
  box-shadow: var(--lift-menu);
}

/* Summary bullets are always shown now (no expand/collapse — see
   Library.jsx) — this is just a plain section label above them, not a
   status-colored control anymore, so it's deliberately muted rather than
   following item-status-line's sorted/pending/failed coloring. */
.item-card-summary-label {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-engraved);
  text-transform: uppercase;
  color: var(--text-faint);
  margin-top: var(--space-5);
}

/* Same 44px touch-target rationale as the nav rule above, applied to
   Library card actions: Reprocess/Delete measured well under 44px tall
   at mobile widths. min-height alone (not font-size) keeps the label/
   icon the same size — the base `button` rule's align-items:center
   re-centers it inside the taller box. .btn-secondary-sm (Change/Remove)
   gets its own shorter 40px, matching these buttons' own natural
   (non-bumped) height rather than the 44px WCAG minimum. */
@media (max-width: 768px) {
  .item-card-footer button {
    min-height: 44px;
  }

  .btn-secondary-sm {
    min-height: 40px;
  }
}

/* Save page */
.save-form {
  display: flex;
  gap: var(--space-3);
  margin: var(--space-7) 0 var(--space-9);
}

.save-form input {
  flex: 1;
}

/* Multi-URL textarea form — stacked (textarea, then a count/limit line,
   then the submit button) rather than the single-line input's row layout,
   since the button no longer sits naturally beside a multi-line field. */
.save-form-multi {
  flex-direction: column;
  align-items: stretch;
}

.save-form-multi textarea {
  width: 100%;
  font-family: inherit;
}

.save-form-multi button {
  align-self: flex-start;
}

/* Imports page's styled file picker — see ImportBatches.jsx for why the
   native <input type=file> is hidden behind this label instead of styled
   directly (file inputs aren't stylable cross-browser). */
.file-input-row {
  display: flex;
  align-items: center;
  gap: var(--space-5);
}

.file-input-label {
  cursor: pointer;
}

.sr-only:focus-visible + .file-input-label {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.bulk-result-detail {
  margin-top: var(--space-3);
}

.bulk-result-urls {
  margin: var(--space-1) 0 0;
  padding-left: 1.15rem;
}

.bulk-result-urls li {
  margin: 0.15rem 0;
}

.import-batch-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
}

.import-batch-card {
  background: var(--surface);
  box-shadow: var(--bevel-panel);
  border-radius: var(--radius);
  padding: var(--space-7) var(--space-8);
  transition: box-shadow var(--dur) var(--ease-hardware);
}

.import-batch-card h3 {
  margin: 0 0 var(--space-2);
}

/* Briefly marks the card a ?batch=<id> link (the import-complete email,
   or "Track this import" right after a fresh submit) scrolled to —
   cleared a couple seconds after landing, see ImportBatches.jsx. */
.import-batch-card-highlighted {
  box-shadow: var(--bevel-panel), inset 0 0 0 1px var(--accent);
}

/* Meter first, then the count it illustrates. */
.import-batch-progress {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  color: var(--text-muted);
}

.import-batch-actions {
  display: flex;
  gap: var(--space-5);
  margin: var(--space-5) 0;
}

.save-result {
  background: var(--surface);
  box-shadow: var(--bevel-panel);
  border-radius: var(--radius);
  padding: var(--space-7) var(--space-8);
}

/* break-all is here for the Save page, whose h3 is a saved page's URL. It
   is deliberately NOT on .save-result h2 (Login's "You're signed in"):
   breaking a real heading mid-word is only ever damage. */
.save-result h3 {
  word-break: break-all;
}

.save-result h2 {
  margin: 0 0 var(--space-5);
  font-size: var(--text-lg);
}

/* A revealed credential — Login.jsx's cross-device session code and
   Settings.jsx's new API key. Previously styled with .item-url, which is
   for URLs: 12px, muted, proportional. That is the wrong treatment for
   the one string on the screen the user has to read character by character
   and retype on another device, where a proportional face makes I/l/1 and
   0/O genuinely ambiguous. Monospace at full body size and full-strength
   text colour, with `all` selection so one tap grabs the whole thing when
   the clipboard API is unavailable. */
.code-reveal {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--lcd-text);
  background: var(--lcd);
  box-shadow: var(--bevel-lcd);
  border-radius: var(--radius-sm);
  letter-spacing: var(--tracking-mono);
  padding: 0.6rem var(--space-5);
  margin: var(--space-5) 0;
  word-break: break-all;
  user-select: all;
  text-align: left;
}

/* The cross-device escape hatch on the sign-in card: subordinate to the
   "Continue here" button above it, hence the separating rule and the
   left-aligned copy (centred text past ~2 lines is harder to scan). */
.code-reveal-block {
  margin-top: var(--space-9);
  padding-top: var(--space-8);
  border-top: 1px solid var(--hairline);
  text-align: left;
}

.code-reveal-block .btn-secondary {
  width: 100%;
}

/* Comfortable touch targets for the two actions on the signed-in card —
   the base button height (38px) is under the 44px guideline, and
   "Continue here" was a 20px-tall text link before. Scoped under
   .auth-card on purpose: .save-result is shared with ImportBatches.jsx,
   whose own direct-child buttons should keep their normal size. */
.auth-card .save-result > button,
.code-reveal-block > button {
  min-height: 44px;
}

.auth-card .save-result > button {
  width: 100%;
}

/* Bookmarklet link — styled like a primary button but is a plain <a> (so
   it's draggable to a bookmarks bar). Being an anchor it inherits nothing
   from the base `button` rule, so it restates the accent fill and its
   --text-on-accent ink itself. */
/* Quieter than a real key on purpose: this is a thing you drag to a
   bookmarks bar, not a thing you press. The accent tint and the grab cursor
   say "pick me up"; the moulding says it is an object rather than a link. */
.bookmarklet-link {
  display: inline-block;
  background: var(--accent-quiet);
  color: var(--accent);
  box-shadow: var(--bevel-raised);
  border-radius: var(--radius);
  padding: 0.6rem 1.1rem;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  font-weight: var(--weight-semibold);
  cursor: grab;
}

.extensions-download-row {
  display: flex;
  gap: var(--space-5);
  margin: var(--space-5) 0;
}

/* Settings — content language preference */
.lang-pref-options {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-9);
}

.lang-pref-option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-md);
}

/* Admin */
.config-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: var(--space-7);
  font-size: var(--text-sm);
}

.config-table th,
.config-table td {
  text-align: left;
  padding: var(--space-3) 0.6rem;
}

/* Only the line between two rows survives: one under the header, one
   between each pair of rows, and nothing trailing under the last. */
.config-table th {
  border-bottom: 1px solid var(--hairline);
  font-family: var(--font-mono);
  font-weight: var(--weight-regular);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-engraved);
  text-transform: uppercase;
  color: var(--text-faint);
}

.config-table td {
  border-top: 1px solid var(--hairline);
}

.config-table tbody tr:first-child td {
  border-top: none;
}

.config-table input {
  width: 100%;
  padding: 0.35rem var(--space-3);
}

/* Day mode — the RAINBOW theme. Single source of truth for the light
   palette. Activation is entirely by attribute: public/boot-theme.js resolves
   data-theme before first paint (cookie if the user picked one, else the OS
   preference) and ThemeContext.jsx keeps it current afterwards, so every
   document always carries an explicit light|dark value. This used to be
   duplicated as a @media (prefers-color-scheme: light) block guarded by
   :not([data-theme="dark"]) — two copies of the same variables that had to
   be edited in lockstep. Do not reintroduce the media query: with data-theme
   always set, it can only disagree with this block.

   Day mode is not the chassis lightened. It is printed sleeve paper: cream
   stock, near-black ink, and the five-colour spectrum reserved for
   categorical data. That is why its action colour is ink rather than a hue —
   on cream, black is the strongest button — and why its accent is a printing
   magenta. LCDs stay dark glass in both themes, because a screen is a
   screen. */

/* Attribute-only, not :root-qualified, so a nested subtree can be flipped as
   well as the document — side-by-side light/dark specimens, an inverted band.
   The design system ships it this way for that reason, and the design-system
   previews need it: every variant on a preview page shares one document, so a
   wrapper that themes itself is the only way for a "Day" card to sit beside a
   dark one without the last one to render winning for both.

   Specificity is unchanged in practice — :root and [data-theme] are both one
   class-equivalent, and this block still comes last. */
[data-theme="light"] {
  /* paper — cream sleeve stock, used instead of the clay ramp so the light
     theme is its own object rather than a tint of the dark one. */
  --paper-50: #fffdf6;
  --paper-100: #fdf6e4;
  --paper-200: #efe6d2;
  --paper-300: #ddd1ba;
  --paper-400: #9d8e78;
  --ink: #1a120c;

  --bg: var(--paper-100);
  --bg-sunk: var(--paper-200);
  --surface: var(--paper-50);
  --surface-raised: #ffffff;
  --surface-inset: var(--paper-200);
  --lcd: #12201d;
  --lcd-text: var(--teal-200);
  --lcd-text-dim: var(--teal-600);

  --text: var(--ink);
  --text-muted: #5a4d3d;
  --text-faint: #7a6a55;
  --text-on-accent: var(--paper-100);
  --text-on-action: var(--paper-100);

  /* Magenta is the light theme's accent — the sleeve's loudest ink. */
  --accent: var(--ch-magenta-ink);
  --accent-strong: #8c0a4e;
  --accent-quiet: rgba(229, 17, 127, 0.12);
  /* Action is ink, not a hue. */
  --action: var(--ink);
  --action-hover: #33241a;
  --danger: #c2213e;
  --danger-quiet: rgba(194, 33, 62, 0.12);
  --danger-ink: #c2213e;
  --danger-key: #c2213e;
  --text-on-danger: var(--paper-100);
  --warning: var(--amber-600);
  --warning-quiet: rgba(242, 206, 0, 0.28);
  /* --warning is a lamp and a tint colour; on cream it only reaches ~3.6:1,
     so small warning TEXT uses this instead. */
  --warning-ink: #6b4f00;
  --success: var(--olive-600);
  --success-quiet: rgba(125, 190, 60, 0.22);
  --info: var(--dusk-600);
  --info-quiet: rgba(14, 156, 208, 0.16);

  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235a4d3d' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  /* Weaker than the dark theme's in relative terms: on paper the well is
     already pale, and a strong sheen washes the bone out to the colour of
     the card behind it — the placeholder disappears as the light passes. */
  --skeleton-sheen: rgba(255, 253, 246, 0.5);

  --hairline: rgba(40, 26, 14, 0.14);
  --bevel-light: rgba(255, 253, 246, 0.8);
  --bevel-dark: rgba(58, 40, 24, 0.2);
  --focus-ring: var(--ch-magenta-ink);
  --scrim: rgba(48, 34, 20, 0.4);

  color-scheme: light;
}

/* There is deliberately no light-mode button color rule here any more.

   There used to be: `button:not(.icon-menu-toggle):not(.icon-menu-option)
   :not(.nav-menu-toggle):not(.link-button):not(.btn-secondary)
   :not(.status-detail-icon):not(.density-option)` forcing color:#fff, plus a
   named .bookmarklet-link entry. That rule was an opt-*out* list: it claimed
   every button in the app and each transparent-backed one had to be excluded
   by hand. Forgetting an exclusion rendered the new button white-on-white in
   day mode, which happened three times — the last of them the density
   toggle, which had to carry a .density-option class that existed for no
   reason except to appear in the list above. That class is now gone from
   Digest.jsx too; .density-toggle button styles it directly.

   It is now an opt-*in*: --text-on-accent carries the ink for accent-filled
   surfaces and the filled variants (base `button`, .bookmarklet-link,
   .density-toggle button[aria-pressed]) name it themselves, .btn-danger names
   its own --danger-key/--text-on-danger pair, and every transparent-backed
   class states its own text color
   next to the `background: none` that makes it necessary — one place to look
   instead of two. Adding a button now means giving it a background and a
   matching color together; nothing at the bottom of this file needs editing
   for it, and forgetting is a visible mistake in both themes rather than an
   invisible one in day mode only. */

/* The system's only continuous animations: the dream-job spinner and the LCD
   cursor blink. Both are decorative readouts of a state that is also stated
   in text, so honouring a reduced-motion preference costs nothing — the
   spinner still shows, it just stops turning. This file carried no
   prefers-reduced-motion block at all before v2; extension/save-bar.source.js
   has had the same guard for its own spinner since it was written. */
@media (prefers-reduced-motion: reduce) {
  .icon-spin,
  .lcd-cursor,
  .skeleton::after {
    animation: none;
  }

  /* Without the sweep there is nothing for the overlay to do, and leaving it
     parked at -100% is a hairline of sheen at the edge of every bone. */
  .skeleton::after {
    display: none;
  }

  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
  }
}
