/* base.css — the layer that turns TechCritic's tokens into a page (Task 5).

   ORDER IN THIS FILE, and nothing may be inserted out of order:
     reset/base -> typography -> layout -> components -> media queries
     -> reduced motion
   The first two stages of the house order (@font-face and :root tokens) live in
   tokens.css, which loads first. This file is the continuation, not a second
   beginning.

   THE SPECIFICITY LAW FOR THIS SITE
   Element selectors appear ONLY in the reset and base typography. Every
   component is a single class selector at (0,1,0). No ID is ever used for
   styling. No !important, anywhere, for any reason. Nesting is capped at TWO
   levels of selector (`.a .b`, never `.a .b .c`) — a third level is a signal
   that the component wants splitting, not that the selector wants lengthening.
   The reason is the same one site.css gives: rules that never gain specificity
   can always be overridden by the next layer in document order, so a page can
   correct this file without a fight.

   NO RAW VALUES. Every colour, size and space in this file is a var(--tc-…).
   The only bare lengths permitted are border widths of 1px-3px, the focus ring,
   and the two clip-rect values in .u-visually-hidden — each justified on its own
   line. render-check.mjs's cross-check hunts for anything else.

   THE TYPE HERE IS MEASURED, NOT CHOSEN. The measurements that set it, taken
   from the shipped binaries and from rendered pixels in Chrome 151:
     - Inter x-height 0.5459em, Newsreader 0.4260em. Inter is 28.1% larger at
       the same nominal size, which is why a Newsreader pull quote has to be set
       LARGER than the Inter body it interrupts, not the same size.
     - `ch` is the advance of "0", and "0" is ~1.32x the mean prose character.
       A measure in `ch` is therefore ~32% wider than its number suggests, and
       differs by family. See tokens.css and DESIGN-SYSTEM.md §3.
   If you change a type token, re-run scripts/render-check.mjs and read the
   characters-per-line and x-height ratio tables. They are there to tell you
   whether your change broke the reading experience, which nothing else will.

   Checked by: scripts/render-check.mjs (painted pixels, reflow, CPL, x-height)
               scripts/font-gate.mjs   (no weight outside a real fvar range)
               scripts/token-contrast.mjs (declared token pairs)
   Written up in: DESIGN-SYSTEM.md */

/* ============================ reset / base ============================ */

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

html {
  /* Stops iOS and Android inflating type when the phone is rotated, which would
     silently invalidate every characters-per-line figure this design is built
     on. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* The masthead is sticky since Task 16. Without this, an anchor jump or a
     Tab-driven scroll puts its target UNDER the masthead — SC 2.4.11 Focus Not
     Obscured. The token clears the measured masthead height with margin. */
  scroll-padding-top: var(--tc-sticky-clear);
}

body {
  margin: 0;
  background: var(--tc-paper);
  color: var(--tc-ink-2);
  font-family: var(--tc-sans);
  font-size: var(--tc-t-body);
  line-height: var(--tc-lh-body);
  /* Kerning on for prose. Deliberately NOT a blanket font-variant-ligatures
     rule: Newsreader declares `liga` and Inter does not, so a rule assuming both
     expose the same feature set would be asserting something untrue of one. */
  font-kerning: normal;
}

/* Margin collapse is handled deliberately rather than being discovered later.
   Every block element starts with no margin; spacing is owned by the flow rule
   below. This means a component can be moved without dragging a mystery gap
   with it. */
h1, h2, h3, p, ul, ol, blockquote, figure, table { margin: 0; }

/* The one flow rule. Anything following anything else gets space above it. */
.flow > * + * { margin-top: var(--tc-s-5); }

a {
  color: var(--tc-accent);
  text-decoration-thickness: 1px;      /* border-class width, permitted */
  text-underline-offset: 0.15em;
}
a:hover { text-decoration-thickness: 2px; }  /* border-class width, permitted */

/* SC 2.4.7 Focus Visible and SC 2.4.11 Focus Not Obscured. Never removed, never
   set to `outline: none`, not on any element, not for any design reason.
   3px / 2px follow the SHAPE of SC 2.4.13 Focus Appearance, which is AAA and
   which we do not claim to meet — see DESIGN-SYSTEM.md §8. */
:focus-visible {
  outline: 3px solid var(--tc-focus);   /* focus ring, permitted */
  outline-offset: 2px;                  /* focus ring, permitted */
}

/* Numbers that must align. Both families carry `tnum`, confirmed in all three
   binaries in Task 4, so this is safe to apply across the pair. */
table, .u-tnum, time { font-variant-numeric: tabular-nums; }

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

/* ============================ typography ============================ */

h1, h2, h3 {
  font-family: var(--tc-serif);
  color: var(--tc-ink-1);
  font-weight: 600;
  text-wrap: balance;
}

h1 { font-size: var(--tc-t-display); line-height: var(--tc-lh-display); letter-spacing: -0.015em; }
h2 { font-size: var(--tc-t-h2);      line-height: var(--tc-lh-h2);      letter-spacing: -0.01em; }
h3 { font-size: var(--tc-t-h3);      line-height: var(--tc-lh-h3); }

/* ============================ layout ============================ */

/* One column at every width. The article sits centred on its own measure; the
   masthead and both footers are full-bleed with the same gutter.
   Task 6 adds the instruments and the ad slot. This file deliberately does NOT
   reserve a rail for them — an empty rail is a layout decision made blind, and
   the right width for one is a thing to measure against a real unit. */
/* The gutter is ADDED to the measure rather than taken out of it. With
   box-sizing:border-box a plain `max-width: var(--tc-measure)` would let the
   padding eat the column, so the real measure would shrink as the gutter grows
   and the token would describe the padded box rather than the text. Measured
   before this calc(): the prose column at 1280px was 663px wide against a token
   asking for 791px. */
.wrap {
  width: 100%;
  max-width: calc(var(--tc-measure) + var(--tc-gutter) * 2);
  margin-inline: auto;
  padding-inline: var(--tc-gutter);
}

/* Full-bleed bands take the gutter but not the measure. */
.band { width: 100%; padding-inline: var(--tc-gutter); }

.main { padding-block: var(--tc-s-7) var(--tc-s-9); }

