/* Asolloc — Rose Vault design system. All-dark, elevated cards. Mobile-first, single file. */

/* Space Grotesk, self-hosted.
   It used to come from fonts.googleapis.com, which is a render-blocking
   cross-origin stylesheet that then triggers a second connection to
   fonts.gstatic.com for the file itself — measured at ~280ms on the live site
   before the first byte of font data even moves. Served from /assets it rides
   the same connection as the page and gets the 1-year immutable cache.
   It is a VARIABLE font: one file covers 500-700, hence the weight range.
   unicode-range means a browser only downloads the subset it actually needs. */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url('../fonts/space-grotesk-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

/* ============================================================
   TOKENS
   One palette, one spacing scale, one radius scale. Every colour
   below is semantic (--surface, --ink, --brand) rather than named
   after its hue, so swapping the palette is this block alone.
   ============================================================ */
:root {
  /* Rose Vault */
  /* Lifted off near-black. This base sits at 0.0069 luminance, above the
     ~0.006 floor recommended for dark UI (the #121212 reference), below which
     bright text feels harsh. Each step up is wide, so elevation reads through
     surface colour rather than shadows — which barely register on a near-black
     ground. */
  --bg: #1a1210;
  --surface: #261a18;
  --raised: #33221f;
  --sunk: #110b0a;
  --line: rgba(255, 255, 255, .07);
  --line-str: rgba(251, 113, 133, .30);

  --ink: #fdf1f2;
  --soft: #b7a09f;
  /* Measured on these backgrounds: 4.68:1 on --raised (worst case) up to
     5.23:1 on --surface — clears the 4.5:1 AA minimum for the small labels it
     is used on. */
  --faint: #a3898a;

  --brand: #fb7185;
  --brand-2: #e879f9;
  --brand-deep: #be123c;
  /* Text that sits ON the accent. Dark, because #fb7185 and #e879f9 are
     both too light to carry white. Dark ink on them reads 7.3:1 and 8.0:1. */
  --brand-ink: #1a0509;
  --brand-soft: rgba(251, 113, 133, .10);
  --grad: linear-gradient(100deg, var(--brand), var(--brand-2));
  --halo: rgba(251, 113, 133, .11);

  /* The podium keeps a warm frame so rank 1-3 read apart from the accent,
     which is doing CTA duty everywhere else. */
  --gold: #f5c451;
  --gold-soft: rgba(245, 196, 81, .10);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Spacing scale. The old sheet used a flat 56px everywhere, which is why
     a hero and an FAQ had identical breathing room. */
  --s1: 8px;
  --s2: 16px;
  --s3: 28px;
  --s4: 48px;
  --s5: 80px;

  --r-sm: 10px;
  --r: 16px;
  --r-lg: 20px;
  --shadow: 0 1px 2px rgba(0, 0, 0, .28);
  --shadow-lg: 0 14px 36px rgba(0, 0, 0, .34);

  /* ---- Theme roles ----
     The accent has TWO jobs. As a FILL (buttons, badges, progress bar) the
     bright rose works on any background. As TEXT or a thin UI mark (links,
     icons, focus rings, the active underline) it only works on dark — on white
     #fb7185 is too light. So text uses --brand-text, which is the same bright
     rose here and a darker one in the light theme. Same for gold. */
  color-scheme: dark;
  --brand-text: var(--brand);
  --grad-text: var(--grad);
  --gold-text: var(--gold);
  --nav-bg: rgba(26, 18, 16, .88);
  --hero-wash: linear-gradient(180deg, rgba(26, 18, 16, .62) 0%, rgba(26, 18, 16, .86) 68%, var(--surface) 100%);
  --fab-shadow: 0 8px 24px rgba(0, 0, 0, .40);
  /* Which icon the theme toggle shows: the sun offers "go light". */
  --icon-sun: block;
  --icon-moon: none;
  --wm-a: #fb7185;
  --wm-b: #e879f9;
}

/* ============================================================
   LIGHT THEME
   Every value below was chosen by measured contrast, not by eye: text passes
   AA on the worst light surface it can land on (#f3ebec, the footer) — ink
   16.0:1, soft 8.9, faint 6.4, brand-text 5.4, gold-text 4.5. The CTA fills
   and the gold badge are NOT redefined; their own dark text reads 9.8:1 and
   10.7:1 on either theme, so the buttons stay identical across both.

   Two routes to the same tokens (hence the list appears twice):
   - no stored choice  -> follow the OS/browser via prefers-color-scheme;
   - a stored choice   -> header.html sets data-theme on <html> before paint.
   :not([data-theme="dark"]) lets an explicit "dark" win over a light OS.
   ============================================================ */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
  color-scheme: light;
  --bg: #faf6f6;
  --surface: #ffffff;
  --raised: #fdf9f9;
  --sunk: #f3ebec;
  --line: rgba(40, 12, 18, .10);
  --line-str: rgba(190, 18, 60, .32);
  --ink: #1f0c11;
  --soft: #52393e;
  --faint: #6a4d52;
  --brand-soft: rgba(225, 29, 72, .11);
  --halo: rgba(225, 29, 72, .12);
  --gold-soft: rgba(245, 196, 81, .16);
  --shadow: 0 1px 2px rgba(40, 16, 22, .06);
  --shadow-lg: 0 14px 36px rgba(40, 16, 22, .11);
  --brand-text: #be123c;
  --grad-text: linear-gradient(100deg, #be123c, #a21caf);
  --gold-text: #92620a;
  --nav-bg: rgba(250, 246, 246, .86);
  --hero-wash: linear-gradient(180deg, rgba(250, 246, 246, .70) 0%, rgba(250, 246, 246, .90) 68%, var(--surface) 100%);
  --fab-shadow: 0 8px 22px rgba(40, 16, 22, .14);
  --icon-sun: none;
  --icon-moon: block;
  --wm-a: #be123c;
  --wm-b: #a21caf;
  }
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #faf6f6;
  --surface: #ffffff;
  --raised: #fdf9f9;
  --sunk: #f3ebec;
  --line: rgba(40, 12, 18, .10);
  --line-str: rgba(190, 18, 60, .32);
  --ink: #1f0c11;
  --soft: #52393e;
  --faint: #6a4d52;
  --brand-soft: rgba(225, 29, 72, .11);
  --halo: rgba(225, 29, 72, .12);
  --gold-soft: rgba(245, 196, 81, .16);
  --shadow: 0 1px 2px rgba(40, 16, 22, .06);
  --shadow-lg: 0 14px 36px rgba(40, 16, 22, .11);
  --brand-text: #be123c;
  --grad-text: linear-gradient(100deg, #be123c, #a21caf);
  --gold-text: #92620a;
  --nav-bg: rgba(250, 246, 246, .86);
  --hero-wash: linear-gradient(180deg, rgba(250, 246, 246, .70) 0%, rgba(250, 246, 246, .90) 68%, var(--surface) 100%);
  --fab-shadow: 0 8px 22px rgba(40, 16, 22, .14);
  --icon-sun: none;
  --icon-moon: block;
  --wm-a: #be123c;
  --wm-b: #a21caf;
}

/* ============================================================
   CROSS-PAGE VIEW TRANSITIONS
   The site is multi-page static HTML; this makes navigating between
   categories cross-fade instead of white-flashing. Browsers without
   support ignore the whole at-rule, so there is nothing to fall back to.
   The nav, footer and back-to-top button are given their own transition
   names so they stay PUT across the navigation rather than fading with
   the page — they are identical on both sides of it.
   ============================================================ */
@view-transition { navigation: auto; }
.rk-nav { view-transition-name: rk-nav; }
.site-footer { view-transition-name: site-footer; }
.ft-top { view-transition-name: ft-top; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: .3s; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

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

/* Tabular (fixed-width) figures wherever numbers stack in a column: scores,
   prices, ranks, stats. With proportional figures "9.9" and "8.4" are
   different widths, so a column of scores visibly wobbles and decimal points
   do not line up. */
.oc-score-num, .oc-price, .rank-num, .stat-num, .hero-count b {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* No scroll-behavior here on purpose: Lenis drives smooth scrolling and the
   two fight each other if both are on. The reduced-motion block at the bottom
   re-asserts `scroll-behavior: auto`, and cards.js skips Lenis entirely in that
   case, so a reduced-motion visitor gets plain instant jumps from both sides. */
html { -webkit-text-size-adjust: 100%; }

/* Required by Lenis. */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }

/* Scrollbar — thin, in-palette.
   Two mechanisms, because no single one is supported everywhere: Firefox reads
   the `scrollbar-*` shorthands, WebKit/Blink read the ::-webkit pseudo-elements.
   The thumb sits on --brand-deep rather than --brand so it reads as chrome
   instead of competing with the CTAs, and brightens to --brand on hover.
   The `.rk-cats` / `.rk-results` hide-rules below are more specific and still win,
   so the category strip keeps its hidden scrollbar. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--brand-deep) var(--bg);
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb {
  background: var(--brand-deep);
  border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover { background: var(--brand); }
::-webkit-scrollbar-corner { background: var(--bg); }
body {
  /* Reserves the header's EXPANDED height. cards.js measures the real value
     into --nav-h; these fallbacks match the measured heights so the first
     paint does not jump before the script runs. */
  padding-top: var(--nav-h, 118px);
  margin: 0;
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.62;
  color: var(--ink);
  background: var(--bg);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}
img, svg { max-width: 100%; height: auto; vertical-align: middle; }
a { color: inherit; }
h1, h2, h3, h4 { line-height: 1.15; margin: 0 0 .6em; font-family: var(--font-display); letter-spacing: -.025em; }
p { margin: 0 0 1em; }

.container { width: 100%; max-width: 1100px; margin: 0 auto; padding: 0 20px; }
.container.narrow { max-width: 800px; }

/* Present for screen readers and the document outline, invisible on screen.
   Not display:none — that would remove it from the accessibility tree too. */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

.grad {
  background: var(--grad-text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ============================================================
   HEADER / NAV
   ============================================================ */
.rk-nav {
  /* FIXED, not sticky. A sticky element keeps its slot in document flow, so
     folding the category strip away shrank that slot and pulled every section
     below it up by ~45px. Chrome's scroll anchoring then corrected scrollY,
     Lenis fought the correction, and the progress bar jittered — the flicker
     on "Today's deals". Fixed takes the header out of flow entirely; the body
     reserves its expanded height instead (--nav-h, measured by cards.js), so
     collapsing it can no longer move the page. */
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  background: var(--nav-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}
.rk-nav__inner { max-width: 1100px; margin: 0 auto; padding: 0 20px; }
.rk-nav__top { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2); padding: 13px 0; }

.rk-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--ink); flex: none; }
.rk-logo__mark {
  width: 34px; height: 34px; border-radius: var(--r-sm);
  background: var(--grad); color: var(--brand-ink);
  display: grid; place-items: center;
  font-family: var(--font-display); font-weight: 700; font-size: 14px;
}
.rk-logo__text { display: block; }
.rk-logo__name { display: block; font-family: var(--font-display); font-weight: 700; font-size: 16.5px; letter-spacing: -.01em; }
.rk-logo__sub { display: block; font-size: 11px; color: var(--faint); font-weight: 500; }

.rk-search { position: relative; flex: 1; min-width: 0; max-width: 460px; margin: 0 auto; }
.rk-search__icon { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: var(--faint); display: flex; }
.rk-search__input {
  width: 100%;
  padding: 10px 46px 10px 36px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
}
.rk-search__input::placeholder { color: var(--faint); }
.rk-search__input:focus { outline: none; border-color: var(--line-str); box-shadow: 0 0 0 3px var(--brand-soft); }
.rk-search__key {
  position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
  font-size: 11px; color: var(--faint);
  border: 1px solid var(--line); border-radius: 5px; padding: 2px 6px;
}

.rk-nav__right { display: flex; align-items: center; gap: 14px; flex: none; }
/* Hidden until 992px — it is nowrap and would blow the bar out on a phone. */
.rk-trust { display: none; margin: 0; font-size: 12.5px; color: var(--soft); align-items: center; gap: 7px; white-space: nowrap; }
.rk-trust strong { color: var(--ink); }
.rk-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); flex: none; }
.rk-cta {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 17px; border-radius: 999px;
  background: var(--ink); color: var(--bg);
  text-decoration: none; font-size: 13.5px; font-weight: 650; white-space: nowrap;
  transition: opacity .18s;
}
.rk-cta:hover { opacity: .88; }

