/* IntentraOps — hand-rolled CSS, no framework.
 *
 * DESIGN INTENT: a register of rulings, not a magazine.
 *
 * What this site actually publishes is a determination. Someone posted a narrow,
 * practical question to a forum — "M5 MacBook Pro, 16GB or 24GB for Lightroom?"
 * — and this page answers it, names a pick, and cites what it read. The reader
 * is mid-purchase and wants the ruling first and the argument second.
 *
 * The previous design dressed that as a literary review page: two serifs, a
 * centred 42rem column from masthead to footer, a stat row of big accent
 * numerals, and every piece of secondary text in letterspaced uppercase. Each
 * decision was defensible alone; together they were the house style of every
 * generated "thoughtful publication", which is the one thing this site cannot
 * afford to look like — it is already answering the question "is this written by
 * a machine?" in every reader's head, and the visual language was answering yes.
 *
 * So the aesthetic is rebuilt around what the content actually is:
 *
 *   REGISTER, NOT COLUMN. The masthead spans the page; the prose does not. An
 *   article is a docket — a sticky rail of structured metadata beside a narrow
 *   body — and the index is a ledger of ruled rows. Nothing is centred in a
 *   tube. Asymmetry is the point: it is what a laid-out page has and a stack of
 *   generated blocks does not.
 *
 *   THREE VOICES, NOT ONE. A wide grotesque rules, a serif argues, a mono
 *   records. The old design had one voice (serif) doing all three jobs, which is
 *   why every label had to be letterspaced uppercase to look like a label — and
 *   why, once everything was letterspaced uppercase, none of it read as emphasis
 *   any more.
 *
 *   THE VERDICT IS MARKED. Every excerpt in the database already opens with the
 *   ruling: "Choose 24GB for...", "Yes, the R1855DB works well with...". So the
 *   opening clause is literally highlighted, the way you would take a marker to
 *   the one line that answers you in a long thread. That is the signature, it is
 *   derived from the content rather than applied to it, and it is the ONLY
 *   saturated non-photographic thing on the page — it appears once per page and
 *   nowhere else.
 *
 * Deliberate, carried over unchanged: CTAs are ink, never Amazon orange, and the
 * highlighter is never used on a buy control. The Associates agreement prohibits
 * implying affiliation or imitating Amazon branding, and a yellow-on-a-buy-button
 * is exactly the imitation it means.
 */

/* ---------------------------------------------------------------- fonts */

/* Filenames carry a content hash on purpose.
   public/_headers serves /fonts/* as immutable with a one-year max-age, so a
   font replaced under the same name stays cached at the edge and in browsers
   for a year. Any future change to a font file must change its filename too.

   All three are OFL and served from our own origin, because the CSP is
   font-src 'self' — a CDN link would be blocked, not merely discouraged.

   Every variable axis we do not drive at runtime is instanced out, which drops
   its deltas from the file. We drive weight (nav 500 -> headline 700), so wght
   stays variable. We never vary width or optical size, so both are pinned:
   Archivo at wdth 112 and Literata at opsz 14. That is where the size went —
   three faces now cost 85KB where the previous two cost 102KB. */

