/* ==========================================================================
   Rankfile — default theme
   --------------------------------------------------------------------------
   Hand-authored, token-driven CSS. No build step and no framework runtime, so
   the whole stylesheet ships in one request.

   Every colour, radius, width and font here is a custom property whose value
   the admin panel writes into an inline <style> block in <head>. Editing the
   theme settings restyles the site without touching this file.

   Structure:
     1. Tokens
     2. Reset and base
     3. Layout primitives
     4. Typography
     5. Buttons, badges, pills
     6. Header and navigation
     7. Rank list  <- the signature element
     8. Comparison cards and their optional blocks
     9. Matrix / table layout
    10. Page sections
    11. Footer, cookie bar, sticky CTA
    12. Responsive
    13. Dark mode, motion, print
   ========================================================================== */

/* 1. Tokens ---------------------------------------------------------------- */

:root {
  /* Overridden per-site from theme settings. */
  --c-primary: #1b4dff;
  --c-secondary: #121826;
  --c-accent: #ffb020;
  --c-positive: #0f9d77;
  --c-negative: #dc3b49;

  --ink: #121826;
  --ink-2: #414b60;
  --ink-3: #656d80;
  --paper: #f6f7f9;
  --surface: #ffffff;
  --line: #e3e6ec;
  --line-strong: #cbd1dc;

  --radius: 14px;
  --radius-sm: 8px;
  --radius-pill: 999px;
  --container: 1180px;
  --scale: 1;

  --shadow-1: 0 1px 2px rgb(18 24 38 / 6%), 0 1px 3px rgb(18 24 38 / 4%);
  --shadow-2: 0 4px 12px rgb(18 24 38 / 8%), 0 2px 4px rgb(18 24 38 / 4%);
  --shadow-3: 0 12px 32px rgb(18 24 38 / 12%);

  --font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  --font-display: var(--font-body);
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* Spacing scale, multiplied by the theme's spacing setting. */
  --s1: calc(0.25rem * var(--scale));
  --s2: calc(0.5rem * var(--scale));
  --s3: calc(0.75rem * var(--scale));
  --s4: calc(1rem * var(--scale));
  --s5: calc(1.5rem * var(--scale));
  --s6: calc(2rem * var(--scale));
  --s7: calc(3rem * var(--scale));
  --s8: calc(4.5rem * var(--scale));

  --header-h: 64px;
  /* Triplets so translucent shades can use rgb(var(--x-rgb) / n%).
     color-mix() does the same but only from 2023, and an unsupported value
     makes the browser drop the whole declaration — on a header background
     that is white text on nothing. */
  --paper-rgb: 246 247 249;
  --surface-rgb: 255 255 255;
  --line-rgb: 227 230 236;
}

/* 2. Reset and base -------------------------------------------------------- */

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

/*
   The hidden attribute has to win.
   
   A browser hides [hidden] with a UA rule of the lowest possible weight, so a
   single class setting `display` anywhere defeats it — which is exactly what
   happened to the mobile call-to-action bar: JavaScript set `hidden`, a media
   query said `display: block`, and the bar sat there from page load instead of
   waiting for the reader to scroll past the card it duplicates.

   Every toggle in this project uses the attribute, so it is worth the
   !important here rather than a lifetime of remembering.
*/
[hidden] { display: none !important; }


html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  /*
   * The footer reaches the bottom of the window on a short page.
   *
   * Without this the page simply ends where its content does, so a page with
   * one paragraph leaves the footer floating with the background showing below
   * it — dark band, then a strip of empty light, which reads as something
   * having failed to load.
   *
   * dvh after vh: the dynamic unit accounts for a mobile browser's collapsing
   * toolbar, and the fixed one covers anything that does not understand it.
   */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;

  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.65;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg, video { max-width: 100%; height: auto; display: block; }
img { border-style: none; }

a { color: var(--c-primary-text); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { text-decoration-thickness: 2px; }

button, input, select, textarea { font: inherit; color: inherit; }

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

/* Visible, consistent keyboard focus everywhere. */
:focus-visible {
  outline: 3px solid rgb(var(--c-primary-rgb) / 55%);
  outline-offset: 2px;
  border-radius: 4px;
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: var(--s4); top: -100px; z-index: 100;
  background: var(--surface); color: var(--ink); padding: var(--s3) var(--s4);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-2);
  transition: top .15s ease;
}
.skip-link:focus { top: var(--s4); }

/* Safety net: a block that somehow renders with no content takes no space. */
.rf-auto:empty { display: none; }

/* 3. Layout primitives ----------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--s4);
}
main { flex: 1 0 auto; }

.wrap--narrow { max-width: 760px; }

/*
   A narrow measure that stays anchored to the container edge.

   wrap--narrow centres itself, which is right for a self-contained block with
   its own centred heading — the FAQ and the author panel both read well that
   way. It is wrong for body copy under a left-aligned page title: the text
   ended up indented by a couple of hundred pixels relative to the heading it
   belongs to, which looks like a mistake rather than a measure.
*/
.wrap--measure { max-width: var(--container); }
.wrap--measure > * { max-width: 760px; }
.wrap--wide { max-width: calc(var(--container) + 200px); }

.stack > * + * { margin-top: var(--s4); }
.stack-lg > * + * { margin-top: var(--s6); }

.row { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center; }
.row--between { justify-content: space-between; }
.row--end { justify-content: flex-end; }

.section { padding-block: var(--s7); }
.section:first-child { padding-top: var(--s6); }
.section--tight { padding-block: var(--s5); }
.section--flush { padding-block: 0; }

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s5);
  box-shadow: var(--shadow-1);
}

/* 4. Typography ------------------------------------------------------------ */

h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.15;
  letter-spacing: -0.022em;
  margin: 0 0 var(--s3);
  font-weight: 700;
  color: var(--ink);
  text-wrap: balance;
  /* text-wrap: balance decides where to break; it cannot break a word that
     has nowhere to break. A long product name or a pasted URL would otherwise
     run straight out of the column. */
  overflow-wrap: anywhere;
}

h1 { font-size: clamp(2rem, 1.35rem + 2.4vw, 3rem); letter-spacing: -0.032em; }
h2 { font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.05rem); }
h3 { font-size: clamp(1.2rem, 1.08rem + 0.5vw, 1.4rem); }
h4 { font-size: 1.05rem; }

p { margin: 0 0 var(--s4); }
p:last-child { margin-bottom: 0; }

.lede { font-size: 1.135rem; color: var(--ink-2); line-height: 1.6; text-wrap: pretty; }
.muted { color: var(--ink-3); }
.small { font-size: 0.875rem; }
.tiny { font-size: 0.8125rem; }

.eyebrow {
  font-size: 0.75rem; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: var(--s2);
}

.prose { overflow-wrap: break-word; }
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose h2, .prose h3 { margin-top: var(--s6); }
.prose ul, .prose ol { padding-left: 1.25em; margin: 0 0 var(--s4); }
.prose li + li { margin-top: var(--s2); }
.prose img { border-radius: var(--radius-sm); margin-block: var(--s5); }
.prose blockquote {
  margin: var(--s5) 0; padding: var(--s2) 0 var(--s2) var(--s4);
  border-left: 3px solid var(--c-accent); color: var(--ink-2); font-style: italic;
}
.prose table { width: 100%; border-collapse: collapse; margin-block: var(--s5); }
.prose th, .prose td { padding: var(--s3); border-bottom: 1px solid var(--line); text-align: left; }

/* Numerals in ranks, scores and prices must never jitter between values. */
.tnum { font-variant-numeric: tabular-nums lining-nums; }

/* 5. Buttons, badges, pills ------------------------------------------------ */

.btn {
  --btn-bg: var(--c-primary);
  --btn-bg-rgb: var(--c-primary-rgb);
  --btn-fg: #fff;
  overflow-wrap: break-word;
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s2);
  padding: 0.72em 1.25em;
  min-height: 44px;               /* comfortable touch target */
  font-weight: 650; font-size: 0.95rem; line-height: 1.2;
  text-decoration: none; text-align: center;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: var(--btn-bg); color: var(--btn-fg);
  cursor: pointer;
  transition: transform .12s ease, box-shadow .12s ease, background-color .12s ease;
}
.btn:hover { text-decoration: none; box-shadow: var(--shadow-2); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }

.btn--block { display: flex; width: 100%; }
.btn--lg { font-size: 1.02rem; padding: 0.85em 1.5em; min-height: 50px; }
.btn--sm { font-size: 0.85rem; padding: 0.5em 0.9em; min-height: 36px; }

.btn--accent { --btn-bg: var(--c-accent); --btn-bg-rgb: var(--c-accent-rgb); --btn-fg: #2a1c00; }
.btn--dark { --btn-bg: var(--c-secondary); --btn-fg: #fff; }
.btn--ghost {
  --btn-bg: transparent; --btn-fg: var(--c-primary);
  border-color: var(--line-strong);
}
.btn--ghost:hover { background: var(--paper); }
/*
 * The call button. Same size and shape as the affiliate button above it so the
 * pair reads as one control with two verbs, and outlined rather than filled so
 * the primary action stays primary — two filled buttons stacked is two first
 * choices, which is no choice at all. It keeps the primary's colour, because a
 * phone number is an offer, not a footnote.
 */
.btn--call {
  --btn-bg: transparent;
  --btn-fg: var(--c-primary-text);
  border-color: var(--c-primary);
  margin-top: var(--s2);
}
.btn--call:hover { background: rgb(var(--c-primary-rgb) / 8%); }
.btn--call svg { width: 18px; height: 18px; flex: none; }

.btn--quiet { --btn-bg: transparent; --btn-fg: var(--ink-2); padding-inline: 0; min-height: 0; }
.btn--quiet:hover { box-shadow: none; transform: none; color: var(--c-primary-text); }

/* Button style variants set from theme settings. */
[data-btn="pill"] .btn { border-radius: var(--radius-pill); }
[data-btn="square"] .btn { border-radius: 2px; }
[data-btn="outline"] .btn {
  background: transparent; color: var(--c-primary); border-color: currentColor;
}
[data-btn="outline"] .btn:hover { background: var(--c-primary); color: #fff; }

.badge {
  display: inline-flex; align-items: center; gap: 0.35em;
  padding: 0.28em 0.7em;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.03em;
  text-transform: uppercase;
  border-radius: var(--radius-pill);
  background: rgb(var(--c-primary-rgb) / 10%);
  color: var(--c-primary);
  white-space: nowrap;
}
/* One under the other at every column width, so an award and a badge read the
   same way on every card. */
.card__labels {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
}

.badge--award { background: rgb(var(--c-accent-rgb) / 22%); color: #7a4d00; }
.badge--positive { background: rgb(var(--c-positive-rgb) / 14%); color: var(--c-positive-text); }
.badge--neutral { background: var(--paper); color: var(--ink-3); }

.pill {
  display: inline-flex; align-items: center;
  padding: 0.3em 0.75em;
  font-size: 0.82rem; border-radius: var(--radius-pill);
  background: var(--paper); color: var(--ink-2); border: 1px solid var(--line);
  /* Short labels sit on one line naturally because the pill sizes to its
     content; a long one wraps instead of escaping the card. */
  max-width: 100%;
  overflow-wrap: break-word;
}

.pill-list { display: flex; flex-wrap: wrap; gap: var(--s2); margin: 0; padding: 0; list-style: none; }

.ribbon {
  position: absolute; top: 0; right: 0;
  padding: 0.3em 0.85em;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  color: #2a1c00; background: var(--c-accent);
  border-radius: 0 var(--radius) 0 var(--radius-sm);
}

/* 6. Header and navigation ------------------------------------------------- */

.site-header {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  position: relative; z-index: 40;
}
.site-header[data-sticky="1"] { position: sticky; top: 0; }

.site-header__inner {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  min-height: var(--header-h);
  padding-block: var(--s2);
}

.brand { display: inline-flex; align-items: center; gap: var(--s3); text-decoration: none; color: var(--ink); }
.brand__logo { max-height: 38px; width: auto; }
.brand__name { font-family: var(--font-display); font-weight: 800; font-size: 1.16rem; letter-spacing: -0.02em; }
.brand__tagline { font-size: 0.8rem; color: var(--ink-3); }

.nav { display: flex; align-items: center; gap: var(--s1); min-width: 0; }
/* Scoped to the direct child: a descendant selector here outranks .nav__sub
   and would force every dropdown open. Shared resets stay separate, and
   deliberately set no `display`. */
/* Wrapping matters: without it a long menu pushes the header wider than the
   viewport and takes the burger button off-screen with it. */
.nav > ul { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s1); }
.nav ul { list-style: none; margin: 0; padding: 0; }
.nav li { position: relative; }

.nav a {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 0.55em 0.75em;
  font-size: 0.94rem; font-weight: 550; color: var(--ink-2);
  text-decoration: none; border-radius: var(--radius-sm);
  /* Labels stay on one line; the list wraps instead. Breaking a menu label
     mid-word looks like a fault rather than a design. */
  white-space: nowrap;
}
.nav a:hover, .nav a[aria-current="page"] { color: var(--ink); background: var(--paper); }

.nav__sub {
  position: absolute; top: 100%; left: 0; min-width: 208px;
  display: none; flex-direction: column; gap: 0;
  padding: var(--s2);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-3);
}
/*
   Hover only where there is a pointer to hover with.
 
   On a touch screen the hover rule fires on tap and then sticks, so a menu
   opened by accident stays open until something else is tapped. The button
   below is the reliable way in, and it works for a keyboard too.
*/
@media (hover: hover) and (pointer: fine) {
  /* No hover opening. A menu that opens by passing over it opens by accident,
     and it opened differently on a touch screen than on a desktop — one
     control, two behaviours. The arrow button is now the only way in, on
     every device, and it says so with aria-expanded. */
  /* The label and its arrow are one control, so they light up together —
     hovering the arrow used to leave the label plain, which read as two
     things sitting next to each other rather than one thing with a menu. */
  .nav > ul > li:hover > a { color: var(--ink); background: var(--paper); }
}

.nav > ul > li:focus-within > a,
.nav > ul > li[data-open] > a { color: var(--ink); background: var(--paper); }

/* Focus alone does not open it either: tabbing onto the label is the
   keyboard's hover, and the same accident follows. The button takes Enter
   and Space like any button, which is the keyboard's click. */
.nav li[data-open] > .nav__sub { display: flex; }

.nav__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  min-height: 32px;
  margin-inline-start: -0.2rem;
  padding: 0;
  font: inherit;
  /* Secondary until its menu is open — an arrow as dark as the label read as
     a second link rather than a control belonging to the first. */
  color: var(--ink-3);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.nav li[data-open] > .nav__toggle,
.nav li:focus-within > .nav__toggle { color: var(--ink); }
.nav__toggle[aria-expanded="true"] { transform: rotate(180deg); }
.nav__toggle:focus-visible { outline: 2px solid var(--c-primary); outline-offset: 2px; }
.nav__sub a { width: 100%; }
/* The items nearest the right edge open leftwards instead of pushing past the
   viewport. Two, not one: a wide panel on the second-to-last item overflowed
   just as readily once the menu was grouped into a few broad headings. */
.nav > ul > li:nth-last-child(-n+2) > .nav__sub { left: auto; right: 0; }
/* Never wider than the viewport, however long the child labels are. */
.nav__external { font-size: 0.75em; opacity: 0.6; }
.nav__sub { max-width: min(320px, calc(100vw - 2rem)); }
.nav__sub a { white-space: normal; }

.nav-toggle {
  display: none; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  background: none; border: 1px solid var(--line); border-radius: var(--radius-sm);
  cursor: pointer;
}

.mode-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: none; cursor: pointer; color: var(--ink-2);
}
.mode-toggle:hover { background: var(--paper); }

/* 7. Rank list — the signature element ------------------------------------- */
/*
   The ranking is the page, so the numbering is the structure rather than an
   ornament: a hairline spine runs down the list with the position set in large
   tabular numerals, and the award winner gets the only filled marker.
*/

.rank-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s4); }