/* Theme toggle. Shows the theme you would switch TO: a sun on dark, a moon on
   light — driven by the --icon-sun / --icon-moon tokens, so it is right on
   first paint whether the theme came from the OS or a saved choice. */
.rk-theme {
  flex: none;
  display: grid; place-items: center;
  width: 38px; height: 38px; padding: 0;
  border: 1px solid var(--line); border-radius: 50%;
  background: transparent; color: var(--soft);
  cursor: pointer;
  transition: color .2s, border-color .2s, background .2s, transform .15s;
}
.rk-theme:hover { color: var(--brand-text); border-color: var(--line-str); background: var(--brand-soft); }
.rk-theme:focus-visible { outline: 2px solid var(--brand-text); outline-offset: 2px; }
.rk-theme:active { transform: scale(.92); }
.rk-theme__sun { display: var(--icon-sun); }
.rk-theme__moon { display: var(--icon-moon); }
.rk-theme svg { transition: transform .45s cubic-bezier(.4, 0, .2, 1); }
.rk-theme:hover svg { transform: rotate(20deg); }
.rk-cta__arrow { transition: transform .18s; }
.rk-cta:hover .rk-cta__arrow { transform: translateX(2px); }

/* Category strip.
   One row, always — 13 pills will not fit across any realistic viewport, and
   letting them wrap produced a ragged second row with three items on it. So
   the strip scrolls horizontally at every size, with fades and arrows to
   signal there is more. The scrollbar itself stays hidden; the fades do that
   job without eating vertical space. */
/* max-height rather than display, because `display` cannot be transitioned —
   toggling it made the strip snap in and out and the sticky bar's height jump
   with it. 64px clears a single row of pills with room to spare; the strip is
   always exactly one row, so there is no taller state to account for. */
.rk-catbar {
  position: relative;
  overflow: hidden;
  max-height: 64px;
  border-top: 1px solid var(--line);
  transition: max-height .32s cubic-bezier(.4, 0, .2, 1),
              opacity .22s ease,
              border-top-color .32s ease;
}
.rk-cats {
  display: flex; flex-wrap: nowrap; gap: 2px;
  overflow-x: auto; scrollbar-width: none;
  scroll-padding-inline: 20px;
}
.rk-cats::-webkit-scrollbar { display: none; }

/* Edge fades, shown only on the side that actually has more content.
   pointer-events:none so they never block a pill underneath them. */
.rk-catbar::before, .rk-catbar::after {
  content: "";
  position: absolute; top: 0; bottom: 0; width: 56px;
  pointer-events: none; z-index: 2;
  opacity: 0; transition: opacity .2s;
}
.rk-catbar::before { left: 0; background: linear-gradient(90deg, var(--bg), transparent); }
.rk-catbar::after { right: 0; background: linear-gradient(270deg, var(--bg), transparent); }
.rk-catbar.has-prev::before, .rk-catbar.has-next::after { opacity: 1; }

/* Arrows are a pointer affordance; a touch user just swipes, so they only
   appear where a real hover pointer exists. */
