/* bobba — “Riso Sunrise”: warm paper, riso spot inks, one indigo text ink.
   See DESIGN.md before adding colours or motion. */

/* ---- the inks: ten pairs, one per day ----
   Every theme is the same print shop with different inks in the trays: tinted
   paper, one deep text ink, and five spot inks in fixed roles — pink is the
   overprint, orange the rays, yellow the sun, blue the horizon, green the
   fresh accent. The names are roles, not promises about hue: sea-glass prints
   its "pink" as coral and its "yellow" as sand.

   Both halves of a pair sit in one light-dark() per token, which is what makes
   the sunset switch cheap: theme.js narrows `color-scheme` on <html> to one
   keyword and every colour below follows, with no second copy of the
   stylesheet to keep in step. With JS off, `light dark` leaves the OS in
   charge exactly as the old media query did.

   Adding a theme is one block here and one id in THEMES (theme.js) — the ids
   and the order have to match, and test/theme.test.js reads both files to make
   sure they do. It also checks each pair's text inks against its own paper and
   card stock, so a palette that reads badly can't land quietly.

   Secondary ink is a shade deeper than it looks like it needs to be
   throughout: the original at #767a94 landed at 4.1:1 on card stock, under AA
   for the 12–14px metadata and summaries it carries. Same hue, printed
   heavier. Every pair below is held to that. */