/*
   The numeral sits inside the card rather than in a column beside it.
   A separate rail column cost 76px of layout width, which pushed every card
   that far right of the page heading, the repeated top pick and every section
   below it — one page with two left edges. The number is just as prominent
   here, the cards line up with everything else, and the numerals still form
   their own vertical line because every card shares that same edge.
*/

.rank-item { position: relative; }

.rank-rail { position: absolute; z-index: 2; }

.rank-num {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: -0.03em;
  font-weight: 800;
}

/* -- Corner: a tab cut into the card ----------------------------------------
   The top-left radius matches the card's own, so the tab reads as part of the
   shape rather than something resting on it. The default for that reason. */

:is(.rank-item, .feature)[data-rank="corner"] .rank-rail { top: 0; left: 0; }

/*
   Quiet by default, loud when it has won something.
 
   This was the other way round: near-black with white text as standard, and a
   softer gold for award winners — so the one entry with no award carried the
   heaviest marker on the page. Awards exist to draw the eye, and this was
   drawing it to whatever had none. The badge style already worked this way;
   the corner tab now matches it.
*/
:is(.rank-item, .feature)[data-rank="corner"] .rank-num {
  min-width: 48px; height: 34px;
  padding-inline: 0.6em;
  font-size: 1.05rem;
  color: var(--ink);
  background: var(--surface);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--radius) 0 var(--radius-sm) 0;
}

.rank-item[data-rank="corner"] > .card { padding-top: calc(var(--s5) + 22px); }

/* -- Badge: a disc straddling the corner ------------------------------------- */

:is(.rank-item, .feature)[data-rank="badge"] .rank-rail { top: 0; left: 0; }

:is(.rank-item, .feature)[data-rank="badge"] .rank-num {
  width: 42px; height: 42px;
  font-size: 1.12rem;
  color: var(--ink);
  background: var(--surface);
  border: 2px solid var(--c-secondary);
  border-radius: 50%;
  box-shadow: var(--shadow-1);
  transform: translate(-28%, -28%);
}

.rank-item[data-rank="badge"] > .card { padding-top: calc(var(--s5) + 12px); }

/* -- Inline: part of the brand column ---------------------------------------
   The most integrated of the three: no absolute positioning, no reserved
   padding, and no empty band above the content. */

.card__rank {
  display: flex; align-items: baseline; gap: 0.35em;
  margin: 0 0 var(--s1);
  line-height: 1;
}

.card__rank-num {
  font-family: var(--font-display);
  font-size: 2.1rem; font-weight: 800; letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink);
}

.card__rank-label {
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* -- The winner is marked in whichever style is chosen ----------------------- */

:is(.rank-item, .feature)[data-award]:not([data-award=""])[data-rank="corner"] .rank-num {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: #2a1c00;
}
:is(.rank-item, .feature)[data-award]:not([data-award=""])[data-rank="badge"] .rank-num {
  border-color: var(--c-accent);
  color: var(--ink);
}
/*
 * Awards are not interchangeable. Every named award drew the same amber, so
 * a page with an editors' choice at four and a value pick at one showed two
 * identical winners and left the reader to guess which was which. The house
 * pick now reads in primary and the value pick keeps the amber: two marks,
 * two meanings, and the rank number underneath them still counts.
 */
:is(.rank-item, .feature):is([data-award="editors-choice"], [data-award="best-overall"])[data-rank="corner"] .rank-num {
  background: var(--c-primary);
  border-color: var(--c-primary);
  color: #fff;
}
:is(.rank-item, .feature):is([data-award="editors-choice"], [data-award="best-overall"])[data-rank="badge"] .rank-num {
  border-color: var(--c-primary);
  color: var(--c-primary-text);
}
.card:is([data-award="editors-choice"], [data-award="best-overall"]),
.minimal-row:is([data-award="editors-choice"], [data-award="best-overall"]) {
  border-color: color-mix(in srgb, var(--c-primary) 55%, var(--line));
}
:is([data-award="editors-choice"], [data-award="best-overall"]) .badge--award {
  background: rgb(var(--c-primary-rgb) / 12%);
  color: var(--c-primary-text);
}

/* Inline keeps its ink: a large numeral in accent amber loses contrast on
   paper, and the ribbon already marks the winner. */
.rank-item[data-award]:not([data-award=""]) .card__rank { position: relative; }

/* 8. Comparison cards ------------------------------------------------------ */

.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  padding: var(--s5);
  transition: box-shadow .16s ease, border-color .16s ease;
}
.card:hover { box-shadow: var(--shadow-2); border-color: var(--line-strong); }
.card[data-award]:not([data-award=""]) {
  border-color: color-mix(in srgb, var(--c-accent) 55%, var(--line));
  box-shadow: var(--shadow-2);
}

/*
   Three zones: brand, body, action. `align-items: start` plus `gap` means a
   zone that renders nothing simply collapses — there is no empty column and
   no leftover margin, because spacing lives entirely in the gap.
*/
.card__grid {
  display: grid;
  grid-template-columns: minmax(140px, 200px) minmax(0, 1fr) minmax(170px, 215px);
  gap: var(--s5);
  align-items: start;
}

.card__brand {
  display: flex; flex-direction: column; gap: var(--s3); align-items: flex-start;
  /* A grid child defaults to min-width:auto, which lets a long business name
     widen the whole column past its maximum. */
  min-width: 0;
}

.card__logo {
  width: 100%; max-width: 190px; height: 58px;
  object-fit: contain; object-position: left center;
}

/* A product photo, where a service would show a wordmark. Contained rather
   than cropped: a monitor with its corners cut off looks like a mistake. */
.card__product {
  width: 100%; max-width: 220px;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  border-radius: var(--radius-sm);
}

/* The maker's mark, secondary to the product itself. */
.card__mark {
  width: 96px; height: 26px;
  object-fit: contain; object-position: left center;
  opacity: 0.72;
}



.card__name {
  font-size: 1.22rem; font-weight: 750; letter-spacing: -0.02em; margin: 0;
  overflow-wrap: break-word;
}
.card__tagline { font-size: 0.87rem; color: var(--ink-3); margin: 0; overflow-wrap: break-word; }

.card__body { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
.card__desc { margin: 0; color: var(--ink-2); }

.card__action {
  display: flex; flex-direction: column; gap: var(--s3);
  align-items: stretch;
  min-width: 0;
}
.card__cta-note { text-align: center; font-size: 0.8rem; color: var(--ink-3); }

/* Affiliate button movement ------------------------------------------------- */
/*
   The ring is drawn on a pseudo-element rather than on the button's own
   box-shadow, so whatever shadow the button already carries survives the
   animation instead of being overwritten by it.

   The colour is mixed from --btn-bg, which every button variant sets, so a
   ghost button pulses in its own colour rather than the primary.
*/

.btn[data-anim] { position: relative; }

.btn[data-anim]::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}