.rk-catnav { display: none; }
@media (hover: hover) and (pointer: fine) {
  .rk-catnav {
    position: absolute; top: 50%; transform: translateY(-50%);
    z-index: 3;
    display: grid; place-items: center;
    width: 28px; height: 28px; padding: 0;
    border: 1px solid var(--line); border-radius: 50%;
    background: var(--surface); color: var(--soft);
    cursor: pointer;
    transition: color .18s, border-color .18s, background .18s, opacity .2s;
  }
  .rk-catnav[hidden] { display: none; }
  .rk-catnav:hover { color: var(--brand-text); border-color: var(--line-str); background: var(--raised); }
  .rk-catnav--prev { left: 2px; }
  .rk-catnav--next { right: 2px; }
}
.rk-cat {
  padding: 11px 13px;
  font-size: 13px; color: var(--soft); text-decoration: none; white-space: nowrap;
  border-bottom: 2px solid transparent;
  transition: color .18s, border-color .18s;
}
.rk-cat:hover { color: var(--ink); }
.rk-cat--active { color: var(--ink); font-weight: 650; }
.rk-cat { position: relative; }
/* The underline is its own element, rendered by build.js inside the active pill
   so it is in place on first paint with no JS. Two motions:
   - on hover, cards.js slides/stretches it under the hovered pill (transform
     only — translateX + scaleX from a left origin), and back on leave;
   - across a page change, its view-transition-name lets the browser MORPH it
     from the old category's pill to the new one instead of jumping. */
.rk-cat__ink {
  position: absolute; left: 0; right: 0; bottom: -2px;
  height: 2px; border-radius: 2px;
  background: var(--brand-text);
  transform-origin: left center;
  transition: transform .35s cubic-bezier(.4, 0, .2, 1);
  view-transition-name: rk-ink;
  pointer-events: none;
}
::view-transition-group(rk-ink) { animation-duration: .38s; animation-timing-function: cubic-bezier(.4, 0, .2, 1); }
/* Inset outline: the strip is an overflow:hidden scroller, so a default
   outline-offset would be clipped on the pill at either edge. */
.rk-cat:focus-visible { outline: 2px solid var(--brand-text); outline-offset: -2px; border-radius: 6px; }

/* Scroll progress: the sticky bar's own underline fills as the page is read.
   Driven by the rAF scroll handler already in cards.js — no second listener —
   which writes --scroll-progress as a 0-1 fraction. No transition on purpose:
   the bar should track the scroll exactly, not lag behind it. That also means
   the reduced-motion blanket rule has nothing to switch off here, since this
   maps position rather than animating on its own. */
.rk-progress {
  position: absolute; left: 0; right: 0; bottom: -1px;
  height: 3px; overflow: hidden;
  pointer-events: none; z-index: 3;
  /* Hidden until cards.js has written a real value. Without this the bar
     painted at 0 on every page load and then snapped to the scroll position —
     on a mid-page landing (a #hash link) that read as a flash. */
  opacity: 0;
  transition: opacity .2s ease;
}
.rk-nav.is-ready .rk-progress { opacity: 1; }
.rk-progress span {
  display: block; height: 100%; width: 100%;
  background: var(--grad);
  transform: scaleX(var(--scroll-progress, 0));
  transform-origin: 0 50%;
  will-change: transform;
}

/* Scrolled: the category strip folds away, identity row stays */
/* Collapsed while reading. Not `display:none` — see above. pointer-events and
   visibility are switched off so the hidden pills cannot be clicked or tabbed
   into while the strip is folded away. */
.rk-nav.is-scrolled .rk-catbar {
  max-height: 0;
  opacity: 0;
  border-top-color: transparent;
  pointer-events: none;
}
.rk-nav.is-scrolled .rk-catbar .rk-cat,
.rk-nav.is-scrolled .rk-catbar .rk-catnav { visibility: hidden; }

/* Search suggestions */
.rk-results {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0;
  list-style: none; margin: 0; padding: 6px;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow-lg);
  max-height: 60vh; overflow-y: auto;
  z-index: 60;
}
.rk-results[hidden] { display: none; }
/* The class sits on the <a> itself (cards.js renders <li><a class="rk-result">),
   so these style the link directly. They used to target `.rk-result a` — a
   link INSIDE .rk-result, which never exists — and the rows rendered with no
   layout or padding. */
.rk-result {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px; border-radius: var(--r-sm);
  text-decoration: none; color: var(--ink); font-size: 14px;
}
.rk-result.is-active, .rk-result:hover { background: var(--brand-soft); }
/* Rows cascade in, 22ms apart; --i is the row index set by cards.js. */
@keyframes rk-row-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.rk-results > li { animation: rk-row-in .22s cubic-bezier(.4, 0, .2, 1) both; animation-delay: calc(var(--i, 0) * 22ms); }
/* The part of the name that matched what was typed. */
.rk-result__name mark { background: none; color: var(--brand-text); font-weight: 700; }
.rk-result__name { font-weight: 600; }
.rk-result__mark { color: var(--brand-text); font-style: normal; }
.rk-result__cat { margin-left: auto; font-size: 11.5px; color: var(--faint); }
.rk-results__msg { padding: 13px; color: var(--faint); font-size: 13.5px; text-align: center; }

/* Anchor targets clear the sticky bar */
:target { scroll-margin-top: calc(var(--nav-h, 118px) + 12px); }

/* ============================================================
   SECTIONS
   .section-dark and .section-light both exist in the templates.
   The page is one dark surface now, so they differ by elevation
   rather than by inverting to a light panel — that inversion was
   what made the old page visually restart halfway down.
   ============================================================ */
.section-dark { background: var(--surface); color: var(--ink); position: relative; }
.section-light { background: var(--bg); color: var(--ink); padding: var(--s5) 0; }
.section-light + .section-light { padding-top: 0; }
.section-title { font-size: 30px; font-weight: 700; color: var(--ink); text-align: center; margin: 0 0 10px; }
.section-sub { text-align: center; color: var(--soft); max-width: 52ch; margin: 0 auto var(--s4); font-size: 15.5px; }

/* ============================================================
   HERO
   ============================================================ */
.hero {
  padding: var(--s5) 0 72px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  position: relative;
  overflow: hidden;
}
/* Aurora: two soft blobs drifting behind the headline. Only `transform` is
   animated, so both layers stay on the compositor and cost essentially no
   CPU — animating background-position or the gradient itself would repaint
   the hero every frame. The reduced-motion block at the bottom stops the
   drift; the colour stays, so the hero never looks unfinished. */
.hero::before, .hero::after {
  content: "";
  position: absolute; pointer-events: none;
  border-radius: 50%;
  will-change: transform;
}
.hero::before {
  inset: -30% 40% 20% -25%;
  background: radial-gradient(closest-side, var(--halo), transparent 72%);
  animation: aurora-a 26s ease-in-out infinite alternate;
}
.hero::after {
  inset: -20% -25% 10% 45%;
  background: radial-gradient(closest-side, rgba(34, 211, 238, .10), transparent 72%);
  animation: aurora-b 32s ease-in-out infinite alternate;
}
@keyframes aurora-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(9%, 7%, 0) scale(1.18); }
}
@keyframes aurora-b {
  from { transform: translate3d(0, 0, 0) scale(1.1); }
  to   { transform: translate3d(-11%, 5%, 0) scale(.94); }
}
.hero > .container { position: relative; z-index: 2; }
.hero-compact { padding: var(--s4) 0 var(--s4); }

.hero-grid { display: grid; grid-template-columns: 1fr; gap: var(--s4); align-items: center; }
.hero-copy { min-width: 0; }
.hero-title { font-size: clamp(34px, 5.4vw, 56px); font-weight: 700; color: var(--ink); line-height: 1.05; letter-spacing: -.032em; margin: 0 0 var(--s2); }
.hero-sub { font-size: 18px; color: var(--soft); max-width: 58ch; margin: 0 0 var(--s3); }
.hero-count { color: var(--soft); font-size: 14px; margin: 0; }
.hero-count b { color: var(--brand-text); }
.hero-badge {
  display: inline-block; padding: 6px 15px; border-radius: 999px;
  border: 1px solid var(--line-str); background: var(--brand-soft); color: var(--brand-text);
  font-size: 11.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  margin-bottom: var(--s3);
}
.hero-ticks, .tick-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 10px var(--s3);
}
.hero-ticks svg { color: var(--brand-text); flex: none; }
.hero-ticks li, .tick-list li {
  display: inline-flex; align-items: center; gap: 8px;
  color: var(--soft); font-size: 14px; font-weight: 600;
}