@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 800;          /* variable: one file covers every weight */
  font-display: swap;            /* show the fallback immediately, swap when ready */
  src: url('/fonts/archivo-latin.cf2a851d.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Literata';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/literata-latin.97671a5a.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Static, one weight. The mono is only ever asked for at label sizes, so a
   variable weight axis would be paying for range nothing uses. */
@font-face {
  font-family: 'Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/fonts/plexmono-latin.f7ee6495.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Metric-matched fallbacks — the CLS fix.
 *
 * font-display: swap paints a fallback immediately and swaps when the webfont
 * lands, which is right for LCP but reflows every line of the article when it
 * happens: the fallback's glyphs are a different width and height, so
 * paragraphs re-wrap and everything below the swap point jumps.
 *
 * Each face below is a system face with its metrics rescaled to match the
 * webfont, so the pre-swap and post-swap layouts occupy identical space and the
 * swap costs no layout shift. They sit between the webfont and the plain system
 * families in each stack, so they apply only during the swap window.
 *
 * ONE FACE PER PLATFORM FAMILY, not one for Georgia.
 *
 * An earlier version matched Georgia only. Lighthouse then attributed a layout
 * shift to "Web font loaded" on 6 of 6 runs, because the test host had no
 * Georgia — `src: local()` simply does not match, the face never loads, and the
 * stack falls through to an unmatched generic. Georgia ships on Windows, macOS
 * and iOS; it ships on neither Android nor most Linux, which is the majority of
 * mobile traffic.
 *
 * NOTE FOR ANYONE EDITING THE STACKS: the display face is now a SANS. Its
 * fallbacks are therefore Arial/Noto Sans/Liberation Sans/DejaVu Sans, NOT the
 * Georgia-derived set the old Fraunces stack used. Standing a serif in for a
 * grotesque mismatches every glyph width and reintroduces exactly the shift
 * these overrides exist to remove.
 *
 * Metrics differ per fallback, so one override set cannot serve all of them —
 * hence four families per face, tried in stack order until one resolves. Every
 * number below is computed from that specific font's own head/hhea/OS-2 tables
 * (Arial and Georgia from published metrics, the rest measured directly):
 *   size-adjust  = (target.xAvgCharWidth / target.upm) / (fallback's same ratio)
 *   ascent/descent-override = (target.metric / target.upm) / size-adjust
 *
 * Nothing is downloaded: local() only. Re-derive if either webfont is
 * re-instanced — changing the axes changes xAvgCharWidth.
 */
@font-face {
  font-family: 'Archivo Fallback';             /* Windows, macOS, iOS */
  src: local('Arial'), local('Helvetica Neue'), local('Helvetica');
  size-adjust: 132.76%; ascent-override: 66.14%; descent-override: 15.82%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Archivo Fallback Noto';        /* Android, most modern Linux */
  src: local('Noto Sans');
  size-adjust: 102.81%; ascent-override: 85.40%; descent-override: 20.43%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Archivo Fallback Liberation';  /* Linux, Arial metrics */
  src: local('Liberation Sans'), local('Arimo');
  size-adjust: 101.11%; ascent-override: 86.84%; descent-override: 20.77%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Archivo Fallback DejaVu';      /* older Linux desktops */
  src: local('DejaVu Sans');
  size-adjust: 115.62%; ascent-override: 75.94%; descent-override: 18.16%; line-gap-override: 0%;
}

/*
 * Plex Mono had no metric-matched fallback, and Archivo and Literata each had
 * four.
 *
 * var(--mono) carries 29 rules on this site — every rail key and value, every
 * date, every intent label — so when Plex Mono swapped in, all of it reflowed.
 * A desktop audit measured CLS 0.127 on an article template, failing Core Web
 * Vitals, attributed to font swap pushing .docket__body down 48px. That is this.
 *
 * Metrics measured from the shipped subset with fontTools rather than recalled:
 * Plex Mono is upm 1000, advance 600, ascent 1025, descent -275. The three
 * Linux/Android faces below were measured the same way from the files on disk.
 * Menlo shares DejaVu Sans Mono's 1233/2048 advance, being derived from the same
 * Bitstream Vera outlines, and SF Mono is within 0.3% of it.
 *
 * CONSOLAS IS THE ONE NOT MEASURED HERE — no Windows font on this machine. Its
 * 1126/2048 advance is the published figure, and the arithmetic below follows
 * the same formula as every other entry, so it is checkable by anyone on
 * Windows. It is included rather than omitted because Windows currently has NO
 * override at all, so an approximate match cannot be worse than none.
 *
 *   size-adjust = (600/1000) / (fallback.advance / fallback.upm)
 *   ascent-override  = (1025/1000) / size-adjust
 *   descent-override =  (275/1000) / size-adjust
 */
@font-face {
  font-family: 'Plex Mono Fallback';             /* macOS */
  src: local('Menlo'), local('SF Mono'), local('SFMono-Regular');
  size-adjust: 99.66%; ascent-override: 102.85%; descent-override: 27.59%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Plex Mono Fallback Consolas';    /* Windows — see note above */
  src: local('Consolas');
  size-adjust: 109.13%; ascent-override: 93.92%; descent-override: 25.20%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Plex Mono Fallback Noto';        /* Android, most modern Linux */
  src: local('Noto Sans Mono');
  size-adjust: 100.00%; ascent-override: 102.50%; descent-override: 27.50%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Plex Mono Fallback Liberation';  /* Linux, Courier New metrics */
  src: local('Liberation Mono'), local('Courier New');
  size-adjust: 99.98%; ascent-override: 102.52%; descent-override: 27.50%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Plex Mono Fallback DejaVu';      /* older Linux desktops */
  src: local('DejaVu Sans Mono');
  size-adjust: 99.66%; ascent-override: 102.85%; descent-override: 27.59%; line-gap-override: 0%;
}

@font-face {
  font-family: 'Literata Fallback';            /* Windows, macOS, iOS */
  src: local('Georgia');
  size-adjust: 101.69%; ascent-override: 115.75%; descent-override: 30.29%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Literata Fallback Noto';       /* Android, most modern Linux */
  src: local('Noto Serif');
  size-adjust: 96.62%; ascent-override: 121.82%; descent-override: 31.88%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Literata Fallback Liberation'; /* Linux, Times New Roman metrics */
  src: local('Liberation Serif'), local('Times New Roman'), local('Tinos');
  size-adjust: 104.04%; ascent-override: 113.13%; descent-override: 29.60%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Literata Fallback DejaVu';     /* older Linux desktops */
  src: local('DejaVu Serif');
  size-adjust: 111.37%; ascent-override: 105.68%; descent-override: 27.66%; line-gap-override: 0%;
}

/* ---------------------------------------------------------------- tokens */

:root {
  /* Drafting paper, not office white and not cream.
     The ground has a faint green-grey cast, which is the colour of the paper
     this kind of work is done on — spec sheets, plan sets, lab notebooks. It
     reads as neither of the two grounds every generated site arrives with (warm
     #f4f1ea cream, or a blue-biased near-white), and it sits under product
     photography without tinting it. */
  --bg:          #f0f1ec;
  --bg-subtle:   #e8eae3;
  --bg-inset:    #e0e3da;
  --bg-raised:   #f8f9f6;

  --fg:          #171b19;
  --fg-muted:    #545b56;
  /* Measured 5.87:1 on --bg and 5.13:1 on --bg-inset. This token carries the
     "(paid link)" FTC disclosure and the captions inside the verdict panel, so
     it has to clear 4.5:1 against BOTH grounds — an earlier value passed on the
     page and failed inside the box. Do not darken the panel without re-running
     `node tools/check-contrast.ts`, which is the arbiter here, not the eye. */
  --fg-faint:    #575e59;

  /* Two rule weights, and the distinction is not decorative.
     --border is a hairline between rows: separation only, no contrast floor.
     --border-firm bounds CONTROLS — the search underline, the jump-link button
     — so it is held to 3:1 under WCAG 1.4.11. It was #b2b7ab and measured
     1.80:1, which is why the checker now covers it. Measured 3.04:1. */
  --border:      #d3d6cc;
  --border-firm: #878c80;

  /* Petrol. One structural accent doing all the quiet work: links, rules that
     matter, the rail labels, the generated artwork. Deep enough to read as ink
     rather than as a UI colour, and specifically not the measured blue that
     every "trustworthy" template reaches for. Measured 8.18:1 on the ground. */
  --accent:      #0e5049;
  --accent-hover:#093a35;
  --accent-soft: #dbe6e3;

  /* THE HIGHLIGHTER. The one saturated thing on the page that is not a
     photograph, and the site's signature.
     Used as a BACKGROUND ONLY, behind the opening clause of a ruling, with ink
     text on top — never as a text colour, never on a control, never on a buy
     button. Dark ink on this yellow measures 12.96:1, so the mechanic is
     accessible by construction; yellow text on the page ground would not be,
     which is exactly why it is a marker and not an accent. Identical in both
     themes: a highlighter does not change colour when the lights go out. */
  --signal:      #f2e14c;
  --signal-ink:  #171b19;

  --cta:         #171b19;
  --cta-hover:   #333c37;
  --cta-fg:      #f0f1ec;

  /* Amber stays for warnings. Semantic colour is separate from the accent, and
     a petrol warning does not read as a warning. Retuned to sit on the new
     ground rather than the old blue-white one. */
  --warn-bg:     #f7eed6;
  --warn-border: #ddc890;
  --warn-fg:     #5e4a15;

  /* Drawn, not rounded. Corners exist only where a control needs a hit area. */
  --radius:    2px;
  --radius-sm: 2px;

  /* ~68 characters at the body size — the range long prose stays readable in.
     Measure and type size are one decision, not two: holding 38rem while the
     text grows to ~20.5px drops the line to ~54 characters, which reads as
     choppy rather than as bigger type. */
  --measure:      42rem;
  /* The page shell. Wider than the prose on purpose — the masthead, the ledger
     and the docket rail all use the difference. Prose never gets this wide. */
  --measure-wide: 74rem;
  /* The docket rail: structured metadata beside the body, not above it. */
  --rail:         12rem;

  /* The *Fallback faces are metric-matched; see the @font-face block above.
     They must sit immediately after the webfont so they cover the swap window,
     and before the plain system entry that handles their absence. */
  --display: 'Archivo', 'Archivo Fallback', 'Archivo Fallback Noto',
             'Archivo Fallback Liberation', 'Archivo Fallback DejaVu',
             'Helvetica Neue', Arial, sans-serif;
  --serif:   'Literata', 'Literata Fallback', 'Literata Fallback Noto',
             'Literata Fallback Liberation', 'Literata Fallback DejaVu',
             'Iowan Old Style', Palatino, Georgia, serif;
  --mono:    'Plex Mono', 'Plex Mono Fallback', 'Plex Mono Fallback Consolas',
             'Plex Mono Fallback Noto', 'Plex Mono Fallback Liberation',
             'Plex Mono Fallback DejaVu',
             ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
  --sans:    var(--display);

  /* Generated artwork. Three inks that overprint well on this paper — the
     shapes multiply where they overlap, so these are chosen for what their
     mixtures look like, not just for themselves. */
  --art-bg: #dfe4dd;
  --art-1:  #0e5049;
  --art-2:  #6f9a90;
  --art-3:  #2f7169;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg:          #121513;
    --bg-subtle:   #191d1a;
    --bg-inset:    #1f2421;
    --bg-raised:   #191d1a;

    --fg:          #e5e8e2;
    --fg-muted:    #99a19b;
    /* Measured 6.45:1 on --bg and 5.53:1 on --bg-inset. The light counterpart of
       this token was once raised for WCAG 1.4.3 while this one was missed,
       shipping at 3.96:1 — so both themes are now checked against both grounds
       on any palette change. It carries the "(paid link)" FTC disclosure, which
       must be legible whichever theme is active. */
    --fg-faint:    #939b95;

    /* See the light block: --border-firm bounds controls and is held to 3:1.
       Measured 3.01:1 here. */
    --border:      #282e2a;
    --border-firm: #5c645f;

    /* The light accent is far too dark to read on this ground; lift it to a
       bright petrol that clears 7:1 rather than reusing the same value. */
    --accent:      #63c9ba;
    --accent-hover:#8adbcf;
    --accent-soft: #16332e;

    --cta:         #e5e8e2;
    --cta-hover:   #ffffff;
    --cta-fg:      #121513;

    /* Amber stays for warnings in both themes. */
    --warn-bg:     #2b2512;
    --warn-border: #4f4421;
    --warn-fg:     #e7d199;

    /* Screen-blended on a dark ground, so these are the light counterparts. */
    --art-bg: #1c221f;
    --art-1:  #2f7169;
    --art-2:  #24564f;
    --art-3:  #3d8175;
  }
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--serif);
  /* Fluid rather than flat: 18px on a narrow window, ~20px from roughly 1100px
     up, where there is room for it. Paired with --measure above so the line
     stays near 68 characters. The <=34rem override further down owns phones. */
  font-size: clamp(1.115rem, 1.03rem + 0.36vw, 1.25rem);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* The site's own selection colour is the highlighter. Small thing; it makes
   the marker read as the house mechanic rather than as one component's styling. */
::selection { background: var(--signal); color: var(--signal-ink); }

.wrap      { max-width: var(--measure);      margin-inline: auto; padding-inline: 1.35rem; }
.wrap-wide { max-width: var(--measure-wide); margin-inline: auto; padding-inline: 1.35rem; }

@media (min-width: 60rem) {
  .wrap, .wrap-wide { padding-inline: 2.5rem; }
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.16em;
  text-decoration-color: color-mix(in srgb, var(--accent) 38%, transparent);
}
a:hover { color: var(--accent-hover); text-decoration-color: currentColor; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

/* ------------------------------------------------------------- the data voice
 *
 * var(--mono) carries every machine-readable fact on the site: intents, dates,
 * source communities, counts, ranks, section markers, nav.
 *
 * It replaces the letterspaced small-caps serif that used to carry all of it.
 * The problem with that treatment was not that it was ugly — it was that nav,
 * breadcrumbs, pills, table headers, brands, tiles, sources, pagination and
 * CTAs ALL had it, so it stopped marking anything. A mono says "this is data"
 * on its own without needing to shout, and it is the right voice for a site
 * whose content is full of part numbers, capacities and torque figures.
 *
 * Applied per component rather than through one utility class, because the
 * sizes genuinely differ by context — a rail key is not a nav link is not a
 * table header. */

/* ---------------------------------------------------------------- header */

/* Full-bleed, and the widest thing on the site. The masthead spanning the page
   while the prose stays at 42rem is most of what stops the layout reading as a
   single centred tube. */
.site-header {
  border-bottom: 1px solid var(--fg);
  background: var(--bg);
  position: sticky; top: 0; z-index: 20;
}
/* While a lightbox is open, drop the header out of its sticky stacking context
   so it cannot paint over the dialog's backdrop. See public/js/gallery.js. */
body.lightbox-open .site-header { position: static; }
.site-header__inner {
  display: flex; align-items: center; gap: 1.25rem;
  padding-block: 0.8rem;
}

/* The wordmark sits next to the SVG mark; see components/Logo.astro. */
.brand {
  display: inline-flex; align-items: center; gap: 0.55rem;
  color: var(--fg); text-decoration: none; white-space: nowrap;
}
.brand__mark { width: 1.5rem; height: 1.5rem; flex: none; display: block; }
.brand__text {
  font-family: var(--display);
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
/* The second half of the wordmark is the accent, not a colour-swap gimmick —
   it is the only place the accent appears in the masthead. */
.brand__text em { font-style: normal; color: var(--accent); }

.site-nav { display: flex; gap: 1.35rem; margin-left: auto; flex-wrap: wrap; }
.site-nav a {
  color: var(--fg-muted); text-decoration: none;
  font-family: var(--mono);
  font-size: 0.72rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.02em;
  white-space: nowrap;
  padding-block: 0.2rem;
  border-bottom: 1px solid transparent;
}
.site-nav a:hover { color: var(--fg); border-bottom-color: var(--accent); }

@media (max-width: 34rem) {
  .site-header__inner { flex-wrap: wrap; gap: 0.5rem 1rem; }
  .site-nav { margin-left: 0; width: 100%; gap: 1.1rem; }
}

main { padding-top: 2.75rem; padding-bottom: 1rem; }

/* ---------------------------------------------------------------- footer */

.site-footer {
  margin-top: 5rem; padding-block: 2.25rem 3.5rem;
  border-top: 1px solid var(--fg);
  color: var(--fg-muted); font-size: 0.9rem;
}
.site-footer nav { display: flex; gap: 1.4rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
.site-footer nav a {
  color: var(--fg-muted); text-decoration: none;
  font-family: var(--mono);
  font-size: 0.7rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.02em;
}
.site-footer nav a:hover { color: var(--accent); }
.site-footer__legal { max-width: 46rem; line-height: 1.6; font-size: 0.84rem; }
.site-footer__legal p { margin: 0 0 0.6rem; }

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

h1, h2, h3 {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.018em;
  text-wrap: balance;
}

h1 {
  font-size: clamp(1.85rem, 1.3rem + 2.2vw, 2.8rem);
  line-height: 1.06;
  margin: 0 0 0.8rem;
}
h2 {
  font-size: clamp(1.35rem, 1.14rem + 0.9vw, 1.65rem);
  line-height: 1.18;
  margin: 2.8rem 0 0.7rem;
  font-weight: 600;
}
h3 {
  font-size: clamp(1.15rem, 1.08rem + 0.3vw, 1.3rem);
  line-height: 1.28; margin: 2rem 0 0.45rem;
  font-weight: 600;
  letter-spacing: -0.008em;
}

p { margin: 0 0 1.15rem; }
ul, ol { margin: 0 0 1.15rem; padding-left: 1.35rem; }
li { margin-bottom: 0.45rem; }
li::marker { color: var(--accent); }
strong { font-weight: 600; }

hr { border: 0; border-top: 1px solid var(--border); margin: 2.75rem 0; }

blockquote {
  margin: 1.5rem 0; padding: 0 0 0 1.1rem;
  border-left: 2px solid var(--accent); color: var(--fg-muted);
}

code {
  font-family: var(--mono); font-size: 0.84em;
  background: var(--bg-inset); padding: 0.12em 0.34em; border-radius: var(--radius-sm);
}
pre {
  background: var(--bg-inset); padding: 1rem 1.15rem;
  border-radius: var(--radius-sm); overflow-x: auto; font-size: 0.84rem; line-height: 1.6;
}
pre code { background: none; padding: 0; }

/* Wide content must scroll inside itself, never scroll the page sideways. */
.table-scroll { overflow-x: auto; margin: 1.5rem 0; }
table { width: 100%; border-collapse: collapse; font-size: 0.94rem; }
th, td { text-align: left; padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--border); }
/* Spec tables are data, so the header row is set in the data voice. */
th {
  font-family: var(--mono); font-weight: 500;
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.02em;
  color: var(--fg-muted);
  border-bottom-color: var(--border-firm);
}
td { font-variant-numeric: tabular-nums; }

img { max-width: 100%; height: auto; }

/* The standfirst. Set in the body serif, not the display face — the display
   face is for rulings and headings, and a deck in it competes with the headline
   directly above it. */
/*
 * Hub intros — the only prose a category or topic page owns.
 *
 * Constrained to the reading measure even though hubs render in the wide shell,
 * because the card grid is wide and prose set to a card grid's width is not
 * readable. Sits between the count line and the cards.
 */
.hub-intro {
  max-width: var(--measure);
  margin: 0 0 2rem;
  color: var(--fg);
}

.hub-intro p {
  margin: 0 0 0.9rem;
  line-height: 1.65;
}

.hub-intro p:last-child {
  margin-bottom: 0;
  color: var(--fg-muted);
  font-size: 0.95em;
}

.lede {
  font-family: var(--serif);
  font-size: clamp(1.15rem, 1.06rem + 0.4vw, 1.35rem);
  line-height: 1.5;
  color: var(--fg-muted);
  margin-bottom: 1.75rem;
  text-wrap: pretty;
  max-width: 46ch;
}
/* The article's determination, at the top of the body, directly under the
   question it answers. Display face because it is a ruling, not a standfirst —
   and it is the one marked passage on an article page. */
.ruling {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(1.22rem, 1.08rem + 0.56vw, 1.48rem);
  line-height: 1.32;
  letter-spacing: -0.014em;
  color: var(--fg);
  margin: 0 0 1.9rem;
  text-wrap: pretty;
}

.muted { color: var(--fg-muted); }
.small { font-size: 0.86rem; }

.prose > :first-child { margin-top: 0; }
.prose p { text-wrap: pretty; }

/* Articles run as long prose, so the opening paragraph does the work a deck
   would: set it a step above the body and let it introduce the piece. */
.prose > p:first-of-type {
  /* em, not rem: this has to stay a step above the body text, and the body is
     fluid. At a flat 1.19rem it ended up smaller than the paragraphs it was
     meant to lead. */
  font-size: 1.08em;
  line-height: 1.52;
  color: var(--fg);
}

/* ------------------------------------------------------------- the marker
 *
 * The signature: a pen stroke under the ruling, the way you would mark the one
 * line that answers you in a long thread.
 *
 * WHY A STROKE AND NOT A HIGHLIGHTED CLAUSE. The first version of this marked
 * the opening clause of the excerpt — bounded by the first comma — on the
 * theory that these excerpts always open with the determination. Run against
 * all 40 published excerpts, that rule was wrong in two ways it could not
 * detect:
 *
 *   It cut lists in half. "The iPad mini 7 is good for college reading, notes,
 *   media, and portability" marked up to the first comma and left the rest of
 *   the list outside the mark.
 *
 *   It marked the condition instead of the ruling. Roughly a third of excerpts
 *   are shaped "For Docker, WSL and browser automation, upgrade to NVMe" — the
 *   comma rule highlighted "For Docker", which is the premise, and left the
 *   actual answer unmarked. Exactly backwards.
 *
 * It also only fired on 52% of them, so the signature was inconsistent as well
 * as sometimes wrong. Marking the whole ruling needs no parsing, cannot cut a
 * phrase in half, and is honest: the excerpt IS the determination.
 *
 * The band is positioned in percentages of the line box so it scales with any
 * type size and wraps correctly across lines — it sits through the base of the
 * lowercase letters and just under the baseline, where a real marker lands.
 * Ink stays dark on it in both themes: a highlighter is a physical object and
 * does not invert. Text is legible whether a glyph sits on the stroke or the
 * page (15.3:1 and 13.0:1 respectively). */
.marked {
  background-image: linear-gradient(
    transparent 62%,
    var(--signal) 62%,
    var(--signal) 92%,
    transparent 92%
  );
  color: var(--fg);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* Dark mode cannot use the stroke, and the reason is structural rather than
 * aesthetic: a partial band means every glyph is half on the highlighter and
 * half on the page, so ONE text colour has to be legible against both. On paper
 * that is free — dark ink reads on yellow (13.0:1) and on the ground (15.3:1).
 * On a dark ground it is impossible: light text over yellow is about 1.6:1, and
 * dark text over the page is worse. The first version of this shipped the light
 * rule into dark mode and the ruling was genuinely unreadable.
 *
 * So on a dark ground the marker fills. The whole glyph then sits on the
 * highlighter with the same dark ink the light theme uses, which is both
 * legible and what a real marker does to a dark page anyway. The ink stays
 * fixed across themes in either case — a highlighter is a physical object and
 * does not invert. */
@media (prefers-color-scheme: dark) {
  .marked {
    background-image: none;
    background-color: var(--signal);
    color: var(--signal-ink);
    padding: 0.04em 0.16em;
    border-radius: 1px;
  }
}

/* The HTML element keeps the solid treatment, for any genuine <mark> in body
   copy. Not used by the verdict — see above. */
mark {
  background: var(--signal);
  color: var(--signal-ink);
  padding: 0.04em 0.2em;
  border-radius: 1px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* ---------------------------------------------------------------- docket
 *
 * The article page: structured metadata in a sticky rail, prose in a narrow
 * body, headline spanning both. Below 62rem it linearises and the rail becomes
 * a plain block above the body.
 *
 * The third column is intentionally empty. It is what makes the composition
 * asymmetric — the text block sits left of centre in the shell rather than
 * being centred in it, which is the difference between a page that was laid out
 * and one that was stacked. */
.docket__head { margin-bottom: 2rem; }

@media (min-width: 62rem) {
  .docket {
    display: grid;
    grid-template-columns: var(--rail) minmax(0, var(--measure)) minmax(0, 1fr);
    column-gap: 3rem;
    align-items: start;
  }
  /* Spans the rail and the body, NOT the empty third column. A headline set
     across the full 74rem shell is far too wide to read and undoes the
     asymmetry the third column exists to create. */
  .docket__head { grid-column: 1 / 3; }
  .docket__rail {
    grid-column: 1;
    position: sticky;
    /* Clears the sticky masthead. */
    top: 5rem;
  }
  .docket__body { grid-column: 2; }
}

/* Key/value pairs, stacked. This is the spec-block shape — the same way a
   datasheet prints its parameters — and it is why the metadata no longer has to
   be crammed into one horizontal line of letterspaced small caps. */
.rail-item { margin-bottom: 1.05rem; }
.rail-item__key {
  display: block;
  font-family: var(--mono);
  font-size: 0.64rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--fg-faint);
  margin-bottom: 0.15rem;
}
.rail-item__val {
  display: block;
  font-family: var(--mono);
  font-size: 0.78rem; font-weight: 500;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  line-height: 1.35;
}
.rail-item__val a { color: var(--fg); text-decoration-color: var(--border-firm); }
.rail-item__val a:hover { color: var(--accent); }

/* On a phone the rail is a wrapping row of pairs rather than a tall stack that
   would push the article a screen down. */
@media (max-width: 61.99rem) {
  .docket__rail {
    display: flex; flex-wrap: wrap; gap: 0.9rem 1.75rem;
    padding-bottom: 1.1rem; margin-bottom: 1.6rem;
    border-bottom: 1px solid var(--border);
  }
  .rail-item { margin-bottom: 0; }
}

/* ---------------------------------------------------------------- breadcrumbs */

.breadcrumbs {
  font-family: var(--mono);
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.02em;
  font-weight: 500;
  color: var(--fg-faint);
  margin-bottom: 1.4rem; display: flex; flex-wrap: wrap; gap: 0.4rem;
  align-items: center;
}
.breadcrumbs a { color: var(--fg-muted); text-decoration: none; }
.breadcrumbs a:hover { color: var(--accent); }
.breadcrumbs span[aria-hidden] { color: var(--border-firm); }

/* The .meta / .pill / .byline rules that used to live here are gone with the
   markup that used them. That horizontal strip of letterspaced pills under the
   headline is now the docket rail on articles and the row rail in the ledger,
   both of which state the same facts as labelled key/value pairs. */

/* ---------------------------------------------------------------- hero
 *
 * No stat tiles. A row of big accent numerals over letterspaced labels is the
 * single most recognisable generated-hero shape there is, and it was the first
 * thing on the homepage. The counts are real and worth stating, so they stay —
 * demoted to one quiet mono line, which is what a figure of that importance
 * actually deserves. */
.hero { padding-bottom: 1.5rem; margin-bottom: 0; }
.hero h1 { max-width: 20ch; }
.hero .lede { margin-bottom: 1.4rem; }

.hero__counts {
  display: flex; flex-wrap: wrap; gap: 0.35rem 1.5rem;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.02em;
  color: var(--fg-faint);
  font-variant-numeric: tabular-nums;
  margin: 0;
}
.hero__counts b { color: var(--fg); font-weight: 500; }

/* ------------------------------------------------------------ the specimen
 *
 * The newest entry, shown as a worked example directly under the hero.
 *
 * The old homepage asserted the premise in a headline and then never showed it;
 * the one genuinely distinctive asset on the site — a real question, quoted
 * verbatim, with a direct ruling — was buried below the fold of article pages.
 * Demonstrating beats claiming, so the top of the page is now the mechanism
 * itself: the question as it was posted, and the answer with its verdict
 * marked. */
.specimen {
  border-top: 2px solid var(--fg);
  border-bottom: 1px solid var(--border);
  padding-block: 1.6rem 1.75rem;
  margin-bottom: 0;
}
.specimen__tag {
  display: flex; gap: 0.75rem; align-items: baseline; flex-wrap: wrap;
  font-family: var(--mono); font-size: 0.68rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--fg-faint);
  margin-bottom: 1rem;
}
.specimen__tag b { color: var(--accent); font-weight: 500; }
/* Community names are case-sensitive in practice and lowercase by convention —
   "R/APPLEWHATSHOULDIBUY" is not what anyone typed. Everything else in this
   line is a label and stays uppercase. */
.specimen__where { text-transform: none; letter-spacing: 0.01em; }

.specimen__grid { display: grid; gap: 1.5rem; }
@media (min-width: 52rem) {
  /* Bounded, not 1fr. Stretching the text track across the whole 74rem shell
     parked the photograph against the right edge with a screen of dead space
     between it and the ruling it illustrates. Both columns are sized, the pair
     sits left, and the space left over is the same margin the docket's empty
     third column creates. */
  .specimen__grid {
    grid-template-columns: minmax(0, 34rem) 210px;
    column-gap: 3rem;
    justify-content: start;
    align-items: start;
  }
}

/* The question, verbatim, in the reading face — it is someone speaking, not a
   headline. Quiet quote marks; the panel-and-oversized-glyph treatment it used
   to have made it read as decorative typography rather than as evidence. */
.specimen__q {
  font-family: var(--serif);
  font-size: clamp(1.02rem, 0.96rem + 0.28vw, 1.15rem);
  line-height: 1.45;
  color: var(--fg-muted);
  margin: 0 0 0.9rem;
  padding-left: 0.9rem;
  border-left: 2px solid var(--border-firm);
  max-width: 44ch;
}

/* The ruling. Display face, because this is the determination. */
/* Sized so a typical ruling runs to two or three lines. The marker is the one
   loud thing on the page and its area is a function of this size — at 1.95rem
   a normal-length excerpt filled four lines and read as a highlighter dump
   rather than as a mark. */
.specimen__a {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(1.3rem, 1.12rem + 0.72vw, 1.62rem);
  line-height: 1.3;
  letter-spacing: -0.012em;
  margin: 0 0 1.15rem;
  text-wrap: pretty;
  max-width: 32ch;
}
.specimen__a a { color: var(--fg); text-decoration: none; }
.specimen__a a:hover { color: var(--accent); }

/* The article title, carrying the internal link. Set in the data voice but NOT
   uppercased — it is a headline, and shouting it turns it into a label. */
.specimen__more {
  font-family: var(--mono); font-size: 0.78rem; font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
  padding-bottom: 0.1rem;
}
.specimen__more:hover { border-bottom-color: var(--accent); }

.specimen__photo {
  display: block; width: 100%; aspect-ratio: 1; object-fit: contain;
  background: #fff; border: 1px solid var(--border); padding: 12px;
}
@media (max-width: 51.99rem) {
  .specimen__photo { aspect-ratio: 16 / 10; padding: 16px; }
}

/* ------------------------------------------------------------- the ledger
 *
 * The index. Ruled rows with a metadata rail, not a grid of cards — cards are
 * what make a recommendation site look like a storefront, and a centred stack
 * of them is what makes it look generated.
 *
 * The rail carries the intent classification, which is a real field on every
 * row and tells the reader what KIND of question this is before they read it:
 * a compatibility check reads differently from a buying recommendation. That is
 * structure encoding something true about the content rather than decorating
 * it. */
.ledger__head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  font-family: var(--mono); font-size: 0.68rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--fg-faint);
  margin: 2.75rem 0 0;
  padding-bottom: 0.5rem;
}

.card-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0; }

.card {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.85rem;
  padding-block: 1.5rem;
  border-top: 1px solid var(--border);
}
.card:first-child { border-top: 1px solid var(--fg); }

@media (min-width: 52rem) {
  .card {
    /* The rule spans the full shell — that is what makes it a ledger — but the
       content does not. A 1fr text track pushed the thumbnail to the right edge
       and left a gap wide enough that the picture stopped reading as part of
       the row. */
    grid-template-columns: var(--rail) minmax(0, 38rem) 120px;
    column-gap: 2.25rem;
    justify-content: start;
    align-items: start;
  }
}

.card__rail { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; }
@media (min-width: 52rem) {
  .card__rail { display: block; }
}
/*
 * The verdict badge, at the top of the listing rail.
 *
 * Set heavier and slightly larger than the intent label below it, because it is
 * the line that decides whether the row is worth opening — "Skip" is the claim
 * no other affiliate listing makes.
 *
 * Colour is never the only carrier: the word Skip / It depends / Buy is always
 * present, so this reads identically in monochrome and to a screen reader. The
 * hues match the product cards on the article itself, so a reader who clicks
 * through meets the same vocabulary in the same colours.
 */
.card__verdict {
  display: inline-block;
  font-family: var(--mono); font-size: 0.7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
  margin-bottom: 0.35rem;
  padding: 0.1rem 0.4rem;
  border-radius: 2px;
  border: 1px solid currentColor;
}
.card__verdict--skip { color: #a3282b; }
.card__verdict--depends { color: #8a6116; }
.card__verdict--buy { color: #1f6b3f; }

.card__intent {
  display: block;
  font-family: var(--mono); font-size: 0.66rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--accent);
  margin-bottom: 0.3rem;
}
.card__where {
  display: block;
  font-family: var(--mono); font-size: 0.68rem; font-weight: 500;
  color: var(--fg-faint);
  font-variant-numeric: tabular-nums;
  margin-bottom: 0.15rem;
}

.card__body { min-width: 0; }
.card h2 {
  font-size: clamp(1.18rem, 1.08rem + 0.42vw, 1.38rem);
  margin: 0 0 0.4rem; line-height: 1.2;
  font-weight: 600;
  letter-spacing: -0.012em;
}
.card h2 a { color: var(--fg); text-decoration: none; }
.card h2 a:hover { color: var(--accent); }
.card p {
  margin: 0; color: var(--fg-muted);
  font-size: 0.97rem; line-height: 1.5;
  max-width: 62ch;
}

.card__media { display: block; }
.card__photo {
  display: block; width: 100%; aspect-ratio: 1; object-fit: contain;
  background: #fff; border: 1px solid var(--border); padding: 8px;
}
.card__art { height: 116px; border: 1px solid var(--border); }

@media (max-width: 51.99rem) {
  /* Stacked on a phone: a side-by-side image large enough to see would leave
     the headline in a column too narrow to read. */
  .card__media { order: -1; }
  .card__photo { aspect-ratio: 16 / 10; padding: 14px; }
  .card__art { height: 180px; }
}

/* ---------------------------------------------------------------- disclosure */

.disclosure {
  background: var(--warn-bg);
  border: 0;
  border-left: 2px solid var(--warn-border);
  color: var(--warn-fg);
  border-radius: 0;
  padding: 0.7rem 0 0.7rem 0.95rem;
  font-size: 0.85rem; line-height: 1.55;
  margin: 0 0 2rem;
}
.disclosure a { color: inherit; text-underline-offset: 0.2em; }
.disclosure--inline { margin: 0 0 1.4rem; font-size: 0.82rem; }

/* ---------------------------------------------------------------- products */

.products { margin-top: 3.25rem; padding-top: 1.75rem; border-top: 2px solid var(--fg); }
.products__head { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.4rem; }
.products__head h2 { margin: 0; font-size: 1.35rem; }
.product-list { list-style: none; padding: 0; margin: 1.4rem 0 0; display: grid; gap: 0; }

/* Numbered entries separated by rules — a ranked list in print, not a grid of
   product cards. */
.product {
  display: grid;
  grid-template-columns: 2.2rem 120px 1fr;
  gap: 1.1rem; align-items: start;
  padding: 1.4rem 0;
  border: 0;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background: none;
  box-shadow: none;
}
.product:first-child { border-top: 0; padding-top: 0.5rem; }

/* The rank is data, so it is set in the data voice — and at a size that reads
   as a position marker rather than as a decorative numeral. */
.product__rank {
  font-family: var(--mono);
  font-size: 0.95rem; font-weight: 500;
  color: var(--accent);
  width: auto; height: auto; border: 0; border-radius: 0;
  display: block; text-align: left;
  line-height: 1.5; letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  padding-top: 0.15rem;
}

.product__thumb {
  width: 120px; height: 120px; object-fit: contain;
  background: #fff; border-radius: 0;
  padding: 5px; border: 1px solid var(--border);
}
/* The drawn tile is not a photograph. The white matte and padding above exist to
   letterbox a product shot on a transparent ground; a tile is full-bleed by
   design and inherits its own themed background, so both are removed here rather
   than being fought inside the component. */
.product__thumbwrap { position: relative; display: block; width: 120px; height: 120px; }
.product__thumb--over { position: absolute; inset: 0; }

/* A drawn tile rendered UNDER a photograph, so a hotlink that fails degrades to
   the tile rather than to the browser's broken-image icon. js/img-fallback.js
   removes the <img>; this is what it uncovers. */
.card__art--under {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.card__media { position: relative; }
.card__photo { position: relative; z-index: 1; }

.product__thumb--drawn {
  background: transparent;
  padding: 0;
}

/* Retained: a product can still have no tile at all if it has no title, and the
   layout has to survive that. */
.product--noimage { grid-template-columns: 2.2rem 1fr; }

.product__body { min-width: 0; }
.product__title {
  font-family: var(--display);
  font-weight: 600; margin: 0 0 0.15rem; font-size: 1.05rem; line-height: 1.25;
  letter-spacing: -0.01em;
}
.product__brand {
  color: var(--fg-faint); margin: 0 0 0.5rem;
  font-family: var(--mono);
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.02em; font-weight: 500;
}
/*
 * The verdict line, and the SKIP is the one that matters.
 *
 * Every product on this site was a recommendation until 2026-09-12 — which is
 * what every affiliate page on the internet says, so a machine repeating it has
 * no reason to name where it read it. "Skip, and here is the checkable reason"
 * is the only claim on the page that could not have come from anywhere else.
 * It therefore sits ABOVE the blurb, not below it.
 *
 * Colour is never the only carrier: the word Skip / Consider / Buy is always in
 * the text, so this reads identically to a screen reader and in monochrome.
 */
.product__verdict { margin: 0 0 0.35rem; font-size: 0.84rem; font-weight: 650; letter-spacing: 0.01em; }
.product__verdict--skip { color: #a3282b; }
.product__verdict--consider { color: #8a6116; }
.product__verdict--buy { color: #1f6b3f; }
.product__verdict-why { font-weight: 400; color: var(--fg-muted); }

.product__blurb { margin: 0 0 0.9rem; font-size: 0.96rem; color: var(--fg-muted); line-height: 1.5; }
.product__blurb p { margin: 0; }

.product__cta {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.5rem 1.05rem;
  background: var(--cta); color: var(--cta-fg);
  text-decoration: none; border-radius: var(--radius-sm);
  font-family: var(--mono);
  font-size: 0.72rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.02em;
}
.product__cta:hover { background: var(--cta-hover); color: var(--cta-fg); }
.product__cta svg { width: 0.8em; height: 0.8em; flex: none; }

.product__note { display: block; margin-top: 0.45rem; font-size: 0.75rem; color: var(--fg-faint); }

/* ---------------------------------------------------------------- bottom line */

/* Spliced into the rendered body after the opening paragraph by
   insertAfterFirstParagraph, so it is styled here rather than in a component. */
.jumpline { margin: 0 0 1.6rem; }
.jumpline__link {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-family: var(--mono);
  font-size: 0.72rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.02em;
  color: var(--accent);
  text-decoration: none;
  background: none;
  border: 1px solid var(--border-firm);
  border-radius: var(--radius);
  padding: 0.6rem 0.95rem;
  min-height: 44px;
}
.jumpline__link:hover { border-color: var(--accent); color: var(--accent-hover); }
.jumpline__link svg { width: 0.9em; height: 0.9em; flex: none; }

/* The mid-article jump link.
   Deliberately louder than .jumpline: that one sits in white space under the
   opening paragraph, this one has to interrupt a column of running prose. The
   accent tint is only ~1.1:1 against the page ground, so the left rule — not the
   fill — is what actually separates it from the paragraphs above and below.
   The control stays transparent so its label sits on --accent-soft, the pair
   check-contrast.ts verifies; giving it its own fill would create an unchecked
   pair. It is not styled as the buy button either: --cta means "this costs
   money" and spending that signal on a scroll link would blunt it. */
.jumpmid {
  margin: 2.75rem 0;
  padding: 1.1rem 1.25rem;
  background: var(--accent-soft);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: 0.5rem 1rem;
}
.jumpmid__lead {
  margin: 0;
  font-size: 0.95rem; font-weight: 500;
  color: var(--fg);
}
.jumpmid__link {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-family: var(--mono);
  font-size: 0.72rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.02em;
  color: var(--accent);
  text-decoration: none;
  background: transparent;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.6rem 0.95rem;
  min-height: 44px;
  white-space: nowrap;
}
.jumpmid__link:hover { color: var(--accent-hover); border-color: var(--accent-hover); }
.jumpmid__link svg { width: 0.9em; height: 0.9em; flex: none; }

/* The verdict panel. The one place on an article page where the marker appears. */
.verdict {
  margin: 3.25rem 0 0;
  padding: 1.5rem 1.4rem 1.4rem;
  background: var(--bg-subtle);
  border: 0;
  border-top: 2px solid var(--fg);
  border-radius: 0;
  /* The header is sticky, so an un-offset anchor jump parks the box's own label
     underneath it. */
  scroll-margin-top: 5.5rem;
}
.verdict__label {
  /* Semantically a heading, visually a label — reset the global h2 scale. */
  font-size: 0.68rem !important;
  line-height: 1.2;
  font-family: var(--mono);
  text-transform: uppercase; letter-spacing: 0.04em;
  font-weight: 500; color: var(--fg-faint);
  margin: 0 0 0.75rem;
}
/* No .verdict__call: BottomLine deliberately does not reprint the ruling
   sentence, because it is already the marked passage one screen up and
   printing it twice made "skip to the picks" land the reader on something they
   had just read. See the comment in BottomLine.astro. */
.verdict__choose { font-size: 0.94rem; color: var(--fg-muted); margin: 0 0 1rem; }

.verdict__picks { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.verdict__picks li::marker { content: none; }
@media (min-width: 46rem) {
  .verdict__picks--two { grid-template-columns: 1fr 1fr; }
}
.verdict__pick {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 0.3rem;
  margin: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.95rem 1rem 1.1rem;
}
/* The call, in the bottom-line box.
   Set larger and heavier than the rank above it because it is the sentence the
   whole page funnels to — the jump link literally reads "Skip to the picks".
   The word always carries the meaning; colour only reinforces it. */
.verdict__call {
  margin: 0.15rem 0 0.1rem;
  font-family: var(--mono);
  font-size: 0.8rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.verdict__call--skip { color: #a3282b; }
.verdict__call--consider { color: #8a6116; }
.verdict__call--buy { color: #1f6b3f; }
.verdict__why {
  margin: 0.25rem 0 0.5rem;
  font-size: 0.93rem; line-height: 1.5;
  color: var(--fg);
}

.verdict__rank {
  font-family: var(--mono);
  font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.04em;
  font-weight: 500; color: var(--fg-faint);
  margin: 0;
}
.verdict__title { font-family: var(--display); font-weight: 600; font-size: 1rem; line-height: 1.28; margin: 0; }
.verdict__brand {
  font-family: var(--mono);
  font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.02em;
  font-weight: 500; color: var(--fg-faint); margin: 0;
}
.verdict__blurb { font-size: 0.95rem; color: var(--fg-muted); line-height: 1.5; margin: 0.2rem 0 0.5rem; }
.verdict__blurb p { margin: 0; }
.verdict__all { margin: 1.15rem 0 0; font-size: 0.9rem; }

@media (max-width: 34rem) {
  .verdict { padding: 1.25rem 1.05rem 1.15rem; }
  .verdict__blurb { font-size: 1rem; }
  .verdict__choose { font-size: 1rem; }
}

@media (max-width: 34rem) {
  /* Two columns at every phone width: rank, then everything else. The image
     stacks above the text inside column 2 rather than claiming a track of its
     own — at 390px a third column left roughly ten characters per line. */
  .product,
  .product--noimage {
    grid-template-columns: 1.6rem 1fr;
    column-gap: 0.7rem;
    row-gap: 0.55rem;
  }
  .product__thumb { grid-column: 2; width: 120px; height: 120px; }
  .product__body  { grid-column: 2; }
  .product__cta   { padding-inline: 0.9rem; }
}

/* ---------------------------------------------------------------- sources */

.sources { margin-top: 3.25rem; padding-top: 1.65rem; border-top: 1px solid var(--border); }
.sources h2 {
  font-family: var(--mono); font-weight: 500;
  font-size: 0.7rem; margin: 0 0 0.35rem;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--fg-muted);
  letter-spacing: 0.04em;
}
.sources__note { font-size: 0.84rem; color: var(--fg-faint); margin-bottom: 0.9rem; }
.sources ol { font-size: 0.88rem; color: var(--fg-muted); margin: 0; padding-left: 1.35rem; }
.sources li { margin-bottom: 0.5rem; }
.sources .host { color: var(--fg-faint); font-family: var(--mono); font-size: 0.78em; }

/* ---------------------------------------------------------------- related */

.related { margin-top: 3.25rem; padding-top: 1.65rem; border-top: 1px solid var(--border); }
.related h2 {
  font-family: var(--mono); font-weight: 500;
  font-size: 0.7rem; margin: 0 0 0.9rem;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--fg-muted);
}
.related ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 0; }
.related a {
  display: block; padding: 0.75rem 0;
  border: 0; border-top: 1px solid var(--border); border-radius: 0;
  text-decoration: none; color: var(--fg);
  font-family: var(--display);
  font-size: 0.98rem; line-height: 1.32; font-weight: 500;
  letter-spacing: -0.008em;
  background: none;
}
.related li:first-child a { border-top: 0; }
.related a:hover { color: var(--accent); }

/* ---------------------------------------------------------------- search */

.search-form { display: flex; gap: 0.6rem; margin-bottom: 2.25rem; }
.search-form input[type="search"] {
  flex: 1; min-width: 0;
  padding: 0.7rem 0; font: inherit; font-size: 1.05rem;
  color: var(--fg); background: none;
  border: 0; border-bottom: 1px solid var(--fg); border-radius: 0;
}
.search-form input[type="search"]::placeholder { color: var(--fg-faint); }
.search-form input[type="search"]:focus { outline: 0; border-bottom-color: var(--accent); }
.search-form button {
  padding: 0.55rem 1.25rem; font-family: var(--mono);
  font-size: 0.72rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.02em;
  color: var(--cta-fg); background: var(--cta);
  border: 0; border-radius: var(--radius-sm); cursor: pointer;
}
.search-form button:hover { background: var(--cta-hover); }

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

.pagination {
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  margin-top: 2.75rem; padding-top: 1.6rem;
  border-top: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 0.72rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.02em;
}
.pagination a { text-decoration: none; }
.pagination .spacer { flex: 1; }

/* ---------------------------------------------------------------- tiles */

.tile-grid {
  list-style: none; padding: 0; margin: 1.35rem 0 0;
  display: grid; gap: 0;
  grid-template-columns: repeat(auto-fill, minmax(13.5rem, 1fr));
  border-top: 1px solid var(--border);
}
.tile {
  display: block; padding: 0.95rem 1.1rem 0.95rem 0;
  border: 0; border-bottom: 1px solid var(--border); border-radius: 0;
  text-decoration: none; color: var(--fg); background: none;
  box-shadow: none;
}
.tile:hover { color: var(--accent); }
.tile strong {
  display: block; font-family: var(--display);
  font-size: 0.98rem; font-weight: 600; margin-bottom: 0.15rem;
  letter-spacing: -0.008em;
}
.tile span {
  color: var(--fg-faint);
  font-family: var(--mono);
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.02em; font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.empty {
  padding: 2.25rem 0; text-align: left; color: var(--fg-muted);
  background: none; border: 0; border-top: 1px solid var(--border);
  border-radius: 0;
}

.skip-link {
  position: absolute; left: -9999px;
  background: var(--cta); color: var(--cta-fg);
  padding: 0.6rem 1.1rem; border-radius: 0 0 var(--radius-sm) 0; z-index: 100;
}
.skip-link:focus { left: 0; top: 0; }

/* The breadcrumb's last item repeats the headline directly beneath it. Fine on
   a wide screen, three wasted lines on a phone. */
@media (max-width: 34rem) {
  .breadcrumbs > :nth-last-child(-n+2) { display: none; }
}

/* ------------------------------------------------------- phone refinements
 *
 * Measured on a 390px viewport, not guessed. Two systematic faults the old
 * design had, both preserved as fixed here:
 *
 *   Text down to 10.5px. Secondary labels sat at 0.66-0.72rem, which is fine at
 *   desk distance and genuinely hard to read held in one hand.
 *
 *   Tap targets down to 18px. WCAG 2.5.8 asks for 24px minimum and Apple's HIG
 *   for 44px; category pills, breadcrumbs and nav links were all under both.
 *
 * Everything here raises size or hit area only. No layout changes, so the
 * desktop composition is untouched.
 */
@media (max-width: 34rem) {
  body { font-size: 1.06rem; }

  .site-nav a { font-size: 0.78rem; padding-block: 0.55rem; }

  .pill { font-size: 0.76rem; padding: 0.3rem 0 0.35rem; }
  .meta { gap: 0.45rem 1rem; font-size: 0.74rem; }

  .breadcrumbs { font-size: 0.72rem; gap: 0.4rem 0.55rem; }
  .breadcrumbs a { padding-block: 0.3rem; }

  .site-footer nav { gap: 1rem 1.4rem; }
  .site-footer nav a { font-size: 0.76rem; padding-block: 0.4rem; }
  .site-footer__legal { font-size: 0.88rem; }

  .rail-item__key { font-size: 0.68rem; }
  .rail-item__val { font-size: 0.82rem; }

  .card__intent, .card__where { font-size: 0.74rem; }
  .card p { font-size: 1rem; }

  .hero__counts { font-size: 0.76rem; gap: 0.3rem 1.25rem; }
  .specimen__tag { font-size: 0.74rem; }
  .specimen__more { font-size: 0.78rem; display: inline-block; padding-block: 0.35rem; }

  .product__brand { font-size: 0.74rem; }
  .product__note  { font-size: 0.82rem; line-height: 1.45; }
  .product__blurb { font-size: 1rem; }
  .product__cta   { font-size: 0.8rem; padding: 0.7rem 1.1rem; }
  .product__rank  { font-size: 0.9rem; }

  .sources h2, .related h2 { font-size: 0.8rem; }
  .sources ol { font-size: 0.95rem; }
  .sources li { margin-bottom: 0.7rem; }
  .sources a  { padding-block: 0.15rem; }

  .related a { font-size: 1.02rem; padding-block: 0.9rem; }

  .tile { padding-block: 1rem; }
  .tile strong { font-size: 1.02rem; }
  .tile span   { font-size: 0.74rem; }

  .ledger__head { font-size: 0.74rem; }
  .pagination { font-size: 0.78rem; }
  .pagination a { padding-block: 0.6rem; }

  /* iOS zooms the page when a focused input is under 16px. */
  .search-form input[type="search"] { font-size: 1rem; }
  .search-form button { font-size: 0.8rem; padding-block: 0.75rem; }

  .disclosure { font-size: 0.92rem; }
}

/* The "← All categories" / "← All topics" link at the foot of a hub.
 *
 * It was a bare anchor in a margin-only <p>, which measured 121x18px — under
 * the 24x24 floor WCAG 2.5.8 sets for a standalone (non-inline) target, and the
 * only element on the site that was. Inline links inside a sentence are exempt;
 * this one is not, because it stands alone as navigation.
 *
 * Padding on the anchor rather than height on the paragraph: the hit area has
 * to belong to the thing being clicked, not to the block around it.
 */
.back-link {
  margin-top: 2rem;
}

.back-link a {
  display: inline-block;
  padding: 0.55rem 0.2rem;
  min-height: 24px;
  font-family: var(--mono);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
}
.back-link a:hover { border-bottom-color: var(--accent); }

/*
 * Verdict colours on a dark ground.
 *
 * The light values (#a3282b, #8a6116, #1f6b3f) are chosen for contrast against
 * white and go muddy and near-unreadable on the dark background. Lifted rather
 * than inverted, so skip still reads as a caution and buy still reads as
 * assent — the meaning has to survive the theme, not just the legibility.
 */
@media (prefers-color-scheme: dark) {
  .product__verdict--skip { color: #f08a8d; }
  .product__verdict--consider { color: #e0b457; }
  .product__verdict--buy { color: #6fd398; }
}

/* Verdict badge on a dark ground — same reasoning as .product__verdict above:
   the light hues go muddy, so they are lifted rather than inverted. */
@media (prefers-color-scheme: dark) {
  .card__verdict--skip { color: #f08a8d; }
  .card__verdict--depends { color: #e0b457; }
  .card__verdict--buy { color: #6fd398; }
}

/* Bottom-line call on a dark ground — lifted, not inverted, so skip still reads
   as caution and buy still reads as assent. */
@media (prefers-color-scheme: dark) {
  .verdict__call--skip { color: #f08a8d; }
  .verdict__call--consider { color: #e0b457; }
  .verdict__call--buy { color: #6fd398; }
}