/* Screen-reader-only, but still focusable and still in the tab order. */
.u-visually-hidden:not(:focus):not(:active) {
  position: absolute;
  width: 1px; height: 1px;        /* clip target; a 0x0 box is dropped from the a11y tree by some AT */
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ============================ components ============================ */

/* --- 1. masthead ------------------------------------------------------- */

/* The skip link is the FIRST focusable element in the document. Visually hidden
   until focused is fine; removed from the tab order is not, and third in the
   order is not. render-check.mjs asserts its position rather than trusting it. */
.skip {
  position: absolute;
  left: var(--tc-s-4);
  top: var(--tc-s-4);
  z-index: 10;
  background: var(--tc-card);
  color: var(--tc-accent);
  padding: var(--tc-s-3) var(--tc-s-4);
  border: 2px solid var(--tc-accent);   /* border width, permitted */
  border-radius: var(--tc-r);
  font-size: var(--tc-t-meta);
  font-weight: 600;
}

/* STICKY SINCE TASK 16, and thin on purpose: a reader mid-article gives up a
   strip of every viewport to it, so it earns that strip by being the way back
   to everything (wordmark home, Archive, the linked topics). z-index 5 sits
   BELOW the skip link's 10 — a focused skip link must paint over the masthead,
   not under it. scroll-padding-top in the reset is this rule's other half. */
.masthead {
  position: sticky;
  top: 0;
  z-index: 5;
  border-bottom: 1px solid var(--tc-rule-strong);  /* border width, permitted */
  background: var(--tc-card);
  padding-block: var(--tc-s-3);
}

/* --tc-page-max, not --tc-measure, since Task 16: the masthead is page
   furniture, and capping it at a prose column was half of why the site read as
   a phone layout on a desktop. Centred, so at 1440 and 1920 the margin is
   symmetric rather than a wasteland on one side. */
.masthead-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tc-s-2) var(--tc-s-5);
  max-width: var(--tc-page-max);
  margin-inline: auto;
}

.wordmark {
  font-family: var(--tc-serif);
  font-size: var(--tc-t-h3);
  font-weight: 700;
  color: var(--tc-ink-1);
  text-decoration: none;
  letter-spacing: -0.02em;
  margin-inline-end: auto;
  /* Measured 37px tall without this — below the 44px tap-target floor the
     Task 16 probe enforces. 8px each side lifts it to ~53px. */
  padding-block: var(--tc-s-2);
}

.masthead-nav { display: flex; flex-wrap: wrap; gap: var(--tc-s-2) var(--tc-s-4); }

/* padding-block makes each link's hit target ~45px tall (14px text x 1.45 line
   + 2 x 12px), which clears the 44px tap-target floor Task 16 verifies. The
   padding is on the LINK, not the nav, so the whole padded box is clickable. */
.masthead-nav a {
  display: inline-block;
  padding-block: var(--tc-s-3);
  font-size: var(--tc-t-meta);
  font-weight: 500;
  color: var(--tc-ink-3);
  text-decoration: none;
}
.masthead-nav a:hover { color: var(--tc-accent); text-decoration: underline; }

/* --- 2. article header ------------------------------------------------- */

.kicker {
  font-size: var(--tc-t-kicker);
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--tc-ink-3);
}

.standfirst {
  font-size: var(--tc-t-lede);
  line-height: var(--tc-lh-lede);
  color: var(--tc-ink-2);
  max-width: var(--tc-measure-lede);
}

/* Not optional furniture. Under the "published by Tireless" ruling this line
   names the pipeline that wrote the piece, the gate that reviewed it, and the
   date of that review. See decisions/2026-08-04-techcritic-design-rulings.md. */
.attribution {
  font-size: var(--tc-t-meta);
  line-height: var(--tc-lh-data);
  color: var(--tc-ink-3);
  padding-block: var(--tc-s-3);
  border-top: 1px solid var(--tc-rule-hair);      /* decorative only, permitted width */
  border-bottom: 1px solid var(--tc-rule-hair);   /* decorative only, permitted width */
}

/* --- 3. table of contents ---------------------------------------------- */

/* Named by its own visible heading via aria-labelledby. There is deliberately
   NO aria-label here: duplicating text that is already on screen is the
   commonest way to create an SC 2.5.3 Label in Name mismatch. */
.toc {
  background: var(--tc-wash);
  border-inline-start: 3px solid var(--tc-rule-strong);  /* border width, permitted */
  padding: var(--tc-s-4) var(--tc-s-5);
}

.toc h2 {
  font-family: var(--tc-sans);
  font-size: var(--tc-t-kicker);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--tc-ink-3);
  line-height: var(--tc-lh-data);
}

.toc ol {
  margin-top: var(--tc-s-3);
  padding-inline-start: var(--tc-s-5);
  font-size: var(--tc-t-data);
  line-height: var(--tc-lh-data);
}
.toc li + li { margin-top: var(--tc-s-2); }

/* --- 4. verdict box ----------------------------------------------------- */

/* Status-colour keyline, icon, the verdict word, one sentence of judgement, and
   best-for / not-for. NO SCORE OUT OF TEN, in any form — DESIGN-SYSTEM.md §6.4:
   a number implies a rigour we have not earned on a pipeline that never touches
   the hardware.

   THE MODIFIERS SET COLOUR AND NOTHING ELSE. Every other property lives on
   .verdict itself. That is what keeps this component at one class of
   specificity, and it makes a fourth status a one-line change rather than a
   fourth copy of the whole box.

   The colour travels through a local custom property rather than through
   `color` inheritance, deliberately. Inheritance would tint the judgement
   sentence and the dl as well, and a status colour is for the verdict WORD —
   prose is ink. This way the modifier stays one declaration and nothing leaks.

   THE ICON HAS NO ACCESSIBLE NAME, and that is a ruling rather than an
   oversight. §4 rule 1 is icon AND word, always, so the word beside it already
   carries the status; naming the icon too would announce "Recommended
   Recommended", which is exactly the SC 2.5.3 Label in Name smell §6.3 warns
   about. It is redundant decoration BY DESIGN, and the redundancy is the point.
   Hence aria-hidden="true" focusable="false" in the markup — the second half
   because older engines put SVG in the tab order without it.
   See decisions/2026-08-05-techcritic-instrument-amendments.md, ruling 1. */