/* Hero visual: photo card, agent bubble, rating pill */
.hero-visual { position: relative; max-width: 480px; margin: 0 auto; }
.hero-photo-card {
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
}
.hero-photo-card img { display: block; width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; }
.agent-bubble {
  position: absolute; left: -10px; bottom: -18px;
  display: flex; align-items: center; gap: 10px;
  max-width: 290px;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--r);
  border-bottom-left-radius: 4px;
  padding: 11px 14px;
  box-shadow: var(--shadow-lg);
  color: var(--ink);
  font-size: 12.5px; line-height: 1.4;
}
.agent-avatar {
  flex: none; width: 40px; height: 40px; border-radius: 50%;
  background-size: 220%; background-position: 50% 12%;
  border: 2px solid var(--brand);
}
.agent-avatar-emoji {
  display: flex; align-items: center; justify-content: center;
  font-size: 20px; line-height: 1;
  background: var(--brand-soft);
  border-color: var(--brand);
}
.agent-text b { display: block; font-size: 13px; margin-bottom: 1px; }
.rating-pill {
  position: absolute; top: -12px; right: -6px;
  background: var(--grad); color: var(--brand-ink);
  font-weight: 700; font-size: 13px;
  padding: 8px 16px; border-radius: 999px;
  box-shadow: 0 10px 30px rgba(251, 113, 133, .28);
}

/* Category with no photo: bubble and pill lay out inline instead of
   floating against a photo card that is not there. */
.hero-grid-nophoto .hero-visual {
  position: static;
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
  max-width: none; margin: 6px 0 0;
}
.hero-grid-nophoto .agent-bubble,
.hero-grid-nophoto .rating-pill { position: static; margin: 0; }
.hero-grid-nophoto .agent-bubble { max-width: 340px; }

/* Category hero photo layer */
.hero.has-photo { position: relative; overflow: hidden; }
.hero.has-photo .hero-photo {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  opacity: .20; filter: saturate(.85);
}
.hero.has-photo::before {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  background: var(--hero-wash);
}
.hero.has-photo .container { position: relative; z-index: 2; }

/* ============================================================
   OFFER CARDS
   One row layout: rank | media | body | score | CTA, with an
   optional price strip and (card #1 only) an assistant bar.
   Mobile-first stack; the full row starts at 992px.
   ============================================================ */
.offer-list { display: grid; grid-template-columns: 1fr; gap: var(--s2); align-items: stretch; }
.offer-list.spotlight { max-width: 760px; margin: 0 auto; }
.offer-card[hidden] { display: none !important; }

.offer-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  transition: transform .22s, box-shadow .22s, border-color .22s, background .22s;
}
.offer-card:hover {
  transform: translateY(-2px);
  background: var(--raised);
  border-color: var(--line-str);
  box-shadow: var(--shadow-lg);
}
.oc-inner { position: relative; padding: var(--s3); isolation: isolate; }
/* Cursor spotlight: a soft glow that follows the pointer across the card.
   isolation + z-index:-1 keeps it BEHIND the card's content (an absolutely
   positioned layer would otherwise paint over the text). --mx/--my come from
   cards.js; pointer devices only. */
@media (hover: hover) and (pointer: fine) {
  .oc-inner::before {
    content: "";
    position: absolute; inset: 0; z-index: -1;
    border-radius: calc(var(--r) - 1px);
    background: radial-gradient(380px circle at var(--mx, 50%) var(--my, 50%), rgba(251, 113, 133, .075), transparent 45%);
    opacity: 0;
    transition: opacity .3s ease;
    pointer-events: none;
  }
  .offer-card:hover .oc-inner::before { opacity: 1; }
}

/* --- top bar --- */
/* Copy is written by cards.js from DOM position, so it stays correct
   after the shuffle. Empty until then — no reserved gap. */
/* The bar used to be a full-width row whose only occupant was the rank
   number, which left a tall void across the top-left of every card. It now
   carries just the kicker/flag text — which is empty except on a pinned card,
   where it is the paid-placement disclosure — and collapses when unused. */
.oc-bar { display: flex; align-items: center; gap: 10px; }
/* Hidden by default, NOT via :empty — :empty looks at child nodes and ignores
   ::after, and the pinned paid-placement disclosure is delivered entirely
   through ::after on these two spans. Keying off .pinned keeps it visible. */
.oc-kicker, .oc-flag { display: none; }
.offer-card.pinned .oc-kicker,
.offer-card.pinned .oc-flag { display: inline; }
.offer-card.pinned .oc-bar { margin-bottom: var(--s2); }

/* Rank moved onto the media as a corner badge: it reads as a position marker
   rather than floating text, and it reclaims the row it used to occupy. */
.offer-rank {
  /* Inset and fully rounded: flush in the corner it straddled the card's
     border and read as a tab stuck on the outside. */
  position: absolute; top: 12px; left: 12px; z-index: 3;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 28px; height: 26px; padding: 0 9px;
  border-radius: 999px;
  background: var(--raised);
  border: 1px solid var(--line);
}
.rank-num {
  font-family: var(--font-display); font-size: 15px; font-weight: 700;
  color: var(--soft); letter-spacing: -.02em; line-height: 1;
}
.top-choice .offer-rank { background: var(--gold); border-color: transparent; }
.top-choice .rank-num { color: #23180a; }
.oc-kicker { font-size: 11.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--brand-text); }
.oc-flag { margin-left: auto; font-size: 11.5px; color: var(--faint); }

/* --- grid --- */
/* Phones: a small square thumbnail beside the brand name. Full-width media
   made a single card ~800px tall and blew small logos up past their real
   resolution. The 640px+ rules below restore the wider media. */
.oc-grid { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 14px var(--s2); align-items: start; }
.oc-media { grid-column: 1; }
.oc-body { grid-column: 2; }
.oc-score, .oc-cta { grid-column: 1 / -1; }

/* --- media --- */
/* The frame owns the aspect ratio, not the image, so a 404 leaves a
   correctly sized plaque rather than collapsing the row. */
.oc-media { min-width: 0; }
.oc-media-inner {
  position: relative;
  width: 100%; aspect-ratio: 4 / 3;
  border-radius: 12px; overflow: hidden;
  background: var(--raised);
  border: 1px solid var(--line);
  display: grid; place-items: center;
}
.oc-media-inner img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A logo shows whole on a light plaque instead of being cropped, and is NEVER
   enlarged past its own pixels: width/height auto with a cap, rather than
   100% + object-fit, which stretched a 32px favicon across a 128px frame. */
