/* THE CATALOGUE - laid out on the Lovable templates pattern.
   Measured from lovable.dev/templates: 3 x 360px grid, 20px gaps, 1120px
   container, 16:9 thumbs at 12px radius with a hairline border and NO shadow,
   flat near-white ground, name + description left, category pill right.
   Structure follows that pattern; the type, colour and content are ours. */

/* Brand tokens are the canonical noboringsites values, taken verbatim from
   .claude/skills/noboringsites-brand/references/design-tokens.md. Do not invent
   new ones here: light is primary, dark is the toggle, one accent per mode,
   and NO gradients anywhere (flat surfaces are a hard brand rule). */
:root {
  --bg: #f6f3ed;          /* paper, warm porcelain */
  --card-bg: #fbf8f2;     /* surface */
  --ink: #141214;
  --ink-2: #141214;
  --muted: #6f695e;
  --accent: #2438e6;      /* Klein Volt */
  --accent-lift: #3346ff; /* the funnel's brighter hover Klein */
  --accent-ink: #ffffff;
  --accent-text: #1b28b8; /* small accent text on paper */
  --line: rgba(20, 18, 20, 0.12);
  --line-2: rgba(20, 18, 20, 0.12);
  --line-soft: rgba(20, 18, 20, 0.07);
  --pill-bg: transparent;
  --ok: #0d6b3f;

  --display: "Fraunces", Georgia, serif;
  --body: "Hanken Grotesk", system-ui, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, monospace;

  --radius-card: 18px;
  --radius-tile: 14px;
  --radius-button: 13px;
  --radius-pill: 999px;

  /* 1.15x pass. Cap raised from 2000 to 2300 so three across can reach ~748px.
     Margin and gap are untouched. Note a 1920 window is already at its
     geometric limit here: 1840 of usable container over three columns is
     595px a card, and 1.15x of that (684) would need 2108px of container.
     Wider screens get the full 1.15x. */
  --container: min(2229px, calc(100vw - 80px));
  --gap: 28px;
  --grain: 0.045;

  /* PAGE FRAME - one left axis. The browse rail is not a card on the page, it
     is the page's left WALL: flush to the viewport edge, full height, one
     hairline on its right. --pad is the single axis the headline, the
     standfirst, the search row, the first card column and the footer all range
     off. A 250px rail clipped "Trades & Home Services" to an ellipsis at
     1100-1150, and 22vw left the wall 80-150px wider than its longest row above
     1300 - the card thumbnails paid for that dead space. Narrower floor,
     gentler slope, lower cap. */
  --rail: clamp(272px, 19vw, 344px);
  --pad: clamp(24px, 3vw, 56px);    /* content axis inset */
  --gut: clamp(18px, 1.5vw, 26px);  /* the wall's own inner inset */
  --navh: 70px;                     /* .nav min-height */
  --ease: cubic-bezier(.22, .61, .36, 1);
  --spring: cubic-bezier(.34, 1.56, .64, 1);   /* the quiz flow's button overshoot */
}

[data-theme="dark"] {
  --bg: #101418;
  --card-bg: #171c24;
  --ink: #e9edf0;
  --ink-2: #e9edf0;
  --muted: #93a0b3;
  --accent: #7c97f5;      /* periwinkle */
  --accent-lift: #93a4ff; /* periwinkle one intensity step up */
  --accent-ink: #0b1020;
  --accent-text: #7c97f5;
  --line: rgba(233, 237, 240, 0.14);
  --line-2: rgba(233, 237, 240, 0.14);
  --line-soft: rgba(233, 237, 240, 0.08);
  --ok: #46c88a;
  --grain: 0.06;
}

@media (max-width: 1100px) { :root { --container: calc(100vw - 72px); --gap: 24px; } }
@media (max-width: 900px)  { :root { --container: calc(100vw - 40px); --gap: 18px; } }

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  position: relative;
}
/* Brand finish: the same grain overlay the storefront/homepage uses, so the
   catalogue shares the studio's tactile print texture instead of reading flat. */
body::before {
  content: "";
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  opacity: var(--grain); mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
[data-theme="dark"] body::before { mix-blend-mode: soft-light; }
img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; }
a { color: inherit; }
/* One global focus ring, mirroring the storefront (site/styles.css) — the
   transposed header + modal controls had no visible ring on keyboard focus. */
a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.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;
}
/* ---------------------------------------------------------------------------
   THE TWO-COLUMN PAGE

   NO page-level `zoom`. A `zoom: 0.75` at >=1400px used to live here, to
   reproduce the 75% browser zoom the page was first reviewed at. It shrank
   every measurement on the page by a third and is exactly the "look how small
   it is" the founder reported. Everything below is authored at 1:1 against real
   device pixels. Do not reintroduce it.

   .cat-shell is display:contents so its two children join <main>'s grid
   directly. That is what lets the wall span the hero row AND the grid row from
   one sticky box, with no position:fixed and no guessed negative margins.
   ------------------------------------------------------------------------ */
main {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  align-items: start;
  /* clip, never hidden: `hidden` would spawn a scrollport and kill the wall's
     position:sticky. clip does not. */
  overflow-x: clip;
  transition: grid-template-columns .42s var(--ease);
}
/* THE WALL, PAINTED. .side is sticky and exactly one viewport tall, so at the
   end of the scroll it lifts off its column and the last screen showed bare
   page where the wall should be. This is an ordinary stretched grid item, so it
   is always exactly as tall as <main>, and it collapses on the same .42s track
   as the column itself. .side is positioned, so it paints over this; same colour. */
main::before {
  content: "";
  grid-column: 1; grid-row: 1 / 3;
  align-self: stretch;
  background: var(--card-bg);
  border-right: 1px solid var(--line-2);
}
/* The hero band: copy left on the page's one axis, search + sort bottom-aligned
   in the right half of the same band. */
.wrap {
  grid-column: 2; grid-row: 1;
  padding: clamp(38px, 4.4vw, 84px) var(--pad) 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  column-gap: clamp(28px, 3vw, 64px);
}
/* The shelf. One hairline spanning the content column, under both halves of the
   hero band, that the first card row hangs from. Replaces the three unrelated
   vertical gaps the old centred hero used to separate itself from the grid. */
.wrap::after {
  content: "";
  grid-column: 1 / -1;
  height: 1px; background: var(--line);
  margin-top: 28px;
}

/* ---------- masthead ---------- */
.masthead {
  position: sticky; top: 0; z-index: 50; height: 72px;
  display: flex; align-items: center; justify-content: space-between;
  padding-inline: max(24px, calc((100vw - var(--container)) / 2));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}
/* Primary wordmark (founder lock 2026-07-05): "boring" and the trailing period
   carry the accent; everything else is ink.
   Size is measured off the reference masthead: its logo runs 31px against 13px
   nav text, a 1.55x ratio. Matching that ratio against our 15.5px nav puts the
   wordmark at 1.5rem. */
.wordmark {
  font-family: var(--display); font-weight: 900; font-size: 1.5rem;
  letter-spacing: -0.035em; text-decoration: none; color: var(--ink);
  flex: 0 0 auto; line-height: 1;
}
.wordmark b, .wordmark i { color: var(--accent-text); font-style: normal; font-weight: 900; }
/* Main nav. Sits between the wordmark and the CTA, centred in the remaining
   space, and collapses to a horizontal scroller before it ever wraps. */
/* Sits directly beside the wordmark, left-aligned. The CTA group takes the
   auto margin instead, so it holds the right edge. Gap and item spacing are
   the reference's: ~70px off the logo, ~30px between items. */