.verdict {
  --verdict-ink: var(--tc-rule-strong);   /* overridden by every modifier below */
  background: var(--tc-wash);
  border-inline-start: 3px solid var(--verdict-ink);   /* border width, permitted */
  padding: var(--tc-s-5);
}

.verdict--good  { --verdict-ink: var(--tc-good); }
.verdict--mixed { --verdict-ink: var(--tc-warn); }
.verdict--poor  { --verdict-ink: var(--tc-bad); }

.verdict-h {
  display: flex;
  align-items: center;
  gap: var(--tc-s-3);
  font-size: var(--tc-t-h3);
  line-height: var(--tc-lh-h3);
  color: var(--verdict-ink);
}

/* Sized in em so it tracks the verdict word it sits beside at every viewport,
   and drawn in currentColor so it takes the status colour with no rule of its
   own. flex:none stops it being squeezed by a long verdict word. */
.verdict-icon { flex: none; width: 1em; height: 1em; }

.verdict-line { margin-top: var(--tc-s-4); color: var(--tc-ink-2); }

/* The UA's default dd indent is the one thing everyone forgets on a dl. */
.verdict-fit { margin: 0; margin-top: var(--tc-s-4); }
.verdict-fit dt { font-size: var(--tc-t-meta); font-weight: 600; color: var(--tc-ink-3); }
.verdict-fit dd { margin-inline-start: 0; margin-block-end: var(--tc-s-3); color: var(--tc-ink-2); }

/* --- 5 and 6. the scroll region, the spec table, the comparison table --- */

/* ONE REGION, TWO TABLES, which is why this class is named for its layout job
   rather than for either of them.

   The comparison table does NOT stack to one column, and DESIGN-SYSTEM.md §6.6
   was amended in Task 6 to say so. Stacking a table means display:block on
   table, tr and td, which destroys the accessibility tree — the rows and cells
   stop being rows and cells, and a screen-reader user loses exactly the
   row/column relationships that were the only reason the data was a table. It
   is also not required: §8 already records that SC 1.4.10 Reflow explicitly
   exempts content needing a two-dimensional layout, and a data table qualifies.
   We had written the exemption down and then written a rule that gave it up.
   See decisions/2026-08-05-techcritic-instrument-amendments.md, ruling 2.

   The focus ring is NOT re-declared here. The global :focus-visible rule in the
   reset already gives this region 3px/2px, and a duplicate would be a second
   place to keep in step. It is asserted rather than assumed: the Task 6 report
   carries the measured outline value for this element. A tabindex="0" element
   that takes focus and shows nothing is an SC 2.4.7 failure, so adding the
   tabindex without a visible ring would be a net loss. */
.table-scroll { overflow-x: auto; width: 100%; max-width: 100%; }

/* border-collapse is SEPARATE, and this is load-bearing rather than stylistic.
   With `collapse` the border belongs to the TABLE, not to the cell, so a sticky
   cell's borders do not paint — the collapsed border scrolls away underneath
   the cell that is holding still. Real browser behaviour, confirmed against the
   320px screenshot in the Task 6 report, not theory. */
.spec, .compare {
  border-collapse: separate;
  border-spacing: 0;
  border-block-start: 1px solid var(--tc-rule-strong);   /* border width, permitted */
  border-inline-start: 1px solid var(--tc-rule-strong);  /* border width, permitted */
  font-size: var(--tc-t-data);
  line-height: var(--tc-lh-data);
}

/* The grid is --tc-rule-strong at 3.41:1, which clears SC 1.4.11's 3:1 floor
   for a border that carries meaning. --tc-rule-hair is 1.37:1 and is
   DECORATIVE ONLY — it must never carry a table grid.
   min-width is what makes the region actually scroll at 320px rather than
   silently squeezing four columns into 280px and making the sticky column
   pointless. Figures are already tabular from the `table` rule above.
   min-width was var(--tc-s-9) until Task 7. Same 6rem, but --tc-s-9 is a SPACE
   token and this is a COLUMN WIDTH: retuning the space scale would have moved
   these columns silently. tokens.css records the full reasoning. */
.spec th, .spec td, .compare th, .compare td {
  min-width: var(--tc-table-col-min);
  padding: var(--tc-s-3);
  border-block-end: 1px solid var(--tc-rule-strong);   /* border width, permitted */
  border-inline-end: 1px solid var(--tc-rule-strong);  /* border width, permitted */
  text-align: start;
  vertical-align: top;
}

.spec th[scope="col"], .compare th[scope="col"] {
  background: var(--tc-wash);
  color: var(--tc-ink-1);
  font-weight: 600;
}

/* At 320px a four-column table scrolls, and scrolling the row keys off the
   screen leaves a column of numbers that mean nothing. The background must be
   OPAQUE or the scrolled cells show through it. */
.spec th[scope="row"], .compare th[scope="row"] {
  position: sticky;
  inset-inline-start: 0;
  z-index: 1;
  background: var(--tc-paper);
  color: var(--tc-ink-1);
  font-weight: 600;
}

.spec td, .compare td { color: var(--tc-ink-2); }

/* The caption is the table's accessible name via aria-labelledby. It is not a
   footnote, so it sits at the top and reads as a title. */
.spec caption, .compare caption {
  caption-side: top;
  text-align: start;
  padding-block-end: var(--tc-s-3);
  font-size: var(--tc-t-meta);
  color: var(--tc-ink-2);
}

/* --- 7. chart figure ---------------------------------------------------- */