.oc-media-inner.is-logo { background: #fff; padding: 10px; }
.oc-media-inner.is-logo img {
  object-fit: contain;
  width: auto; height: auto;
  max-width: 100%; max-height: 100%;
}
.oc-media-inner.is-blank img { display: none; }
.oc-logo-fallback {
  display: none;
  align-items: center; justify-content: center;
  width: 100%; height: 100%;
  font-family: var(--font-display); font-weight: 700; font-size: 17px;
  color: var(--faint); text-align: center; padding: 10px;
  /* overflow-wrap, NOT word-break: word-break split names mid-word
     ("Aloraircrawls / pace"). This breaks between words first and only splits
     a word that cannot fit on a line by itself. */
  overflow-wrap: break-word; hyphens: auto; line-height: 1.2;
}
.oc-logo-fallback.is-long { font-size: 13px; letter-spacing: -.01em; }
.oc-media-inner.is-logo .oc-logo-fallback { color: #64748b; }

/* No logo and no photo: skip the white plaque entirely. A run of blank white
   boxes down the list was louder than the cards themselves; the name sits on
   the card's own surface instead. */
.oc-media-inner.is-mark { background: var(--raised); padding: 8px; }
.oc-media-inner.is-mark .oc-logo-fallback { color: var(--soft); }
/* The wash only applies to photos; on a light logo plaque it would
   just grey the logo out. */
.oc-veil { position: absolute; inset: 0; background: linear-gradient(180deg, transparent 55%, rgba(2, 11, 7, .5)); pointer-events: none; }
.oc-media-inner.is-logo .oc-veil { display: none; }
.oc-discount {
  position: absolute; top: 8px; right: 8px;
  background: var(--grad); color: var(--brand-ink);
  font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
}
/* The whole plaque is a second route to the merchant. */
.oc-media-link { position: absolute; inset: 0; display: block; }
.oc-media-arrow {
  position: absolute; right: 8px; bottom: 8px;
  width: 26px; height: 26px; border-radius: 50%;
  background: rgba(2, 11, 7, .62); color: #fff;
  display: grid; place-items: center; font-size: 13px;
  opacity: 0; transition: opacity .2s;
}
.offer-card:hover .oc-media-arrow { opacity: 1; }

/* --- body --- */
.oc-body { min-width: 0; }
.oc-name { font-size: 19px; font-weight: 700; margin: 0 0 4px; letter-spacing: -.018em; }
.oc-desc { font-size: 14px; color: var(--soft); margin: 0 0 10px; line-height: 1.45; max-width: 52ch; }
.oc-highlights { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 3px; }
.oc-highlights li { display: flex; align-items: flex-start; gap: 8px; font-size: 13.5px; color: var(--soft); }
.oc-check { color: var(--brand-text); flex: none; font-weight: 700; }
.oc-tags { display: flex; flex-wrap: wrap; gap: 7px; }
.oc-tag {
  font-size: 11.5px; font-weight: 600; color: var(--soft);
  background: var(--brand-soft); padding: 4px 11px; border-radius: 999px;
}
.oc-tag-primary { color: var(--brand-text); }

/* --- score --- */
.oc-score { text-align: left; }
.oc-score-num { font-family: var(--font-display); font-size: 32px; font-weight: 700; color: var(--brand-text); letter-spacing: -.03em; line-height: 1; }
.oc-score-label { font-size: 10px; color: var(--faint); letter-spacing: .06em; text-transform: uppercase; margin-top: 5px; }

/* --- CTA --- */
.oc-cta { min-width: 0; }
.oc-cta-wrap { position: relative; display: block; }
.oc-btn {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  padding: 13px 20px;
  border-radius: var(--r-sm);
  background: var(--grad);
  color: var(--brand-ink);
  text-decoration: none;
  font-weight: 700; font-size: 14.5px;
  min-height: 46px;
  transition: filter .18s, transform .18s;
  position: relative; overflow: hidden;
}
.oc-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
/* Press: a quick dip, then spring back on release. On a phone this is the
   ONLY feedback the CTA gives — there is no hover state to lean on. */
.oc-btn:active, .rk-cta:active, .nf-btn:active, .hero-btn:active {
  transform: translateY(0) scale(.97);
  transition-duration: .08s;
}
.oc-btn-label { display: flex; flex-direction: column; line-height: 1.2; }
.oc-btn-main { font-weight: 700; }
.oc-btn-arrow { transition: transform .18s; }
.oc-btn:hover .oc-btn-arrow { transform: translateX(2px); }
.oc-url { font-size: 11.5px; color: var(--faint); text-align: center; margin-top: var(--s1); word-break: break-all; }
.oc-code { display: none; }

/* --- price strip --- */
/* --- spec row --- */
/* A four-column grid, identical on every card, so values form vertical
   columns the eye can run down. Labels are tiny and quiet; the values carry
   the weight. */
.oc-specs {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s2);
  margin: var(--s2) 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
.oc-spec dt {
  font-size: 10px; letter-spacing: .07em; text-transform: uppercase;
  color: var(--faint); margin-bottom: 3px;
}
.oc-spec dd {
  margin: 0; font-size: 13px; color: var(--soft); font-weight: 600;
  line-height: 1.35;
}
.oc-price { font-family: var(--font-display); font-size: 16px; color: var(--ink); font-weight: 700; }
.oc-spec-none { color: var(--faint); font-weight: 400; }

/* --- assistant bar (card #1 only) --- */
/* Card #1 ONLY. The copy build.js renders here says "<brand> is our
   top-ranked pick on this list", so on any other card it is simply false.
   The pre-redesign sheet scoped it to nth-child(1); the rewrite lost that and
   put the claim on all ten cards. Restored — keep this scoping. */
.oc-assistant {
  display: none; align-items: flex-start; gap: 12px;
  margin-top: 14px; padding-top: 14px;
  border-top: 1px solid var(--line);
}
#offer-list .offer-card:nth-child(1) .oc-assistant { display: flex; }
.oc-ai {
  flex: none;
  width: 28px; height: 28px; border-radius: 8px;
  background: var(--brand-soft); color: var(--brand-text);
  display: grid; place-items: center;
  font-size: 10px; font-weight: 700; letter-spacing: .04em;
}
.oc-assistant-body { flex: 1; min-width: 0; }
.oc-assistant-text { display: block; font-size: 13.5px; color: var(--soft); line-height: 1.5; }
.oc-assistant-text b { color: var(--ink); }
.oc-assistant-chips { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 9px; }
.oc-chip {
  font: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--soft); background: transparent;
  border: 1px solid var(--line); border-radius: 999px;
  padding: 6px 13px; cursor: pointer;
  transition: border-color .18s, color .18s, background .18s;
}
.oc-chip:hover, .oc-chip[aria-expanded="true"] { border-color: var(--line-str); color: var(--ink); background: var(--brand-soft); }
.oc-answer {
  margin-top: 10px; padding: 12px 14px;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-sm);
  font-size: 13.5px; color: var(--soft); line-height: 1.55;
}
.oc-answer[hidden] { display: none; }
/* Same rise-and-fade as the FAQ panels, so the two disclosures feel alike. */
.oc-answer:not([hidden]) { animation: faq-reveal .3s cubic-bezier(.4, 0, .2, 1) both; }
.oc-answer b { color: var(--ink); }
.oc-assistant-cue {
  display: none;
  align-items: center; gap: 6px;
  /* WCAG 2.2 target size: was 21px tall. The padding grows the hit area to
     28px; the matching negative margin keeps the visible text where it was. */
  min-height: 28px; padding: 4px 0; margin: -4px 0;
  font-size: 13px; font-weight: 650; color: var(--brand-text); text-decoration: none; white-space: nowrap;
}
.oc-assistant-cue:hover { text-decoration: underline; }
.oc-assistant-arrow { transition: transform .18s; }
.oc-assistant-cue:hover .oc-assistant-arrow { transform: translateX(2px); }


/* ============================================================
   SCROLL REVEAL + LOGO TILT
   The hidden state is applied by JS (.will-reveal), never in the base
   sheet — if the script fails to run, every card must still be visible.
   ============================================================ */
.will-reveal { opacity: 0; transform: translateY(14px); }
.will-reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity .5s ease, transform .5s cubic-bezier(.4, 0, .2, 1);
  transition-delay: var(--reveal-delay, 0ms);
}

/* Logo plaque tilt. The perspective lives on the parent so the plaque itself
   only carries a rotation — cheaper, and it keeps the hover lift on the card
   from compounding into the 3D transform. */
@media (hover: hover) and (pointer: fine) {
  .oc-media { perspective: 700px; }
  .oc-media-inner { transition: transform .35s cubic-bezier(.4, 0, .2, 1); will-change: transform; }
  .oc-media-inner.is-tilting { transition-duration: .08s; }
}

/* ============================================================
   PODIUM — same layout, warm frame
   The accent is doing CTA duty, so rank uses gold to stay distinct.
   ============================================================ */
.offer-card.top-choice {
  border-color: rgba(245, 196, 81, .34);
  background: linear-gradient(180deg, var(--gold-soft), transparent 58%), var(--surface);
  box-shadow: 0 10px 30px rgba(245, 196, 81, .09);
}
.offer-card.top-choice:hover { border-color: rgba(245, 196, 81, .5); box-shadow: 0 16px 40px rgba(245, 196, 81, .14); }
/* A band of light travels along the gold frame, then rests — pulling the eye
   to the top pick without moving the CTA. It is a 1px ring (the gradient is
   masked down to the border), sweeping for ~2s out of every 6. Only rank 1
   carries it, so it is one small repaint rather than ten. */