.btn[data-anim="pulse"]::after {
  animation: rf-cta-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

@keyframes rf-cta-pulse {
  0% { box-shadow: 0 0 0 0 rgb(var(--btn-bg-rgb) / 58%); }
  70% { box-shadow: 0 0 0 14px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.btn[data-anim="glow"]::after {
  animation: rf-cta-glow 2.4s ease-in-out infinite;
}

@keyframes rf-cta-glow {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  50% { box-shadow: 0 0 20px 3px rgb(var(--btn-bg-rgb) / 48%); }
}

/*
   Movement stops while the button is being used: a ring expanding out from
   under the cursor as you go to click is a distraction at exactly the wrong
   moment, and it must never obscure the focus outline.
*/
.btn[data-anim]:hover::after,
.btn[data-anim]:focus-visible::after { animation: none; }

/* A button that will not stop moving is a genuine problem for vestibular
   disorders, so this is not left to the editor's setting. */
@media (prefers-reduced-motion: reduce) {
  .btn[data-anim]::after { animation: none; }
}

@media print {
  .btn[data-anim]::after { display: none; }
}

/* Where the brand and the button sit in their columns.
   Both grids that give these zones their own track are covered: the ranked
   cards and the magazine's top pick, which is the same card in a larger
   frame. In the stacked, minimal and matrix layouts the zones are laid out
   differently and these rules deliberately do not reach them. */

.card__grid > .card__brand[data-brand="middle"],
.card__grid > .card__action[data-cta="middle"],
.verdict-box > .verdict-box__brand[data-brand="middle"],
.verdict-box > .verdict-box__action[data-cta="middle"] {
  align-self: center;
}

/* Sticky needs somewhere to travel, so the zone stays top-aligned and the
   offset clears the sticky header. On a short card there is no travel and it
   simply behaves like "top". */
.card__grid > .card__brand[data-brand="floating"],
.card__grid > .card__action[data-cta="floating"],
.verdict-box > .verdict-box__brand[data-brand="floating"],
.verdict-box > .verdict-box__action[data-cta="floating"] {
  align-self: start;
  position: sticky;
  top: calc(var(--header-h) + var(--s4));
}

@media (max-width: 900px) {
  /* The grid has collapsed and the zones have rearranged, so both alignments
     stop meaning anything. */
  .card__grid > .card__brand[data-brand],
  .card__grid > .card__action[data-cta],
  .verdict-box > .verdict-box__brand[data-brand],
  .verdict-box > .verdict-box__action[data-cta] {
    align-self: start;
    position: static;
  }
}


@media print {
  .card__grid > .card__action[data-cta="floating"],
  .card__grid > .card__brand[data-brand="floating"] {
    position: static;
  }
}

/* Page header --------------------------------------------------------------- */
/*
   Three treatments for a comparison heading: plain, a full-bleed cover image,
   or a picture beside the text.

   On a cover the text must stay legible over a photograph nobody has vetted,
   so contrast is not left to the image. A gradient scrim sits between the two,
   weighted towards whichever side the text is on, and there is a floor beneath
   even the lightest setting — a "no overlay" option would just be a promise
   the theme cannot keep.
*/

.page-head { padding-block: var(--s6) var(--s5); }
.page-head__text { overflow-wrap: anywhere; min-width: 0; }
.page-head__text > :last-child { margin-bottom: 0; }
.page-head__meta { margin: var(--s3) 0 0; font-size: 0.875rem; color: var(--ink-3); }
.page-head__disclosure { margin-top: var(--s4); }

/*
   Centring is more than text-align. A centred paragraph running the full
   container width reads worse than a left-aligned one, because the eye has to
   hunt for the start of every line — so the measure is capped and the block is
   centred within it. Cover and generated headers already did this; the plain
   style is the default and could not be centred at all.
*/
.page-head[data-align="center"] .page-head__text {
  max-width: 46rem;
  margin-inline: auto;
  text-align: center;
}

/* The disclosure is a card rather than a line of prose: it centres as a block
   and keeps its own wording left. */
.page-head[data-align="center"] .page-head__disclosure {
  max-width: 42rem;
  margin-inline: auto;
  text-align: start;
}

/* A centred heading above a list would strand the markers. */
.page-head[data-align="center"] .page-head__text :is(ul, ol) { text-align: start; }

/* -- Cover ------------------------------------------------------------------ */

.page-head--cover {
  position: relative;
  display: flex;
  align-items: center;
  padding-block: var(--s7);
  overflow: hidden;
  background: var(--c-secondary);
  --scrim-near: 0.62;
  --scrim-far: 0.28;
}

.page-head--cover[data-height="compact"] { min-height: 220px; }
.page-head--cover[data-height="standard"] { min-height: 330px; }
.page-head--cover[data-height="tall"] { min-height: 460px; padding-block: var(--s8); }

.page-head--cover[data-overlay="subtle"] { --scrim-near: 0.42; --scrim-far: 0.16; }
.page-head--cover[data-overlay="light"] { --scrim-near: 0.54; --scrim-far: 0.22; }
.page-head--cover[data-overlay="medium"] { --scrim-near: 0.68; --scrim-far: 0.34; }
.page-head--cover[data-overlay="strong"] { --scrim-near: 0.84; --scrim-far: 0.58; }

.page-head__image {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}

/* The scrim: heavier behind the text, lighter where the photograph can show. */
.page-head--cover::after {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(
      100deg,
      rgb(10 14 21 / var(--scrim-near)) 0%,
      rgb(10 14 21 / var(--scrim-near)) 34%,
      rgb(10 14 21 / var(--scrim-far)) 78%,
      rgb(10 14 21 / var(--scrim-far)) 100%
    ),
    linear-gradient(to top, rgb(10 14 21 / 30%) 0%, rgb(10 14 21 / 0%) 55%);
}

.page-head--cover[data-align="center"]::after {
  background:
    linear-gradient(
      to bottom,
      rgb(10 14 21 / var(--scrim-far)) 0%,
      rgb(10 14 21 / var(--scrim-near)) 55%,
      rgb(10 14 21 / var(--scrim-near)) 100%
    );
}

.page-head--cover .page-head__inner {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--s5);
}

.page-head--cover .page-head__text {
  flex: 1 1 auto;
  max-width: 46rem;
  color: #fff;
  /* Second line of defence on a busy photograph: costs nothing and rescues
     the awkward cases the scrim alone does not. */
  text-shadow: 0 1px 2px rgb(0 0 0 / 45%);
}
.page-head--cover h1 { color: #fff; }
.page-head--cover .lede { color: rgb(255 255 255 / 88%); }
.page-head--cover .page-head__meta { color: rgb(255 255 255 / 72%); }

.page-head--cover[data-align="center"] .page-head__inner { justify-content: center; }
.page-head--cover[data-align="center"] .page-head__text { text-align: center; margin-inline: auto; }

/* The disclosure is a card, so it keeps its own surface and drops the shadow
   the surrounding text is carrying. */
.page-head--cover .page-head__disclosure .disclosure {
  background: rgb(255 255 255 / 94%);
  border-color: transparent;
  text-shadow: none;
}

.page-head--cover .page-head__badge {
  flex: none;
  width: 120px; height: 120px;
  object-fit: contain;
  filter: drop-shadow(0 4px 12px rgb(0 0 0 / 35%));
}

/* Site-wide backdrop ---------------------------------------------------------- */
/*
   Optional, off by default, and deliberately not the header treatment stretched
   across the window.

   The header version is dark with white text laid over it. This sits behind
   body copy in the site's normal ink, so the base stays paper-coloured and the
   brand colours arrive as soft tints. Anything that has to be read — cards,
   tables, the header bar — keeps its own opaque surface and sits on top.

   Worth being clear about the inspiration: Stripe uses its gradient as a hero,
   with body content on plain white. A full-page wash behind paragraphs is a
   different proposition, which is why the tints here are restrained and why
   "subtle" is the default.
*/

body[data-backdrop] { background: transparent; }

.site-backdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  background: var(--paper);
  --tint: 0.10;
}

.site-backdrop[data-strength="medium"] { --tint: 0.18; }
.site-backdrop[data-strength="strong"] { --tint: 0.28; }

.site-backdrop__shape {
  position: absolute;
  will-change: transform;
  animation-iteration-count: infinite;
  animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}

/* -- Mesh: soft fields drifting past one another ----------------------------- */

.site-backdrop[data-pattern="mesh"] .site-backdrop__shape {
  aspect-ratio: 1;
  border-radius: 50%;
}

.site-backdrop[data-pattern="mesh"] .site-backdrop__shape:nth-child(1) {
  width: 74vmax; top: -28vmax; left: -22vmax;
  background: radial-gradient(circle at 50% 50%,
    rgb(var(--c-primary-rgb) / var(--tint)) 0%, transparent 64%);
  animation-name: rf-backdrop-a; animation-duration: 44s;
}
.site-backdrop[data-pattern="mesh"] .site-backdrop__shape:nth-child(2) {
  width: 62vmax; top: -18vmax; right: -18vmax;
  background: radial-gradient(circle at 50% 50%,
    rgb(var(--c-accent-rgb) / calc(var(--tint) * 0.8)) 0%, transparent 62%);
  animation-name: rf-backdrop-b; animation-duration: 56s; animation-delay: -12s;
}
.site-backdrop[data-pattern="mesh"] .site-backdrop__shape:nth-child(3) {
  width: 70vmax; bottom: -34vmax; left: 12vmax;
  background: radial-gradient(circle at 50% 50%,
    rgb(var(--c-positive-rgb) / calc(var(--tint) * 0.7)) 0%, transparent 64%);
  animation-name: rf-backdrop-c; animation-duration: 66s; animation-delay: -24s;
}

/* -- Beams: angled light crossing the window -------------------------------- */

.site-backdrop[data-pattern="beams"] .site-backdrop__shape {
  top: -30vmax; bottom: -30vmax;
  animation-name: rf-backdrop-sweep;
  animation-timing-function: linear;
}

.site-backdrop[data-pattern="beams"] .site-backdrop__shape:nth-child(1) {
  width: 34vmax;
  background: linear-gradient(90deg, transparent,
    rgb(var(--c-primary-rgb) / var(--tint)), transparent);
  animation-duration: 52s;
}
.site-backdrop[data-pattern="beams"] .site-backdrop__shape:nth-child(2) {
  width: 20vmax;
  background: linear-gradient(90deg, transparent,
    rgb(var(--c-accent-rgb) / calc(var(--tint) * 0.9)), transparent);
  animation-duration: 68s; animation-delay: -20s;
}
.site-backdrop[data-pattern="beams"] .site-backdrop__shape:nth-child(3) {
  width: 44vmax;
  background: linear-gradient(90deg, transparent,
    rgb(var(--c-positive-rgb) / calc(var(--tint) * 0.6)), transparent);
  animation-duration: 84s; animation-delay: -44s;
}

/*
   Slower than the header's by roughly double. A header is looked at once; this
   is behind everything the reader does, and movement they keep noticing is
   movement in the way. These durations are long enough that it reads as the
   page breathing rather than as something happening.
*/
@keyframes rf-backdrop-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(8vmax, 6vmax, 0) scale(1.12); }
}
@keyframes rf-backdrop-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.06); }
  50% { transform: translate3d(-10vmax, 8vmax, 0) scale(0.96); }
}
@keyframes rf-backdrop-c {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(9vmax, -10vmax, 0) scale(1.14); }
}
@keyframes rf-backdrop-sweep {
  from { transform: translateX(-140vmax) skewX(-14deg); }
  to { transform: translateX(240vmax) skewX(-14deg); }
}

/* Off for everyone when the editor says so, and off regardless for anyone who
   has asked their system for less motion. */
.site-backdrop[data-motion="still"] .site-backdrop__shape { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .site-backdrop__shape { animation: none; will-change: auto; }
}

/* Nothing decorative belongs on paper. */
@media print {
  .site-backdrop { display: none; }
  body[data-backdrop] { background: #fff; }
}

/* -- Generated backgrounds --------------------------------------------------- */
/*
   Headers the theme draws for itself. Worth knowing what they cost and what
   they do not: with no image there is nothing to download and no largest-
   contentful paint to wait for, so a generated header is the quickest of the
   four styles.

   Every pattern animates transform only. Colours come from the theme tokens,
   so a generated header follows the site's palette instead of looking like a
   widget dropped onto it.

   An uploaded image is never used here unless it is explicitly asked for —
   switching a page to a generated header should not silently keep the picture
   you were switching away from.
*/

.page-head--generated {
  position: relative;
  display: flex;
  align-items: center;
  padding-block: var(--s7);
  overflow: hidden;
  isolation: isolate;
    /* Solid first: a browser that cannot read the gradient below keeps this
     rather than dropping the background entirely. */
  background: var(--c-secondary);
background: linear-gradient(
    148deg,
    var(--c-secondary) 0%,
    color-mix(in srgb, var(--c-primary) 40%, var(--c-secondary)) 100%
  );
}

.page-head--generated[data-height="compact"] { min-height: 250px; }
.page-head--generated[data-height="standard"] { min-height: 350px; }
.page-head--generated[data-height="tall"] { min-height: 480px; }

.page-head--generated .page-head__inner {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--s5);
}