/* A NOTE ON THE ORDER OF THIS FILE, because a reader who finds 7 in the right
   place and 8 in the wrong one deserves an explanation here rather than in a
   commit message: the component sections below run 1, 2, 3, 4, "5 and 6", then
   7, then 9, THEN 8, then 10, 11. Sections 8 and 9 are transposed. That
   predates Task 7, it is harmless, and Task 7 was forbidden to reorder them —
   so section 7 was placed in numeric order relative to its neighbours and the
   existing transposition was left exactly as it was found.

   THERE IS NO SVG HERE, AND THAT IS AN AMENDMENT TO THE SPECIFICATION.
   DESIGN-SYSTEM.md §6.7 called for inline SVG. It was measured on Chrome
   151.0.7922.76 and overturned; §6.7 now records the amendment and the three
   figures. Briefly: an SVG stretched to a fluid width scales x and y by
   different factors, so a 4-unit corner radius rendered 3.200px wide at 320px
   and 12.800px at 1280px — the same corner, four times wider; SVG <text> does
   not inherit `color` (computed `color` rgb(44,49,56), computed `fill`
   rgb(0,0,0)), so a CSS-only colour change would silently paint black and any
   checker reading `color` would report a token that is not on screen; and the
   same label spanned 101.404px at 320px against 405.711px at 1280px, glyphs
   stretched 4x, because the x axis is scaled and text goes with it.

   A bar chart is not a picture. It is a table of numbers drawn to scale, so it
   is built from boxes and real text: border-radius is exact in CSS pixels at
   every viewport, the labels sit on the real type scale, they inherit `color`
   normally, and render-check.mjs's existing collectors see them without being
   taught anything new.

   NOTHING HERE ANIMATES. No transition on the bar width, no entry animation,
   not even under prefers-reduced-motion: no-preference. A bar that grows on
   load is a bar whose length is wrong for the first 400ms, and this site's job
   is not being wrong for 400ms. There is deliberately no `transition` and no
   `animation` declaration in this section.

   PRINT is out of scope in Task 7 and is not half-done here: there is no
   @media print rule for the chart, deliberately, rather than an untested one. */

.chart { margin-block: var(--tc-s-6); }

/* The caption names the measure, the unit and the axis maximum. A <figcaption>
   already names its own <figure>, so there is no aria-labelledby and no
   aria-label: adding one would duplicate visible text, which is the SC 2.5.3
   Label in Name smell §6.3 of the design system warns about. */
.chart-caption { font-size: var(--tc-t-meta); color: var(--tc-ink-2); }

/* Rows are separated by --tc-s-3 of plain paper, so two bars never read as one
   shape. A gap between rows is exactly what the space scale is for, so this one
   correctly borrows from it — unlike the column width the same task moved off
   --tc-s-9. tokens.css records that distinction. */
.chart-plot {
  display: grid;
  gap: var(--tc-s-3);
  margin-block-start: var(--tc-s-4);
}

/* THE ROW. Two layouts, and which one ships at which width is a measurement.
   Measured 2026-08-06 at --tc-t-meta in Inter, .wrap content box 280.0px at a
   320px viewport: the longest name "Pellum Tessellate 4" sets at 127.58px and
   the value "94W" at 31.95px (tabular, so all three values are equal).

   Single-line (name | track | value) is therefore POSSIBLE at 320px — it leaves
   104.47px of track — which is not what we expected before measuring. It is
   still not what ships there, because possible is not the same as good: the
   two-line form leaves 240.05px of track, 2.30x as much, and the difference is
   the difference between reading the data and guessing at it. The 61W and 47W
   bars sit 33.6px apart with two lines and 14.6px apart with one.

   So: name on its own line up to 30em, single line from 30em up. */
.chart-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--tc-s-1) var(--tc-s-2);   /* --tc-s-2 is the track-to-value gap measured above */
}

.chart-name { grid-column: 1 / -1; font-size: var(--tc-t-meta); color: var(--tc-ink-2); }

/* THE TRACK is the full axis, 0 to the stated maximum. Its inline-start border
   is the zero baseline — the only rule this chart draws, in --tc-rule-strong at
   3.41:1, because the bars must visibly start from something. No gridlines:
   --tc-rule-hair is decorative-only and may never carry meaning, and a gridline
   strong enough to see is loud enough to compete with the bars. Direct labels
   replace them.
   max-width so a percentage over 100 CLIPS rather than overflowing the column;
   render-check.mjs then fails on the ratio, which is the right order of events. */
.chart-track {
  width: 100%;
  max-width: 100%;
  min-height: var(--tc-chart-bar-h);
  border-inline-start: 1px solid var(--tc-rule-strong);   /* border width, permitted */
}

/* THE BAR. Its length is read from --bar, set once per row in the markup, so the
   percentage is authored exactly once and lives next to the number it encodes.
   Bars start at zero, always — a truncated bar axis exaggerates differences and
   is the single most common dishonest chart. There is no case on this site where
   it is permitted.
   The data end is rounded and the zero end is square: a rounded corner at zero
   would visually lift the bar off its baseline.
   aria-hidden in the markup, not here — the bar is a redrawing of the number
   beside it, and announcing both gives "94W, 94 percent" for one datum. Same
   reasoning as the verdict icon, DESIGN-SYSTEM.md §6.4. */
.chart-bar {
  width: var(--bar);
  max-width: 100%;
  height: var(--tc-chart-bar-h);
  background: var(--tc-cat-1);
  border-start-end-radius: var(--tc-r-lg);
  border-end-end-radius: var(--tc-r-lg);
}

/* OUTSIDE the bar, on paper, in --tc-ink-2 — and this is a contrast ruling, not
   a taste one. Measured: --tc-ink-2 on --tc-cat-1 is 2.75, which FAILS the 4.5
   floor. The only in-bar colour that passes is white at 4.76, a margin of 0.26
   that depends on --tc-cat-1 never changing. Task 6 was spent discovering that a
   0.22 margin we had lived with was an artefact of a broken measurement; we are
   not shipping a new one on purpose. Outside the bar the same label measures
   12.55 and nothing about it is fragile. */
.chart-value {
  font-size: var(--tc-t-meta);
  color: var(--tc-ink-2);
  font-variant-numeric: tabular-nums;
}

/* The fallback table is a real table and follows the spec table's rules. It is
   NOT the courtesy it would have been under the SVG plan: with role="img" the
   whole plot would have been one opaque node and its <text> invisible to
   assistive technology, so the table was the only path to the numbers. With HTML
   bars each row's name and value are real text and read normally, and this table
   is now a second, tabular route to the same figures rather than the only one. */
.chart-table { margin-block-start: var(--tc-s-3); }
.chart-table summary { font-size: var(--tc-t-meta); color: var(--tc-accent); cursor: pointer; }

/* --- 9. advertising slot ------------------------------------------------ */