.mast-nav {
  display: flex; align-items: center; gap: 30px; margin-left: 66px;
  overflow-x: auto; scrollbar-width: none; min-width: 0;
}
.mast-nav::-webkit-scrollbar { display: none; }
.mast-nav a {
  flex: 0 0 auto; text-decoration: none; color: var(--muted);
  font-size: 0.97rem; font-weight: 500; white-space: nowrap;
  padding: 6px 0; position: relative; transition: color 180ms ease;
}
.mast-nav a:hover { color: var(--ink); }
.mast-nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 3px; }
.mast-nav a[aria-current="page"] { color: var(--ink); }
.mast-nav a[aria-current="page"]::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px;
  height: 2px; background: var(--accent); border-radius: 2px;
}

/* Log in sits on the right beside the CTA, mirroring the storefront masthead.
   Same type treatment as the nav links so the two groups read as one system. */
.mast-login {
  flex: 0 0 auto; text-decoration: none; color: var(--muted);
  font-size: 0.97rem; font-weight: 500; white-space: nowrap;
  padding: 6px 0; transition: color 180ms ease;
}
.mast-login:hover { color: var(--ink); }
.mast-login:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 3px; }

@media (max-width: 1100px) { .mast-nav { display: none; } }

.mast-right { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; margin-left: auto; }
.theme {
  width: 42px; height: 42px; border-radius: 12px; border: 1.5px solid var(--line-2);
  background: var(--card-bg); color: var(--ink); cursor: pointer;
  display: grid; place-items: center; transition: border-color 0.2s ease;
}
.theme:hover { border-color: var(--accent); }
.theme:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.theme .ico { width: 18px; height: 18px; }
.theme .ico-sun { display: block; }
.theme .ico-moon { display: none; }
[data-theme="dark"] .theme .ico-sun { display: none; }
[data-theme="dark"] .theme .ico-moon { display: block; }
.theme.spin { animation: tgl-spin 0.55s cubic-bezier(0.4, 0, 0.2, 1); }
@keyframes tgl-spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
/* SQUARED, not a stadium (founder 2026-07-26, raised more than once: "my feedback persists on
   the rounded buttons that are buy. its off brand and needs to be squared out"). 12px is the house
   button radius - the funnel's .btn, its Back and Next, every CTA on the buyer path. 999px belongs
   to chips and tags only. Padding and weight matched to .btn too, so a Buy button here and a Next
   button in the funnel are visibly the same object. */
.cta {
  background: var(--accent); color: var(--accent-ink); text-decoration: none;
  font-size: 0.875rem; font-weight: 600; padding: 12px 18px; border-radius: 12px;
  border: 1.5px solid var(--accent);
  box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--accent) 55%, transparent);
  transition: background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease,
    transform 0.3s var(--spring);
}
.cta:hover {
  background: var(--accent-lift); border-color: var(--accent-lift); transform: translateY(-1px);
  box-shadow: 0 12px 26px -10px color-mix(in srgb, var(--accent) 60%, transparent);
}
.cta:active {
  transform: translateY(1px);
  box-shadow: 0 6px 14px -10px color-mix(in srgb, var(--accent) 50%, transparent);
}
.cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .cta { transition: none; }
  .cta:hover, .cta:active { transform: none; }
}