.page-head--generated .page-head__text {
  flex: 1 1 auto;
  max-width: 46rem;
  color: #fff;
  text-shadow: 0 1px 3px rgb(0 0 0 / 32%);
}
.page-head--generated h1 { color: #fff; }
.page-head--generated .lede { color: rgb(255 255 255 / 88%); }
.page-head--generated .page-head__meta { color: rgb(255 255 255 / 72%); }
.page-head--generated .page-head__disclosure .disclosure {
  background: rgb(255 255 255 / 94%);
  border-color: transparent;
  text-shadow: none;
}

.page-head--generated[data-align="center"] .page-head__inner { justify-content: center; }
.page-head--generated[data-align="center"] .page-head__text { text-align: center; margin-inline: auto; }

.page-head--generated .page-head__badge {
  flex: none;
  width: 120px; height: 120px;
  object-fit: contain;
  filter: drop-shadow(0 4px 14px rgb(0 0 0 / 34%));
}

/* Only present when the editor has opted into one. */
.page-head--generated .page-head__image { opacity: 0.5; z-index: 0; }
.page-head--generated[data-overlay="subtle"] .page-head__image { opacity: 0.72; }
.page-head--generated[data-overlay="light"] .page-head__image { opacity: 0.62; }
.page-head--generated[data-overlay="strong"] .page-head__image { opacity: 0.3; }

.backdrop {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

/* Softens the join into the page below, so the header ends rather than stops. */
.page-head--generated[data-pattern="mesh"]::after,
.page-head--generated[data-pattern="beams"]::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 90px;
  z-index: 1;
  background: linear-gradient(to bottom, transparent, rgb(var(--paper-rgb) / 80%));
}

/* -- Mesh: drifting colour fields -------------------------------------------- */
/*
   Large radial gradients that overlap and move slowly past one another. The
   softness is in the gradient stops rather than a blur filter — blurring a
   viewport-sized element every frame is expensive, and a gradient that fades
   to transparent looks the same for nothing.
*/

.page-head--generated[data-pattern="mesh"] {
    /* Solid first: a browser that cannot read the gradient below keeps this
     rather than dropping the background entirely. */
  background: var(--c-secondary);
background: linear-gradient(
    150deg,
    var(--c-secondary) 0%,
    color-mix(in srgb, var(--c-primary) 32%, var(--c-secondary)) 55%,
    color-mix(in srgb, var(--c-primary) 55%, var(--c-secondary)) 100%
  );
}

.backdrop--mesh .backdrop__blob {
  position: absolute;
  aspect-ratio: 1;
  border-radius: 50%;
  will-change: transform;
  animation-iteration-count: infinite;
  animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}

.backdrop--mesh .backdrop__blob:nth-child(1) {
  width: 72%; top: -30%; left: -14%;
  background: radial-gradient(circle at 50% 50%,
    rgb(var(--c-primary-rgb) / 78%) 0%, transparent 66%);
  animation-name: rf-drift-a; animation-duration: 26s;
}
.backdrop--mesh .backdrop__blob:nth-child(2) {
  width: 62%; top: -24%; right: -10%;
  background: radial-gradient(circle at 50% 50%,
    rgb(var(--c-accent-rgb) / 52%) 0%, transparent 64%);
  animation-name: rf-drift-b; animation-duration: 32s; animation-delay: -6s;
}
.backdrop--mesh .backdrop__blob:nth-child(3) {
  width: 68%; bottom: -38%; left: 18%;
  background: radial-gradient(circle at 50% 50%,
    rgb(var(--c-positive-rgb) / 44%) 0%, transparent 66%);
  animation-name: rf-drift-c; animation-duration: 38s; animation-delay: -11s;
}
.backdrop--mesh .backdrop__blob:nth-child(4) {
  width: 54%; bottom: -26%; right: 6%;
  background: radial-gradient(circle at 50% 50%,
    rgb(var(--c-primary-rgb) / 55%) 0%, transparent 62%);
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--c-primary) 60%, #ffffff) 0%, transparent 62%);
  opacity: 0.5;
  animation-name: rf-drift-d; animation-duration: 29s; animation-delay: -3s;
}

@keyframes rf-drift-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(14%, 10%, 0) scale(1.16); }
}
@keyframes rf-drift-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.08); }
  50% { transform: translate3d(-16%, 12%, 0) scale(0.94); }
}
@keyframes rf-drift-c {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(12%, -14%, 0) scale(1.2); }
}
@keyframes rf-drift-d {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.1); }
  50% { transform: translate3d(-12%, -8%, 0) scale(0.92); }
}

/* -- Beams: angled light sweeping across ------------------------------------- */

.page-head--generated[data-pattern="beams"] {
    /* Solid first: a browser that cannot read the gradient below keeps this
     rather than dropping the background entirely. */
  background: var(--c-secondary);
background: linear-gradient(
    118deg,
    var(--c-secondary) 0%,
    color-mix(in srgb, var(--c-primary) 46%, var(--c-secondary)) 100%
  );
}

.backdrop--beams .backdrop__beam {
  position: absolute;
  top: -40%; bottom: -40%;
  width: 26%;
  will-change: transform;
  animation: rf-beam-sweep 26s linear infinite;
}

.backdrop--beams .backdrop__beam:nth-child(1) {
  background: linear-gradient(90deg, transparent,
    rgb(var(--c-primary-rgb) / 48%), transparent);
  animation-duration: 21s;
}
/*
   The warm beam is a hint, not a stripe.
 
   At 34% over a dark navy base the accent read as a distinct orange band — it
   is close to the base's complement, so it carries much further than the blue
   beam beside it at the same opacity. Three hues over one base means whichever
   contrasts most wins, and matching their numbers does not match their weight.
   Wider and much fainter, it reads as warmth in the light rather than as a
   painted stripe.
*/
.backdrop--beams .backdrop__beam:nth-child(2) {
  width: 24%;
  background: linear-gradient(90deg, transparent,
    rgb(var(--c-accent-rgb) / 13%), transparent);
  animation-duration: 29s; animation-delay: -9s;
}
.backdrop--beams .backdrop__beam:nth-child(3) {
  width: 34%;
  background: linear-gradient(90deg, transparent,
    rgb(255 255 255 / 20%), transparent);
  animation-duration: 37s; animation-delay: -18s;
}

@keyframes rf-beam-sweep {
  from { transform: translateX(-160%) skewX(-16deg); }
  to { transform: translateX(460%) skewX(-16deg); }
}

/* -- Waves: layered swells --------------------------------------------------- */

.page-head--generated[data-pattern="waves"] { padding-block: var(--s7) calc(var(--s7) + 40px); }

.waves {
  position: absolute;
  inset-inline: 0;
  bottom: -1px;              /* hides the hairline some browsers leave */
  height: clamp(60px, 9vw, 120px);
  z-index: 1;
  pointer-events: none;
}
.waves svg { display: block; width: 100%; height: 100%; }

/*
   One period of the path is 176 user units and the path runs 704, so a
   176-unit translate lands on an identical position with no edge ever
   entering the viewBox. Anything less than a whole period visibly jumps.
*/
.waves__set use {
  animation: rf-wave-drift 18s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
  will-change: transform;
}

.waves__set use:nth-child(1) {
  fill: rgb(var(--c-primary-rgb) / 34%);
  animation-duration: 13s; animation-delay: -2s;
}
.waves__set use:nth-child(2) {
  fill: rgb(var(--c-primary-rgb) / 22%);
  animation-duration: 17s; animation-delay: -3s;
}
.waves__set use:nth-child(3) {
  fill: rgb(var(--c-accent-rgb) / 16%);
  animation-duration: 22s; animation-delay: -4s;
}
.waves__set use:nth-child(4) {
  /* The last one is the page's own background, so the header dissolves into
     the content below rather than stopping at a hard edge. */
  fill: var(--paper);
  animation-duration: 28s; animation-delay: -5s;
}

@keyframes rf-wave-drift {
  from { transform: translateX(-176px); }
  to { transform: translateX(0); }
}

/* -- Motion ------------------------------------------------------------------ */
/*
   Opt-out twice over: the editor can still the background for everyone, and a
   visitor who has asked their system for less motion gets a static header
   regardless of that choice. Continuous background movement is a real problem
   for vestibular disorders, not a preference.
*/

.page-head--generated[data-motion="still"] .waves__set use,
.page-head--generated[data-motion="still"] .backdrop__blob,
.page-head--generated[data-motion="still"] .backdrop__beam { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .waves__set use,
  .backdrop__blob,
  .backdrop__beam { animation: none; will-change: auto; }
}

@media (max-width: 760px) {
  .page-head--generated { padding-block: var(--s6); }
  .page-head--generated[data-pattern="waves"] { padding-block: var(--s6) calc(var(--s6) + 30px); }
  .page-head--generated[data-height="standard"] { min-height: 280px; }
  .page-head--generated[data-height="tall"] { min-height: 360px; }
  .page-head--generated .page-head__inner { flex-direction: column; align-items: flex-start; gap: var(--s4); }
  .page-head--generated[data-align="center"] .page-head__inner { align-items: center; }
  .page-head--generated .page-head__badge { width: 84px; height: 84px; }
}

@media print {
  .waves, .backdrop { display: none; }
  .page-head--generated { min-height: 0; background: none; padding-block: var(--s4); }
  .page-head--generated::after { display: none; }
  .page-head--generated .page-head__text { color: var(--ink); text-shadow: none; }
  .page-head--generated h1, .page-head--generated .lede { color: var(--ink); }
}

/* -- Thumbnail beside the text ---------------------------------------------- */

.page-head--thumb { padding-block: var(--s6) var(--s5); }

.page-head--thumb .page-head__inner {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(240px, 0.9fr);
  gap: var(--s6);
  align-items: center;
}

.page-head--thumb[data-side="left"] .page-head__text { order: 2; }
.page-head--thumb[data-side="left"] .page-head__aside { order: 1; }

.page-head__aside { position: relative; }

.page-head__thumb {
  width: 100%;
  aspect-ratio: 13 / 9;
  object-fit: cover;
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}

.page-head--thumb .page-head__badge {
  position: absolute;
  right: calc(-1 * var(--s3));
  bottom: calc(-1 * var(--s3));
  width: 110px; height: 110px;
  object-fit: contain;
  filter: drop-shadow(0 4px 12px rgb(18 24 38 / 22%));
}

@media (max-width: 900px) {
  .page-head--thumb .page-head__inner { grid-template-columns: minmax(0, 1fr); gap: var(--s5); }
  /* Text first on a narrow screen whichever side the picture sits on: the
     heading is what the reader came for. */
  .page-head--thumb[data-side="left"] .page-head__text,
  .page-head--thumb .page-head__text { order: 1; }
  .page-head--thumb[data-side="left"] .page-head__aside,
  .page-head--thumb .page-head__aside { order: 2; }
  .page-head__thumb { aspect-ratio: 16 / 9; }
  .page-head--thumb .page-head__badge { width: 84px; height: 84px; }
}

@media (max-width: 760px) {
  .page-head--cover { padding-block: var(--s6); }
  .page-head--cover[data-height="tall"] { min-height: 340px; padding-block: var(--s7); }
  .page-head--cover[data-height="standard"] { min-height: 260px; }
  .page-head--cover .page-head__inner { flex-direction: column; align-items: flex-start; gap: var(--s4); }
  .page-head--cover[data-align="center"] .page-head__inner { align-items: center; }
  .page-head--cover .page-head__badge { width: 84px; height: 84px; }
}

@media print {
  .page-head__image, .page-head--cover::after { display: none; }
  .page-head--cover { min-height: 0; background: none; padding-block: var(--s4); }
  .page-head--cover .page-head__text { color: var(--ink); text-shadow: none; }
  .page-head--cover h1, .page-head--cover .lede { color: var(--ink); }
}

/* Repeated top pick --------------------------------------------------------- */
/*
   The same entry as rank 1, shown again under the list. Tinted and labelled so
   it reads as a deliberate recap rather than a stray duplicate row.
*/

.top-pick {
  display: block;
  margin-top: var(--s5);
  padding: var(--s5);
  background: var(--surface);
  background: color-mix(in srgb, var(--c-accent) 7%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--c-accent) 50%, var(--line));
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}

.top-pick__label {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3);
  margin-bottom: var(--s4);
}
.top-pick__rank {
  font-size: 0.78rem; font-weight: 650; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* Slightly quieter than the card above it: this is a reminder, not a second
   first place. */
.top-pick .card__name { font-size: 1.15rem; }

@media (max-width: 760px) {
  .top-pick { padding: var(--s4); }
}

@media print {
  .top-pick { break-inside: avoid; }
}

/* Whole-card affiliate links ------------------------------------------------ */
/*
   Opt-in per comparison page. The attribute is written by the template; the
   -ready flag is added by the script, so the pointer cursor only appears when
   the behaviour is genuinely wired up.
*/

[data-card-link][data-card-ready] { cursor: pointer; }

[data-card-link][data-card-ready]:hover {
  border-color: color-mix(in srgb, var(--c-primary) 45%, var(--line));
  box-shadow: var(--shadow-2);
}

/* Text stays selectable, so give it the cursor that says so — the click
   handler ignores a click that follows a selection. */
[data-card-link][data-card-ready] p,
[data-card-link][data-card-ready] li,
[data-card-link][data-card-ready] dd,
[data-card-link][data-card-ready] .card__desc,
[data-card-link][data-card-ready] .details__body { cursor: text; }

/* Controls inside keep their own cursor and their own click. */
[data-card-link][data-card-ready] a,
[data-card-link][data-card-ready] button,
[data-card-link][data-card-ready] summary { cursor: pointer; }

/* Table rows and dense rows get the same affordance. */
tr[data-card-link][data-card-ready]:hover td { background: rgb(var(--c-primary-rgb) / 6%); }
.minimal-row[data-card-link][data-card-ready]:hover { box-shadow: var(--shadow-2); }

/* Keyboard users reach the button inside, not the card, so make that
   obvious by lifting the whole card when anything in it is focused. */
[data-card-link][data-card-ready]:focus-within { box-shadow: var(--shadow-2); }

/* Rating -------------------------------------------------------------------- */

.rating { display: flex; align-items: center; gap: var(--s2); }

.rating__score {
  display: flex; align-items: baseline; gap: 0.1em;
  font-family: var(--font-display); font-weight: 800;
  font-size: 1.75rem; letter-spacing: -0.04em; line-height: 1;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink);
}
.rating__max { font-size: 0.85rem; font-weight: 600; color: var(--ink-3); }