/* DESIGN-SYSTEM.md §7 is the whole specification. NOTHING IN px ON THE INLINE
   AXIS, EVER — one width:728px in an ad wrapper breaks SC 1.4.10 Reflow on
   every page that carries it. scripts/css-contract.mjs assertion 4 enforces
   that mechanically now, against these very declarations.

   The wrapper is named for its layout job, never for its contents: cosmetic
   filter lists match class names like `ad-container` and `banner`, and a hidden
   wrapper takes our reserved space with it, which collapses the layout and
   causes the exact page-jump the reservation exists to prevent. The reader
   still sees the word "Advertisement" in visible text either way — the neutral
   name is about layout stability, not concealment. css-contract.mjs assertion 5
   enforces the naming. */
.slot { width: 100%; max-width: 100%; }

/* Extra air around a slot, overriding the flow rule. An ad needs to be clearly
   not-the-article, and separation is how a printed page has always said so. */
.slot-inline { margin-block: var(--tc-s-8); }

/* A policy requirement, not a courtesy, and it must be VISIBLE text — not a
   ::before, not an aria-label, not visually-hidden. */
.slot-label { font-size: var(--tc-t-meta); color: var(--tc-ink-3); }

/* min-height, NEVER height: an ad that comes back taller must push the page,
   not be clipped by it. Both reserve tokens are PROVISIONAL until a real unit
   is measured at the AdSense step — DESIGN-SYSTEM.md §11 open item 1. */
.slot-box { min-height: var(--tc-slot-reserve-s); }

/* SPECIMEN ONLY, scoped so it cannot leak to production: the reserved box is
   empty, and a reader looking at the specimen needs to see the space that is
   being held. Hair rule is decorative-only and this is a decorative use. */
.specimen .slot-box { outline: 1px dashed var(--tc-rule-hair); }  /* border width, permitted */

/* --- 8. pull quote / key fact ------------------------------------------ */

/* Rule and space, never a box. No shadow tokens exist and none are to be
   invented — depth on this site is a rule and a wash, as a printed page does it.
   Set in Newsreader and therefore sized ABOVE the body: see the header note on
   x-height. */
.pullquote {
  margin-block: var(--tc-s-7);
  padding-block: var(--tc-s-5);
  border-top: 1px solid var(--tc-rule-strong);     /* border width, permitted */
  border-bottom: 1px solid var(--tc-rule-strong);  /* border width, permitted */
  font-family: var(--tc-serif);
  font-size: var(--tc-t-quote);
  line-height: var(--tc-lh-h3);
  color: var(--tc-ink-1);
  max-width: var(--tc-measure-quote);
}

.pullquote-note {
  display: block;
  margin-top: var(--tc-s-3);
  font-family: var(--tc-sans);
  font-size: var(--tc-t-meta);
  line-height: var(--tc-lh-data);
  color: var(--tc-ink-3);
}

/* --- 10. article footer attribution block ------------------------------ */

.article-attribution {
  margin-top: var(--tc-s-8);
  padding: var(--tc-s-5);
  background: var(--tc-wash);
  border-top: 3px solid var(--tc-rule-strong);   /* border width, permitted */
  font-size: var(--tc-t-data);
  line-height: var(--tc-lh-data);
}

.article-attribution h2 {
  font-family: var(--tc-sans);
  font-size: var(--tc-t-kicker);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--tc-ink-3);
  line-height: var(--tc-lh-data);
}

.article-attribution dl { margin: 0; margin-top: var(--tc-s-4); }
.article-attribution dt {
  font-size: var(--tc-t-meta);
  font-weight: 600;
  color: var(--tc-ink-3);
}
.article-attribution dd { margin: 0; margin-bottom: var(--tc-s-3); color: var(--tc-ink-2); }

/* --- 11. site footer --------------------------------------------------- */

.site-footer {
  border-top: 1px solid var(--tc-rule-strong);   /* border width, permitted */
  background: var(--tc-card);
  padding-block: var(--tc-s-6);
  font-size: var(--tc-t-meta);
  line-height: var(--tc-lh-data);
  color: var(--tc-ink-3);
}

/* WIDENED AND REGROUPED IN TASK 16 — this is now the site's ONE footer. The
   homepage's prose "Where to go next" section is gone; its links live here as
   two labelled groups: the articles (Archive + linked topics) and the standing
   pages. --tc-page-max for the same reason as the masthead. */
.site-footer-inner {
  max-width: var(--tc-page-max);
  margin-inline: auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--tc-s-4) var(--tc-s-7);
  align-items: baseline;
}

.site-footer-nav { display: flex; flex-wrap: wrap; gap: var(--tc-s-2) var(--tc-s-4); }
/* Same tap-target arithmetic as the masthead links: ~44px tall hit boxes. */
.site-footer-nav a { display: inline-block; padding-block: var(--tc-s-3); }
.site-footer p { margin-inline-end: auto; }

/* --- 12. the source apparatus (Task 11) --------------------------------- */

/* VENTURE.md commits that every factual claim is cited to a source a reader can
   open and check. Until Task 11 nothing on the site could express that.

   THE MARKER IS A LINK, NOT A SUPERSCRIPT DECORATION. It is an <a> whose href is
   the source entry's id, so a click goes somewhere; and the source entry carries
   a link back to the marker, so a reader who followed one can return to the
   sentence it came from. A numeral that goes nowhere is the thing this rejects.

   POSITIONED WITH CSS, NOT WITH <sup>, and `line-height: 0` is the reason. A
   real <sup> element raises the glyph by growing the line box, which would pull
   the measured line height of any paragraph containing a citation away from the
   line height of every paragraph that does not — and render-check measures line
   boxes. line-height: 0 on a raised inline keeps the box the size it was.

   No new colour pair: --tc-accent on the page background is already the link
   colour used by .masthead-nav a:hover and .chart-table summary. */
.cite {
  font-size: 0.72em;
  vertical-align: super;
  line-height: 0;
  font-weight: 600;
  color: var(--tc-accent);
  text-decoration: none;
  padding-inline: 0.08em;
}
.cite:hover, .cite:focus-visible { text-decoration: underline; }

.sources {
  margin-top: var(--tc-s-8);
  padding: var(--tc-s-5);
  background: var(--tc-wash);
  border-top: 3px solid var(--tc-rule-strong);   /* border width, permitted */
  font-size: var(--tc-t-data);
  line-height: var(--tc-lh-data);
}

.sources h2 {
  font-family: var(--tc-sans);
  font-size: var(--tc-t-kicker);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--tc-ink-3);
  line-height: var(--tc-lh-data);
}