/* animated sunset->night theme transition, ported verbatim from the site masthead */
.theme-blinds { position: fixed; inset: 0; z-index: 9999; pointer-events: none; display: flex; flex-direction: column; }
.theme-blinds span { flex: 1 0 0; transform: scaleY(0); transform-origin: top center; will-change: transform; }
.theme-blinds span:nth-child(1) { background: #ffd27a; }
.theme-blinds span:nth-child(2) { background: #ffb259; }
.theme-blinds span:nth-child(3) { background: #ff8a4c; }
.theme-blinds span:nth-child(4) { background: #ff5f52; }
.theme-blinds span:nth-child(5) { background: #e25470; }
.theme-blinds span:nth-child(6) { background: #9d5fc4; }
.theme-blinds span:nth-child(7) { background: #5a5fb0; }
.theme-blinds span:nth-child(8) { background: #26304f; }
.theme-blinds.closing span { animation: blind-close 0.26s cubic-bezier(0.5, 0, 0.2, 1) forwards; animation-delay: calc(var(--i) * 38ms); }
.theme-blinds.opening span { transform: scaleY(1); transform-origin: bottom center; animation: blind-open 0.26s cubic-bezier(0.5, 0, 0.2, 1) forwards; animation-delay: calc(var(--r) * 38ms); }
@keyframes blind-close { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes blind-open { from { transform: scaleY(1); } to { transform: scaleY(0); } }
@media (prefers-reduced-motion: reduce) {
  .theme.spin { animation: none; }
  .theme-blinds span { animation: none !important; }
}

/* ---------- opener ----------
   Hero is left-ranged on the page's one axis, and big. Two centred boxes of
   different widths cannot align their text, which is what the old centred flex
   column plus an 18ch/57ch double measure produced: a headline and a standfirst
   on two different left edges. Both range off --pad now, and the .wrap grid
   supplies the vertical rhythm. */
.opener { display: block; padding: 0; }
.eyebrow {
  font-family: var(--mono); font-weight: 500; font-size: 0.72rem;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
  margin: 0 0 22px;
}
/* The old clamp capped at 2.6rem, which under the .75 zoom rendered ~31px on a
   1920 monitor. Sized so it does not step down at any seam.
   SPECIFICITY: variants.css loads after this file and `body.v-a .opener h1`
   (0,2,2) would otherwise win the measure back to 16ch. The `:root .wrap`
   prefix is what holds this rule; drop it once variants.css goes. */
:root .wrap .opener h1 {
  font-family: var(--display); font-weight: 900;
  font-size: clamp(2.25rem, 5.6vw, 5.1rem); line-height: 0.98;
  letter-spacing: -0.028em; color: var(--ink); margin: 0; max-width: 13ch;
}
/* "boring" carries the accent, the same device as the wordmark. Large display
   text uses --accent rather than the small-text --accent-text variant. */
.opener h1 b { color: var(--accent); font-weight: 900; }

/* ---------- the headline types itself in ----------
   Founder 2026-07-26: "there should be an animation similar to the quiz flow of the
   hero text being typed in at the start when u first load or reload the page."
   These are the FUNNEL'S OWN class names and cadence (site/start/index.html, "Typewriter
   titles"), not a second take on the same idea, so the storefront and the buying flow
   read as one product. typewriter.js drives them; without it nothing here matches and
   the headline renders exactly as authored.
   Every glyph is in flow from the first frame and only OPACITY changes, and the caret is
   a zero-width ::after on the current character, so the line never reflows mid-type. */
.opener h1.tw .tw-c { opacity: 0; position: relative; }
.opener h1.tw .tw-c.tw-on { opacity: 1; }
/* the full stop is held back and lands last: blue word, black dot */
.opener h1.tw .accent-dot { opacity: 0; }
.opener h1.tw.tw-done .accent-dot { opacity: 1; transition: opacity 0.22s ease; }
.opener h1.tw .tw-c.tw-cur::after {
  content: ""; position: absolute; left: 100%; top: 0.2em; bottom: 0.16em;
  margin-left: 0.06em; width: 0.055em; background: var(--accent);
  border-radius: 1px; opacity: 1; transition: opacity 0.2s ease;
}
.opener h1.tw .tw-c.tw-cur.tw-blink::after { animation: twcaret 0.53s steps(1, end) 2; }
.opener h1.tw .tw-c.tw-cur.tw-resolve::after { opacity: 0; }
@keyframes twcaret { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }
/* Sits under the headline on the same left edge, on a measure short enough to
   leave the right half of the hero band to the search + sort controls.
   Do NOT set text-wrap balance or pretty here: either one overrides the measure
   and picks its own split. */
.standfirst {
  font-family: var(--body); font-weight: 400;
  font-size: clamp(1rem, 1.05vw, 1.22rem); line-height: 1.5; letter-spacing: -0.004em;
  color: color-mix(in srgb, var(--ink) 78%, var(--bg));
  /* Founder 2026-07-26: "its too compressed and compacted when there is so much space."
     46ch under an 82px headline on a 1920 monitor was a three-line brick with a canyon
     to its right. The measure and the gap above it both scale now; the split-band rule
     below hands the copy the whole width back the moment the toolbar stops competing. */
  margin: clamp(20px, 1.8vw, 30px) 0 0;
  max-width: 52ch;
  text-wrap: wrap;
}
/* "forever" is emboldened AND italic. */
.standfirst em {
  font-style: italic; font-weight: 700; color: var(--ink);
}

/* ================= SIDEBAR SHELL ================= */
/* display:contents so .side and .cat-main are direct children of <main>'s grid
   (see THE TWO-COLUMN PAGE above). */
.cat-shell { display: contents; }
/* minmax(0,1fr), never a bare 1fr: the nowrap .card-trade sets a min-content
   width a bare 1fr will happily grow to. */
.cat-main {
  grid-column: 2; grid-row: 2;
  padding: 38px var(--pad) 0;
  min-width: 0; container-type: inline-size; container-name: cat;
}

/* THE WALL. Flush to the viewport's left edge, full viewport height under the
   masthead, right hairline only, radius 0, no shadow ring, thin quiet
   scrollbar. Every one of the removed properties - the 18px radius, the
   four-sided border, the 96/140px top and bottom insets, the container-gutter
   offset, the loud 10px 34%-ink scrollbar - existed to make a CARD legible, and
   this is not a card.
   No z-index: a sticky element without one passes under the masthead's 100. */
.side {
  grid-column: 1; grid-row: 1 / 3;
  position: sticky;
  top: var(--navh);
  align-self: start;
  height: calc(100dvh - var(--navh));
  max-height: none;
  margin: 0;
  padding: 30px var(--gut) 34px;
  background: var(--card-bg);
  border: 0;
  border-right: 1px solid var(--line-2);
  border-radius: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--ink) 20%, transparent) transparent;
  transition: transform .42s var(--ease);
}
.side::-webkit-scrollbar { width: 9px; }
.side::-webkit-scrollbar-track { background: transparent; }
.side::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--ink) 20%, transparent);
  border: 0; border-radius: 9px;
}

.cat-group { border: 0; margin: 0; padding: 0; min-width: 0; }
.cat-legend { padding: 0; }
/* Display anchor for the wall, set in Fraunces like the funnel's .sec-title,
   not a bold sans line and never a quiet mono eyebrow. */
.cat-h {
  font-family: var(--display); font-weight: 700; font-size: 1.32rem;
  letter-spacing: -0.015em; text-transform: none; color: var(--ink);
  margin: 0 0 16px; padding-inline: 10px;
}
.cat-list { display: flex; flex-direction: column; gap: 0; }

/* A scrolling text list, not a stack of chips: selection is carried by colour,
   weight and one Klein bar, never by a pill. The row is the full width of the
   wall's inner box so the hover tint reads as a band, not a tag. */
.cat-row {
  position: relative; display: flex; align-items: center; gap: 12px;
  width: 100%; height: 44px; padding: 0 10px;
  cursor: pointer; text-align: left;
  border-radius: 12px;
  font-size: 0.99rem; font-weight: 400; letter-spacing: -0.004em;
  color: var(--muted);
  transition: color 140ms ease, background-color 140ms ease;
}
/* The 3px Klein bar, flush against the wall's own left edge. */
.cat-row::after {
  content: "";
  position: absolute;
  left: calc(-1 * var(--gut));
  top: 9px; bottom: 9px;
  width: 3px;
  border-radius: 0 2px 2px 0;
  background: var(--accent);
  opacity: 0;
  transition: opacity 140ms ease;
}
/* The input covers the row rather than using .sr-only. A 1px clipped box would
   make the browser scroll THAT into view, landing the visible row off-screen
   inside this scrolling rail. opacity:0 keeps it focusable and in the a11y tree. */
.cat-input {
  position: absolute; inset: 0; margin: 0; width: 100%; height: 100%;
  appearance: none; opacity: 0; cursor: pointer;
}
/* opacity:1 always - colour de-emphasises these, never opacity (the old 0.75
   composited under AA). */
.cat-ico {
  flex: 0 0 auto; width: 21px; height: 21px; margin-top: 0;
  stroke: currentColor; fill: none; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  vector-effect: non-scaling-stroke; overflow: visible;
  opacity: 1; transition: stroke 140ms ease;
}
/* Count sits inline right after the label, in parentheses, as the reference
   does. Not a right-aligned column: that reads as a data table. */
.cat-text {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: baseline; gap: 6px;
}
.cat-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The longest names ("Trades & Home Services") run 22 chars; a touch of
   tracking keeps every label whole inside the wall instead of ellipsing. */
#cat-list .cat-label { font-size: 1em; letter-spacing: -0.012em; }
/* opacity:1 deliberately. The old .tab-n 0.65 composites to 2.549:1, an AA fail.
   Never use opacity to de-emphasise text. */
.cat-n {
  flex: 0 0 auto; font-weight: 400; font-size: 0.99rem;
  font-variant-numeric: tabular-nums; color: var(--muted); opacity: 1;
}
.cat-n::before { content: "("; }
.cat-n::after { content: ")"; }

.cat-rule { height: 0; margin: 12px 0; border: 0; border-top: 1px solid var(--line); width: auto; }

/* .cat-row.cat-sub is gone with the nested taxonomy (2026-07-25). The indent and the
   0.92em label were the only thing making Legal and Finance read as smaller, subordinate
   entries; both are top-level rows now and set identically to every other industry. */

.cat-row:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--ink) 5%, transparent);
}

/* Selection is accent text, weight and the Klein bar. */
.cat-row:has(.cat-input:checked) {
  color: var(--accent-text);
  font-weight: 600;
  background: color-mix(in srgb, var(--accent) 10%, var(--card-bg));
}
.cat-row:has(.cat-input:checked)::after { opacity: 1; }
.cat-row:has(.cat-input:checked) .cat-ico { stroke: var(--accent-text); }
.cat-row:has(.cat-input:checked) .cat-n { color: var(--accent-text); }

/* Inset ring: a positive offset gets clipped by .side's overflow-y. */
.cat-row:has(.cat-input:focus-visible) {
  outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 12px;
}

/* Hearts share one path; the fill is revealed by opacity so there is no
   geometric jump between states. */
.cat-ico .h-fill, .card-fav .h-fill, .pv-fav .h-fill { fill: currentColor; fill-opacity: 0; }
/* Heads the rail now, so no separating margin - it is a peer of All Templates. */
.cat-fav { margin-top: 0; }
.cat-fav[data-has="1"] .cat-ico { color: var(--accent); stroke: var(--accent); opacity: 1; }
.cat-fav[data-has="1"] .h-fill { fill-opacity: 1; }

/* ---------------------------------------------------------------------------
   TABLET + MOBILE - one DOM, two layouts.
   A wall cannot be a wall on a phone, so it becomes a persistent,
   always-reachable filter bar pinned under the masthead: same rows, same
   selected treatment, still one left axis.
   ------------------------------------------------------------------------ */
@media (max-width: 1099px) {
  main { display: block; overflow-x: clip; }
  main::before { display: none; }
  .cat-shell { display: block; width: auto; margin: 0; padding-top: 0; }

  .wrap { padding: clamp(30px, 6vw, 52px) var(--pad) 0; }
  /* The filter bar's own bottom border is the shelf here, so the hero's rule
     goes: two hairlines 40px apart read as an accident. */
  .wrap::after { display: none; }
  /* No wall to balance against here, so the copy takes the width back. */
  .standfirst { max-width: 58ch; }

  .side {
    position: sticky; top: var(--navh); z-index: 40;
    height: auto; max-height: none;
    padding: 11px var(--pad);
    margin: 0 0 26px;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
    backdrop-filter: blur(10px);
    border: 0; border-bottom: 1px solid var(--line);
    border-radius: 0;
    overflow: visible;
  }
  .cat-h { display: none; }
  /* Bleeds into the right gutter so a half-chip is the scroll cue. */
  .cat-list {
    flex-direction: row; gap: 8px; overflow-x: auto; padding-top: 0;
    scrollbar-width: none; scroll-snap-type: x proximity; padding-bottom: 2px;
    margin-right: calc(-1 * var(--pad)); padding-right: var(--pad);
  }
  .cat-list::-webkit-scrollbar { display: none; }
  .cat-row {
    flex: 0 0 auto; width: auto; height: 44px; scroll-snap-align: start;
    border-inline-start: 0; border: 1px solid var(--line);
    border-radius: var(--radius-pill); padding: 0 16px;
    font-size: 0.95rem;
  }
  .cat-row::after { display: none; }
  .cat-row:has(.cat-input:checked) {
    border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--bg));
  }
  .cat-text { gap: 6px; justify-content: flex-start; }
  .cat-rule { display: none; }
  .cat-main { padding: 0 var(--pad); }
}