/* Sits with the number it describes, rather than underneath it as a second
   score — which is what it was doing, on every card that had both. */
.rating__verdict {
  font-size: 0.76rem; font-weight: 700; letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--c-positive-text);
}
.rating__meta { font-size: 0.78rem; color: var(--ink-3); }

/* Stars are one outline row with a filled row clipped over it, so a 4.3 shows
   as exactly 86% and there is no half-star sprite to maintain. */
.stars { position: relative; display: inline-block; line-height: 0; }
.stars__row { display: flex; gap: 1px; }
.stars__row svg { width: 15px; height: 15px; }
.stars__row--base { color: var(--line-strong); }
.stars__row--fill {
  position: absolute; inset-block: 0; inset-inline-start: 0;
  overflow: hidden; width: var(--fill, 0%); color: var(--c-accent);
}

/* Pros and cons ------------------------------------------------------------- */

.proscons { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); }
.proscons--single { grid-template-columns: minmax(0, 1fr); }

.ticklist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); font-size: 0.9rem; }
.ticklist li { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: var(--s2); align-items: start; }
.ticklist svg { width: 15px; height: 15px; margin-top: 4px; }
.ticklist--pro svg { color: var(--c-positive-text); }
.ticklist--con svg { color: var(--c-negative-text); }

.list-title {
  font-size: 0.74rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: var(--s2);
}

/* Facts strip — renders only the entries that exist, so no empty slots ------ */

.facts { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); margin: 0; padding: 0; }
.facts > div { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.facts dt { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); }
.facts dd { margin: 0; font-size: 0.9rem; font-weight: 600; overflow-wrap: break-word; }

/* Scores -------------------------------------------------------------------- */

.scores { display: grid; gap: var(--s2); margin: 0; }
.score { display: grid; grid-template-columns: minmax(84px, 110px) minmax(0, 1fr) 34px; gap: var(--s3); align-items: center; }
.score__label { font-size: 0.8rem; color: var(--ink-2); }
.score__track { height: 6px; background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-pill); overflow: hidden; }
.score__fill { display: block; height: 100%; background: var(--c-primary); border-radius: inherit; }
.score__value { font-size: 0.82rem; font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; }

/* Expandable long description ---------------------------------------------- */

.details { border-top: 1px solid var(--line); padding-top: var(--s3); margin-top: var(--s1); }
.details > summary {
  cursor: pointer; font-size: 0.88rem; font-weight: 650; color: var(--c-primary-text);
  list-style: none; display: inline-flex; align-items: center; gap: var(--s2);
}
.details > summary::-webkit-details-marker { display: none; }
.details > summary::after { content: "▾"; font-size: 0.75em; transition: transform .15s ease; }
.details[open] > summary::after { transform: rotate(180deg); }
.details__body { padding-top: var(--s3); font-size: 0.93rem; color: var(--ink-2); }

/* Layout variants ----------------------------------------------------------- */
/* Each variant reuses the same card partials; only the container changes. */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: var(--s4);
  align-items: stretch;
}
.card--stacked { display: flex; flex-direction: column; gap: var(--s4); }
/* Pushes the action zone to the bottom so buttons align across a row
   regardless of how much copy each entry carries. */
.card--stacked .card__action { margin-top: auto; }
.card--stacked .card__brand { align-items: center; text-align: center; }
.card--stacked .card__logo { object-position: center; margin-inline: auto; }

.rank-chip {
  position: absolute; top: var(--s3); left: var(--s3);
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 30px; height: 26px; padding-inline: 0.4em;
  font-size: 0.8rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2); background: var(--paper);
  border: 1px solid var(--line); border-radius: var(--radius-pill);
}
.card[data-award]:not([data-award=""]) .rank-chip {
  color: #2a1c00; background: var(--c-accent); border-color: transparent;
}

.minimal-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.minimal-row {
  display: grid;
  /* The action track fits an ordinary label on one line: "Get Northgate" at
     the large button's size needs about 145px including its padding, and 150
     was leaving it to wrap while a shorter label beside it did not. */
  grid-template-columns: 40px minmax(0, 1fr) auto minmax(175px, 205px);
  gap: var(--s4);
  align-items: center;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.minimal-row[data-award]:not([data-award=""]) {
  border-color: color-mix(in srgb, var(--c-accent) 55%, var(--line));
}
.minimal-row__rank { font-size: 1.1rem; font-weight: 800; color: var(--ink-3); text-align: center; font-variant-numeric: tabular-nums; }
.minimal-row__brand { display: flex; align-items: center; gap: var(--s3); min-width: 0; overflow-wrap: break-word; }
.minimal-row__brand img { width: 104px; height: 40px; object-fit: contain; object-position: left center; flex: none; }
.minimal-row__meta { display: flex; align-items: center; gap: var(--s3); }
/*
   The action stacks here too.
 
   This was a row, which put the button and its note side by side inside a
   180-pixel track — so "30-day money-back guarantee" had nowhere to go but
   out through the right edge of the card, four words tall and clipped by the
   window. A note that explains the offer is worth a second line; it is not
   worth leaving the card.
*/
.minimal-row .card__action { flex-direction: column; gap: var(--s2); }
.minimal-row .card__cta-note { font-size: 0.76rem; line-height: 1.3; }
.minimal-row .rating__score { font-size: 1.15rem; }

/*
   A frame, not a layout. The grid inside it is .card__grid, the same one every
   ranked card uses, so the two cannot drift.
*/
.feature {
  position: relative;
  padding: var(--s6);
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--c-accent) 45%, var(--line));
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}

/* Room for the numeral, matching what the cards below reserve. */
/* The same card, larger: the only differences are the frame and the emphasis
   on the brand, which is what a top pick is for. */
.feature .card__logo { max-width: 200px; max-height: 80px; }
.feature .card__name { font-size: 1.5rem; }
.feature .eyebrow { margin: 0 0 var(--s2); }

.feature[data-rank="corner"] { padding-top: calc(var(--s6) + 18px); }
.feature[data-rank="badge"] { padding-top: calc(var(--s6) + 10px); }

.landing-hero {
  display: flex; flex-direction: column; align-items: center; gap: var(--s4);
  text-align: center;
  padding: var(--s7) var(--s5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}
.landing-hero h2 { margin: 0; font-size: clamp(1.8rem, 1.3rem + 2vw, 2.6rem); }
.landing-hero__logo { max-height: 88px; width: auto; object-fit: contain; }
.landing-hero__action { width: min(340px, 100%); }
.landing-hero .facts { justify-content: center; }
.landing-hero .proscons { text-align: left; width: min(720px, 100%); }

@media (max-width: 900px) {
  .feature { padding: var(--s5); }
  /* Same reasoning as the full-width track: a label that fits on one line at
     1180px should not start wrapping at 900. */
  .minimal-row { grid-template-columns: 34px minmax(0, 1fr) minmax(165px, 190px); grid-template-areas: "rank brand action" "meta meta meta"; }
  .minimal-row__rank { grid-area: rank; }
  .minimal-row__brand { grid-area: brand; }
  .minimal-row__meta { grid-area: meta; }
  .minimal-row .card__action { grid-area: action; }
}

@media (max-width: 760px) {
  .minimal-row { grid-template-columns: 34px minmax(0, 1fr); grid-template-areas: "rank brand" "meta meta" "action action"; }
  .card--stacked .card__brand { align-items: flex-start; text-align: left; }
  .card--stacked .card__logo { margin-inline: 0; object-position: left center; }
}

/* 9. Matrix / table layout -------------------------------------------------- */

.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}

.matrix { width: 100%; border-collapse: separate; border-spacing: 0; min-width: 680px; }
/*
   break-word, not anywhere.
 
   Both break a word too long for its column, but "anywhere" also tells the
   browser a cell may shrink to a single character — so nine columns in a
   1180px table squeezed until "Visit Mistral" came out one letter per line
   and "$3.95/mo" broke into three. "break-word" keeps each column at least as
   wide as its longest word, which is what makes the table exceed its
   container and scroll instead of collapsing.
*/
.matrix th, .matrix td {
  padding: var(--s3) var(--s4);
  text-align: left; vertical-align: middle;
  border-bottom: 1px solid var(--line);
  overflow-wrap: break-word;
  min-width: 5.5rem;
}

/* A button is a target, not a paragraph. */
.matrix .btn { white-space: nowrap; }

/* Short values read as one unit or not at all. */
.matrix td.is-rank, .matrix .cell-price, .matrix .cell-rating { white-space: nowrap; }
.matrix thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--paper);
  font-size: 0.74rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--line-strong);
  white-space: nowrap;
}
.matrix tbody tr:last-child td { border-bottom: 0; }
.matrix tbody tr:hover td { background: rgb(var(--c-primary-rgb) / 3%); }
.matrix td.is-rank { width: 56px; min-width: 56px; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--ink-3); }
.matrix .cell-brand { display: flex; align-items: center; gap: var(--s3); min-width: 0; }
.matrix .cell-brand img { width: 96px; height: 36px; object-fit: contain; object-position: left center; flex: none; }
.matrix .cell-brand strong { font-weight: 700; }

/* Sticky first column keeps the business visible while scanning wide tables. */
.matrix--sticky td:nth-child(2), .matrix--sticky th:nth-child(2) {
  position: sticky; left: 0; z-index: 2; background: var(--surface);
  /* Wide enough for a two-word business name. Without this the name spilled
     over the rating column and the two printed on top of each other. */
  min-width: 11rem;
}
.matrix--sticky thead th:nth-child(2) { z-index: 3; background: var(--paper); }

/*
   The action stays put while the middle scrolls.
 
   A table wide enough to need scrolling pushes its last column off the right
   edge, and on a comparison page that column is the whole point — the reader
   could scan every figure without ever seeing the way out. The business stays
   pinned on the left and the button on the right, so both ends of the decision
   are always visible and only the evidence moves.
*/
.matrix--sticky td.cell-action, .matrix--sticky th.cell-action {
  position: sticky; right: 0; z-index: 1;
  background: var(--surface);
  box-shadow: -8px 0 12px -8px rgb(18 24 38 / 14%);
}
.matrix--sticky thead th.cell-action { z-index: 3; background: var(--paper); }

/* Single-business review ----------------------------------------------------- */

/*
   Three columns: who it is, what we found, what to do.
 
   Scoped entirely to .verdict-box, which only a review page renders — the
   comparison layouts use .card__grid and are untouched by anything here.
*/
.verdict-box {
  display: grid;
  grid-template-columns: minmax(180px, 220px) minmax(0, 1fr) minmax(220px, 260px);
  gap: var(--s6);
  align-items: start;
  padding: var(--s6);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}

/* A high score earns the accent edge, the same signal the ranked cards use. */
.verdict-box[data-award="top"] { border-color: var(--c-accent); }

.verdict-box__brand {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  align-items: stretch;
  min-width: 0;
}

/* The photograph fills its column; the logo beside it stays a mark. */
.verdict-box__brand .card__product { width: 100%; height: auto; border-radius: var(--radius-sm); }
.verdict-box__brand .card__mark { max-width: 96px; height: auto; }
.verdict-box__brand .card__logo { max-width: 180px; height: auto; }

.verdict-box__score {
  margin: 0;
  font-size: 2.6rem;
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
}
.verdict-box__max { font-size: 0.9rem; font-weight: 600; color: var(--ink-3); }

.verdict-box__word {
  margin: 0;
  font-size: 0.78rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-positive-text);
}