.sources ol { margin: 0; margin-top: var(--tc-s-4); padding-inline-start: var(--tc-s-6); }
.sources li + li { margin-top: var(--tc-s-4); }
.source-publisher { font-weight: 600; color: var(--tc-ink-1); }
.source-title { color: var(--tc-ink-1); font-style: italic; }
/* A source URL is printed in full rather than hidden behind link text. A reader
   checking a citation is entitled to see where it goes before they go there, and
   long URLs must wrap rather than force the page sideways. */
.source-url { display: block; color: var(--tc-accent); overflow-wrap: anywhere; }
.source-read { display: block; color: var(--tc-ink-3); font-size: var(--tc-t-meta); }
.source-back { color: var(--tc-accent); font-size: var(--tc-t-meta); }

/* --- 13. the method and limits box (Task 11) ---------------------------- */

/* VENTURE.md: reviews state their method and their limits, including what was not
   tested and whether any hardware was handled at all.

   IT SITS NEAR THE TOP, and that placement is the component. A reader deciding
   whether to trust a set of numbers needs to know where the numbers came from
   BEFORE the numbers, not in a footnote after they have already believed them.

   IT IS NOT THE VERDICT BOX AND BORROWS NONE OF ITS COLOURS. The verdict box
   (component 4) carries --tc-good / --tc-warn / --tc-bad because it delivers a
   judgement. This is a disclosure, not a judgement: colouring it amber would tell
   a reader something is wrong, and colouring it green would tell them the method
   was approved. It uses the neutral wash and rule that .article-attribution uses,
   distinguished from it by a leading rule rather than a top rule, and by sitting
   at the head of the article rather than the foot. No new colour pair. */
.method {
  margin-block: var(--tc-s-6);
  padding: var(--tc-s-5);
  background: var(--tc-wash);
  border-inline-start: 3px solid var(--tc-rule-strong);   /* border width, permitted */
  font-size: var(--tc-t-data);
  line-height: var(--tc-lh-data);
  color: var(--tc-ink-2);
}

.method h2 {
  font-family: var(--tc-sans);
  font-size: var(--tc-t-kicker);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--tc-ink-3);
  line-height: var(--tc-lh-data);
}

.method dl { margin: 0; margin-top: var(--tc-s-4); }
.method dt { font-size: var(--tc-t-meta); font-weight: 600; color: var(--tc-ink-1); margin-top: var(--tc-s-4); }
.method dd { margin-inline-start: 0; color: var(--tc-ink-2); }

/* --- 14. the article card (Task 11) ------------------------------------- */

/* Built against ONE REAL ARTICLE, which is the whole reason it did not exist
   before. Open item 11 records what goes wrong when a component is designed
   against invented entries: the invented entry has no long headline, no awkward
   date, no standfirst that runs to four lines, and the component ships fitting
   nothing.

   THERE IS NO GRID AND NO "LATEST ARTICLES" HEADING. One article gets one card.
   A grid whose only job is to hold a single item is a promise about volume the
   site cannot keep, and a heading in the plural over a list of one is the same
   claim made in words. The grid arrives when there are enough articles to shape
   one. */
/* REBUILT IN TASK 16: the card gained a thumbnail and three sizes.
   The thumbnail is the article's own 1200x630 share card — the one picture
   SOURCE-RULES permits without a licence question, generated by
   share-image.mjs and already proven against the page by measurement 18. It
   ships with explicit width/height attributes in the markup so the box is
   reserved before the image arrives (no layout shift), and loading="lazy" on
   every card below the fold.

   ONE CARD, THREE SIZES — hero, secondary, list — and the modifiers set TYPE
   SIZE AND NOTHING ELSE, the same discipline the verdict modifiers set for
   colour. Every other property lives on .card itself. The generator in
   integrate-article.mjs emits this exact markup; change either in step with
   the other or measurement 18's discipline is lost at the card level.

   THE WHOLE CARD IS ONE LINK. The <a class="card-link"> wraps media and body,
   so the tap target is the card and there is no dead zone between headline and
   summary. The focus ring is the global :focus-visible on the link, which
   draws around the full card box because the link is display:block. */
.card {
  background: var(--tc-card);
  border: 1px solid var(--tc-rule-hair);   /* border width, permitted */
  border-radius: var(--tc-r);
  overflow: hidden;
}

.card-meta {
  font-family: var(--tc-sans);
  font-size: var(--tc-t-kicker);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--tc-ink-3);
  line-height: var(--tc-lh-data);
}

/* The headline link carries its own class rather than being reached as
   `.card h3 a`, which is selector depth 3 and which css-contract assertion 3
   correctly rejected when this component was first written. */
.card-link { display: block; height: 100%; color: var(--tc-ink-1); text-decoration: none; }
.card-link:hover .card-title, .card-link:focus-visible .card-title { color: var(--tc-accent); text-decoration: underline; text-decoration-thickness: 1px; }  /* border-class width, permitted */

.card-media {
  display: block;
  width: 100%;
  height: auto;
  border-bottom: 1px solid var(--tc-rule-hair);   /* border width, permitted */
  background: var(--tc-paper);
}

.card-body { display: block; padding: var(--tc-s-4) var(--tc-s-5) var(--tc-s-5); }

.card-title {
  margin-top: var(--tc-s-3);
  font-size: var(--tc-t-h3);
  line-height: var(--tc-lh-h3);
}

.card-summary { margin-top: var(--tc-s-3); color: var(--tc-ink-2); max-width: var(--tc-measure-lede); }

/* The two size modifiers. List cards carry no summary — the generator omits
   it rather than CSS hiding it, because content that exists only to be hidden
   is weight a phone still downloads. */
.card--hero .card-title { font-size: var(--tc-t-h2); line-height: var(--tc-lh-h2); }
.card--list .card-title { font-size: var(--tc-t-lede); line-height: var(--tc-lh-h3); }

/* --- 15. the share card (Task 12) --------------------------------------- */