/* ============================================================
   Browse-panel collapse (SPEC PART 6). A Canva-style pill that slides the whole
   wall off to the left and hands the freed width back to the cards, mirroring
   .vf-side / .vf-collapse in start/flow.html.
   It only slides; it never fades, because a fade is how you dismiss a card and
   this is a wall.
   Desktop only: below 1100px .side is already the pinned filter bar (above) and
   needs no drawer, exactly as the funnel swaps to a bottom sheet on mobile.
   ============================================================ */
.side-collapse { display: none; }              /* off until the desktop rail exists */

@media (min-width: 1100px) {
  .side { will-change: transform; }
  /* collapsed: rail track -> 0 so the cards reflow wider; the panel keeps
     its width (so the 0-width track can't squish it) and slides off-left.
     The selector reaches <main>, not .cat-shell: .cat-shell is display:contents
     and has no box of its own to size. */
  main:has(.cat-shell.rail-collapsed) { grid-template-columns: 0px minmax(0, 1fr); }
  /* COLLAPSED, THE PILL NEEDS ITS OWN LANE. It parks at left:10px and is 32px wide, so it
     occupies x 10-42. The content axis is --pad = clamp(24px, 3vw, 56px), which is UNDER
     42px for every viewport below ~1400: at 1366 it computes to 41px and the first card
     row renders straight under the button. Measured, collapsed at 1366: the pill overlaps
     .card and .card-shot; at 1440 (--pad 43px) it just clears, which is why this hid from
     a sweep that only sampled round numbers.
     Floor the axis at 52px while collapsed (42px pill + 10px air) so it can never happen
     at any width, and derive nothing by hand: expanded, --pad is untouched. */
  :root:has(.cat-shell.rail-collapsed) { --pad: max(clamp(24px, 3vw, 56px), 52px); }
  .cat-shell.rail-collapsed .side {
    width: var(--rail);
    transform: translateX(-100%);
  }

  /* the pill = a viewport-FIXED control (like the funnel's, so it can't be
     clipped by the rail's own overflow and stays reachable while scrolling).
     Both X positions are CSS: the rail is flush to the viewport edge, so they
     are pure functions of --rail. */
  .side-collapse {
    position: fixed; z-index: 55; top: 50%; transform: translateY(-50%);
    left: calc(var(--rail) - 16px);
    display: grid; place-items: center;
    width: 32px; height: 60px; padding: 0; cursor: pointer;
    border: 1px solid var(--line); border-radius: var(--radius-pill);
    background: var(--card-bg); color: var(--accent-text);
    box-shadow: 0 6px 20px -6px rgba(21, 22, 26, .34),
                0 2px 6px -2px rgba(21, 22, 26, .2);
    transition: left .42s cubic-bezier(.22, .61, .36, 1), background .16s ease;
  }
  .side-collapse:hover { background: color-mix(in srgb, var(--accent) 8%, var(--card-bg)); }
  .side-collapse:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .side-collapse.is-collapsed { left: 10px; }
  .side-chev { transition: transform .3s cubic-bezier(.22, .61, .36, 1); }
  .side-collapse.is-collapsed .side-chev { transform: rotate(180deg); }
}

@media (prefers-reduced-motion: reduce) {
  main, .footer, .side, .side-collapse, .side-chev,
  .cat-row, .cat-row::after { transition: none; }
}

/* ---------- category tabs (legacy horizontal rail, retained for reference) ---------- */
.tabs-outer { position: relative; border-bottom: 1px solid var(--line); }
.tabs {
  display: flex; gap: 42px; overflow-x: auto; scrollbar-width: none;
  padding-block: 26px 0;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 14px;
  background: none; border: 0; cursor: pointer; padding: 0 4px 20px;
  font-size: 1.06rem; font-weight: 500; color: var(--muted); white-space: nowrap;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: color 180ms ease;
}
/* One stroke weight, round caps and joins across the whole set. vector-effect
   keeps the stroke exactly 1.5 device-independent pixels, so the row stays
   crisp on a 4K panel instead of thickening with the scale factor. */
.tab svg {
  width: 30px; height: 30px; stroke: currentColor; fill: none;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
  vector-effect: non-scaling-stroke; overflow: visible;
}
.tab:hover { color: var(--ink); }
.tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.tab-n { font-family: var(--mono); font-size: 0.7rem; opacity: 0.65; }

/* ---------- toolbar ----------
   The right half of the hero band: bottom-aligned to the headline block and
   sitting on the same shelf. Not floating, not centred on a third axis.
   SPECIFICITY: every #q / #sort / .count rule below carries a `:root .toolbar`
   prefix because variants.css loads after this file and `body.v-a #q` (1,1,1)
   would otherwise strip the field back to an underline. Drop the prefix once
   variants.css goes. */
.toolbar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
  gap: 10px; padding: 0; margin: 0;
}
/* SHOWING 89 OF 89 IS GONE FROM THE PAGE. Founder 2026-07-26: "kill the words
   'showing 89 of 89'. i dont like that part there." It stays in the DOM as the
   sr-only aria-live region it was born as, which is the whole reason it exists:
   a buyer on a screen reader filtering the grid gets told how many survived.
   Sighted buyers can see the cards. Do not re-promote it to a visible line to
   "give the right side a reason to exist" — the search field is that reason. */