/* A review page has one rating, so it can afford to be read at a glance —
   the comparison cards size theirs to sit in a column of five. */
.verdict-box__brand .stars__row svg { width: 21px; height: 21px; }
.verdict-box__brand .rating__score { font-size: 2.1rem; }
.verdict-box__brand .rating { flex-wrap: wrap; gap: var(--s2); }

.verdict-box__reviews { margin: 0; font-size: 0.82rem; color: var(--ink-3); }

.verdict-box__used {
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s2) 0 0;
  font-size: 0.84rem; color: var(--c-positive-text);
}
.verdict-box__used svg { width: 15px; height: 15px; flex: none; }

.verdict-box__votes {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2) var(--s3);
  margin: var(--s3) 0 0;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
  font-size: 0.82rem;
}
.verdict-box__votes-label { color: var(--ink-3); font-weight: 650; }
.verdict-box__vote { display: inline-flex; align-items: center; gap: 4px; font-weight: 650; }
.verdict-box__vote svg { width: 14px; height: 14px; }
.verdict-box__vote.is-up { color: var(--c-positive-text); }
.verdict-box__vote.is-down { color: var(--ink-3); }

/* The counts became buttons. They keep the quiet look of the figures they
   replaced — a control that shouts competes with the offer button, which is
   the point of the card — and grow a border on hover so they read as
   pressable. The pressed one stays fully visible after the click: disabled
   is what "you have voted" looks like here, so it must not fade away. */
button.verdict-box__vote {
  margin: 0;
  padding: 2px 8px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: none;
  font: inherit;
  font-weight: 650;
  cursor: pointer;
}
button.verdict-box__vote:hover { border-color: var(--line-strong); background: var(--paper); }
button.verdict-box__vote[aria-pressed="true"] { border-color: currentColor; background: var(--paper); }
button.verdict-box__vote:disabled { cursor: default; }
button.verdict-box__vote:disabled:not([aria-pressed="true"]) { opacity: 0.55; }

/* Counts to the real end date, so the figures are the same on every device. */
.verdict-box__countdown {
  display: flex; align-items: center; justify-content: center; gap: var(--s2);
  margin: 0;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}
.verdict-box__countdown-label { color: var(--ink-3); }
.verdict-box__countdown [data-countdown-value] { font-weight: 700; }

.verdict-box__body { min-width: 0; }
.verdict-box__body > h2 { margin-top: 0; }

/* The pros and cons ran straight into the paragraph above them, because
   .proscons carries no margin of its own — in a card it inherits spacing from
   the stack around it, and here there was none. */
.verdict-box__body > * + * { margin-top: var(--s4); }
.verdict-box__body > .proscons { margin-top: var(--s5); }

/* The action column ------------------------------------------------------- */

.verdict-box__action {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  align-items: stretch;
  min-width: 0;
  padding: var(--s4);
  background: var(--paper);
  border-radius: var(--radius-sm);
}

/* An offer inside the card reads as part of it, not as a panel bolted on. */
.verdict-box__action[data-coupon] {
  background: rgb(var(--c-accent-rgb) / 10%);
  border: 1px dashed rgb(var(--c-accent-rgb) / 45%);
}

/* The mark only exists in the docked bar; in the card it would repeat the
   column beside it. */
.verdict-box__mark { display: none; }

.coupon-dock .verdict-box__mark {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex: 0 0 auto;
  font-size: 0.9rem;
}
.coupon-dock .verdict-box__mark { overflow: hidden; max-width: 10rem; }
.coupon-dock .verdict-box__mark img { max-height: 26px; width: auto; }

/* Clipped inside its own box rather than spilling out of it. flex: 0 0 auto
   stops the box shrinking but does nothing about content wider than the box,
   which is what printed a name across the offer beside it. */
/* flex shrank the name to a single letter beside the offer. It either fits or
   it is not shown — a stray "M" in front of the headline is worse than no name
   at all. */
.coupon-dock .verdict-box__mark strong {
  flex: 0 0 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 12rem;
}

@media (max-width: 1100px) {
  .coupon-dock .verdict-box__mark strong { display: none; }
}

@media (max-width: 760px) {
  .coupon-dock .verdict-box__mark { display: none; }
}

.verdict-box__offer {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

.verdict-box__expiry,
.verdict-box__terms {
  margin: 0;
  font-size: 0.78rem;
  color: var(--ink-3);
  line-height: 1.45;
}

/* Coupon --------------------------------------------------------------------- */

.coupon {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(240px, 300px);
  gap: var(--s5);
  align-items: center;
  padding: var(--s5) var(--s6);
  background: rgb(var(--c-accent-rgb) / 8%);
  border: 2px dashed rgb(var(--c-accent-rgb) / 45%);
  border-radius: var(--radius);
}

.coupon__offer { min-width: 0; }

.coupon__eyebrow {
  margin: 0 0 var(--s2);
  font-size: 0.75rem; font-weight: 700; letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.coupon__headline {
  margin: 0;
  font-size: 1.35rem; font-weight: 750; line-height: 1.25;
  letter-spacing: -0.01em;
}

.coupon__expiry { margin: var(--s2) 0 0; font-size: 0.85rem; color: var(--ink-3); }

.coupon__action { display: flex; flex-direction: column; gap: var(--s3); min-width: 0; }

/* The revealed code reads as something to copy, not as body text. */
.coupon__reveal {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 2px solid var(--c-accent);
  border-radius: var(--radius-sm);
}

.coupon__code {
  flex: 1 1 auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.1rem; font-weight: 700; letter-spacing: 0.08em;
  overflow-wrap: break-word; min-width: 0;
}

.coupon__hint { margin: 0; font-size: 0.82rem; color: var(--c-positive-text); }

/* How many people took it. Deliberately quiet — a figure shouted is a figure
   doubted, and this one is either measured or stated by the editor. */
.coupon__usage {
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s3) 0 0;
  font-size: 0.85rem; font-weight: 600;
  color: var(--c-positive-text);
}
.coupon__usage svg { width: 16px; height: 16px; flex: none; }

/* The docked copy, on the same edge and above the same dock as everything
   else that lives down there. */
.coupon-dock {
  position: fixed;
  inset-inline: 0;
  bottom: var(--dock-h, 0px);
  z-index: 49;
  padding: var(--s3) var(--s4);
  padding-bottom: max(var(--s3), env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--line);
  box-shadow: 0 -4px 18px rgb(18 24 38 / 10%);
}

/* A docked review action lies down: it is a bar now, not a column in a card. */
.coupon-dock .verdict-box__action {
  max-width: var(--container);
  margin-inline: auto;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) var(--s4);
  border-width: 1px;
}
/* btn--block is width:100% by design, which is right in a card column and
   wrong in a bar — it pushed the offer text into a four-line sliver. */
.coupon-dock .verdict-box__action .btn { flex: 0 0 auto; width: auto; min-width: 14rem; }
.coupon-dock .verdict-box__action .coupon__reveal { flex: 0 0 auto; }
.coupon-dock .verdict-box__offer { flex: 1 1 auto; min-width: 0; font-size: 1rem; }

/* The bar is the compressed offer: brand, offer, action. Urgency ticks on
   the card and fine print stays with it — a countdown in a sixty-pixel bar
   is noise, and terms in one are unreadable. The short dated line stays:
   "Ends 3 March" earns its width. */
.coupon-dock .verdict-box__countdown,
.coupon-dock .verdict-box__terms { display: none; }
.coupon-dock .coupon-peel { flex: 0 0 auto; }

/* The peeled reveal, second pass. The first was a dashed pill and read as a
   code chip, not a control — and a control's first honesty is looking
   pressable. Now the label half is unmistakably the button: solid primary,
   white text, the same shadow and lift on hover as every other CTA. The
   tail is the ticket stub peeling off it — paper, dashed edge, the code's
   real last letters — with an accent fold big enough to look liftable. */
.coupon-peel {
  position: relative;
  display: inline-flex;
  align-items: stretch;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-1);
  font-weight: 650;
  text-decoration: none;
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.coupon-peel:hover { text-decoration: none; box-shadow: var(--shadow-2); transform: translateY(-1px); }
.coupon-peel__label {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 12px 18px;
  background: var(--c-primary);
  color: #fff;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}
.coupon-peel__tail {
  position: relative;
  display: flex;
  align-items: center;
  padding: 12px 16px 12px 14px;
  background: var(--paper);
  border: 2px dashed var(--line-strong);
  border-left: none;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  letter-spacing: 0.06em;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
}
.coupon-peel__tail::after {
  content: "";
  position: absolute;
  top: -2px;
  right: -2px;
  width: 22px;
  height: 22px;
  background: linear-gradient(225deg, var(--surface) 0 48%, var(--c-accent) 48% 100%);
  border-left: 1px solid var(--line-strong);
  border-bottom: 1px solid var(--line-strong);
  border-bottom-left-radius: var(--radius-sm);
  transition: width 0.15s ease, height 0.15s ease;
}
.coupon-peel:hover .coupon-peel__tail::after,
.coupon-peel:focus-visible .coupon-peel__tail::after { width: 30px; height: 30px; }

.coupon-dock .coupon-peel__label { padding: 9px 14px; }
.coupon-dock .coupon-peel__tail { padding: 9px 13px 9px 11px; }

@media (prefers-reduced-motion: reduce) {
  .coupon-peel { transition: none; }
  .coupon-peel:hover { transform: none; }
  .coupon-peel__tail::after { transition: none; }
}

.footer-disclosure { margin-bottom: var(--s4); }
.coupon-dock .verdict-box__terms,
.coupon-dock .verdict-box__expiry,
.coupon-dock .card__cta-note { display: none; }

@media (max-width: 760px) {
  .coupon-dock .verdict-box__action { flex-direction: column; align-items: stretch; gap: var(--s2); }
  .coupon-dock .verdict-box__offer { font-size: 0.9rem; }
}

.coupon-dock .coupon {
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--s3) var(--s4);
  border-width: 1px;
}

/* A docked copy is a reminder, not the whole block again. */
.coupon-dock .coupon__terms,
.coupon-dock .coupon__expiry,
.coupon-dock .coupon__eyebrow { display: none; }
.coupon-dock .coupon__headline { font-size: 1.05rem; }

@media (max-width: 760px) {
.coupon-dock .coupon { grid-template-columns: minmax(0, 1fr); gap: var(--s3); }
  .coupon-dock .coupon__usage { display: none; }
}

@media print { .coupon-dock { display: none; } }

/* Contact form ---------------------------------------------------------------- */

.contact-form { display: flex; flex-direction: column; gap: var(--s4); margin-top: var(--s5); }
.contact-form .btn { align-self: flex-start; }

.contact-form__row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); }
.contact-form__field { display: flex; flex-direction: column; gap: var(--s2); min-width: 0; }
.contact-form__label { font-size: 0.9rem; font-weight: 650; }
.contact-form__hint { font-size: 0.82rem; color: var(--ink-3); }

.contact-form input,
.contact-form textarea {
  width: 100%;
  padding: 0.7em 0.85em;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
}
.contact-form textarea { resize: vertical; min-height: 8em; }

.contact-form input:focus-visible,
.contact-form textarea:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 1px;
  border-color: var(--c-primary);
}

@media (max-width: 600px) {
  .contact-form__row { grid-template-columns: minmax(0, 1fr); }
}

/* Off screen rather than display:none — a bot skips what is obviously hidden. */
.contact-form__trap {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.notice {
  margin: 0 0 var(--s4);
  padding: var(--s4);
  border-radius: var(--radius-sm);
  border-left: 4px solid var(--line-strong);
  background: var(--surface);
}
.notice--good { border-left-color: var(--c-positive); background: rgb(var(--c-positive-rgb) / 8%); }
.notice--bad { border-left-color: var(--c-negative); background: rgb(var(--c-negative-rgb) / 8%); }

.coupon__terms {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 0.8rem; color: var(--ink-3);
}

/* Two columns before one: the action keeps its own track longer than the brand
   does, because it is the thing the page exists to offer. */
@media (max-width: 1080px) {
  .verdict-box {
    grid-template-columns: minmax(160px, 200px) minmax(0, 1fr);
    grid-template-areas: "brand body" "action action";
  }
  .verdict-box__brand { grid-area: brand; }
  .verdict-box__body { grid-area: body; }
  .verdict-box__action { grid-area: action; }
}

@media (max-width: 760px) {
  .verdict-box { grid-template-columns: minmax(0, 1fr); grid-template-areas: "brand" "body" "action"; gap: var(--s5); padding: var(--s5); }
  .coupon { grid-template-columns: minmax(0, 1fr); padding: var(--s4) var(--s5); }
}


/* 10. Page sections --------------------------------------------------------- */

.hero { padding-block: var(--s7) var(--s6); }
.hero__inner { display: grid; gap: var(--s5); align-items: center; }
.hero--split .hero__inner { grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); }
.hero--center { text-align: center; }
.hero--center .hero__inner { justify-items: center; max-width: 760px; margin-inline: auto; }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--s3); }
.hero__image { border-radius: var(--radius); box-shadow: var(--shadow-2); }