/* THE PICTURE WE ARE ALLOWED TO USE, AND IT IS OURS.
   SOURCE-RULES bars manufacturer press images and the standing interim ruling
   bars Pexels from the automated path, so a TechCritic article has no photograph
   to share. A shared link with no image is materially less likely to be read, and
   weakening the image rules to fix that would have been the wrong trade.
   So the card is TYPOGRAPHY ON OUR OWN PAPER, in our own licensed fonts, carrying
   our own words. No licence is involved, no attribution is owed, and nobody can
   withdraw it.

   1200 x 630 is the size every major platform crops from, and it is asserted
   against the written PNG's own header rather than against the viewport we asked
   for. See scripts/share-image.mjs.

   THE TYPE SIZES ARE DELIBERATELY OUTSIDE THE PAGE SCALE, and that is the one
   place this component departs from the rest of the system. The page scale is
   built for a reader at a reading distance; a share card is a fixed-pixel image
   scaled arbitrarily by whichever client renders it, so it is set in absolute px
   at a size that survives being shown as a thumbnail. Every COLOUR is a token at
   full strength — no new colour pair — and every text run's painted contrast is
   measured at the worst pixel by the generator, because a share card is a page
   like any other and the accessibility floors are not suspended by turning it
   into an image. */
.share-card {
  box-sizing: border-box;
  width: 1200px;
  height: 630px;
  padding: 72px;
  background: var(--tc-paper);
  border-bottom: 12px solid var(--tc-accent);   /* border width, permitted */
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.share-card-top { display: flex; align-items: baseline; gap: 24px; }

.share-card-wordmark {
  font-family: var(--tc-serif);
  font-size: 40px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--tc-ink-1);
}

.share-card-kicker {
  font-family: var(--tc-sans);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  line-height: 1.1;
  color: var(--tc-ink-3);
}

.share-card-headline {
  margin: 0;
  margin-top: 40px;
  font-family: var(--tc-serif);
  font-size: 62px;
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.015em;
  color: var(--tc-ink-1);
}

/* The figure is optional: a card for a page with no single useful number omits
   it rather than inventing one to fill the space. */
.share-card-figure {
  margin-top: auto;
  font-family: var(--tc-sans);
  font-size: 104px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--tc-accent);
}

.share-card-note {
  margin-top: 12px;
  font-family: var(--tc-sans);
  font-size: 26px;
  line-height: 1.3;
  color: var(--tc-ink-2);
}

.share-card-foot {
  margin-top: 32px;
  padding-top: 20px;
  border-top: 1px solid var(--tc-rule-strong);   /* border width, permitted */
  font-family: var(--tc-sans);
  font-size: 22px;
  line-height: 1.2;
  color: var(--tc-ink-3);
}

/* --- the specimen banner ------------------------------------------------ */

/* Not a shipped component. It exists so a template specimen can never be
   mistaken for a published article, and it is deliberately loud. */
.specimen-caveat {
  background: var(--tc-wash);
  border-bottom: 3px solid var(--tc-warn);   /* border width, permitted */
  color: var(--tc-ink-2);
  padding-block: var(--tc-s-4);
  font-size: var(--tc-t-meta);
  line-height: var(--tc-lh-data);
}
.specimen-caveat strong { color: var(--tc-warn); }

/* --- the trust pages (Task 8) ------------------------------------------- */

/* PAGE CHROME, NOT A §6 COMPONENT, and the distinction is deliberate.
   DESIGN-SYSTEM.md §6 is the component contract for ARTICLE FURNITURE — the
   things an article is built out of, each of which the engine will one day emit.
   `.notfound` above already lives outside that contract for the same reason, and
   so does this: a definition list of data disclosures is page chrome on four
   static documents nobody generates. Adding it to §6 would put a construct in the
   article contract that no article will ever contain, and open item 11 records
   exactly why building a component before an article needs it goes wrong.

   It exists at all because base.css styles no bare `dl` — both existing lists are
   component-scoped (.verdict-fit and .article-attribution) — so a plain <dl> on
   the privacy page would inherit the UA's dd indent and match nothing else on the
   site. This is the minimum: no new colour pair, no new type size. */
.disclosures dt {
  margin-top: var(--tc-s-4);
  color: var(--tc-ink-1);
  font-weight: 600;
}
.disclosures dd { margin-inline-start: 0; color: var(--tc-ink-2); }

/* --- the 404 ------------------------------------------------------------ */

.notfound { padding-block: var(--tc-s-9); }
.notfound p { margin-top: var(--tc-s-5); max-width: var(--tc-measure-lede); }

/* --- 16. the page shell (Task 16) --------------------------------------- */

/* .wrap caps a column of PROSE at the reading measure. This caps a PAGE — the
   homepage grids, the archive and the topic listings — at --tc-page-max, so a
   1920px screen gets a filled, centred page instead of a phone column adrift
   in margin. Same gutter arithmetic as .wrap, for the same reason: the gutter
   is ADDED to the cap rather than eaten out of it. */
.shell {
  width: 100%;
  max-width: calc(var(--tc-page-max) + var(--tc-gutter) * 2);
  margin-inline: auto;
  padding-inline: var(--tc-gutter);
}

/* --- 17. breadcrumbs (Task 16) ------------------------------------------ */

/* Home / Topic / Headline, on every article. The topic is a link only once its
   topic page exists (the 2-article rule — DESIGN-SYSTEM §12); until then it is
   named without being linked, because a link to a page that does not exist is
   worse than no link. The separator is a slash drawn in ink, not a glyph the
   fonts do not carry (§3's six-character rule bars the arrow). Marked up as a
   nav with an ordered list; BreadcrumbList JSON-LD travels in the same
   generator-owned region. */
.crumbs { font-size: var(--tc-t-meta); line-height: var(--tc-lh-data); color: var(--tc-ink-3); }
.crumbs ol { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--tc-s-2); }
.crumbs li { display: flex; gap: var(--tc-s-2); }
.crumbs li + li::before { content: "/"; color: var(--tc-ink-3); }
.crumbs a { color: var(--tc-ink-3); text-decoration: none; }
.crumbs a:hover { color: var(--tc-accent); text-decoration: underline; }

/* --- 18. the homepage grids (Task 16) ----------------------------------- */

/* THE POPULATION RULES, WRITTEN BEFORE THE CSS WAS (DESIGN-SYSTEM §12 holds
   the four rendered states). The grid fills from available content and only
   from it: the hero is always the newest article, secondary treatment begins
   at the second, the earlier-articles band renders only when at least one
   article remains for it. No placeholder cards, no "coming soon" tiles, no
   empty slots. The `:has(> :nth-child(4))` switch below is how three articles
   and thirty both look deliberate WITHOUT JavaScript: with fewer than three
   secondary cards they stack in one column beside the hero; from three they
   open into two columns. */