:root, [data-theme="riso-sunrise"] {
  --paper:      light-dark(#f8f2e5, #151829);
  --ink:        light-dark(#23305a, #f0ecdf);
  --ink-soft:   light-dark(#676b87, #a3a7c4);
  --card-solid: light-dark(#fffdf6, #20243c);
  --pink:       light-dark(#ff48b0, #ff6ec1);
  --orange:     light-dark(#ff6c2f, #ff824d);
  --yellow:     light-dark(#ffce00, #ffd83d);
  --blue:       light-dark(#0078bf, #4aa3e0);
  --green:      light-dark(#00a95c, #2ec27e);
}
/* Pale aqua stock, deep teal ink — the seaside end of the same press. */
[data-theme="sea-glass"] {
  --paper:      light-dark(#e8f4f0, #0d1e24);
  --ink:        light-dark(#10405a, #e4f1ee);
  --ink-soft:   light-dark(#4e6874, #96b1b8);
  --card-solid: light-dark(#f7fcfa, #16303a);
  --pink:       light-dark(#e5407c, #ff77ad);
  --orange:     light-dark(#f26a2c, #ff9463);
  --yellow:     light-dark(#e0b400, #ffd95c);
  --blue:       light-dark(#0083b0, #45b6d8);
  --green:      light-dark(#009874, #33c99b);
}
/* Lilac stock, plum ink. */
[data-theme="lilac-press"] {
  --paper:      light-dark(#f2eef8, #191428);
  --ink:        light-dark(#3a2a5c, #ece6f7);
  --ink-soft:   light-dark(#655b80, #a9a0c4);
  --card-solid: light-dark(#fbf8ff, #241e3a);
  --pink:       light-dark(#d92f8c, #ff74c2);
  --orange:     light-dark(#e8632c, #ff9060);
  --yellow:     light-dark(#d8ac00, #ffd85a);
  --blue:       light-dark(#4d5cc4, #8b9bff);
  --green:      light-dark(#1f9260, #4cc48c);
}
/* Mint stock, forest ink. */
[data-theme="mint-julep"] {
  --paper:      light-dark(#e9f5e8, #0f1c17);
  --ink:        light-dark(#1d4232, #e6f2e6);
  --ink-soft:   light-dark(#526d5f, #9bb3a4);
  --card-solid: light-dark(#f8fdf6, #182c23);
  --pink:       light-dark(#d93672, #ff6fa3);
  --orange:     light-dark(#e66c22, #ff9257);
  --yellow:     light-dark(#d4ae00, #f5d24a);
  --blue:       light-dark(#0d7a9c, #43aecc);
  --green:      light-dark(#2c8f3d, #52c26a);
}
/* Peach stock, oxblood ink. */
[data-theme="peach-melba"] {
  --paper:      light-dark(#fdeee4, #22151a);
  --ink:        light-dark(#59243a, #fbe8de);
  --ink-soft:   light-dark(#815a67, #bfa0a8);
  --card-solid: light-dark(#fff8f2, #33222a);
  --pink:       light-dark(#d92f72, #ff6f9f);
  --orange:     light-dark(#e0611c, #ff8b4d);
  --yellow:     light-dark(#cf9c00, #f4c944);
  --blue:       light-dark(#3a68ab, #7ea3e0);
  --green:      light-dark(#478e42, #6fbd68);
}
/* Blue-grey stock, navy ink — the drawing-office pair. */
[data-theme="blueprint"] {
  --paper:      light-dark(#e9eff7, #101724);
  --ink:        light-dark(#17325c, #e6eefb);
  --ink-soft:   light-dark(#546480, #9aabc6);
  --card-solid: light-dark(#f8fbff, #1b2436);
  --pink:       light-dark(#d93384, #ff70b8);
  --orange:     light-dark(#e56328, #ff8f5c);
  --yellow:     light-dark(#d8a800, #f7cd4a);
  --blue:       light-dark(#1362b8, #5fa5ee);
  --green:      light-dark(#0d8f64, #3cc294);
}
/* Rose stock, aubergine ink. */
[data-theme="rose-quartz"] {
  --paper:      light-dark(#fbecef, #201419);
  --ink:        light-dark(#4a2440, #f9e6ea);
  --ink-soft:   light-dark(#7d5a6c, #c2a2ae);
  --card-solid: light-dark(#fff7f8, #31212a);
  --pink:       light-dark(#d42873, #ff6cae);
  --orange:     light-dark(#e06134, #ff8d63);
  --yellow:     light-dark(#cc9c00, #f2c94f);
  --blue:       light-dark(#4d5eae, #8f9ce8);
  --green:      light-dark(#428e5a, #62bd84);
}
/* Oat stock, espresso ink. */
[data-theme="oat-milk"] {
  --paper:      light-dark(#f4eee0, #1c1813);
  --ink:        light-dark(#3a2d20, #f2ebdd);
  --ink-soft:   light-dark(#6f6250, #b0a493);
  --card-solid: light-dark(#fdf9f1, #2a251d);
  --pink:       light-dark(#cc3670, #ff7aa8);
  --orange:     light-dark(#d4611c, #f79452);
  --yellow:     light-dark(#c29600, #e8c44e);
  --blue:       light-dark(#2a6789, #66a3c6);
  --green:      light-dark(#578528, #90b95c);
}
/* Butter stock, bitumen ink. */
[data-theme="sunflower"] {
  --paper:      light-dark(#fdf3d8, #1e1a0f);
  --ink:        light-dark(#3d3218, #faf1d6);
  --ink-soft:   light-dark(#756848, #b6a882);
  --card-solid: light-dark(#fffbec, #2c2718);
  --pink:       light-dark(#d43364, #ff7897);
  --orange:     light-dark(#dc6215, #ff9448);
  --yellow:     light-dark(#c99900, #f3c634);
  --blue:       light-dark(#26709f, #5ea9d4);
  --green:      light-dark(#468a2c, #86bd5a);
}
/* Sage stock, pine ink. */
[data-theme="sage-note"] {
  --paper:      light-dark(#eef2e6, #141a12);
  --ink:        light-dark(#2b3a26, #ecf1e4);
  --ink-soft:   light-dark(#5d6a54, #a4b099);
  --card-solid: light-dark(#fafcf4, #202a1d);
  --pink:       light-dark(#c73a6e, #ff749d);
  --orange:     light-dark(#d96628, #ff9457);
  --yellow:     light-dark(#c59b00, #eec84c);
  --blue:       light-dark(#2a6a8b, #62a7c6);
  --green:      light-dark(#3a8540, #6fb96f);
}

:root {
  /* The pair above is a pair until theme.js narrows this to one keyword. It's
     also what puts the text field, the scroll bars and the overscroll gutter
     into the same scheme as the paper — the app never declared this before, so
     in the dark those were all still being drawn light by the UA. */
  color-scheme: light dark;
  /* Derived rather than restated per theme: card stock is the theme's own
     near-solid sheet, hairlines are the text ink at a tenth. (The dark card
     was 0.90 before this and is 0.92 now — the two schemes had no reason to
     disagree, and at that alpha nothing behind a card gets through either
     way.) */
  --card: color-mix(in srgb, var(--card-solid) 92%, transparent);
  --line: color-mix(in srgb, var(--ink) 10%, transparent);
  --radius: 20px;

  /* The two shadow recipes, held side by side so the switch below is a
     one-line pick rather than a second copy of the values. Paper takes a tight
     ink hairline under a wide soft cast; the dark takes neither, because the
     hairline is invisible on it and the cast has to be much heavier to read. */
  --shadow-paper: 0 1px 0 color-mix(in srgb, var(--cast) 4%, transparent),
                  0 10px 30px color-mix(in srgb, var(--cast) 10%, transparent);
  --shadow-night: 0 4px 14px color-mix(in srgb, var(--cast) 35%, transparent),
                  0 14px 40px color-mix(in srgb, var(--cast) 30%, transparent);
  --lift-paper: 0 2px 0 color-mix(in srgb, var(--cast) 5%, transparent),
                0 22px 48px color-mix(in srgb, var(--cast) 16%, transparent);
  --lift-night: 0 8px 20px color-mix(in srgb, var(--cast) 50%, transparent),
                0 24px 60px color-mix(in srgb, var(--cast) 40%, transparent);
  --dock-paper: 0 12px 32px color-mix(in srgb, var(--cast) 18%, transparent),
                0 2px 8px color-mix(in srgb, var(--cast) 8%, transparent);
  --dock-night: 0 12px 32px color-mix(in srgb, var(--cast) 50%, transparent),
                0 2px 8px color-mix(in srgb, var(--cast) 35%, transparent);
  /* The hard print offset under the two action buttons. One recipe: --cast
     already moves it from theme ink to black between the schemes. */
  --press: 0 3px 0 color-mix(in srgb, var(--cast) 22%, transparent),
           0 10px 24px color-mix(in srgb, var(--cast) 16%, transparent);
  --press-lift: 0 5px 0 color-mix(in srgb, var(--cast) 22%, transparent),
                0 16px 32px color-mix(in srgb, var(--cast) 20%, transparent);

  /* The dock's glass. White at varying strengths in both schemes — it's the
     light on the lens, not an ink — so it's the one group the themes leave
     alone. */
  --glass-top: light-dark(rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.14));
  --glass-mid: light-dark(rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.03));
  --glass-bottom: light-dark(rgba(255, 255, 255, 0.30), rgba(255, 255, 255, 0.07));
  --glass-edge: light-dark(rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.16));
  --glass-specular: light-dark(rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.28));
  --glass-glow: light-dark(rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.06));
  --chip-edge: light-dark(rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.14));
  --chip-top: light-dark(rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.16));
  --chip-mid: light-dark(rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
  --chip-bottom: light-dark(rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.06));
  --chip-specular: light-dark(rgba(255, 255, 255, 0.60), rgba(255, 255, 255, 0.20));
  --lens-edge: light-dark(rgba(255, 255, 255, 0.90), rgba(255, 255, 255, 0.55));
  --lens-top: light-dark(rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.38));
  --lens-mid: light-dark(rgba(255, 255, 255, 0.30), rgba(255, 255, 255, 0.06));
  --lens-bottom: light-dark(rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.12));
  --lens-specular: light-dark(rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.50));
  --lens-underglow: light-dark(rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.08));
  --lens-ring: light-dark(rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.40));
  /* Agency marks are drawn for white paper, so in the dark they get a small
     white plate to sit on. */
  --logo-plate: light-dark(transparent, #fff);
}

/* Everything the two schemes disagree about that ISN'T a colour: shadow
   geometry, the grain's opacity, how hard a spot ink is printed through the
   glass, the padding on that white plate. light-dark() carries colours and
   nothing else, so this group is stated per scheme instead — as a pick from
   the pairs above, so there is still only one copy of each value.
   The media query is the no-JS path and has to come first: all four selectors
   weigh the same, so source order is what breaks the tie. */
:root {
  --cast: var(--ink);
  --shadow: var(--shadow-paper);
  --shadow-lift: var(--lift-paper);
  --dock-shadow: var(--dock-paper);
  --grain: 0.06;
  --chip-tint: 22%;     /* spot ink through the dock glass */
  --chip-flat: 16%;     /* and on a card, where there's no glass over it */
  --chip-cast: 8%;
  --lens-tint: 34%;
  --lens-glow: 45%;
  --lens-cast: 22%;
  --plate-pad: 0;
  --plate-opacity: 0.85;
}
@media (prefers-color-scheme: dark) {
  :root {
    --cast: #000;
    --shadow: var(--shadow-night);
    --shadow-lift: var(--lift-night);
    --dock-shadow: var(--dock-night);
    --grain: 0.045;
    --chip-tint: 26%;
    --chip-flat: 26%;
    --chip-cast: 30%;
    --lens-tint: 40%;
    --lens-glow: 55%;
    --lens-cast: 45%;
    --plate-pad: 1px 2px;
    --plate-opacity: 0.92;
  }
}
[data-scheme="light"] {
  /* This one declaration is the whole sunset switch: narrowing color-scheme
     picks a side of every light-dark() above, palette included. The rest of
     this block is only the handful of tokens that aren't colours. */
  color-scheme: light;
  --cast: var(--ink);
  --shadow: var(--shadow-paper);
  --shadow-lift: var(--lift-paper);
  --dock-shadow: var(--dock-paper);
  --grain: 0.06;
  --chip-tint: 22%;
  --chip-flat: 16%;
  --chip-cast: 8%;
  --lens-tint: 34%;
  --lens-glow: 45%;
  --lens-cast: 22%;
  --plate-pad: 0;
  --plate-opacity: 0.85;
}
[data-scheme="dark"] {
  color-scheme: dark;
  --cast: #000;
  --shadow: var(--shadow-night);
  --shadow-lift: var(--lift-night);
  --dock-shadow: var(--dock-night);
  --grain: 0.045;
  --chip-tint: 26%;
  --chip-flat: 26%;
  --chip-cast: 30%;
  --lens-tint: 40%;
  --lens-glow: 55%;
  --lens-cast: 45%;
  --plate-pad: 1px 2px;
  --plate-opacity: 0.92;
}

* { box-sizing: border-box; }
/* html carries the paper colour so iOS overscroll and the Safari/status
   bar area blend into the app instead of flashing white. It is also where
   theme.js reads the day's paper back from, to tint the status bar — the
   used background-color is a resolved colour, where the custom property
   behind it is still an unevaluated light-dark(). */
html { -webkit-text-size-adjust: 100%; background: var(--paper); }
body {
  margin: 0;
  font-family: "Avenir Next", Avenir, Futura, "Segoe UI", Corbel, system-ui, sans-serif;
  color: var(--ink);
  min-height: 100dvh;
  padding-bottom: calc(env(safe-area-inset-bottom) + 28px);
  overflow-x: hidden;
}

/* One keyboard focus ring for the whole app, in the text ink so it reads at
   full contrast on paper and in the dark. Only for keyboard/AT focus —
   :focus-visible keeps taps and clicks silent. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}
/* ...except on <main>, which the router focuses after every swap so a screen
   reader starts reading from the top of the page it just moved to. That's a
   programmatic move, but the browser judges :focus-visible on modality rather
   than on how focus arrived: type in the mute field on /settings, go back, and
   the ring paints around <main>. On the feed <main> is 168,000px tall, so the
   only edge on screen is the top one — a hairline right under the header, in
   --ink, which in the dark is near-white. tabindex="-1" keeps <main> out of the
   tab order entirely, so a ring here is never the one a keyboard user needs. */
main:focus, main:focus-visible { outline: none; }
/* Text that only the screen reader gets: page headings the wordmark already
   carries visually, live status for things that only change colour or opacity,
   and "opens in a new tab" notes. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap;
}
/* iOS paints a grey block over anything tappable; every control below has its
   own press state, so the default flash is just noise. Text links keep it.
   touch-action: manipulation opts each one out of double-tap-to-zoom, which is
   the last thing that can make the browser hold a tap back to see what comes
   next — nothing here is worth zooming into. */
.card-link, .icon-btn, .settings-btn, .filter-chip, .logo,
.refresh-btn, .save, .original, .label-btn, .clear-cache, .sign-out, .invite-revoke {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

/* Apple's 44pt minimum target, without redrawing anything: a transparent
   overlay centred on each control extends what a thumb can hit past what the
   eye sees. The circles paint at 40px and the dock chips at ~36px tall, so
   every one of them was a few pixels short of a reliable hit — which reads as
   "I tapped it and nothing happened", indistinguishable from lag.
   The overlay is a child of the button, so the event still targets the button
   itself and closest() keeps working. Only the short axis grows: every chip is
   already wider than 44px, so the dock's 8px gaps stay real gaps. */
.icon-btn, .settings-btn, .filters .filter-chip, .label-btn, .invite-revoke { position: relative; }
.icon-btn::after, .settings-btn::after,
.filters .filter-chip::after, .label-btn::after, .invite-revoke::after {
  content: "";
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 100%; height: 100%;
  min-width: 44px; min-height: 44px;
}

/* Hover is a lie on a phone. iOS fires it on tap and then leaves it stuck, so
   the card you just came back from sits lifted, zoomed and shadowed with
   nothing under your finger — which is what made tapping a card confusing.
   Every hover state in the app is decoration on top of an :active state that
   already does the acknowledging, so they all move behind a real pointer.
   Declared here, above each component's own rules, so the :active states that
   follow still win the ties on a desktop press. */
@media (hover: hover) and (pointer: fine) {
  .card-link:hover { transform: translateY(-3px) scale(1.012); box-shadow: var(--shadow-lift); }
  .card-link:hover .card-media img { transform: scale(1.045); }
  .icon-btn:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
  .refresh-btn:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
  .save:hover, .original:hover { transform: translateY(-2px); box-shadow: var(--press-lift); }
  .reader-p a:hover { text-decoration-color: var(--orange); }
  .label-body h2 a:hover { text-decoration: underline; }
}

/* ---- the paper: cream stock, faint grain, three soft ink washes ---- */
.sky {
  position: fixed; inset: 0; z-index: -1;
  background: var(--paper);
}
.sky::after {
  content: ""; position: absolute; inset: 0;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='140' height='140' filter='url(%23n)' opacity='0.55'/></svg>");
  pointer-events: none;
}
/* Prints lighter in the dark, where noise reads louder over a flat near-black
   than it does over cream. */
.sky::after { opacity: var(--grain); }
.blob {
  position: absolute; border-radius: 50%;
  filter: blur(64px);
  animation: bob 16s ease-in-out infinite alternate;
}
/* The washes carry their strength in the ink rather than in `opacity`: a lone
   blurred disc composites the same either way, and as a colour the two schemes
   fit in one light-dark() instead of a second rule. */
.blob-a {
  width: 42vmax; height: 42vmax; left: -12vmax; top: -10vmax;
  background: light-dark(color-mix(in srgb, var(--yellow) 30%, transparent),
                         color-mix(in srgb, var(--yellow) 10%, transparent));
}
.blob-b {
  width: 36vmax; height: 36vmax; right: -10vmax; top: 22vh; animation-delay: -5s;
  background: light-dark(color-mix(in srgb, var(--pink) 16%, transparent),
                         color-mix(in srgb, var(--pink) 8%, transparent));
}
.blob-c {
  width: 30vmax; height: 30vmax; left: 18vw; bottom: -14vmax; animation-delay: -9s;
  background: light-dark(color-mix(in srgb, var(--blue) 14%, transparent),
                         color-mix(in srgb, var(--blue) 8%, transparent));
}
@keyframes bob {
  from { transform: translateY(-3%) scale(1); }
  to { transform: translateY(4%) scale(1.07); }
}

/* ---- header ---- */
.top {
  display: flex; align-items: baseline; gap: 10px;
  padding: calc(env(safe-area-inset-top) + 18px) max(20px, env(safe-area-inset-right)) 6px
           max(20px, env(safe-area-inset-left));
  max-width: 640px; margin: 0 auto;
}
.logo { display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit; }
.mark { width: 42px; height: 42px; }
/* The mark is printed in whichever inks are in the trays today. The hexes on
   the SVG itself are the riso-sunrise set, kept as the no-stylesheet fallback;
   these four are what actually paint, and they're why a new theme reaches the
   logo without anyone touching render.js. */
.mark-sun-offset { fill: var(--pink); }
.mark-sun { fill: var(--yellow); }
.mark-rays { stroke: var(--orange); }
.mark-horizon { stroke: var(--blue); }
/* The rays sway like a hand-stamped print never quite lining up. */
.mark-rays { transform-origin: 32px 39px; animation: ray-sway 7s ease-in-out infinite alternate; }
@keyframes ray-sway { from { transform: rotate(-4deg); } to { transform: rotate(4deg); } }
.wordmark {
  font-size: 26px; font-weight: 800; letter-spacing: -0.02em;
  color: var(--ink);
  /* Pink overprint shadow — the riso misregistration signature. */
  text-shadow: 0.055em 0.055em 0 color-mix(in srgb, var(--pink) 60%, transparent);
  white-space: nowrap;
}
.wordmark em { font-style: normal; font-weight: 800; color: var(--logo-accent); text-shadow: none; }
/* `news` runs warm on paper: the mark is a sunrise, and at 26px hot pink was
   the loudest thing on a cream page. In the dark it stays pink — on #151829 the
   orange goes muddy and the pink is the ink that still carries.
   Both offset ghosts — behind `bobba` here, behind the sun in the mark — stay
   pink in both schemes. Warming them was tried and lost the mark: an orange
   ghost behind a yellow disc with orange rays around it simply disappears, and
   that misregistration is the whole idea (DESIGN.md). Two inks, not one. */
:root { --logo-accent: light-dark(var(--orange), var(--pink)); }
.settings-btn {
  margin-left: auto; align-self: center;
  position: relative;
  /* Flex items shrink by default, so on a narrow phone the wordmark could
     squeeze this into an oval — width is a suggestion, flex-basis is the deal.
     aspect-ratio keeps it a circle even if something later sets a height. */
  width: 40px; height: 40px; flex: 0 0 40px; aspect-ratio: 1; border-radius: 50%;
  display: grid; place-items: center;
  text-decoration: none; color: var(--ink);
  background: var(--card); border: 1px solid var(--line); box-shadow: var(--shadow);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.18s ease;
}
.settings-btn svg { width: 22px; height: 22px; }
.settings-btn:active { transform: scale(0.92); }

/* Compact bar for inner pages: back chevron left, save and share right. The
   chevron takes the free space rather than the row spreading everything out —
   with three buttons, space-between would have stranded save in the middle. */
.top-compact { justify-content: flex-start; align-items: center; gap: 10px; }
.top-compact .back-btn { margin-right: auto; }
.icon-btn {
  width: 40px; height: 40px; flex: 0 0 40px; aspect-ratio: 1; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--line); cursor: pointer; font: inherit;
  text-decoration: none; color: var(--ink);
  background: var(--card); box-shadow: var(--shadow);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.18s ease;
}
.icon-btn svg { width: 20px; height: 20px; }
.icon-btn:active { transform: scale(0.92); }

/* Save: one bookmark outline that fills in. The state is carried by
   aria-pressed rather than a class so the paint and the announcement can't
   drift apart — there is only one thing to set. */
.save-btn svg { fill: none; transition: fill 0.2s ease; }
.save-btn[aria-pressed="true"] { color: var(--orange); border-color: color-mix(in srgb, var(--orange) 35%, transparent); }
.save-btn[aria-pressed="true"] svg { fill: currentColor; }

/* ---- category dock: a floating liquid-glass capsule, built for thumbs ---- */
body.with-dock { padding-bottom: calc(env(safe-area-inset-bottom) + 104px); }
.filters {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(env(safe-area-inset-bottom) + 12px);
  width: min(608px, calc(100% - 24px));
  z-index: 20;
  border-radius: 999px;
  overflow: hidden;
  /* Two layers: the specular white gradient that makes it read as glass, over a
     paper-tinted base that stops the feed showing through. The gradient alone
     dipped to 12% opacity in the middle, which let card text read straight
     through the dock. Blur still applies to what's behind, so it stays glass —
     just frosted rather than clear. */
  background:
    linear-gradient(180deg, var(--glass-top), var(--glass-mid) 45%, var(--glass-bottom)),
    light-dark(color-mix(in srgb, var(--paper) 84%, transparent),
               color-mix(in srgb, var(--paper) 88%, transparent));
  backdrop-filter: blur(28px) saturate(1.7); -webkit-backdrop-filter: blur(28px) saturate(1.7);
  border: 1px solid var(--glass-edge);
  box-shadow:
    inset 0 1px 0 var(--glass-specular),       /* specular top edge */
    inset 0 0 18px var(--glass-glow),          /* soft inner glow */
    var(--dock-shadow);
}
.filter-row {
  display: flex; align-items: flex-end; gap: 8px; overflow-x: auto;
  /* Roomy padding + scroll-padding so the magnified selected pill never
     clips against the capsule edge (it grows upward, dock-style). */
  padding: 14px 16px 10px;
  scroll-padding-inline: 16px;
  /* No scroll-snap. A snap keeps animating after the flick has ended, and a
     touch landing in that window stops the scroll instead of hitting the chip
     under it — the row felt dead for a beat after every swipe. Free-scrolling
     is what native chip rows do anyway. */
  scrollbar-width: none; -ms-overflow-style: none;
}
.filter-row::-webkit-scrollbar { display: none; }
.filters .filter-chip {
  border: 1px solid var(--chip-edge); cursor: pointer; font: inherit;
  flex: 0 0 auto;
  text-transform: none; /* .chip lowercases — dock chips are proper category names */
  font-size: 13.5px; font-weight: 700; padding: 9px 15px;
  border-radius: 999px; color: var(--ink);
  /* Frosted pill with the category's spot ink showing through the glass. */
  background:
    linear-gradient(180deg, var(--chip-top), var(--chip-mid) 55%, var(--chip-bottom)),
    color-mix(in srgb, var(--chip-ink, var(--ink-soft)) var(--chip-tint), transparent);
  box-shadow: inset 0 1px 0 var(--chip-specular),
              0 2px 6px color-mix(in srgb, var(--cast) var(--chip-cast), transparent);
  /* Grows from its base like a macOS dock icon. */
  transform-origin: center bottom;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
  opacity: 0.78;
}
.filters .filter-chip:active { transform: scale(0.92); }
/* Selected category is a glass lens: luminous, ringed, glowing its own ink,
   and magnified from the dock base. Unmissable in either colour scheme. */
.filters .filter-chip.is-on {
  opacity: 1; transform: scale(1.16);
  border-color: var(--lens-edge);
  background:
    radial-gradient(120% 95% at 50% 0%, var(--lens-top), var(--lens-mid) 62%, var(--lens-bottom)),
    color-mix(in srgb, var(--chip-ink, var(--yellow)) var(--lens-tint), transparent);
  box-shadow:
    inset 0 1px 1px var(--lens-specular),
    inset 0 -6px 12px var(--lens-underglow),
    0 0 0 1.5px var(--lens-ring),
    0 6px 18px color-mix(in srgb, var(--cast) var(--lens-cast), transparent),
    0 0 22px color-mix(in srgb, var(--chip-ink, var(--yellow)) var(--lens-glow), transparent);
}
.card.filtered-out { display: none; }

/* ---- settings ---- */
/* No entrance animation on the panel you just navigated to, here or on .reader.
   It started at opacity 0 and took half a second to arrive, on top of the
   cross-fade the navigation itself already plays — so tapping the cog bought
   you a fade, then another fade, then the page. Two arrival gestures for one
   arrival reads as lag, not as polish. The view transition is the gesture; the
   content it lands on should already be there. */
.settings {
  background: var(--card); border-radius: var(--radius);
  border: 1px solid var(--line);
  box-shadow: var(--shadow); padding: 20px 22px 24px;
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
}
.settings h1 { margin: 6px 0 20px; font-size: 24px; letter-spacing: -0.01em; }
.setting-block { margin: 0 0 26px; }
.setting-block:last-child { margin-bottom: 4px; }
.setting-block h2 { margin: 0 0 4px; font-size: 16px; }
.refresh-btn {
  border: none; cursor: pointer; font: inherit;
  display: inline-flex; align-items: center; gap: 9px;
  font-size: 15px; font-weight: 700; color: var(--ink);
  padding: 12px 20px; border-radius: 999px;
  background: var(--card-solid); border: 1px solid var(--line); box-shadow: var(--shadow);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.18s ease;
}
.refresh-btn:active { transform: scale(0.96); }
.refresh-icon { display: block; }
.refresh-btn.busy .refresh-icon { animation: spin 1.2s linear infinite; }
.refresh-btn.busy, .refresh-btn:disabled { pointer-events: none; opacity: 0.8; cursor: default; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Live narration under the button: stage messages while the pipeline runs,
   then a brief success/error note. aria-live, so it must stay in the DOM. */
.refresh-note {
  margin: 12px 0 0; font-size: 13.5px; font-weight: 700; color: var(--ink-soft);
  animation: card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.refresh-note:empty { display: none; }
/* Both statuses are the relevant spot ink pulled towards the text ink, which
   is what keeps them legible without a second pair of hard-coded hexes: on
   paper the ink is dark and deepens them, in the dark it's near-white and
   lifts them. Error mixes towards orange first — pink alone reads as an
   accent, and this line has to read as a problem. */
.refresh-note.is-success, .save-note, .invite-note.is-success { color: color-mix(in srgb, var(--green) 72%, var(--ink)); }
.refresh-note.is-error, .auth-error, .invite-note.is-error {
  color: color-mix(in srgb, color-mix(in srgb, var(--pink) 55%, var(--orange)) 70%, var(--ink));
}
/* The note above this collapses when empty, so most of the time this line sits
   directly under the button and 8px read as cramped against a 44px pill.
   Matched on both its classes deliberately: the element is `class="hint
   refresh-last"`, and the generic `.hint` rule is declared further down the
   file. At equal specificity the later rule wins, so a bare `.refresh-last`
   here loses to `.hint`'s `margin: 0 0 12px` and silently does nothing. */
.hint.refresh-last { margin: 16px 0 0; }
.save-note {
  margin: 12px 0 0; font-size: 13.5px; font-weight: 700;
  animation: card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Quieter than .refresh-btn on purpose — throwing away what you've saved should
   never be the loudest thing on the screen. Signing out shares the recipe for
   the same reason: it takes the offline copy with it, and a button that loud
   would be tapped by someone only meaning to have a look at the settings. */
.clear-cache, .sign-out {
  border: 1px solid var(--line); cursor: pointer; font: inherit;
  font-size: 14px; font-weight: 700; color: var(--ink-soft);
  padding: 10px 16px; border-radius: 999px;
  background: var(--card-solid); box-shadow: var(--shadow);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.clear-cache:active, .sign-out:active { transform: scale(0.96); }
.clear-cache[disabled] { opacity: 0.5; cursor: default; transform: none; }
.mute-controls { display: flex; gap: 8px; }
.mute-controls .text-input { flex: 1; }
.mute-controls .save { padding: 11px 18px; opacity: 1; animation: none; }

/* ---- account and invites (/settings) ----
   The invite list is an admin table with no table in it: one row per link, the
   two things that decide what to do with it, and the control that does it. It
   borrows .label-row's shape — the app's other list of things to act on. */
.account-block strong { color: var(--ink); }
.account-block form { margin: 0 0 12px; }
/* Where app.js reports a freshly-made link. It carries a URL, which is one
   ninety-character word and would otherwise push the settings card wider than
   the phone. */
.invite-note {
  margin: 12px 0 0; font-size: 13.5px; font-weight: 700; color: var(--ink-soft);
  overflow-wrap: anywhere;
  animation: card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.invite-note:empty { display: none; }
.invite-list {
  display: flex; flex-direction: column; gap: 8px;
  margin: 14px 0 0; padding: 0; list-style: none;
}
/* The list and the "none yet" line are alternatives, and which one shows is
   settled here rather than in script — so a row the client prepends after
   minting a link takes the line away on its own. */
.invite-list:not(:empty) + .invite-empty { display: none; }
.hint.invite-empty { margin: 12px 0 0; }
.invite-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border-radius: 14px;
  background: var(--card-solid); border: 1px solid var(--line);
}
/* A claimed invite is the record of how somebody got in, so it's kept — just
   pulled back, so the ones still hanging open are what the eye lands on. */
.invite-spent { opacity: 0.6; }
.invite-for { flex: 1 1 auto; min-width: 0; }
.invite-who { font-size: 14px; font-weight: 700; overflow-wrap: anywhere; }
.invite-admin { margin-left: 6px; --chip-ink: var(--yellow); }
.invite-status { display: block; margin-top: 2px; font-size: 12.5px; color: var(--ink-soft); }
.invite-revoke {
  flex: 0 0 auto;
  border: 1px solid var(--line); cursor: pointer; font: inherit;
  font-size: 13px; font-weight: 700; color: var(--ink-soft);
  padding: 5px 14px; border-radius: 999px; background: transparent;
  transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.15s ease;
}
.invite-revoke:active { transform: scale(0.94); }

/* ---- feed ---- */
/* Side padding respects the notch/home-indicator insets so nothing hides under
   the rounded corners when the phone is held landscape. */
main {
  max-width: 640px; margin: 0 auto;
  padding: 10px max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left));
}
/* The feed is a <ul> so it announces as "list, N items"; the reset just undoes
   the browser's bullets and indent. (role="list" is on the element too —
   Safari drops list semantics when list-style is none.) */
.feed { display: flex; flex-direction: column; gap: 16px; list-style: none; margin: 0; padding: 0; }

/* Only the cards that can still be on screen while the entrance plays get it
   (render.js marks them). The stagger used to be clamped instead, which meant
   every card past the twelfth fired one synchronised fade at 840ms — hundreds
   of compositor animations at once, during load, for content nobody has
   scrolled to. */
.card-enter {
  opacity: 0;
  animation: card-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  animation-delay: calc(var(--i) * 70ms);
}
@keyframes card-in {
  from { opacity: 0; transform: translateY(22px) scale(0.97); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
/* No backdrop-filter here, deliberately, unlike the other card surfaces: this
   one exists ~355 times, and each instance makes the compositor snapshot and
   blur its own rounded-rect slice of the backdrop every frame it's on screen.
   What's behind a card is the paper plus washes that are already blur(64px),
   sampled through a 92%-opaque stock — re-blurring that by 14px changes the
   8% that shows through by well under one 8-bit step. The dock, reader and
   settings panels keep theirs: they're one element each, and the dock in
   particular is the glass. */
.card-link {
  display: block; text-decoration: none; color: inherit;
  background: var(--card);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease;
  /* A ~355-card feed: skip layout, style and paint for the cards that are
     nowhere near the viewport. `auto` in contain-intrinsic-size means the real
     height is remembered once a card has been rendered, so nothing shifts when
     it scrolls back out. Sits on the link rather than the <li> so that paint
     containment clips only the card's own contents (which .card-link already
     clips with overflow:hidden) and leaves the soft ink shadow — which is
     drawn by this element, not inside it — spilling out as before. */
  content-visibility: auto;
  contain-intrinsic-size: auto 470px;
}
/* The placeholder height only matters for cards that have never been on screen,
   but it's what scroll position is restored against, so it's worth being close.
   Modelled over the live feed on a 393px-wide iPhone: a card with a hero image
   averages ~475px, one without ~219px, and the two don't overlap at all. */
.card-textonly { contain-intrinsic-size: auto 220px; }
.card-link:active { transform: scale(0.985); }
.card-media { aspect-ratio: 16 / 9; overflow: hidden; }
.card-media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 0.5s ease;
}
.card-body { padding: 16px 18px 16px; }
.card-meta {
  display: flex; align-items: center; gap: 7px; min-width: 0;
  font-size: 12.5px; font-weight: 600; color: var(--ink-soft);
}
/* A few feeds carry very long publication names ("Belfast Live — Northern
   Ireland's …"). The name gives way first so the timestamp and the "+2 more
   sources" badge stay on the row instead of being clipped off the card. */
.card-meta .source { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-meta .dot, .card-meta time, .card-meta .also, .card-meta .label-conf { flex: 0 0 auto; }
.dot { opacity: 0.5; }
/* Agency marks are drawn for white paper, so in the dark they get a small
   white plate to sit on rather than being left to disappear into it. */
.source-logo {
  height: 14px; width: auto; max-width: 52px; object-fit: contain;
  flex: 0 0 auto;
  border-radius: 3px;
  background: var(--logo-plate); padding: var(--plate-pad); opacity: var(--plate-opacity);
}
.also {
  font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--blue) 16%, transparent);
  color: var(--ink);
}
/* Headlines occasionally arrive with an unbroken token (a bare URL, a very
   long hyphen-free compound) — break it rather than let it push past the card. */
.card h2, .reader h1, .reader-p, .label-body h2 { overflow-wrap: break-word; }
.card h2 { margin: 9px 0 6px; font-size: 18.5px; font-weight: 700; line-height: 1.28; letter-spacing: -0.01em; }
.card p { margin: 0 0 10px; font-size: 14.5px; line-height: 1.5; color: var(--ink-soft); }

/* ---- chips: spot-ink tints. Each category sets one --chip-ink; the shared
   rule prints it at tint strength over the paper. One text ink, riso-style. ---- */
.chips { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
  font-size: 12px; font-weight: 700; padding: 4px 11px;
  border-radius: 999px; color: var(--ink);
  background: color-mix(in srgb, var(--chip-ink, var(--ink-soft)) var(--chip-flat), transparent);
  text-transform: lowercase;
}
/* Every category ink is mixed out of the five in the trays, so a new theme
   reaches the chips without restating fifteen hexes ten times over. The five
   that map straight onto an ink take it; the rest are two-ink mixes, which is
   the same thing a riso press does to get a colour it doesn't stock.
   Pulling towards --ink deepens a hue on paper and lifts it in the dark, both
   of which is what you want; #fff only ever lightens, which is why it's used
   for the two that need to stay pale in both schemes. */
.chip-culture { --chip-ink: var(--pink); }
.chip-animals { --chip-ink: var(--orange); }
.chip-science { --chip-ink: var(--blue); }
.chip-environment { --chip-ink: var(--green); }
.chip-health { --chip-ink: color-mix(in srgb, var(--pink) 60%, var(--orange)); }
.chip-knitting { --chip-ink: color-mix(in srgb, var(--pink) 55%, #fff); }
.chip-gaming { --chip-ink: color-mix(in srgb, var(--pink) 60%, var(--blue)); }
.chip-space { --chip-ink: color-mix(in srgb, var(--blue) 70%, var(--pink)); }
.chip-technology { --chip-ink: color-mix(in srgb, var(--blue) 45%, var(--green)); }
.chip-sport { --chip-ink: color-mix(in srgb, var(--blue) 55%, #fff); }
.chip-local { --chip-ink: color-mix(in srgb, var(--blue) 62%, var(--ink)); }
.chip-plants { --chip-ink: color-mix(in srgb, var(--green) 70%, var(--yellow)); }
.chip-gardening { --chip-ink: color-mix(in srgb, var(--green) 45%, var(--yellow)); }
.chip-education { --chip-ink: color-mix(in srgb, var(--yellow) 80%, var(--orange)); }
.chip-interior-design { --chip-ink: color-mix(in srgb, var(--orange) 60%, var(--ink)); }
/* Saved isn't a subject, so it doesn't take a subject's ink — it borrows the
   accent and keeps its capital, which is what marks it out in a dock of
   lowercase topics. */
.chip-saved { --chip-ink: var(--orange); text-transform: none; }

/* The corner mark on a saved card. Absolutely placed over the media so it
   doesn't move a card that has no photo, and pointer-events off so it can't
   eat a tap meant for the card underneath it. */
.card-saved {
  position: absolute; top: 10px; right: 10px; z-index: 1;
  width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  color: var(--orange); background: var(--card-solid);
  box-shadow: var(--shadow); pointer-events: none;
}
.card-saved svg { width: 15px; height: 15px; fill: currentColor; }
.card-link.is-saved { position: relative; }

/* ---- empty state ---- */
.empty { text-align: center; padding: 16vh 24px 0; }
.empty-face { font-size: 64px; animation: float-bob 3.6s ease-in-out infinite; }
@keyframes float-bob { 0%, 100% { transform: translateY(0) rotate(-2deg); } 50% { transform: translateY(-4px) rotate(3deg); } }
.empty h2 { margin: 12px 0 6px; }
.empty p { color: var(--ink-soft); }
.empty code {
  background: var(--card); padding: 2px 8px; border-radius: 8px;
  font-family: ui-monospace, monospace; font-size: 13px;
}

/* ---- inputs ---- */
.hint { margin: 0 0 12px; font-size: 13.5px; color: var(--ink-soft); }
.text-input {
  width: 100%; border: none; border-radius: 14px;
  padding: 13px 15px; font: inherit; font-size: 15px;
  background: var(--card-solid); color: var(--ink);
  box-shadow: inset 0 0 0 1.5px var(--line);
}
.text-input:focus { outline: none; box-shadow: inset 0 0 0 2px var(--blue); }
.save {
  border: none; cursor: pointer;
  font: inherit; font-size: 16px; font-weight: 700; color: #fff8ec;
  padding: 15px; border-radius: 999px;
  /* Orange→pink overprint — the two loudest inks, saved for actions. */
  background: linear-gradient(100deg, var(--orange), var(--pink));
  box-shadow: var(--press);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.18s ease;
}
.save:active { transform: scale(0.96); }

/* ---- signed out: /login and /join ----
   One card in the middle of the paper. These are the only two pages with no
   header, so the safe-area insets it used to spend are spent here instead. */
body.auth-page {
  display: flex; justify-content: center;
  /* `safe` is the whole reason this is a flex centre rather than a margin: with
     the keyboard up the viewport can end up shorter than the card, and an
     ordinary centre would hang the top of it — the logo, the heading, and on a
     bad sign-in the error — off a screen that has nothing to scroll. */
  align-items: safe center;
  padding: calc(env(safe-area-inset-top) + 20px) max(16px, env(safe-area-inset-right))
           calc(env(safe-area-inset-bottom) + 20px) max(16px, env(safe-area-inset-left));
}
/* main's own side padding and 640px column are for a feed; here the body holds
   the insets and the card is a phone-width sheet whatever the screen. */
body.auth-page main { padding: 0; width: 100%; max-width: 400px; }
.auth {
  background: var(--card); border-radius: var(--radius);
  border: 1px solid var(--line);
  box-shadow: var(--shadow); padding: 26px 22px 24px;
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  text-align: center;
}
.auth-logo { justify-content: center; margin-bottom: 16px; }
.auth h1 { margin: 0 0 6px; font-size: 21px; line-height: 1.25; letter-spacing: -0.01em; }
.hint.auth-tagline { margin: 0 0 20px; }
/* Signing out worked; there is nothing to fix and nothing to press. It says so
   at the volume of a caption rather than of an alert. */
.auth-bye { margin: 0 0 16px; font-size: 13.5px; color: var(--ink-soft); }
.auth-error {
  margin: 0 0 16px; padding: 11px 14px; border-radius: 14px;
  font-size: 13.5px; font-weight: 700; text-align: left;
  background: color-mix(in srgb, var(--orange) 14%, transparent);
  animation: card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
/* The card centres its mark and its heading; a form is read down the left. */
.auth-form { text-align: left; }
.field { margin: 0 0 14px; }
.field label { display: block; margin: 0 0 6px; font-size: 13px; font-weight: 700; }
.field-optional { font-weight: 600; color: var(--ink-soft); }
.hint.field-note { margin: 7px 0 0; font-size: 12.5px; }
/* An invite made out to one address: the field still submits and still reads,
   it just isn't yours to change. */
.text-input[readonly] {
  color: var(--ink-soft);
  background: color-mix(in srgb, var(--card-solid) 55%, transparent);
}
.auth-submit { display: block; width: 100%; margin-top: 18px; }
.auth-dead .empty-face { font-size: 52px; }
.auth-alt {
  display: inline-block; margin-top: 6px;
  font-size: 14px; font-weight: 700; color: var(--ink);
  text-decoration: underline; text-decoration-color: var(--pink);
  text-decoration-thickness: 2px; text-underline-offset: 3px;
}

/* ---- reader ---- */
.reader {
  background: var(--card); border-radius: var(--radius);
  border: 1px solid var(--line);
  box-shadow: var(--shadow); padding: 20px 22px 24px;
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
}
.reader h1 { margin: 10px 0 10px; font-size: 24px; font-weight: 700; line-height: 1.25; letter-spacing: -0.01em; }
.reader .chips { margin-bottom: 14px; }
.reader-media { border-radius: 16px; overflow: hidden; margin: 4px 0 14px; }
.reader-media img { width: 100%; display: block; }
/* The staggered fade that used to be here held the eighth paragraph back by
   400ms and started every one of them at opacity 0. Words are the thing you
   tapped for — they don't get to make an entrance. */
.reader-p {
  font-size: 16px; line-height: 1.65; margin: 0 0 14px;
}
.reader-p a {
  color: inherit; font-weight: 700;
  text-decoration: underline; text-decoration-color: var(--pink);
  text-decoration-thickness: 2px; text-underline-offset: 3px;
}
.reader-figure {
  margin: 6px 0 16px; border-radius: 16px; overflow: hidden;
}
.reader-figure img { width: 100%; display: block; }
.reader-figure figcaption {
  font-size: 12.5px; color: var(--ink-soft); padding: 8px 12px;
  background: var(--card-solid);
}
.reader-fallback { text-align: center; padding: 18px 8px; color: var(--ink-soft); }
.reader-fallback .empty-face { font-size: 44px; }
.reader-summary { font-style: italic; }
.original {
  display: block; text-align: center; margin-top: 18px;
  padding: 14px; border-radius: 999px;
  text-decoration: none; font-weight: 700; color: #fff8ec;
  background: linear-gradient(100deg, var(--orange), var(--pink));
  box-shadow: var(--press);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.18s ease;
}

/* ---- sparkles (tap delight, added by app.js) ---- */
.sparkle {
  position: fixed; pointer-events: none; z-index: 10;
  font-size: 18px; animation: sparkle-fly 0.7s ease-out forwards;
}
@keyframes sparkle-fly {
  from { opacity: 1; transform: translate(0, 0) scale(0.6) rotate(0deg); }
  to { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(1.25) rotate(40deg); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0ms !important; transition-duration: 0.01ms !important; }
  .card-enter { opacity: 1; }
}

/* Cross-document view transitions: a quick fade between feed and reader makes
   cache-instant navigations feel seamless instead of flashing. Progressive
   enhancement — browsers without support just navigate normally. */
@view-transition {
  navigation: auto;
}
/* The default cross-fade is 250ms in each direction, and it is dead time: the
   page you asked for is already decoded and sitting behind a half-opaque copy
   of the one you left. That's most of what "tapping a card feels slow" was,
   because the pages themselves come from the cache in single figures of
   milliseconds. Short enough to read as one movement rather than a wait. */
::view-transition-old(root), ::view-transition-new(root) {
  animation-duration: 160ms;
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

/* ---- training labels (/label) ----
   A dense audit list, not a feed: the job here is scanning many rows fast and
   flipping the wrong ones, so rows are compact and the two verdict buttons sit
   at a constant position down the page. */
.labels {
  background: var(--card); border-radius: var(--radius);
  border: 1px solid var(--line); box-shadow: var(--shadow);
  padding: 20px 22px 24px;
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
}
.labels h1 { margin: 6px 0 6px; font-size: 24px; letter-spacing: -0.01em; }
.labels code {
  font-size: 12px; padding: 1px 5px; border-radius: 6px;
  background: color-mix(in srgb, var(--ink) 7%, transparent);
}
.label-list { display: flex; flex-direction: column; gap: 10px; margin: 18px 0 0; list-style: none; padding: 0; }
.label-row {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px; border-radius: 14px;
  background: var(--card-solid); border: 1px solid var(--line);
  animation: card-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards;
  animation-delay: calc(var(--i) * 12ms);
  transition: opacity 0.2s ease, border-color 0.2s ease;
}
/* Already adjudicated rows recede rather than disappear — they stay where they
   were so the list doesn't reflow under your thumb mid-scroll. */
.label-done { opacity: 0.55; }
.label-failed { border-color: var(--orange); }
.label-media { flex: 0 0 72px; }
.label-media img {
  width: 72px; height: 72px; object-fit: cover;
  border-radius: 10px; display: block;
}
.label-body { flex: 1 1 auto; min-width: 0; }
.label-body h2 { margin: 3px 0 4px; font-size: 15px; line-height: 1.3; }
.label-body h2 a { color: var(--ink); text-decoration: none; }
.label-note { margin: 0 0 6px; font-size: 12.5px; color: var(--ink-soft); font-style: italic; }
.label-conf { font-size: 11px; color: var(--ink-soft); }
.label-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.label-btn {
  border: 1px solid var(--line); cursor: pointer; font: inherit;
  font-size: 13px; font-weight: 700; color: var(--ink-soft);
  padding: 5px 14px; border-radius: 999px; background: transparent;
  transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.15s ease, color 0.15s ease;
}
.label-btn:active { transform: scale(0.94); }
.label-keep.is-on { background: var(--green); border-color: var(--green); color: #fff; }
.label-drop.is-on { background: var(--pink); border-color: var(--pink); color: #fff; }
.label-by { font-size: 11px; color: var(--ink-soft); margin-left: 2px; }
@media (max-width: 520px) {
  .label-media { flex-basis: 56px; }
  .label-media img { width: 56px; height: 56px; }
}