@keyframes tc-sweep {
  0%        { background-position: 160% 0; }
  35%, 100% { background-position: -60% 0; }
}
.offer-card.top-choice::after {
  content: "";
  position: absolute; inset: -1px;
  padding: 1px; border-radius: inherit;
  background: linear-gradient(110deg, transparent 40%, rgba(255, 228, 150, .95) 50%, transparent 60%) 160% 0 / 250% 100% no-repeat;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  animation: tc-sweep 6s cubic-bezier(.4, 0, .2, 1) 1.2s infinite;
  pointer-events: none;
  z-index: 4;
}
/* No .rank-num colour here: the badge above already pairs dark ink with the
   gold fill. Re-stating it gold made gold-on-gold and the digit vanished. */
.top-choice .oc-kicker { color: var(--gold-text); }

/* Pinned (versioned visit): gold frame. The two ::after labels are a
   PAID-PLACEMENT DISCLOSURE, not decoration — a pinned card is promoted,
   and the visitor has to be told so. Do not remove them. */
.offer-card.pinned { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold), 0 16px 40px rgba(245, 196, 81, .18); }
.offer-card.pinned .oc-flag::after { content: "\2605 Special recommendation"; }
.offer-card.pinned .oc-kicker::after { content: "Promoted placement" !important; }
/* Pinned reuses the same filled badge so the number stays legible. */
.offer-card.pinned .offer-rank { background: var(--gold); border-color: transparent; }
.offer-card.pinned .rank-num { color: #23180a; }

/* Versioned visit: compact the hero so the pinned offer lands above the fold. */
html.versioned .category-page .hero-compact { padding: 18px 0 22px; }
html.versioned .category-page .hero-visual { display: none; }
html.versioned .category-page .hero-grid { grid-template-columns: 1fr; gap: 0; text-align: left; }
html.versioned .category-page .hero-title { font-size: 27px; margin-bottom: 8px; }
html.versioned .category-page .hero-sub { display: none; }
html.versioned .category-page .offers-section { padding-top: 26px; }

/* ============================================================
   404
   ============================================================ */
.nf { text-align: center; }
.nf-code {
  font-family: var(--font-display);
  font-size: clamp(76px, 17vw, 156px);
  font-weight: 700;
  line-height: .9;
  letter-spacing: -.055em;
  margin: 0 0 var(--s2);
  background: var(--grad-text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.nf .hero-sub { margin-left: auto; margin-right: auto; }
/* Shared by the 404 actions and the hero CTAs — one button system, not two. */
.nf-actions, .hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin: var(--s3) 0 0; }
.nf-actions { justify-content: center; }
.nf-btn, .hero-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 46px; padding: 12px 22px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink); text-decoration: none; font-weight: 650; font-size: 14.5px;
  transition: color .18s, border-color .18s, background .18s, transform .18s;
}
.nf-btn:hover, .hero-btn:hover { border-color: var(--line-str); background: var(--brand-soft); transform: translateY(-1px); }
.nf-btn-primary, .hero-btn-primary {
  background: var(--grad); border-color: transparent; color: var(--brand-ink);
}
.nf-btn-primary:hover, .hero-btn-primary:hover { filter: brightness(1.08); background: var(--grad); }

/* ============================================================
   CATEGORY TILES
   ============================================================ */
.category-tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s2); }
.cat-tile {
  position: relative;
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--s1);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 20px 18px;
  text-decoration: none; color: var(--ink);
  font-family: var(--font-display); font-weight: 700; font-size: 15px;
  transition: transform .2s, border-color .2s, background .2s, box-shadow .2s;
}
.cat-tile:hover, .cat-tile:focus-visible {
  transform: translateY(-2px);
  background: var(--raised);
  border-color: var(--line-str);
  box-shadow: var(--shadow-lg);
}
.cat-tile svg { width: 28px; height: 28px; color: var(--brand-text); flex: none; }
.cat-tile strong { font-weight: 700; }
.cat-tile span { font-family: var(--font); font-weight: 500; font-size: 12.5px; color: var(--faint); }

/* Photo tiles. The image arrives as --tile-img on the element itself (build.js
   sets it inline), not as an <img>, so it layers under the wash as a second
   background rather than needing a stacking context of its own. */
.cat-tile.has-img {
  overflow: hidden;
  min-height: 132px;
  justify-content: flex-end;
  border-color: transparent;
  background-image:
    /* Deepened: the label sits over the lower third, and on the brighter
       photos (Fashion Trends, Prime Finance, Entertainment) the old wash
       left white text fighting the image. */
    linear-gradient(180deg, rgba(2, 11, 7, .22) 0%, rgba(2, 11, 7, .68) 52%, rgba(2, 11, 7, .94) 100%),
    var(--tile-img);
  background-size: cover;
  background-position: center;
  color: #fff;
}
.cat-tile.has-img strong { color: #fff; text-shadow: 0 1px 6px rgba(0, 0, 0, .45); }
.cat-tile.has-img span { color: rgba(255, 255, 255, .82); }
.cat-tile.has-img svg {
  color: #fff;
  background: var(--brand-deep);
  border-radius: var(--r-sm);
  padding: 5px; width: 34px; height: 34px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
}
.cat-tile.has-img:hover, .cat-tile.has-img:focus-visible { transform: translateY(-2px) scale(1.012); }

/* ============================================================
   STATS BAND
   ============================================================ */
.stats-section { padding: var(--s4) 0; }
.stats-band {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s3) var(--s2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s3);
  text-align: center;
}
.stat { min-width: 0; }
.stat-num { display: block; font-family: var(--font-display); font-size: 27px; font-weight: 700; color: var(--ink); letter-spacing: -.025em; }
.stat-label { display: block; font-size: 12.5px; color: var(--faint); margin-top: 3px; }

/* ============================================================
   HOW WE RANK
   ============================================================ */
.how-steps { display: grid; grid-template-columns: 1fr; gap: var(--s2); }
.how-step {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s3);
}
.step-num {
  display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: var(--r-sm);
  background: var(--brand-soft); color: var(--brand-text);
  font-family: var(--font-display); font-weight: 700; font-size: 15px;
  margin-bottom: 14px;
}
.how-step h3 { font-size: 17px; margin: 0 0 7px; }
.how-step p { margin: 0; color: var(--soft); font-size: 14.5px; }

/* ============================================================
   FAQ (native details/summary)
   ============================================================ */