.faq { display: grid; gap: var(--s2); }
.faq__item { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.faq__q {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: var(--s4); cursor: pointer; font-weight: 650; list-style: none;
}
.faq__q::-webkit-details-marker { display: none; }
.faq__q::after { content: "+"; font-size: 1.3em; font-weight: 400; color: var(--ink-3); flex: none; }
.faq__item[open] .faq__q::after { content: "–"; }
.faq__a { padding: 0 var(--s4) var(--s4); color: var(--ink-2); }

.cta-panel {
  background: var(--c-secondary); color: #fff;
  border-radius: var(--radius); padding: var(--s6);
  display: grid; gap: var(--s4); align-items: center;
}
.cta-panel h2, .cta-panel h3 { color: #fff; }
.cta-panel p { color: rgb(255 255 255 / 78%); }
.cta-panel--split { grid-template-columns: minmax(0, 1fr) auto; }

.testimonials { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--s4); }
.testimonial { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--s5); }
.testimonial__quote { font-size: 1.02rem; color: var(--ink-2); }
.testimonial__who { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s4); }
.testimonial__who img { width: 38px; height: 38px; border-radius: 50%; object-fit: cover; }

.author-card { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: var(--s4); align-items: start; }
.author-card img { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; }

.video-frame {
  position: relative; aspect-ratio: 16 / 9;
  border-radius: var(--radius); overflow: hidden; background: var(--c-secondary);
}
.video-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.related { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--s4); }
.related a {
  display: block; padding: var(--s4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  text-decoration: none; color: var(--ink);
  transition: border-color .14s ease, box-shadow .14s ease;
}
.related a:hover { border-color: var(--c-primary); box-shadow: var(--shadow-2); }

.divider-dots { display: flex; justify-content: center; gap: var(--s2); color: var(--line-strong); }

/* Breadcrumbs and disclosure ------------------------------------------------ */

.crumbs { font-size: 0.84rem; color: var(--ink-3); padding-block: var(--s4) 0; }
.crumbs ol { display: flex; flex-wrap: wrap; gap: var(--s2); list-style: none; margin: 0; padding: 0; }
.crumbs li + li::before { content: "/"; margin-right: var(--s2); color: var(--line-strong); }
.crumbs a { color: inherit; text-decoration: none; }
.crumbs a:hover { color: var(--c-primary-text); text-decoration: underline; }

.disclosure {
  font-size: 0.82rem; line-height: 1.55; color: var(--ink-3);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: var(--s3) var(--s4);
}
.disclosure strong { color: var(--ink-2); }
.disclosure--floating {
  position: fixed; right: var(--s4); bottom: var(--s4); z-index: 30;
  max-width: 320px; box-shadow: var(--shadow-3);
}

/* 11. Footer, cookie bar, sticky CTA ---------------------------------------- */

/* Footer columns are h2 for the outline's sake — a matrix page has no other
   heading between the title and here, and an h3 made it skip a level. They are
   sized as the small labels they look like. */
.footer__heading {
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 var(--s3);
}

.site-footer {
  background: var(--c-secondary); color: rgb(255 255 255 / 72%);
  padding-block: var(--s7) var(--s5); margin-top: var(--s8);
}
.site-footer a { color: rgb(255 255 255 / 88%); text-decoration: none; }
.site-footer a:hover { color: #fff; text-decoration: underline; }
.site-footer h3 { color: #fff; font-size: 0.78rem; letter-spacing: 0.09em; text-transform: uppercase; }

.footer-grid { display: grid; grid-template-columns: minmax(220px, 1.4fr) repeat(auto-fit, minmax(150px, 1fr)); gap: var(--s6); }
.footer-nav { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); font-size: 0.92rem; }
.footer-bottom {
  display: flex; flex-wrap: wrap; gap: var(--s3); justify-content: space-between;
  margin-top: var(--s6); padding-top: var(--s4);
  border-top: 1px solid rgb(255 255 255 / 12%);
  font-size: 0.84rem;
}
.social { display: flex; gap: var(--s3); }
.social a { display: inline-flex; width: 34px; height: 34px; align-items: center; justify-content: center; border: 1px solid rgb(255 255 255 / 18%); border-radius: var(--radius-sm); }

.cookie-bar {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 60;
  background: var(--surface); border-top: 1px solid var(--line);
  box-shadow: 0 -6px 24px rgb(18 24 38 / 10%);
  padding: var(--s4);
}
.cookie-bar__inner { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s4); justify-content: space-between; }
.cookie-bar__text { font-size: 0.875rem; color: var(--ink-2); max-width: 62ch; }
.cookie-bar--box {
  inset-inline: auto 0; max-width: 380px; margin: var(--s4);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.cookie-bar--box .cookie-bar__inner { flex-direction: column; align-items: stretch; }

/*
   Three things can be pinned to the bottom edge: the cookie notice, the promo
   bar and the sticky call to action. They stack rather than overlap — the
   notice sits at the floor because it is the one that must be answered, and
   --dock-h is the height a script measures for it (0 when it is absent, which
   is every visit after the first).
*/
.promo-bar {
  position: fixed; inset-inline: 0; bottom: var(--dock-h, 0px); z-index: 55;
  background: var(--c-secondary); color: #fff;
  border-top: 1px solid rgb(255 255 255 / 12%);
  box-shadow: 0 -6px 24px rgb(18 24 38 / 22%);
  padding: var(--s3) 0;
  padding-bottom: max(var(--s3), env(safe-area-inset-bottom));
}

.promo-bar__inner {
  display: flex; align-items: center; gap: var(--s4);
  flex-wrap: wrap;
}

.promo-bar__image { width: 76px; height: 30px; object-fit: contain; flex: none; }

.promo-bar__highlight {
  flex: none;
  padding: 0.3em 0.7em;
  font-size: 0.76rem; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase;
  color: #2a1c00; background: var(--c-accent);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

.promo-bar__body { flex: 1 1 320px; min-width: 0; }
.promo-bar__text { margin: 0; font-size: 0.95rem; font-weight: 600; line-height: 1.35; overflow-wrap: break-word; }
.promo-bar__note { margin: 2px 0 0; font-size: 0.8rem; color: rgb(255 255 255 / 66%); }

.promo-bar__cta { flex: none; }

.promo-bar__close {
  flex: none;
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.2rem; line-height: 1;
  color: rgb(255 255 255 / 70%);
  background: none; border: 1px solid rgb(255 255 255 / 22%);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.promo-bar__close:hover { color: #fff; border-color: rgb(255 255 255 / 45%); }

/* Reserves the bar's height at the end of the flow. JavaScript replaces these
   with the measured height; the values are the fallback when it does not run. */
.promo-bar__spacer { height: 74px; }

@media (max-width: 760px) {
  .promo-bar { padding-block: var(--s3); }
  .promo-bar__inner { gap: var(--s3); row-gap: var(--s2); }
  .promo-bar__body { flex-basis: 100%; order: 2; }
  .promo-bar__cta { order: 3; flex: 1 1 auto; }
  .promo-bar__close { order: 4; }
  .promo-bar__image, .promo-bar__highlight { order: 1; }
  .promo-bar__spacer { height: 132px; }
}

@media print {
  .promo-bar, .promo-bar__spacer { display: none !important; }
}

.sticky-cta {
  position: fixed; inset-inline: 0; bottom: var(--dock-h, 0px); z-index: 50;
  display: none;
  background: var(--surface); border-top: 1px solid var(--line);
  box-shadow: 0 -4px 18px rgb(18 24 38 / 10%);
  padding: var(--s3) var(--s4);
  padding-bottom: max(var(--s3), env(safe-area-inset-bottom));
}
.sticky-cta__inner { display: flex; align-items: center; gap: var(--s3); justify-content: space-between; }

/*
   min-width: 0 on every box the name sits inside.
   
   A flex item defaults to min-width: auto, which means it refuses to shrink
   below its content — so text-overflow never fires and a long product name
   pushes the button off the edge instead of ellipsising. The rule has to be
   on each box in the chain, not only the outermost one.
*/
.sticky-cta__brand { display: flex; align-items: center; gap: var(--s3); flex: 1 1 auto; min-width: 0; }
.sticky-cta__brand > * { min-width: 0; }
.sticky-cta__brand img { width: 76px; height: 30px; object-fit: contain; object-position: left center; flex: none; }
.sticky-cta__name { font-weight: 700; font-size: 0.92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }

/* The button is the point of the bar and never gives up room. */
.sticky-cta__inner > .btn, .sticky-cta .btn { flex: 0 0 auto; }

@media (max-width: 480px) {
  /* The logo is the first thing to go: the name identifies it too, and the
     button needs the room more than either. */
  .sticky-cta__brand img { display: none; }
}

/* 12. Responsive ------------------------------------------------------------ */

@media (max-width: 1080px) {
  .card__grid { grid-template-columns: minmax(130px, 170px) minmax(0, 1fr) minmax(160px, 190px); gap: var(--s4); }
}

@media (max-width: 900px) {
  .card__grid {
    grid-template-columns: minmax(0, 1fr) minmax(150px, 200px);
    grid-template-areas: "brand action" "body body";
  }
  .card__brand { grid-area: brand; }
  .card__body { grid-area: body; }
  .card__action { grid-area: action; }
  .hero--split .hero__inner { grid-template-columns: minmax(0, 1fr); }
  .cta-panel--split { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 760px) {
  :root { --header-h: 58px; }

  .nav-toggle { display: inline-flex; }

  .nav {
    display: none;
    position: absolute; top: 100%; inset-inline: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    padding: var(--s3);
    background: var(--surface); border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-3);
    max-height: calc(100dvh - var(--header-h)); overflow-y: auto;
  }
  .nav[data-open="1"] { display: flex; }
  .nav ul { flex-direction: column; align-items: stretch; gap: 0; }
  .nav a { padding: 0.8em 0.6em; }
  /* Stacked rather than floating, and closed until asked for. This forced
     display:flex, so every submenu was already open on a phone and the arrow
     had nothing left to do. */
  .nav__sub { position: static; display: none; box-shadow: none; border: 0; padding: 0 0 0 var(--s4); }
  .nav li[data-open] > .nav__sub { display: flex; }
  /*
   * A row, not a huddle. The link takes the width it needs and the arrow sits
   * at the right edge where a thumb expects it, at the full 44px target the
   * rest of the site's controls use. The panel then drops below both.
   */
  .nav > ul > li { display: flex; flex-wrap: wrap; align-items: center; }
  .nav > ul > li > a { flex: 1 1 auto; }
  .nav > ul > li > .nav__toggle { min-width: 44px; min-height: 44px; margin-inline-start: 0; }
  .nav > ul > li > .nav__sub { flex-basis: 100%; }

  .card { padding: var(--s4); }

  :is(.rank-item, .feature)[data-rank="corner"] .rank-num { min-width: 42px; height: 30px; font-size: 0.98rem; }
  .rank-item[data-rank="corner"] > .card { padding-top: calc(var(--s4) + 20px); }

  /* Less overhang near the viewport edge, where there is no gutter to spill
     into and the disc would be clipped. */
  :is(.rank-item, .feature)[data-rank="badge"] .rank-num { width: 36px; height: 36px; transform: translate(-18%, -22%); }
  .rank-item[data-rank="badge"] > .card { padding-top: calc(var(--s4) + 10px); }

  .card__rank-num { font-size: 1.8rem; }
    .card__grid { grid-template-columns: minmax(0, 1fr); grid-template-areas: "brand" "body" "action"; gap: var(--s4); }
  .card__logo { max-width: 160px; height: 50px; }
  .proscons { grid-template-columns: minmax(0, 1fr); }
  .score { grid-template-columns: minmax(74px, 96px) minmax(0, 1fr) 30px; }

  .sticky-cta { display: block; }
  body[data-sticky-cta="1"] { padding-bottom: 76px; }

  .footer-grid { grid-template-columns: minmax(0, 1fr); gap: var(--s5); }
  .disclosure--floating { position: static; max-width: none; box-shadow: none; }
}

@media (max-width: 420px) {
  .wrap { padding-inline: var(--s3); }
  .minimal-row__brand img { width: 84px; height: 32px; }
  .facts { gap: var(--s2) var(--s3); }
  .btn { font-size: 0.9rem; }
}

/* Landscape phones: keep the sticky bars from eating the viewport. */
@media (max-height: 460px) and (orientation: landscape) {
  .site-header[data-sticky="1"] { position: relative; }
  .sticky-cta { display: none; }
  body[data-sticky-cta="1"] { padding-bottom: 0; }
}

/* 13. Dark mode, motion, print ---------------------------------------------- */

[data-mode="dark"] {
  /* Derived against the dark paper rather than the light one: a colour dark
     enough to read on white disappears on near-black. */
  --c-primary-text: var(--c-primary-text-dark);
  --c-positive-text: var(--c-positive-text-dark);
  --c-negative-text: var(--c-negative-text-dark);
  --ink: #eef1f6;
  --ink-2: #b9c1d0;
  --ink-3: #8b93a5;
  --paper: #0d1119;
  --surface: #151b26;
  --line: #262f3f;
  --line-strong: #364155;
  --c-secondary: #0a0e15;
  --shadow-1: 0 1px 2px rgb(0 0 0 / 40%);
  --shadow-2: 0 4px 14px rgb(0 0 0 / 45%);
  --shadow-3: 0 12px 32px rgb(0 0 0 / 55%);
  color-scheme: dark;
  /* Triplets so translucent shades can use rgb(var(--x-rgb) / n%).
     color-mix() does the same but only from 2023, and an unsupported value
     makes the browser drop the whole declaration — on a header background
     that is white text on nothing. */
  --paper-rgb: 13 17 25;
  --surface-rgb: 21 27 38;
  --line-rgb: 38 47 63;
}
/* Before the script resolves "auto", let form controls and scrollbars follow
   the device on their own. */
[data-mode="auto"] { color-scheme: light dark; }

[data-mode="dark"] .btn--accent { --btn-fg: #1a1200; }
[data-mode="dark"] .badge--award { color: #ffd489; }
[data-mode="dark"] .site-footer { border-top: 1px solid var(--line); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .btn:hover { transform: none; }
}

@media print {
  .site-header, .site-footer, .cookie-bar, .sticky-cta, .card__action, .nav { display: none !important; }
  .card, .panel { border: 1px solid #ccc; box-shadow: none; break-inside: avoid; }
  body { background: #fff; }
}

/* 24. Banner ads and the sidebar column ------------------------------------ */

/*
 * A paid slot says so, and holds its own space. The label is not decoration:
 * a banner that reads as editorial is the one thing this project will not
 * ship. The reserved box comes from the chosen size as unitless numbers —
 * aspect-ratio cannot divide pixels — so the page is the same height before
 * and after the network answers, and nothing below it moves.
 */
.ad {
  /* Declared so the stylesheet documents what it reads; the real numbers
     arrive inline, one slot at a time, from the size the editor chose. */
  --ad-w: 0;
  --ad-h: 0;
  margin: var(--s5) 0;
}
.ad--left { text-align: left; }
.ad--center { text-align: center; }
.ad--right { text-align: right; }

.ad__label {
  margin: 0 0 var(--s2);
  font-size: 0.7rem;
  font-weight: 650;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.ad__slot { display: inline-block; max-width: 100%; }
.ad__slot img { display: block; max-width: 100%; height: auto; }
.ad[style*="--ad-w"] .ad__slot {
  width: min(calc(var(--ad-w) * 1px), 100%);
  aspect-ratio: var(--ad-w) / var(--ad-h);
}
.ad[style*="--ad-w"] .ad__slot > * { width: 100%; height: 100%; }

/*
 * The column appears only when the editor put something in it, so the grid
 * is applied by the view rather than sitting empty. Below the breakpoint the
 * sidebar becomes the last thing on the page instead of a squeezed column.
 */
.with-side { display: grid; gap: var(--s5); align-items: start; }
.side { display: flex; flex-direction: column; gap: var(--s5); min-width: 0; }

@media (min-width: 1100px) {
  .with-side { grid-template-columns: minmax(0, 1fr) 300px; }
  /* Sticky needs a scroll container taller than the column, which only a wide
     screen reliably has; on a narrow one it would pin a banner over the list. */
  .side--sticky { position: sticky; top: var(--s5); }
}

/* 25. Currency switcher ----------------------------------------------------- */

.currency { position: relative; display: inline-flex; }
.currency__select {
  appearance: none;
  padding: 6px 26px 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 4.5 6 8.5l4-4' fill='none' stroke='%23667' stroke-width='1.5'/%3E%3C/svg%3E") no-repeat right 8px center/10px;
  color: var(--ink);
  font: inherit;
  font-size: 0.86rem;
  font-weight: 650;
  cursor: pointer;
}
.currency__select:hover { border-color: var(--line-strong); }

/*
 * The warning appears where the choice was made, not at the top of the page:
 * a message about a control belongs beside the control. It leaves on its own
 * after a few seconds because it is a caveat, not an error.
 */
.currency-note {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 40;
  width: max(240px, 22rem);
  max-width: min(90vw, 26rem);
  margin: 0;
  padding: var(--s3) var(--s4);
  background: var(--secondary-surface, var(--c-secondary));
  color: #fff;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-2);
  font-size: 0.84rem;
  line-height: 1.45;
  opacity: 1;
  transition: opacity .5s ease;
}
.currency-note.is-going { opacity: 0; }

.money[data-money-converted] { white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .currency-note { transition: none; }
}

/* 26. Showcase -------------------------------------------------------------- */

/*
 * Two images and one destination. The pair sits side by side where there is
 * room and stacks where there is not, and each figure holds its shape before
 * the image arrives so the button below it does not move while the page loads.
 */
/*
 * The block sits in the content column like every other section — it used to
 * open with a bare div, so the panels ran to the screen edge while the heading
 * above them stopped at the column, and the pair swallowed the page. The pair
 * is narrower than the column again on purpose: two portrait panels at full
 * width are the whole screen and nothing else.
 */
.showcase { margin: 0; }
.showcase__heading { margin: 0 0 var(--s2); text-align: center; }
.showcase__text {
  margin: 0 auto var(--s5);
  max-width: 46rem;
  text-align: center;
  color: var(--ink-2);
}

.showcase__images {
  display: grid;
  gap: var(--s4);
  max-width: 820px;
  margin-inline: auto;
}
@media (min-width: 700px) {
  .showcase__images { grid-template-columns: 1fr 1fr; }
}

.showcase__figure {
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
}

/* One shape for both panels, so two images that were never the same
   proportion still line up, and the image is cropped rather than squashed. */
.showcase__images--portrait .showcase__figure { aspect-ratio: 4 / 5; }
.showcase__images--square .showcase__figure { aspect-ratio: 1 / 1; }
.showcase__images--landscape .showcase__figure { aspect-ratio: 16 / 10; }

/* A lone panel centres itself rather than sitting in the left half of a
   two-column grid, waiting for a partner that was never added. */
.showcase__figure:only-child { grid-column: 1 / -1; max-width: 420px; margin-inline: auto; }

.showcase__figure img { display: block; width: 100%; height: 100%; object-fit: cover; }

.showcase__action { margin-top: var(--s5); text-align: center; }
.showcase__note { margin: var(--s2) 0 0; font-size: 0.86rem; color: var(--ink-2); }

/* The lift belongs to the surface that is actually clickable, and only once
   the script has wired it — the same rule the cards follow. */
.showcase--clickable[data-card-ready] { border-radius: var(--radius); transition: transform 0.15s ease; }
.showcase--clickable[data-card-ready]:hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .showcase--clickable[data-card-ready] { transition: none; }
  .showcase--clickable[data-card-ready]:hover { transform: none; }
}

/* 27. Destination warning --------------------------------------------------- */

.showcase__warn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  margin: var(--s3) 0 0;
  font-size: 0.84rem;
  color: var(--ink-2);
}
.showcase__warn-mark {
  padding: 2px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: var(--ink);
}

/*
 * A native dialog, so the browser handles the focus trap, the Escape key and
 * the backdrop rather than a hand-rolled imitation that gets one of the three
 * wrong. It is the same width as a paragraph of text on a phone.
 */
.leaving {
  width: min(30rem, calc(100vw - 2rem));
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-2);
}
.leaving::backdrop { background: rgb(0 0 0 / 55%); }
.leaving__title { margin: 0 0 var(--s2); font-size: 1.15rem; }
.leaving__text { margin: 0 0 var(--s5); color: var(--ink-2); line-height: 1.5; }
.leaving__actions { display: flex; flex-wrap: wrap; gap: var(--s3); justify-content: flex-end; }

/* 28. Accessibility toolbar -------------------------------------------------- */

/*
 * The launcher sits above the offer bar rather than on top of it, using the
 * height the dock already publishes for exactly this reason.
 */
.a11y { position: fixed; bottom: calc(var(--dock-h, 0px) + var(--s4)); z-index: 60; }
.a11y--right { right: var(--s4); }
.a11y--left { left: var(--s4); }

.a11y__button {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-2);
  cursor: pointer;
}
.a11y__button:hover { background: var(--paper); }

.a11y__panel {
  position: absolute;
  bottom: calc(100% + var(--s3));
  width: min(19rem, calc(100vw - 2rem));
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-2);
}
.a11y--right .a11y__panel { right: 0; }
.a11y--left .a11y__panel { left: 0; }
.a11y__panel[hidden] { display: none; }

.a11y__title { margin: 0 0 var(--s3); font-size: 1rem; }
.a11y__group { margin-top: var(--s3); }
.a11y__legend { display: block; margin-bottom: var(--s2); font-size: 0.82rem; color: var(--ink-2); }
.a11y__steps { display: flex; align-items: center; gap: var(--s2); }
.a11y__step {
  flex: 1;
  padding: 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.a11y__step:hover { background: var(--paper); }
.a11y__value { min-width: 3.4rem; text-align: center; font-size: 0.86rem; color: var(--ink-2); }

.a11y__list { list-style: none; margin: var(--s4) 0 0; padding: 0; display: grid; gap: var(--s2); }
.a11y__option {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
}
.a11y__option:hover { background: var(--paper); }
.a11y__option[aria-pressed="true"] {
  border-color: var(--c-primary);
  background: rgb(var(--c-primary-rgb) / 10%);
  color: var(--c-primary-text);
  font-weight: 650;
}
.a11y__option svg { width: 18px; height: 18px; flex: none; }

.a11y__reset { margin-top: var(--s3); }
.a11y__note { margin: var(--s3) 0 0; font-size: 0.78rem; line-height: 1.45; color: var(--ink-2); }

/*
 * What the controls actually do. Each is a class on the document element, so
 * the whole page answers at once — and because this theme is built from
 * variables, higher contrast is a change of palette rather than a pile of
 * overrides fighting the stylesheet from outside.
 */
html.a11y-contrast {
  --ink: #000;
  --ink-2: #1a1a1a;
  --paper: #fff;
  --surface: #fff;
  --line: #333;
  --line-strong: #000;
  --c-primary-text: #00379e;
}
html.a11y-contrast [data-mode="dark"],
[data-mode="dark"] html.a11y-contrast {
  --ink: #fff;
  --ink-2: #f0f0f0;
  --paper: #000;
  --surface: #000;
  --line: #ccc;
  --line-strong: #fff;
}

html.a11y-links a { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 2px; }

html.a11y-spacing body { line-height: 1.85; letter-spacing: 0.02em; word-spacing: 0.14em; }
html.a11y-spacing p { margin-bottom: 1.4em; }

html.a11y-still *,
html.a11y-still *::before,
html.a11y-still *::after {
  animation: none !important;
  transition: none !important;
  scroll-behavior: auto !important;
}

html.a11y-cursor,
html.a11y-cursor * {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 24 24'%3E%3Cpath d='M5 2l14 9-6 1.4L10 20z' fill='%23fff' stroke='%23000' stroke-width='1.6' stroke-linejoin='round'/%3E%3C/svg%3E") 4 2, auto;
}