/* align-items: start, measured rather than assumed: with the default stretch,
   the three-article state left a half-empty hero card whose border enclosed
   nothing — a stretched card reads as missing content, a shorter one reads as
   finished. Cards take their natural height and the paper shows below. */
.home-top { display: grid; gap: var(--tc-s-5); align-items: start; }
.earlier { margin-top: var(--tc-s-8); }
.earlier-grid { display: grid; gap: var(--tc-s-5); margin-top: var(--tc-s-5); }

/* --- 19. the article lead image (Task 16) -------------------------------- */

/* The article's own share card, shown as its lead image — the same licence
   logic as the card thumbnail (component 14). Explicit width/height in the
   markup reserve the box; the hairline is decorative. */
.lead { margin-block-start: var(--tc-s-5); }
.lead img { display: block; width: 100%; height: auto; border: 1px solid var(--tc-rule-hair); border-radius: var(--tc-r); }  /* border width, permitted */

/* --- 20. related articles (Task 16) -------------------------------------- */

/* The route onward at the foot of every article: two or three cards, same
   topic first, newest otherwise — maintained by integrate-article.mjs on every
   run, never hand-kept, so it cannot go stale when the next article lands.
   If nothing genuinely related exists the generator links the archive instead
   of padding with something irrelevant. */
.related { margin-top: var(--tc-s-8); }
.related-grid { display: grid; gap: var(--tc-s-5); margin-top: var(--tc-s-4); }

/* ============================ media queries ============================ */

/* One column at every width, so there is little to switch. What does change is
   that the attribution block loosens into two columns once there is room for
   the term and its value to sit side by side. */
@media (min-width: 40em) {
  .article-attribution dl {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: var(--tc-s-5);
    row-gap: var(--tc-s-3);
  }
  .article-attribution dd { margin-bottom: 0; }
}

/* THE CHART ROW GOES SINGLE-LINE AT 30.5em, AND 30.5em IS THE EXACT WIDTH AT
   WHICH THE CRITERION FIRST HOLDS. Up to here the product name has its own line
   and the bar gets the full column; from here the name, the bar and the value
   share one row.

   The criterion had to be chosen twice, because the obvious one did not
   discriminate. "The smallest bar must stay at least 24px" gives a track floor
   of 51.1px (the 47W bar is 47% of the 100W axis), and single-line clears that
   at EVERY viewport including 320px — so it never binds and is useless as a
   breakpoint. The criterion that does bind: switching must never produce a
   narrower track than the one we already accept at the narrowest viewport.
   Two-line at 320px gives 240.047px of track.

   CORRECTED IN TASK 8. THIS BLOCK SHIPPED AT 30em UNTIL THEN, AND 30em VIOLATED
   ITS OWN STATED JUSTIFICATION. Task 7 computed the crossover as
   425.347 - 144 - 31.953 = 249.4px at a 480px viewport and called it 30em. That
   arithmetic omitted the two 8px grid gaps that sit inside the row, overstating
   the track by 16px. The real figure at 480px is
   425.347 - 144 - 8 - 8 - 31.953 = 233.4px, measured 233.422px — 6.625px
   NARROWER than the 240.047px the criterion demands. The band vw 480..487 was
   switching to a layout with less track than the form it replaced, by up to
   2.76%.
   Swept over every integer viewport 320..1400 on 2026-08-07: the single-line
   track first reaches 240.047px at a 488px viewport, measuring 240.672px against
   432.614px of .wrap content box. 488 / 16 = 30.5em exactly. The breakpoint
   moved rather than being kept with a documented violation — the fix was half an
   em, so there was no reason to accept a known-wrong number.

   Both layouts are still measured on every run: render-check.mjs's viewports are
   320, 768 and 1280, so 320 exercises the two-line form and the other two
   exercise this one. A breakpoint above 768 would have left this rule untested. */
@media (min-width: 30.5em) {
  .chart-row { grid-template-columns: var(--tc-chart-name-col) 1fr auto; }
  .chart-name { grid-column: auto; }
}

/* THE WIDE LAYOUT (Task 16). Until this task the two blocks above were the
   only breakpoints in the file — nothing changed above 640px, which is why a
   laptop rendered the same column a phone did. Two blocks are ADDED and the
   measured 30.5em and 40em blocks above are untouched, per the task's own
   rule: add above them, do not disturb them.

   48em (768px) — tablet portrait: the card grids open to two columns and the
   hero goes full-width above them.

   64em (1024px) — laptop and up: the homepage top becomes hero-left with a
   secondary column (or two, from three secondary cards — the :has() switch),
   and the earlier-articles grid takes three columns. Desktop beyond this is
   NOT another breakpoint: --tc-page-max caps and centres the shell, so 1440
   and 1920 are the same layout with symmetric margin, which is the deliberate
   answer to "a narrow column adrift in empty space". */
@media (min-width: 48em) {
  .home-top { grid-template-columns: repeat(2, 1fr); }
  .card--hero { grid-column: 1 / -1; }
  .earlier-grid { grid-template-columns: repeat(2, 1fr); }
  .related-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 64em) {
  .home-top { grid-template-columns: 1.6fr 1fr; }
  .card--hero { grid-column: 1; grid-row: 1 / span 2; }
  /* Three or more secondary cards: open the secondary side into two columns.
     Below three they stack in one, which is what makes THREE articles look
     composed rather than half-empty. :has() adds no selector depth. */
  .home-top:has(> :nth-child(4)) { grid-template-columns: 1.6fr 1fr 1fr; }
  .earlier-grid { grid-template-columns: repeat(3, 1fr); }
  .related-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ============================ reduced motion ============================ */

/* tokens.css already collapses --tc-t-fast and --tc-t-base to 0ms under this
   query. This block is the belt to that braces: it also stops anything that
   animates without going through a duration token, including smooth scrolling,
   which is a genuine vestibular trigger and is easy to add without thinking. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms;
    animation-iteration-count: 1;
    transition-duration: 0.01ms;
    scroll-behavior: auto;
  }
}