.faq { border-bottom: 1px solid var(--line); }
.faq summary {
  cursor: pointer;
  padding: 18px 44px 18px 0;
  font-weight: 650; font-size: 15.5px; color: var(--ink);
  list-style: none; position: relative;
  transition: color .2s ease;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { color: var(--brand-text); }

/* The +/- marker is built from two bars rather than swapped ::after text,
   because `content` cannot be transitioned — the old version snapped between
   glyphs. Closed, the bars form a plus; opening rotates the upright one flat
   so the two merge into a minus. */
.faq summary::before,
.faq summary::after {
  content: "";
  position: absolute; right: 8px; top: 50%;
  width: 13px; height: 2px; border-radius: 2px;
  background: var(--brand-text);
  transition: transform .3s cubic-bezier(.4, 0, .2, 1);
}
.faq summary::before { transform: translateY(-50%); }
.faq summary::after  { transform: translateY(-50%) rotate(90deg); }
.faq[open] summary::after { transform: translateY(-50%) rotate(0deg); }

/* Panel reveal. A <details> that is closing drops its content from the box
   tree on the same frame, so without JS only the opening direction can be
   animated — which is the direction that reads as "opening" anyway. */
@keyframes faq-reveal {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.faq[open] .faq-body { animation: faq-reveal .3s cubic-bezier(.4, 0, .2, 1) both; }

/* Progressive enhancement: where ::details-content and interpolate-size are
   supported, the panel also animates its HEIGHT, so the questions below slide
   down instead of jumping. Everywhere else the fade above still runs. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .faq::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size .3s cubic-bezier(.4, 0, .2, 1), content-visibility .3s allow-discrete;
  }
  .faq[open]::details-content { block-size: auto; }
}

.faq summary:focus-visible { outline: 2px solid var(--brand-text); outline-offset: 3px; border-radius: 4px; }
.faq-body { padding: 0 0 18px; color: var(--soft); font-size: 14.5px; max-width: 38em; }
.faq-body p { margin: 0 0 .8em; }
.faq-body p:last-child { margin-bottom: 0; }

/* ============================================================
   TRUST BAND
   ============================================================ */
.trust-band {
  display: flex; align-items: flex-start; gap: 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: var(--r-sm);
  padding: 20px 22px;
  margin-bottom: var(--s4);
  color: var(--soft); font-size: 14.5px; line-height: 1.6;
}
.trust-band svg { flex: none; color: var(--brand-text); margin-top: 2px; }
.trust-band p { margin: 0; }
.trust-band strong { color: var(--ink); }

/* ============================================================
   INFO PAGE CONTENT
   ============================================================ */
/* Info pages: the hero used the 1100px column while the body used the 800px
   one, so their left edges sat 130px apart and looked broken. Both are narrow
   now, and the dead band between them (measured 160px) is closed. */
.info-hero { padding-bottom: var(--s3); }
.info-hero + .section-light { padding-top: var(--s4); }
/* padding-block, NOT the padding shorthand: the shorthand zeroed the 20px
   side padding .container provides, which left the body text 20px to the left
   of the heading above it. */
.page-content { padding-block: 0; }
/* The section already spaces this; a leading heading's own margin would
   otherwise stack on top and make the gap uneven page to page. */
.page-content > :first-child { margin-top: 0; }
.page-content h1 { font-size: clamp(30px, 4.4vw, 42px); margin-bottom: var(--s2); }
.page-content h2 { font-size: 23px; margin: var(--s4) 0 var(--s2); }
.page-content h3 { font-size: 17.5px; margin: var(--s3) 0 var(--s1); }
/* Measure capped at 72 characters. The column was 760px wide, which works out
   at 98 characters a line — well past the 45-75 range where the eye reliably
   finds the start of the next line. Headings and cards keep the full width.
   In em, not ch: the ch unit is the width of "0", which is narrower than this
   font's average character, so 72ch measured out at ~90 characters. */
.page-content p, .page-content li { color: var(--soft); font-size: 15.5px; max-width: 36em; }
/* :not(.btn-visit) — this rule (0,1,1) outranked `.btn-visit` (0,1,0) and
   repainted the button's label in the same emerald as its own gradient:
   1.00:1 contrast in dark, i.e. invisible. axe misses it because it skips
   elements with gradient backgrounds. */
.page-content a:not(.btn-visit) { color: var(--brand-text); }
.page-content ul, .page-content ol { padding-left: 20px; }
.page-content li { margin-bottom: 7px; }
.contact-card > svg { color: var(--brand-text); }
.contact-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s3);
  margin-top: var(--s3);
}

/* Legacy button class, kept so any stray markup still renders. */
.btn-visit {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 13px 22px; border-radius: var(--r-sm);
  background: var(--grad); color: var(--brand-ink);
  text-decoration: none; font-weight: 700;
}

/* ============================================================
   FOOTER
   Three bands instead of one four-column row. The old layout put all
   Categories sit as the middle link column, between Company and Legal. They
   had their own full-width band while this site carried 12 of them: one column
   three times taller than its 4-link neighbours read as lopsided. After the
   split there are 7 here and 5 on the sibling site, close enough to the 4-link
   columns that the band was mostly empty space.
   ============================================================ */
.site-footer {
  background: var(--sunk);
  border-top: 1px solid var(--line);
  /* No margin-top: the preceding section's padding and the footer's own
     already separate them. Together they measured 160px of emptiness. */
  padding: var(--s5) 0 var(--s3);
}

/* --- band 1: brand + link columns --- */
.ft-head { display: grid; grid-template-columns: 1fr; gap: var(--s4); }
.ft-brand svg { display: block; margin-bottom: 14px; color: var(--ink); }
/* CSS overrides the SVG's stop-color attributes, so "Ten" can switch to the
   text-safe gradient in the light theme. */
#lg-footmark stop:first-child { stop-color: var(--wm-a); }
#lg-footmark stop:last-child { stop-color: var(--wm-b); }
.ft-pitch { color: var(--soft); font-size: 14px; line-height: 1.65; max-width: 44ch; margin: 0; }
/* Two columns on a phone, three from 640px. At 390px a third column leaves
   94px per link and wraps "Travel & Tourism" and "Affiliate Disclosure"
   onto two lines each; two columns keep every link on one line. */
.ft-links { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s3); }
.ft-col .ft-label {
  font-family: var(--font); font-size: 11px; letter-spacing: .11em; text-transform: uppercase;
  color: var(--faint); margin: 0 0 14px; font-weight: 700;
}
.ft-col ul { list-style: none; margin: 0; padding: 0; }
.ft-col a {
  color: var(--soft); text-decoration: none; font-size: 14px;
  display: inline-block; padding: 5px 0;
  position: relative;
  transition: color .2s;
}
.ft-col a:hover, .ft-col a:focus-visible { color: var(--brand-text); }

/* The categories column. It carries .ft-col too, so the label, link colour
   and hover underline all come from the rules above; only the list needs
   resetting from the old two-up band to a plain stack. */
.ft-cats ul {
  list-style: none; margin: 0; padding: 0;
}
.ft-cats a {
  display: inline-block;
  color: var(--soft); text-decoration: none; font-size: 14px;
  padding: 5px 0;
  position: relative;
  transition: color .2s;
}
/* Underline grows left-to-right on hover.
   scaleX on a full-width rule, not an animated `width`: width is a layout
   property and would relayout the footer on every frame, while a transform
   stays on the compositor. transform-origin is what makes it grow FROM the
   start rather than outward from the middle. inline-block keeps the rule the
   width of the text instead of the whole grid column. */
.ft-cats a::after,
.ft-col a::after {
  content: "";
  position: absolute; left: 0; bottom: 4px;
  width: 100%; height: 1px;
  background: var(--brand-text);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .28s cubic-bezier(.4, 0, .2, 1);
}
.ft-cats a:hover, .ft-cats a:focus-visible { color: var(--brand-text); }
.ft-cats a:hover::after,
.ft-cats a:focus-visible::after,
.ft-col a:hover::after,
.ft-col a:focus-visible::after { transform: scaleX(1); }

/* --- band 3: disclosure + copyright --- */
.ft-bottom {
  display: grid; grid-template-columns: 1fr; gap: var(--s2);
  margin-top: var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
/* The disclosure is a legal obligation, not fine print to bury — it keeps a
   visible accent marker and the same size as the rest of the bottom row. */
.ft-disclosure {
  display: flex; align-items: flex-start; gap: 10px;
  color: var(--faint); font-size: 12.5px; line-height: 1.6;
  max-width: 68ch; margin: 0;
}
.ft-dot {
  flex: none; width: 6px; height: 6px; border-radius: 50%;
  background: var(--brand); margin-top: 7px;
}
.ft-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); }
.ft-copy { color: var(--faint); font-size: 12.5px; margin: 0; }
/* Floating back-to-top.
   Fixed to the viewport, not part of the footer row — it has to be reachable
   from anywhere in a 690KB category page, not only once you have already
   scrolled to the bottom. Hidden (and taken out of the tab order) until there
   is enough page behind you for it to mean anything. */