:root .toolbar #q,
:root .toolbar #sort {
  height: 46px;
  border: 1px solid var(--line-2);
  border-radius: 13px;
  background: var(--card-bg);
  color: var(--ink);
  font-family: var(--body);
  font-size: 0.95rem;
  padding: 0 15px;
  transition: border-color 160ms ease;
}
:root .toolbar #q { flex: 0 1 300px; max-width: none; caret-color: var(--accent); }
:root .toolbar #sort { flex: 0 0 auto; cursor: pointer; }
#q::placeholder { color: var(--muted); }
:root .toolbar #q:hover,
:root .toolbar #sort:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--line-2)); }
:root .toolbar #q:focus-visible,
:root .toolbar #sort:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}
/* iOS refuses to zoom-on-focus only at >=16px. */
@media (pointer: coarse) { :root .toolbar #q { font-size: 16px; } }

/* ---------------------------------------------------------------------------
   THE HERO BAND ONLY SPLITS WHEN THERE IS ROOM FOR TWO REAL MEASURES.
   The toolbar sits in an `auto` track, which takes max-content (300px field +
   164px select + gap) and never yields. Below ~1360 that starved the copy
   column: at 1100 the headline rendered 226px wide and 302px tall, a five-line
   tower next to an empty middle. Stack instead; the axis is the point of this
   direction, not the split.
   ------------------------------------------------------------------------ */
@media (max-width: 1359px) {
  .wrap { grid-template-columns: minmax(0, 1fr); }
  .toolbar { justify-content: flex-start; margin-top: 26px; }
  :root .toolbar #q { flex: 1 1 260px; max-width: 460px; }
  /* Stacked: nothing is competing for the right side any more, so the copy takes the
     width back instead of staying on a measure sized around a toolbar that has moved. */
  .standfirst { max-width: 62ch; }
}
/* The other half of the same complaint: at 1360+ the field was a 300px box marooned at
   the far edge with ~500px of nothing between it and the copy. A search field on a
   storefront of 89 sites deserves the room, and filling that gap is what stops the band
   reading as two objects that happen to share a line.
   `0 0 440px` and nowrap, NOT `0 1 440px`: the toolbar sits in the grid's `auto` track,
   and a shrinkable basis lets that track resolve to 388px — under the two controls' own
   width — so the select wrapped onto a second line under a half-width field. A rigid
   basis is what makes the track measure the row it is actually holding. */
@media (min-width: 1360px) {
  .toolbar { flex-wrap: nowrap; }
  :root .toolbar #q { flex: 0 0 440px; }
}

/* ---------- grid ---------- */
/* Three across, edge to edge. Cards scale with the window rather than being
   pinned to a fixed column width. */
/* Column count is driven by the SPACE THE GRID ACTUALLY HAS, not the viewport.
   With a sidebar, viewport width no longer predicts grid width: widening the
   rail was silently squeezing cards to 275px, well under the floor at which
   business names stop fitting on one line.
   The floor is 370px, MEASURED, not assumed: sweeping the column width against
   the live 89 titles, "Wattlebrook Library & Commons" (the longest that still
   fits) wraps to two lines at 365 and clears at 370. The old comment said 360;
   at 360 and 365 that title wraps, so the 1136 threshold it derived was letting
   the 3-up step land on 361px columns and break the file's own rule. Re-measure
   here if the longest card name changes.
     2 cols need 2*370 + 28  =  768px of .cat-main -> kept at 748, see below
     3 cols need 3*370 + 56  = 1166px of .cat-main
   The 2-col threshold stays 748: raising it to 768 would drop the 1100-1110px
   band to a single 762px card, which is worse than one title wrapping in a 10px
   window. Only the 3-col step was landing under the floor on real screens. */
.grid {
  display: grid; grid-template-columns: minmax(0, 1fr);
  column-gap: var(--gap); row-gap: 54px; padding-bottom: 130px;
}
@container cat (min-width: 748px)  { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container cat (min-width: 1166px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* Same numbers hardcoded, for any engine without container queries. */
@supports not (container-type: inline-size) {
  .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  @media (max-width: 1599px) { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
  @media (max-width: 767px)  { .grid { grid-template-columns: minmax(0, 1fr); } }
}
/* The old viewport rule that lived here is gone: it came AFTER the container
   queries and so silently beat them. Column count is now decided solely by
   .cat-main's width, which is the only measure that knows about the sidebar. */

.card { display: flex; flex-direction: column; min-width: 0; }
.card[hidden] { display: none; }

.card-shot {
  position: relative; display: block; width: 100%; padding: 0; cursor: pointer;
  /* 720 x 508: the 4:3 capture (720x540) less 6% of its height. Was 720x475
     (-12%); lengthened 7% per founder 2026-07-23. Still inside the capture, so
     it never letterboxes. Cropped from the bottom by object-position: top. */
  aspect-ratio: 720 / 508; overflow: hidden; border-radius: var(--radius-card);
  background: var(--c-bg, var(--card-bg));
  border: 1px solid var(--line);
  transition: opacity 150ms ease-in-out;
}
.card-shot img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
}
.card-shot:hover { opacity: 0.8; }
.card-shot:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Hover autoplay scroll-through: the tall img/<slug>-full.webp layered over the
   4x3 thumbnail and scrolled top->bottom by a single CSS pass. The full image is
   built lazily in catalogue.js on first hover (and skipped entirely under
   reduced-motion); pointer-events:none so the click-to-open-modal is untouched.
   Resting state stays the thumbnail underneath. .card-shot.playing must sit
   AFTER .card-shot:hover (equal specificity) so its opacity:1 wins over the
   0.8 hover dim while scrolling. */
.card-shot .shot-full {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center top;
  opacity: 0; pointer-events: none; transition: opacity 220ms ease;
}
.card-shot.playing { opacity: 1; }
/* 12s, not 6 (founder 2026-07-26: "the hover animation goes too quickly when u go over the
   templates and its too jarring. it needs to slow down so people can observe the details... reduce
   scroll speed by 50%"). The pass exists so a buyer can READ the page on the way past; at 6s a
   3000px page went by at ~500px a second, which is faster than anyone reads a headline.
   linear, not ease: an eased scroll spends its middle third at double the average speed, which is
   the exact part of the page the buyer is trying to look at. */
.card-shot.playing .shot-full { opacity: 1; animation: shotScroll 12s linear forwards; }
@keyframes shotScroll {
  from { object-position: center top; }
  to   { object-position: center bottom; }
}

/* ---------- heart ---------- */
/* .card-frame is mandatory: .card-shot is an <a>, and a button inside an anchor
   is silently hoisted out by the parser, dropping the heart below the
   thumbnail with no console error. (Same trap when it was a <button>.) */
.card-frame { position: relative; }

/* Opaque, flat, hairlined. That is the only treatment legible over both a white
   hero and a black hero without a scrim, and a scrim would be a gradient, which
   the brand forbids outright. 12px clears the 18px card radius. */
.card-fav {
  position: absolute; top: 12px; left: 12px; z-index: 2;
  width: 36px; height: 36px; padding: 0; cursor: pointer;
  display: grid; place-items: center;
  border-radius: 999px; border: 1px solid var(--line);
  background: var(--bg); color: var(--muted);
  transition: transform 160ms cubic-bezier(.2,0,0,1), color 160ms ease,
              border-color 160ms ease, background-color 160ms ease;
}
/* Expands the hit target to 44px without moving the visible disc. */
.card-fav::before { content: ""; position: absolute; inset: -4px; border-radius: inherit; }
.card-fav svg {
  width: 20px; height: 20px; stroke: currentColor; fill: none;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
  vector-effect: non-scaling-stroke; pointer-events: none;
}
.card-fav:hover { color: var(--accent); border-color: var(--accent); transform: scale(1.06); }
.card-fav:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-fav[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
.card-fav[aria-pressed="true"] .h-fill { fill-opacity: 1; }

@keyframes favPop { 0% { transform: scale(1); } 45% { transform: scale(1.22); } 100% { transform: scale(1); } }
.card-fav.is-popping { animation: favPop 260ms cubic-bezier(.2,0,0,1); }
@media (prefers-reduced-motion: reduce) {
  .card-fav, .card-fav.is-popping { transition: none; animation: none; }
}

/* Preview Room heart, first in the tools row: the room is the highest-intent
   moment, so saving must not require closing the dialog. */
.pv-fav {
  display: grid; place-items: center; width: 36px; height: 36px; padding: 0;
  cursor: pointer; border-radius: 999px; border: 1px solid var(--line-2);
  background: transparent; color: var(--muted);
  transition: color 160ms ease, border-color 160ms ease;
}
.pv-fav svg {
  width: 20px; height: 20px; stroke: currentColor; fill: none;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; pointer-events: none;
}
.pv-fav:hover { color: var(--accent); border-color: var(--accent); }
.pv-fav:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pv-fav[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
.pv-fav[aria-pressed="true"] .h-fill { fill-opacity: 1; }

/* Un-hearting inside the favourites view dims the card in place rather than
   snatching it away, so a mis-tap is recoverable and focus is never destroyed. */
.card.is-pending { opacity: 0.4; transition: opacity 200ms ease; }

.fav-empty {
  grid-column: 1 / -1; padding: 70px 20px; text-align: center; color: var(--muted);
}
.fav-empty[hidden] { display: none; }

.card-meta { margin-top: 18px; display: flex; flex-direction: column; gap: 7px; }
.card-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
/* The business name is the primary label on the card, so it carries full ink
   and never competes with an indicator glyph. */
.card-name {
  font-family: var(--display); font-weight: 700; font-size: 1.28rem;
  letter-spacing: -0.022em; line-height: 1.22; color: var(--ink); margin: 0;
  min-width: 0; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
/* Prices are mono per the brand's chip recipe. */
.card-price {
  font-family: var(--mono); font-weight: 500; font-size: 0.86rem; color: var(--ink);
  flex: 0 0 auto; font-variant-numeric: tabular-nums;
}
/* Category is now a pastel pill whose hue evokes the industry's emotional
   register (founder 2026-07-20). Base pill below; per-room colours follow,
   generated + WCAG-AA verified both themes by scratchpad/pastel-pills.mjs. */
.card-trade {
  font-family: var(--mono); font-size: 0.68rem; letter-spacing: 0.07em;
  text-transform: uppercase; margin: 0; align-self: flex-start;
  padding: 4px 11px; border-radius: 999px;
  background: var(--line-soft); color: var(--muted);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

[data-room="Legal"] .card-trade{background:#e6e8f4;color:#3b4491;}  /* 7.1:1 */
[data-room="Professional"] .card-trade{background:#eaedf1;color:#50677c;}  /* 5.0:1 */
[data-room="Finance"] .card-trade{background:#e7f4f0;color:#327662;}  /* 4.8:1 */
[data-room="Medical"] .card-trade{background:#e6f2f4;color:#34757f;}  /* 4.6:1 */
[data-room="Health"] .card-trade{background:#e6f4ec;color:#2f794f;}  /* 4.7:1 */
[data-room="Wellness"] .card-trade{background:#e9f2e8;color:#427740;}  /* 4.7:1 */
[data-room="Beauty"] .card-trade{background:#f6e5ec;color:#9b315f;}  /* 5.7:1 */
[data-room="Food"] .card-trade{background:#f7e8e4;color:#a34529;}  /* 5.1:1 */
[data-room="Drink"] .card-trade{background:#f4e6ea;color:#913b52;}  /* 5.9:1 */
[data-room="Hospitality"] .card-trade{background:#f6ece4;color:#975a2b;}  /* 4.7:1 */
[data-room="Trades"] .card-trade{background:#f6f0e4;color:#866628;}  /* 4.7:1 */
[data-room="Home Services"] .card-trade{background:#e7f0f4;color:#397084;}  /* 4.7:1 */
[data-room="Property"] .card-trade{background:#f4f1e6;color:#7d6b31;}  /* 4.6:1 */
[data-room="Travel"] .card-trade{background:#e5edf6;color:#31629b;}  /* 5.3:1 */
[data-room="Creative"] .card-trade{background:#f0e5f5;color:#763597;}  /* 6.3:1 */
[data-room="Retail"] .card-trade{background:#f6e6e4;color:#9f382d;}  /* 5.6:1 */
[data-room="Technology"] .card-trade{background:#e8e5f6;color:#46319b;}  /* 7.8:1 */
[data-room="Sport"] .card-trade{background:#f8eae2;color:#a9511e;}  /* 4.6:1 */
[data-room="Leisure"] .card-trade{background:#f6f3e4;color:#7d6e21;}  /* 4.6:1 */
[data-room="Education"] .card-trade{background:#e6eaf4;color:#3b5291;}  /* 6.2:1 */
[data-room="Community"] .card-trade{background:#ecf3e7;color:#4e7434;}  /* 4.8:1 */
[data-room="Automotive"] .card-trade{background:#eaecf0;color:#525f7a;}  /* 5.4:1 */

[data-theme="dark"] [data-room="Legal"] .card-trade{background:#313345;color:#9da3dd;}  /* 6.0:1 */
[data-theme="dark"] [data-room="Professional"] .card-trade{background:#353b40;color:#aabecf;}  /* 5.9:1 */
[data-theme="dark"] [data-room="Finance"] .card-trade{background:#31443e;color:#9edbc9;}  /* 5.4:1 */
[data-theme="dark"] [data-room="Medical"] .card-trade{background:#314245;color:#9dd4dd;}  /* 5.6:1 */
[data-theme="dark"] [data-room="Health"] .card-trade{background:#304539;color:#9cdeb8;}  /* 5.4:1 */
[data-theme="dark"] [data-room="Wellness"] .card-trade{background:#344234;color:#a6d5a5;}  /* 5.6:1 */
[data-theme="dark"] [data-room="Beauty"] .card-trade{background:#472e39;color:#e396b8;}  /* 5.6:1 */
[data-theme="dark"] [data-room="Food"] .card-trade{background:#49332d;color:#e6a794;}  /* 5.8:1 */
[data-theme="dark"] [data-room="Drink"] .card-trade{background:#453136;color:#dd9dae;}  /* 5.8:1 */
[data-theme="dark"] [data-room="Hospitality"] .card-trade{background:#48392e;color:#e6b794;}  /* 5.7:1 */
[data-theme="dark"] [data-room="Trades"] .card-trade{background:#483f2e;color:#e4ca95;}  /* 5.6:1 */
[data-theme="dark"] [data-room="Home Services"] .card-trade{background:#313f44;color:#9ecbdb;}  /* 5.7:1 */
[data-theme="dark"] [data-room="Property"] .card-trade{background:#454030;color:#dece9c;}  /* 5.6:1 */
[data-theme="dark"] [data-room="Travel"] .card-trade{background:#2e3a47;color:#96bae3;}  /* 5.9:1 */
[data-theme="dark"] [data-room="Creative"] .card-trade{background:#3e3046;color:#c999e1;}  /* 5.9:1 */
[data-theme="dark"] [data-room="Retail"] .card-trade{background:#48302e;color:#e69c94;}  /* 5.7:1 */
[data-theme="dark"] [data-room="Technology"] .card-trade{background:#332e47;color:#a696e3;}  /* 5.9:1 */
[data-theme="dark"] [data-room="Sport"] .card-trade{background:#4b362a;color:#e6b294;}  /* 5.8:1 */
[data-theme="dark"] [data-room="Leisure"] .card-trade{background:#48442d;color:#e6d894;}  /* 5.3:1 */
[data-theme="dark"] [data-room="Education"] .card-trade{background:#313645;color:#9daedd;}  /* 6.0:1 */
[data-theme="dark"] [data-room="Community"] .card-trade{background:#394332;color:#b7daa0;}  /* 5.5:1 */
[data-theme="dark"] [data-room="Automotive"] .card-trade{background:#36393f;color:#abb7ce;}  /* 6.0:1 */

/* Pricing-tier chip: production tier derived from price, generated by
   catalogue.js from DATA[slug].price. Same editorial pill recipe as
   .card-trade; ascending-premium hue ramp (slate -> blue -> gold -> violet).
   Every pair computed >=4.5:1 both themes (scratchpad/tier-contrast.py). */
.card-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
.card-tier {
  font-family: var(--mono); font-size: 0.68rem; letter-spacing: 0.07em;
  text-transform: uppercase; margin: 0; flex: 0 0 auto;
  padding: 4px 11px; border-radius: 999px;
  background: var(--line-soft); color: var(--muted); white-space: nowrap;
}
.card-tier[data-tier="basic"]     { background:#e9ebef; color:#545c6b; }  /* 5.6:1 */
.card-tier[data-tier="cinematic"] { background:#e4eefa; color:#2f5c96; }  /* 5.8:1 */
.card-tier[data-tier="showpiece"] { background:#f7efd9; color:#7d5e16; }  /* 5.3:1 */
.card-tier[data-tier="flagship"]  { background:#efe6f6; color:#6b3597; }  /* 6.7:1 */
[data-theme="dark"] .card-tier[data-tier="basic"]     { background:#363a42; color:#aeb6c4; }  /* 5.6:1 */
[data-theme="dark"] .card-tier[data-tier="cinematic"] { background:#2c3a4c; color:#93bce6; }  /* 5.8:1 */
[data-theme="dark"] .card-tier[data-tier="showpiece"] { background:#453d29; color:#e3c88f; }  /* 6.6:1 */
[data-theme="dark"] .card-tier[data-tier="flagship"]  { background:#3d3049; color:#cd9be1; }  /* 5.5:1 */

/* Buy control. Present ONLY on cards whose slug is in
   functions/api/_templates-registry.js — the sellable set. A card with no
   registry entry gets no buy control at all, because /api/template-checkout
   400s on an unknown sku and an offer that cannot be fulfilled is worse than
   no offer. Reuses the .cta pill (Klein primary) and only resizes it for the
   card; the price text is written from the card's data-price pin, which
   check-gallery-integrity.mjs holds to LADDER.usd.templates. */
.card-buy {
  align-self: flex-start; margin-top: 4px;
  font-size: 0.88rem; padding: 10px 18px;
  font-variant-numeric: tabular-nums;
}

.noresults {
  grid-column: 1 / -1; padding: 70px 20px; text-align: center; color: var(--muted);
}
.noresults[hidden] { display: none; }

/* ---------- preview room ---------- */
.preview {
  padding: 0; border: 0; width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh;
  background: var(--bg); color: var(--ink);
  display: none; grid-template-rows: auto 1fr auto;
}
.preview[open] { display: grid; }
.preview::backdrop { background: rgba(0, 0, 0, 0.6); }
.pv-head {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: 13px 22px; border-bottom: 1px solid var(--line); flex-wrap: wrap;
}
.pv-id { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.pv-name { font-weight: 600; font-size: 1.02rem; }
.pv-trade { font-size: 0.85rem; color: var(--muted); }
.pv-tools { display: flex; align-items: center; gap: 12px; }
.pv-price { font-size: 0.85rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.devices { display: flex; gap: 3px; border: 1px solid var(--line-2); border-radius: 999px; padding: 3px; }
.devices button {
  background: transparent; border: 0; cursor: pointer; font-size: 0.78rem;
  padding: 5px 12px; border-radius: 999px; color: var(--muted);
}
.devices button[aria-pressed="true"] { background: var(--ink); color: var(--bg); }
.presets { display: flex; gap: 3px; border: 1px solid var(--line-2); border-radius: 999px; padding: 3px; }
.presets[hidden] { display: none; }
.presets button {
  background: transparent; border: 0; cursor: pointer; font-size: 0.78rem;
  padding: 5px 10px; border-radius: 999px; color: var(--muted);
  display: inline-flex; align-items: center; gap: 6px;
}
.presets button[aria-pressed="true"] { background: var(--ink); color: var(--bg); }
.presets .sw {
  width: 11px; height: 11px; border-radius: 50%; flex: none;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 22%, transparent);
}
@media (max-width: 720px) { .presets .lbl { display: none; } }
/* The typeface rail. Same pill as the colourway rail on purpose - two rails that do the same
   KIND of thing must look identical (surface law: same component everywhere). The swatch is a
   letterform set in the face itself, so the chip previews what it does. */
.presets.fonts .sw {
  border-radius: 4px;
  display: grid;
  place-items: center;
  font-size: 12px;
  line-height: 1;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--line-2);
}
.presets.fonts button[aria-pressed="true"] .sw { color: var(--bg); border-color: transparent; }
.pv-close {
  background: transparent; border: 1px solid var(--line-2); border-radius: 999px;
  width: 32px; height: 32px; cursor: pointer;
}
.pv-stage { overflow: auto; background: color-mix(in srgb, var(--ink) 4%, var(--bg)); display: grid; justify-items: center; align-items: start; padding: 22px 0; }
.pv-frame {
  background: var(--pv-bg, #fff); border-radius: 10px; overflow: hidden;
  box-shadow: 0 18px 50px -18px rgba(0, 0, 0, 0.28); width: 100%;
}
.pv-frame img { width: 100%; height: auto; display: block; }
.pv-frame iframe { width: 100%; height: 78vh; border: 0; display: block; }
.pv-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 11px 22px; border-top: 1px solid var(--line);
}
.pv-nav {
  background: transparent; border: 0; cursor: pointer; font-size: 0.85rem; color: var(--muted);
  max-width: 30ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pv-nav:hover { color: var(--ink); }
.pv-pos { font-size: 0.8rem; color: var(--muted); margin: 0; }


/* ============================================================================
   HEADER - transposed from the storefront (site/styles.css) so both pages
   render one identical masthead. Rules copied verbatim; only the token names
   are remapped to this stylesheet's equivalents (identical VALUES):
     --paper -> --bg,  --surface -> --card-bg,  --radius-button -> 13px
   The nav is full-bleed, not a centred container: a centred 1140-ish nav over a
   full-bleed page was a fifth axis. The wordmark ranges off the wall's inner
   inset (+8px so it lines up with the row icons, not the wall's border) and the
   CTAs off the content column's right edge.
   ========================================================================== */
.nav {
  width: auto; margin: 0; min-height: var(--navh);
  padding-inline: calc(var(--gut) + 8px) var(--pad);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 24px;
  position: sticky; top: 0; z-index: 100;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.brand { display: inline-flex; align-items: center; padding: 14px 0; justify-self: start; }
.brand-logo { height: 22px; width: auto; }
.light-only { display: block; }
.dark-only { display: none; }
[data-theme="dark"] .light-only { display: none; }
[data-theme="dark"] .dark-only { display: block; }
.nav-links { display: flex; align-items: center; gap: 26px; color: var(--muted); font-size: 0.9rem; font-weight: 500; justify-self: center; }
.nav-links a { transition: color 0.2s; text-decoration: none; color: inherit; }
.nav-links a:hover { color: var(--ink); }
.nav-right { display: flex; align-items: center; gap: 12px; justify-self: end; }
.theme-toggle {
  width: 44px; height: 44px; border-radius: 12px; border: 1px solid var(--line);
  background: var(--card-bg); color: var(--ink); cursor: pointer; font-size: 15px;
  line-height: 1; display: grid; place-items: center;
  transition: border-color 0.2s, background 0.2s;
}
.theme-toggle:hover { border-color: var(--accent); }
.ico-moon { display: none; }
[data-theme="dark"] .ico-sun { display: none; }
[data-theme="dark"] .ico-moon { display: block; }
.nav-login-btn {
  display: inline-flex; align-items: center; gap: 5px; height: 44px; padding: 0 17px;
  border-radius: 12px; border: 1px solid var(--line); background: var(--card-bg);
  color: var(--ink); font-weight: 600; font-size: 0.875rem; cursor: pointer;
  text-decoration: none; white-space: nowrap;
  transition: border-color 0.2s ease, color 0.2s ease, transform 0.3s var(--spring);
}
.nav-login-btn:active { transform: translateY(1px); }
.nav-login-btn:hover { border-color: var(--accent); color: var(--accent-text); transform: translateY(-1px); }
.nav-login-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nav-cta {
  display: inline-flex; align-items: center; gap: 7px; height: 44px; padding: 0 18px;
  background: var(--accent); color: var(--accent-ink); border-radius: 12px;
  font-weight: 600; font-size: 0.875rem; white-space: nowrap; text-decoration: none;
  box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--accent) 55%, transparent);
  transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.3s var(--spring);
}
.nav-cta:hover {
  background: var(--accent-lift); transform: translateY(-1px);
  box-shadow: 0 12px 26px -10px color-mix(in srgb, var(--accent) 60%, transparent);
}
.nav-cta:active { transform: translateY(1px); box-shadow: 0 6px 14px -10px color-mix(in srgb, var(--accent) 50%, transparent); }
.nav-cta .chev-n { transition: transform 0.4s var(--ease); }
.nav-cta:hover .chev-n { transform: translateX(3px); }
.nav-cta .chev-n { width: 15px; height: 15px; flex: 0 0 auto; }
/* Nav buttons keep the brand's squared 12px shape + sentence case (the pill + uppercase
   override was off-brand vs the funnel/landing; removed, founder 2026-07-24). */

/* Two columns once .nav-links is hidden. The 3-column template left the burger
   and theme toggle sitting in the MIDDLE `auto` cell with a dead gutter beside
   them, and the drawer panel (position:absolute; right:0 off .nav-drawer) then
   opened from the middle of the screen and ran off the left edge. */
@media (max-width: 860px) {
  .nav-links { display: none; }
  .nav { grid-template-columns: 1fr auto; }
}
@media (max-width: 720px) { .nav-login-btn { padding: 0 12px; } }

/* Unified nav drawer (mobile) — mirrors the storefront styles.css pattern,
   adapted to this stylesheet's tokens. Desktop: hidden (nav-links shows). */
.nav-drawer { display: none; }
@media (max-width: 860px) {
  .nav-right .nav-login-btn, .nav-right > .nav-cta { display: none; }
  .nav-drawer { display: block; position: relative; }
  .nav-burger {
    display: grid; place-items: center; width: 44px; height: 44px; list-style: none;
    border-radius: 12px; border: 1px solid var(--line); background: var(--card-bg); color: var(--ink); cursor: pointer;
  }
  .nav-burger::-webkit-details-marker { display: none; }
  .nav-burger:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .burger-lines, .burger-lines::before, .burger-lines::after {
    content: ""; display: block; width: 18px; height: 2px; border-radius: 2px; background: currentColor;
    transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s;
  }
  .burger-lines { position: relative; }
  .burger-lines::before { position: absolute; top: -6px; left: 0; }
  .burger-lines::after  { position: absolute; top: 6px;  left: 0; }
  .nav-drawer[open] .burger-lines { background: transparent; }
  .nav-drawer[open] .burger-lines::before { transform: translateY(6px) rotate(45deg); }
  .nav-drawer[open] .burger-lines::after  { transform: translateY(-6px) rotate(-45deg); }
  .nav-drawer-panel { display: none; }
  .nav-drawer[open] .nav-drawer-panel {
    display: flex; flex-direction: column; align-items: stretch; gap: 2px;
    position: absolute; top: calc(100% + 12px); right: 0; z-index: 200;
    width: min(80vw, 300px); padding: 10px;
    background: var(--card-bg); border: 1px solid var(--line); border-radius: 14px;
    box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.35);
  }
  .nav-drawer-panel a { padding: 12px 14px; border-radius: 10px; color: var(--ink); font-size: 0.98rem; font-weight: 500; text-decoration: none; }
  .nav-drawer-panel a:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); }
  .nav-drawer-panel a[aria-current="page"] { color: var(--accent); font-weight: 600; }
  .nav-drawer-panel .drawer-login { margin-top: 6px; padding-top: 14px; border-top: 1px solid var(--line); }
  /* colour explicitly: `.nav-drawer-panel a` (0,2,1) beats `.nav-cta` (0,1,0),
     so the drawer CTA was rendering ink-on-Klein at ~2.5:1. */
  .nav-drawer-panel .nav-cta {
    justify-content: center; height: auto; margin-top: 8px; padding: 13px 16px;
    color: var(--accent-ink);
  }
}

/* The storefront's body line-height is 1.55. This stylesheet's differs, which
   left the transposed CTA 38px tall against the original's 42 and the nav links
   18px against 22. Pin it on the header only, so the rest of the page keeps its
   own rhythm. */
.nav, .nav a, .nav button { line-height: 1.55; }
.theme-toggle { padding: 0; }

/* Footer — a branded close matching the storefront (kept to logo + copyright
   per the copyright-only footer rule), ranged off the same left axis as
   everything above it.
   margin-top 0, not 64: that gap was bare page under the wall's bottom edge,
   which is where the wall stopped looking like a wall. The space lives inside
   the footer now, above its own hairline, so the wall runs into that rule. */
.footer {
  width: auto; margin: 0;
  padding: 116px var(--pad) 60px calc(var(--rail) + var(--pad));
  border-top: 1px solid var(--line); text-align: left;
  display: grid; gap: 12px; justify-items: start;
  transition: padding-left .42s var(--ease);
}
/* The footer is <main>'s sibling, so `main:has(.rail-collapsed)` cannot reach
   it: it kept a full-rail indent while the grid above it moved to the axis. */
:root:has(.cat-shell.rail-collapsed) .footer { padding-left: var(--pad); }
@media (max-width: 1099px) { .footer { padding-left: var(--pad); } }
.footer .brand-logo { height: 24px; width: auto; }
.footer p { color: var(--muted); font-size: 0.9rem; margin: 0; }
/* Brand signature under the wordmark, echoing the email footer (italic serif). */
.footer-tag { font-family: var(--display); font-style: italic; font-size: 1rem; color: var(--muted); }
.footer-fine { font-size: 0.85rem; }

/* NAV DROPDOWNS - Resources / Community (desktop). Theme-aware surface panel. */
.nav-dd { position: relative; display: inline-flex; }
.nav-dd-toggle { display: inline-flex; align-items: center; gap: 5px; font-family: var(--body, "Hanken Grotesk", system-ui, sans-serif); font-size: 0.9rem; font-weight: 500; line-height: 1; color: var(--muted); background: none; border: 0; padding: 0; cursor: pointer; transition: color 0.2s; }
.nav-dd-toggle:hover, .nav-dd:hover .nav-dd-toggle, .nav-dd:focus-within .nav-dd-toggle { color: var(--ink); }
.dd-chev { width: 13px; height: 13px; flex: 0 0 auto; transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1); }
.nav-dd:hover .dd-chev, .nav-dd:focus-within .dd-chev { transform: rotate(180deg); }
.nav-dd-panel { position: absolute; top: calc(100% + 12px); left: 50%; z-index: 200; width: max-content; max-width: 340px; padding: 8px; display: flex; flex-direction: column; gap: 2px; background: var(--card-bg); border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.35); opacity: 0; visibility: hidden; pointer-events: none; transform: translateX(-50%) translateY(6px); transition: opacity 0.18s ease, transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), visibility 0.18s; }
.nav-dd-panel::before { content: ""; position: absolute; top: -12px; left: 0; right: 0; height: 12px; }
.nav-dd:hover .nav-dd-panel, .nav-dd:focus-within .nav-dd-panel { opacity: 1; visibility: visible; pointer-events: auto; transform: translateX(-50%) translateY(0); }
.dd-item { display: flex; flex-direction: column; gap: 1px; padding: 9px 12px; border-radius: 10px; text-decoration: none; transition: background 0.15s; }
.dd-item:hover, .dd-item:focus-visible { background: color-mix(in srgb, var(--accent) 8%, transparent); outline: none; }
.dd-item b { font-family: var(--body, "Hanken Grotesk", system-ui, sans-serif); font-weight: 700; font-size: 0.92rem; color: var(--ink); letter-spacing: -0.01em; }
.dd-item span { font-size: 0.8rem; line-height: 1.3; color: var(--muted); }
.dd-item[aria-current="page"] b { color: var(--accent-text); }
.nav-drawer-panel .drawer-label { font-family: var(--mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); padding: 12px 14px 4px; margin: 0; }
@media (max-width: 860px) { .nav-dd { display: none; } }