.ft-top {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 40;
  display: grid; place-items: center;
  width: 46px; height: 46px;
  border: 1px solid var(--line-str);
  border-radius: 50%;
  background: var(--surface);
  color: var(--brand-text);
  text-decoration: none;
  box-shadow: var(--fab-shadow);
  opacity: 0;
  transform: translateY(10px) scale(.92);
  pointer-events: none;
  visibility: hidden;
  transition: opacity .25s ease, transform .25s cubic-bezier(.4, 0, .2, 1),
              background .2s, color .2s, border-color .2s, visibility .25s;
}
/* Progress ring: fills as the page is read. cards.js writes --p (0-1) from
   the same scroll value that drives the header bar, so the two always agree.
   The conic gradient is masked to a 2.5px ring around the button. */
.ft-top::before {
  content: "";
  position: absolute; inset: -4px;
  border-radius: 50%;
  background: conic-gradient(var(--brand-text) calc(var(--p, 0) * 1turn), rgba(251, 113, 133, .14) 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
  pointer-events: none;
}
.ft-top.is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
  visibility: visible;
}
.ft-top:hover {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
  transform: translateY(-2px) scale(1.04);
}
.ft-top:focus-visible { outline: 2px solid var(--brand-text); outline-offset: 3px; }
.ft-top svg { transition: transform .2s; }
.ft-top:hover svg { transform: translateY(-2px); }

/* ============================================================
   PHONES (< 640px)
   The identity row cannot also hold the search field — sharing one row
   leaves it about 70px wide on a 390px screen — so search wraps to its
   own full-width row underneath.
   ============================================================ */
@media (max-width: 639px) {
  .rk-nav__inner { padding: 0 16px; }
  .rk-nav__top { gap: var(--s1); padding: 10px 0; }
  /* The toggle rides on the SEARCH row, not the identity row. Measured at
     375px the identity row needed 360px of 332 once the toggle joined it and
     wrapped the whole header to three rows; the search row has room to spare.
     display:contents lifts the toggle and CTA out of their wrapper so `order`
     can place them — the CTA stays up top, the toggle drops beside search. */
  .rk-nav__right { display: contents; }
  .rk-cta { order: 2; margin-left: auto; }
  .rk-search { order: 3; flex: 1 1 calc(100% - 46px); max-width: none; margin: 0 0 8px; }
  .rk-theme { order: 4; margin-bottom: 8px; }
  .rk-search__key { display: none; }
  /* The input kept 46px of right padding reserved for the ⌘K hint even though
     the hint is hidden here — dead space that clipped the placeholder once the
     theme toggle narrowed the field. */
  .rk-search__input { padding-right: 14px; }
  .rk-cat { padding: 10px 11px; }
  /* Measured with Playwright: 170px from 360-639px wide (search wraps to its
     own row). cards.js overwrites this with the live value; the fallback only
     has to be right for the first paint. */
  body { padding-top: var(--nav-h, 170px); }

  .hero { padding: var(--s4) 0 var(--s4); }
  .hero-compact { padding: var(--s3) 0 var(--s3); }
  .section-light { padding: var(--s4) 0; }
  .section-title { font-size: 25px; }
  .section-sub { margin-bottom: var(--s3); }

  .oc-inner { padding: var(--s2); }
  /* Square thumb: more room for a logo than 4:3 at this size. */
  .oc-media-inner { aspect-ratio: 1; }
  /* The discount pill was sized for the wide media and wrapped to two lines
     on the 88px thumb. */
  .oc-discount { font-size: 9.5px; padding: 2px 6px; top: 5px; right: 5px; }
  /* Two columns on a phone: four would wrap mid-value. */
  .oc-specs { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px var(--s2); }
  .oc-assistant { gap: var(--s1); }

  .stats-band { padding: var(--s3) var(--s2); gap: var(--s3) var(--s1); }
  .trust-band { padding: 16px 18px; }
  .page-content { padding: var(--s4) 0; }
  .site-footer { margin-top: var(--s4); padding: var(--s4) 0 var(--s3); }
  .ft-meta { flex-direction: column; align-items: flex-start; gap: 12px; }
  .ft-top { right: 16px; bottom: 16px; width: 44px; height: 44px; }
}

/* Narrow phones: the placeholder alone drops to 13px so it fits beside the
   theme toggle at 360px (measured: 9px short at 14px). Only the PLACEHOLDER —
   the typed text stays 14px. */
@media (max-width: 399px) {
  .rk-search__input::placeholder { font-size: 13px; }
}

/* Below 360px the logo and CTA no longer fit on one row either, so the
   header grows to three rows — measured at 218px on a 320px screen. */
@media (max-width: 359px) {
  body { padding-top: var(--nav-h, 218px); }
}

/* ============================================================
   640px
   ============================================================ */
@media (min-width: 640px) {
  .stats-band { grid-template-columns: repeat(4, 1fr); }
  .how-steps { grid-template-columns: repeat(3, 1fr); }
  .category-tiles { grid-template-columns: repeat(3, 1fr); }
  /* 1.4fr/1fr fitted two link columns; three need the wider share. */
  .ft-head { grid-template-columns: 1fr 1.8fr; gap: var(--s4); }
  .ft-links { grid-template-columns: repeat(3, 1fr); }
  .ft-bottom { grid-template-columns: 1fr; }

  .oc-grid { grid-template-columns: 132px 1fr; gap: var(--s3); align-items: start; }
  .oc-media-inner { aspect-ratio: 4 / 3; }
  .oc-score { grid-column: 2; display: flex; align-items: baseline; gap: 10px; }
  .oc-score-label { margin-top: 0; }
  .oc-cta { grid-column: 1 / -1; }
  .oc-assistant-cue { display: inline-flex; }
}

/* ============================================================
   992px — the full row
   ============================================================ */
@media (min-width: 992px) {
  .rk-cat { padding: 11px 13px; }
  .rk-trust { display: flex; }

  .hero-grid { grid-template-columns: 1.1fr .9fr; gap: var(--s5); }
  .hero-grid-nophoto { grid-template-columns: 1fr; gap: 0; }
  .hero-visual { margin: 0; max-width: none; }

  /* Bento: one tile carries the visual weight instead of twelve equal boxes.
     The maths works out with no gaps — tile 1 takes 2x2 (4 cells), tile 6
     takes 2x1 (2 cells) and the other ten take one each: 16 cells in a 4x4
     grid. Uniform grids stay below 992px, where a featured tile has no room
     to read as featured. */
  .category-tiles { grid-template-columns: repeat(4, 1fr); grid-auto-rows: 152px; }
  .cat-tile { height: 100%; }
  .cat-tile:nth-child(1) { grid-column: span 2; grid-row: span 2; }
  .cat-tile:nth-child(6) { grid-column: span 2; }
  /* The featured tile gets a bigger label to match its new size. */
  .cat-tile:nth-child(1) strong { font-size: 26px; letter-spacing: -.03em; }
  .cat-tile:nth-child(1) span { font-size: 14px; }
  .cat-tile:nth-child(1) svg { width: 40px; height: 40px; }
  /* display:contents lifts the two link columns into the parent grid, so the
     row is brand | Company | Categories | Legal at even widths. Nested, the
     brand column took 1.6fr while its text caps at 44ch, leaving a 295px hole
     mid-footer. */
  .ft-head { grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: var(--s4); }
  .ft-links { display: contents; }
  .ft-bottom { grid-template-columns: 1fr auto; align-items: center; gap: var(--s4); }
  .ft-meta { gap: var(--s3); }

  .oc-grid {
    grid-template-columns: 150px minmax(0, 1fr) auto 176px;
    gap: var(--s3);
    align-items: center;
  }
  .oc-score { grid-column: auto; display: block; text-align: center; min-width: 86px; }
  .oc-score-label { margin-top: 5px; }
  .oc-cta { grid-column: auto; }
  /* Score and CTA sit level with the brand name rather than floating in the
     middle of a tall row — the three columns now share one baseline. */
  .oc-media, .oc-score, .oc-cta { padding-top: 2px; }
  /* The spotlight drops its cap here: the four-column row needs ~740px, and
     below that the body column collapses and wraps the brand name. */
  .offer-list.spotlight { max-width: none; }
  .offer-list.spotlight .oc-grid { grid-template-columns: 170px minmax(0, 1fr) auto 186px; }
}

/* ============================================================
   Motion
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}
