:root {
  --action: #2563eb;
  --action-hover: #1d4ed8;
  --cyan: #22a6c4;
  --paper: #ffffff;
  --card: #ffffff;
  --grid-line: #dbe6f7;
  --grid-line-dark: rgba(255, 255, 255, 0.06);
  --text: #000000;
  --text-muted: #5c6b85;
  --header-bg: #101010;
  --radius: 3px;
  --sans: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

  /* Page-intro type scale, shared by .category-hero, .roadmap-head,
     .ip-head, .palettes-hero and .colors-hero so every page opens with
     the same editorial hierarchy: a large title, a clear gap, a large
     readable description, then generous air before the content.
     hero    ~76px at 1440 · ~61px at 1024 · 38px on phones
     section ~64px at 1440 · ~53px at 1024 · 34px on phones
     dek     21px desktop  · 17px on phones */
  --intro-title-hero: clamp(38px, 3.8vw + 22px, 76px);
  --intro-title-section: clamp(34px, 3.2vw + 20px, 64px);
  --intro-dek: clamp(17px, 0.4vw + 15.5px, 21px);
  --intro-title-gap: clamp(16px, 1.6vw + 8px, 28px);
  --intro-space-top: clamp(12px, 4vw, 56px);
  --intro-space-bottom: clamp(40px, 5vw + 16px, 88px);
}
* {
  box-sizing: border-box;
}
html {
  scroll-behavior: smooth;
}
html,
body {
  margin: 0;
  padding: 0;
}
/* Flat, borderless backdrop site-wide — the "clean, minimalist,
   resourceboy.com-style" treatment. This used to be homepage-only
   (body.page-home overrode a graph-paper grid + radial glow that every
   other template — guide, roadmap, legal — kept), which made the home
   page look like a different site from the rest. Every page now shares
   this same plain background so the whole site reads as one template. */
body {
  font-family: var(--sans);
  color: var(--text);
  background-color: var(--paper);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  /* Page shell: header, content, footer stacked in a column that is at
     least one viewport tall, so a short page (an empty search, a filtered
     grid, a tall screen) pushes the footer to the bottom edge instead of
     leaving it floating mid-screen. Long pages are unaffected — the footer
     stays in flow after the content (see body > .site-footer). */
  display: flex;
  flex-direction: column;
}
/* Flex items with `margin: 0 auto` shrink to their content instead of
   stretching, so the full-width .wrap containers that sit directly on
   <body> are given back the width they had as blocks. */
body > .wrap {
  width: 100%;
}
a {
  color: inherit;
}
button {
  font-family: inherit;
  cursor: pointer;
}
h1,
h2,
h3,
p {
  margin: 0;
}
ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
:focus-visible {
  outline: 2px solid var(--action);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
  html {
    scroll-behavior: auto;
  }
}
.wrap {
  max-width: 1160px;
  margin: 0 auto;
  padding: 0 24px;
}
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
/* Skip link: off-screen until it receives keyboard focus, then slides into
   view. Unlike .sr-only (always hidden), this one needs to be visible while
   focused so sighted keyboard users can see and use it. */
.skip-link {
  position: absolute;
  top: -48px;
  left: 8px;
  z-index: 100;
  background: var(--header-bg);
  color: #fff;
  padding: 10px 16px;
  border-radius: 0 0 var(--radius) var(--radius);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: top 0.15s ease;
}
.skip-link:focus {
  top: 0;
}
.mono {
  font-family: var(--sans);
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 0;
  border-radius: var(--radius);
  font-size: 14px;
  font-weight: 600;
  padding: 11px 18px;
  text-decoration: none;
  transition:
    background 0.15s ease,
    color 0.15s ease,
    border-color 0.15s ease,
    transform 0.15s ease;
}
.btn-primary {
  background: var(--action);
  color: #fff;
}
.btn-primary:hover {
  background: var(--action-hover);
}
.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.btn-ghost {
  background: transparent;
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.35);
}
.btn-ghost:hover {
  border-color: #fff;
}

/* ---------- header ----------
   Two structural variants now, matching how resourceboy.com itself
   splits its header rather than running one shape everywhere:
     - .site-header--home (index.html only): a single full-bleed,
       near-black bar (--header-bg) — wordmark left, every browse link
       spread across the same row, right where resourceboy's own home
       header sits. No search lives here because the home page already
       has a large, prominent search box in the hero right below —
       duplicating it in the header would just be noise.
     - .site-header--inner (every other page — guide, category,
       roadmap, search, about, contact, privacy, terms): two rows on
       the same dark bar. Top row is wordmark + a persistent,
       full-width search field, because these pages have no hero to
       carry search instead. Bottom row is the nav link list. This
       mirrors resourceboy's own inner-page header (logo+search on
       top, category nav underneath).
   Both variants end in .header-auth (Sign in / Sign up) on the right —
   the home row's third grid track, and after the search pill on the
   inner header's top row — see "header auth actions" below. Both
   variants share .brand, .site-nav, .menu-toggle and #mobile-menu
   markup/styles below. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--header-bg);
}
.header-inner,
.header-top {
  display: flex;
  align-items: center;
  gap: 24px;
  height: 84px;
  padding: 0 32px;
}
.header-inner {
  gap: 32px;
}
/* MAIN HEADER (.site-header--home) layout: logo left, nav centred,
   actions right. Done with the existing layout system rather than
   absolute positioning — .header-inner swaps from flex to a three-track
   grid (side | nav | side), so the middle track sits in the true centre
   of the header's content box no matter how wide the wordmark or the
   right-hand controls are. The flex rule above still supplies
   align-items, height, padding and gap (all valid in grid), so header
   height and edge padding are unchanged.
   Each child is placed explicitly by grid-column rather than left to
   auto-placement: at <=640px .site-nav is display:none, and explicit
   placement keeps .brand in track 1 and .menu-toggle in track 3 instead
   of the button sliding into the vacated middle track.
   Side tracks are minmax(0, 1fr) and the nav track minmax(0, auto), so
   in a tight viewport the nav track is capped at the space available
   and .site-nav's own pre-existing overflow-x: auto absorbs the spill
   instead of the row overflowing the header.
   .header-top (the inner-page header) is deliberately untouched: it has
   no nav to centre, and its .header-bottom row is out of scope. */
.site-header--home .header-inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, 1fr);
}
.site-header--home .header-inner > .brand {
  grid-column: 1;
  justify-self: start;
}
.site-header--home .header-inner > .site-nav {
  grid-column: 2;
  justify-self: center;
  min-width: 0;
}
.site-header--home .header-inner > .menu-toggle {
  grid-column: 3;
  justify-self: end;
}
/* ---------- header auth actions ----------
   Sign in / Sign up, right-aligned. Both reuse the site's existing button
   classes: .btn-ghost (the on-dark outline) for the quieter Sign in and
   .btn-primary (--action blue) for Sign up. Only the padding is trimmed so
   the pair sits at the search pill's 40px height rather than the 44px of
   a page-body CTA.
   On the home row it shares grid track 3 with .menu-toggle; the two are
   never shown at once (the toggle only exists at <=640px, where these
   hide), so neither takes a second grid row. Below 641px the same two
   buttons live in the mobile panel instead (.mobile-menu__auth). */
.header-auth {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}
.header-auth .btn {
  padding: 9px 16px;
  white-space: nowrap;
}
.site-header--home .header-inner > .header-auth {
  grid-column: 3;
  justify-self: end;
}
/* One row cannot hold the logo, the eight centred links and the two auth
   buttons below ~1260px — measured: the nav meets Sign in at 1250 and, with
   or without the buttons, already runs under the logo at 1024 and below.
   So in that band the home header takes the inner header's shape: logo and
   auth on the 84px top row, the unchanged centred nav on a row of its own
   underneath, still on the dark bar. At <=640px .site-nav and .header-auth
   are both hidden and this rule no longer applies.
   Header "desktop" queries are written `not all and (max-width: 640px)`
   — the exact complement of the mobile query — rather than
   `(min-width: 641px)`: fractional viewports (640.8px at 125% scaling)
   matched neither, and the home row fell back to its one-row layout with
   no menu button. */
@media not all and (max-width: 640px) {
  @media (max-width: 1259px) {
    .site-header--home .header-inner {
      grid-template-columns: minmax(0, 1fr) auto;
      grid-template-rows: 84px 56px;
      row-gap: 0;
      height: auto;
    }
    .site-header--home .header-inner > .brand {
      grid-row: 1;
    }
    .site-header--home .header-inner > .header-auth {
      grid-row: 1;
      grid-column: 2;
    }
    .site-header--home .header-inner > .site-nav {
      grid-row: 2;
      grid-column: 1 / -1;
      align-self: stretch;
      /* The row rule spans the full header width, not just the padded box. */
      margin: 0 -32px;
      padding: 0 32px;
      width: calc(100% + 64px);
      /* `safe` keeps the first link reachable if the row ever has to scroll
         (the narrowest tablets); centring alone would clip it off the left. */
      justify-content: center;
      justify-content: safe center;
      border-top: 1px solid rgba(255, 255, 255, 0.08);
    }
  }
}
.brand {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  text-decoration: none;
}
.brand-mark {
  width: 48px;
  height: 48px;
  border-radius: 6px 6px 0 0;
  background: var(--header-bg);
  flex-shrink: 0;
  display: block;
  object-fit: cover;
}
.brand-text {
  font-size: 26px;
  font-weight: 800;
  letter-spacing: 0.01em;
  color: #fff;
}
/* ---------- header logo image (visual brand migration) ----------
   Supplied SVG wordmark (assets/bpozz-logo.svg, viewBox 0 0 1200 300,
   white glyphs on transparent) replaces the old .brand-text span in
   both header partials.

   The artwork carries symmetric transparent padding: the glyphs occupy
   x 195.22-1004.79 of 1200 (16.27% dead space each side) and y 3.11-296.89
   of 300. Their true aspect ratio is 2.7557:1, so an 88x32 box (2.75:1)
   with object-fit: cover scales the art to 128x32 and clips exactly 20px
   of transparent padding per side -- 0.82px short of the first glyph pixel
   on each edge. No glyph is cropped; only dead space is removed.

   Rendering at its natural 128x32 instead would spend 41.6px of the
   header's 24px flex gap on invisible padding and push the wordmark off
   the 32px/20px padding edge. The <img> width/height attributes match
   this box so the header reserves the right space before the SVG loads,
   holding .header-top at 84px. Only ever placed on the dark
   --header-bg bar. */
.brand-logo {
  display: block;
  width: 88px;
  height: 32px;
  object-fit: cover;
}
.site-nav {
  display: flex;
  align-items: center;
  gap: 34px;
  font-size: 14px;
  font-weight: 600;
  overflow-x: auto;
  scrollbar-width: none;
}
.site-nav::-webkit-scrollbar {
  display: none;
}
/* One step up (14px -> 15px, +7%) for the header nav rows, so the four
   centred links read clearly without the bar feeling oversized. Same
   family (--sans), same 600 weight, same 34px gap — only the size
   changes. Both rows now carry the same four links, so .header-bottom
   shares this step rather than staying a size behind at the base 14px;
   the value is defined once here so the two rows cannot drift. */
.site-header--home .site-nav,
.header-bottom .site-nav {
  font-size: 15px;
}
/* Seven links since Fonts joined the nav: at tablet widths the inner-page
   row ran ~6px wider than the viewport at 768px and clipped the last link.
   A tighter gap in just this band keeps the row whole at 768 and above;
   below ~720 it still scrolls, as it already did with six links. Desktop
   (900px+) keeps the base 34px gap. */
/* Icons made it eight links: at 24px the inner-page row ran 30px short at
   768px (Learning Roadmap clipped), and the home page's one-row header ran
   18px past the viewport. An 18px gap keeps both rows whole at 768 and
   above again, and applies the same step to the home row so the two navs
   match. */
@media not all and (max-width: 640px) {
  @media (max-width: 899px) {
    .header-bottom .site-nav,
    .site-header--home .site-nav {
      gap: 18px;
    }
  }
}
/* MAIN HEADER nav link colours. These base rules only ever paint the
   home header's nav: .header-bottom overrides colour, hover colour and
   both border-bottom-colors for the inner-page nav further down, so
   changing the values here leaves .header-bottom rendering identically.
   - resting link: #cdd6e8, the on-dark link colour .mobile-menu__nav
     already uses inside this same header — an established BPOZZ colour,
     not a new one. It replaces #9db3da, which is the FOOTER's link
     colour that had been borrowed onto the header bar.
   - hover/active accent: var(--action) (#2563eb, the BPOZZ primary
     blue) replaces var(--cyan). It measures 3.68:1 against --header-bg
     (#101010), so the 2px rule clears WCAG 1.4.11's 3:1 non-text
     minimum, and the nav now shares the accent already used by buttons,
     links and the focus ring instead of running a second cyan accent of
     its own. */
.site-nav a {
  flex: 0 0 auto;
  text-decoration: none;
  color: #cdd6e8;
  white-space: nowrap;
  padding: 4px 0;
  border-bottom: 2px solid transparent;
}
.site-nav a:hover {
  color: #fff;
  border-bottom-color: var(--action);
}
.site-nav a[aria-current="page"] {
  color: #fff;
  border-bottom-color: var(--action);
}
/* Split out of the rule above and left exactly as it was: the mobile
   panel is shared by both header variants, so its active state stays
   outside this main-header-only change. */
.mobile-menu__nav a[aria-current="page"] {
  color: #fff;
  border-bottom-color: var(--cyan);
}
.version-pill {
  font-size: 11px;
  color: var(--cyan);
  border: 1px solid rgba(34, 166, 196, 0.35);
  padding: 3px 9px;
  border-radius: 2px;
  background: rgba(34, 166, 196, 0.07);
  white-space: nowrap;
}
/* ---------- inner-header search + nav row ----------
   .header-search is shared with the homepage's big .hero-search (see
   below) but sized small here by default; .header-search--full is the
   modifier that stretches it to fill .header-top's top row on every
   non-home page, matching resourceboy's inner-page search bar. No
   collapse/toggle behavior anymore — it's plain and always visible at
   every viewport width, since these pages have no hero to fall back
   on for search. */
.header-search {
  display: flex;
  align-items: center;
  gap: 8px;
  position: relative;
}
.header-search--full {
  flex: 1 1 auto;
  min-width: 0;
}
.header-search input {
  width: 180px;
  max-width: 40vw;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 2px;
  padding: 7px 10px;
  font-family: inherit;
  font-size: 13px;
  color: #fff;
  background: rgba(255, 255, 255, 0.06);
}
.header-search--full input {
  width: 100%;
  max-width: none;
  padding: 10px 14px;
}
.header-search input::placeholder {
  color: #8a93a6;
}
.header-search input:focus {
  border-color: var(--cyan);
  outline: none;
}
.header-search__submit {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.06);
  color: #9db3da;
}
.header-search--full .header-search__submit {
  width: 38px;
  height: 38px;
}
.header-search__submit svg {
  width: 15px;
  height: 15px;
}
.header-search__submit:hover {
  color: #fff;
  border-color: var(--cyan);
}
/* ---------- inner-header search pill (Uiverse.io, @satyamchaudharydev) ----------
   Restyles #header-search-input's form (.header-search.header-search--full,
   now also carrying a "form" class) into the rounded white pill from the
   requested Uiverse component. Every selector here is scoped under
   .site-header--inner, which only wraps the non-home header — so the
   homepage's big hero search (.hero-search, index.html only) still shares
   the base .header-search rules above completely untouched, even though
   both forms carry the .header-search class. Specificity note: each rule
   here uses two classes (".site-header--inner .input" etc.), which is
   deliberately higher-specificity than the old single-class/element rules
   above (".header-search input" etc.), so this cleanly wins on the inner
   header without needing !important or deleting the shared rules.
   No focus-state styling on purpose: the pill looks identical clicked or
   not (outline: none below only cancels the browser's own default focus
   ring, which would otherwise be the one appearance change left).

   PHASE 4 STEP 9 (accessibility) qualifies that last sentence. Cancelling
   the ring for a POINTER user is a look; cancelling it for a KEYBOARD user
   leaves them with no way to tell where they are. This was measured, not
   assumed: screenshotting the pill focused and unfocused and differencing
   the two produced zero changed pixels, on all 40 pages carrying the inner
   header. It was the only control on the whole site with no focus
   indication at all — the hero search's ring, the contact fields'
   underline and the level filter's colour change each move real pixels —
   and that is a WCAG 2.4.7 failure, so the focused pill must change.

   WHAT THIS COSTS, STATED PLAINLY: the ring shows on click too, so "looks
   identical clicked or not" no longer holds while the pill is FOCUSED.
   That is not a choice this rule makes, it is the spec — a focused text
   field always matches :focus-visible, in every current engine, precisely
   so that a typing caret is never the only thing marking where input goes.
   Verified here by clicking the field and reading back .matches(
   ':focus-visible') === true. Keeping the clicked pill bare therefore
   means keeping the keyboard broken, which is not a trade this step takes.

   The cost is also smaller than it sounds, on two counts. The RESTING
   appearance — the pill as it looks on every page load, which is what the
   design above is actually about — is untouched. And the pill was the
   exception, not the rule: clicking the hero search, any contact field or
   the level filter already restyles them today, unchanged by this step.
   This brings the last control into line with the other five.

   The ring now belongs to the PILL, not the input. The global ring drawn
   on the bare <input> was a square-cornered blue rectangle floating inside
   the rounded white pill — the browser-default look this header does not
   want. Instead the input's own outline is cancelled on every focus, and
   the form draws a soft light halo that follows the pill's radius while
   the field is focused (:focus-within). Same rule for click and keyboard,
   because a text field matches :focus-visible on click anyway (above); the
   halo is ~3.8:1 against the dark header bar, so keyboard focus stays
   plainly visible. The transparent outline is for forced-colors mode,
   which drops box-shadow but paints outlines in the system colour. */
.site-header--inner .form {
  --width-of-input: 200px;
  --height-of-input: 40px;
  --input-bg: #fff;
  --border-radius: 30px;
  position: relative;
  width: var(--width-of-input);
  height: var(--height-of-input);
  padding-inline: 0.8em;
  border-radius: var(--border-radius);
  background: var(--input-bg, #fff);
}
.site-header--inner .form button {
  border: none;
  background: none;
  color: #8b8ba7;
}
.site-header--inner .input {
  font-size: 0.9rem;
  background-color: transparent;
  width: 100%;
  height: 100%;
  padding-inline: 0.5em;
  padding-block: 0.7em;
  border: none;
  color: var(--text);
}
.site-header--inner .input::placeholder {
  color: #8b8ba7;
}
.site-header--inner .input:focus,
.site-header--inner .input:focus-visible {
  outline: none;
}
.site-header--inner .form:focus-within {
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.4);
  outline: 2px solid transparent;
}
.site-header--inner .reset {
  opacity: 0;
  visibility: hidden;
}
.site-header--inner .input:not(:placeholder-shown) ~ .reset {
  opacity: 1;
  visibility: visible;
}
.site-header--inner .form svg {
  width: 17px;
  margin-top: 3px;
}
/* Bottom row of .site-header--inner: just the nav list, white background
   per request. .site-nav a's default colors above are tuned for the dark
   .header-inner (home header) and would be near-invisible here, so this
   overrides them with dark, on-white-appropriate colors — scoped to
   .header-bottom only, so the home header's nav (still on the dark bar)
   is untouched. */
.header-bottom {
  display: flex;
  align-items: center;
  height: 70px;
  padding: 0 32px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  background-color: #ffffff;
  box-shadow: 0 2px 6px rgba(16, 16, 16, 0.06);
}
.header-bottom .site-nav a {
  color: var(--text-muted);
}
.header-bottom .site-nav a:hover {
  color: var(--action);
  border-bottom-color: var(--action);
}
.header-bottom .site-nav a[aria-current="page"] {
  color: var(--action);
  border-bottom-color: var(--action);
}

/* ---------- mobile menu ---------- */
.menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 2px;
  background: transparent;
  color: #fff;
}
.menu-toggle svg {
  width: 18px;
  height: 18px;
}
.menu-toggle:hover {
  border-color: var(--cyan);
  color: var(--cyan);
}
/* Last place var(--cyan) still painted inside the MAIN HEADER: the
   hamburger's hover state (visible at <=640px, where the button is the
   header's only remaining control). Same substitution the nav links
   got — var(--action) on the border, #fff on the icon — so the main
   header hovers on one accent throughout instead of blue links and a
   cyan button. The icon stays #fff rather than turning blue so its
   contrast on --header-bg doesn't drop on hover, and the blue border
   still clears WCAG 1.4.11's 3:1 non-text minimum (3.7:1).
   Scoped to .site-header--home on purpose: the same button in the
   inner-page header (.header-top) is out of this change's scope and
   keeps its existing cyan hover. */
.site-header--home .menu-toggle:hover {
  border-color: var(--action);
  color: #fff;
}
/* Two icons live in the button; only one shows at a time, keyed off the
   aria-expanded state app.js already toggles when the menu opens. */
.menu-toggle__icon--close {
  display: none;
}
.menu-toggle[aria-expanded="true"] .menu-toggle__icon--open {
  display: none;
}
.menu-toggle[aria-expanded="true"] .menu-toggle__icon--close {
  display: block;
}
.mobile-menu {
  display: none;
  flex-direction: column;
  gap: 32px;
  position: fixed;
  inset: 84px 0 0 0;
  padding: 28px 28px 40px;
  background: var(--header-bg);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.mobile-menu[hidden] {
  display: none;
}
/* Keep the page underneath from scrolling behind the full-screen panel. */
body:has(#mobile-menu:not([hidden])) {
  overflow: hidden;
}
/* .mobile-account__nav is the signed-in panel's Profile / Saved / Settings
   (src/client/auth.js), drawn exactly like the panel's own links. */
.mobile-menu__nav,
.mobile-account__nav {
  display: flex;
  flex-direction: column;
}
.mobile-menu__nav a,
.mobile-account__nav a {
  text-decoration: none;
  color: #cdd6e8;
  font-weight: 600;
  font-size: 18px;
  padding: 14px 2px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.mobile-menu__nav a:last-child,
.mobile-account__nav a:last-child {
  border-bottom: none;
}
.mobile-menu__nav a:hover,
.mobile-menu__nav a:active,
.mobile-account__nav a:hover,
.mobile-account__nav a:active {
  color: #fff;
}
/* The header's Sign in / Sign up, moved into the panel below 641px: two
   full-width buttons under the links, so neither is squeezed into the
   narrow header row beside the search pill. */
.mobile-menu__auth {
  display: grid;
  gap: 10px;
}
.mobile-menu__auth .btn {
  justify-content: center;
  padding: 13px 18px;
  font-size: 15px;
}
/* ---------- header auth state (src/client/auth.js) ----------
   Both auth areas stay invisible — space kept, so nothing shifts — until
   auth.js has asked /api/auth/session and set <html data-auth>. A signed-in
   visitor therefore never sees Sign in flash first. The buttons do nothing
   without JavaScript, so a script-less page loses nothing by hiding them. */
.header-auth,
.mobile-menu__auth {
  visibility: hidden;
}
html[data-auth] .header-auth,
html[data-auth] .mobile-menu__auth {
  visibility: visible;
}
/* .btn sets display, which would otherwise beat the hidden attribute. */
.header-auth [hidden],
.mobile-menu__auth [hidden] {
  display: none !important;
}
/* Signed in, desktop: an account button in the style of the header's
   outline button, disclosing a small panel with who is signed in and
   Sign out. */
.account {
  position: relative;
}
.account__toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 38px;
  max-width: 240px;
  padding: 0 12px 0 4px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: var(--radius);
  background: transparent;
  color: #fff;
  font-size: 14px;
  font-weight: 600;
}
.account__toggle:hover {
  border-color: #fff;
}
.account__toggle::after {
  content: "";
  flex-shrink: 0;
  width: 6px;
  height: 6px;
  margin-left: 2px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}
.account__toggle[aria-expanded="true"]::after {
  transform: translateY(1px) rotate(-135deg);
}
.account__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
}
.account__avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The account menu: "Your Account", Profile / Saved / Settings, a hairline,
   Sign out. The name is already on the button, so the panel is only the
   actions. Every row is the same compact text row; the label above them
   takes the legal pages' small muted caption. */
.account__menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 5;
  width: 220px;
  padding: 6px;
  background: var(--paper);
  color: var(--text);
  border: 1px solid var(--grid-line);
  border-radius: var(--radius);
  box-shadow: 0 12px 28px rgba(15, 42, 82, 0.18);
}
.account__title {
  padding: 8px 10px 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
}
.account__item,
.account__signout {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  text-align: left;
  text-decoration: none;
}
.account__item:hover,
.account__signout:hover {
  background: rgba(37, 99, 235, 0.06);
  color: var(--action);
}
.account__divider {
  height: 0;
  margin: 6px 0;
  border: 0;
  border-top: 1px solid var(--grid-line);
}
.account__signout:disabled,
.mobile-account .btn:disabled {
  opacity: 0.6;
  cursor: progress;
}
/* Signed in, mobile panel: "Your Account", who is signed in, Profile /
   Saved / Settings, then a full-width Sign out, in the slot the two
   sign-in buttons use. A hairline sets it apart from the primary links. */
.mobile-account {
  display: grid;
  gap: 16px;
  padding-top: 24px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.mobile-account__title {
  color: #cdd6e8;
  font-size: 13px;
  font-weight: 600;
}
.mobile-account__nav {
  margin-top: -8px;
}
.mobile-account__who {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
.mobile-account__who .account__avatar {
  width: 40px;
  height: 40px;
  font-size: 16px;
}
.mobile-account__text {
  display: grid;
  min-width: 0;
}
.mobile-account__name,
.mobile-account__email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mobile-account__name {
  color: #fff;
  font-size: 16px;
  font-weight: 600;
}
.mobile-account__email {
  color: #cdd6e8;
  font-size: 14px;
}
@media (max-width: 640px) {
  .header-auth {
    display: none;
  }
  .header-inner,
  .header-top {
    padding: 0 20px;
  }
  /* The inner header's search pill shares this row with the logo and the
     menu button. At the desktop gap and padding its text field was left
     about 46px wide at a 375px viewport — two characters of a query.
     Tightening the row's spacing (not its contents) roughly doubles that. */
  .header-top {
    gap: 12px;
  }
  .site-header--inner .form {
    gap: 4px;
    padding-inline: 0.5em;
  }
  .site-header--inner .input {
    padding-inline: 0.25em;
  }
  .site-nav {
    display: none;
  }
  /* The inner header's nav row has nothing left to show once .site-nav
     is hidden above — fold it away entirely rather than leave an empty
     bar under the search row. */
  .header-bottom {
    display: none;
  }
  .menu-toggle {
    display: inline-flex;
    /* On the home header nothing else claims the row's remaining
       space once .site-nav hides, so pin the button to the right
       edge explicitly. (On the inner header .header-search--full's
       flex-grow already does this; the rule is a no-op there.) */
    margin-left: auto;
  }
  .mobile-menu:not([hidden]) {
    display: flex;
  }
}
@media not all and (max-width: 640px) {
  /* Safety net: if the viewport is resized past the breakpoint while the
     panel happens to be open, don't let it linger under the desktop header. */
  .mobile-menu {
    display: none !important;
  }
}

/* ---------- hero ----------
   The backdrop is the sign-in dialog's visual (.auth__visual, further
   down), reused class for class: gradient field and blueprint grid.
   .hero__visual only turns that panel into a full-bleed background
   layer — absolutely placed behind .hero-inner, clipped to the hero,
   and kept visible below 900px, where the dialog folds its own copy
   away. The field is centred and oversized, so a narrower hero crops
   it rather than squeezing it.

   Height: the viewport less the home header (--hero-header), so the
   header and hero together fill the first screen, with the content
   centred vertically. svh, where supported, is the viewport with the
   mobile browser bars shown, so the hero never runs under them. It is
   a minimum: content taller than that (short landscape screens) still
   grows the hero. */
.hero {
  --hero-header: 84px;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: calc(100vh - var(--hero-header));
  min-height: calc(100svh - var(--hero-header));
  padding: 88px 24px 72px;
  text-align: center;
  border-bottom: 1px solid var(--grid-line);
}
/* Between 641 and 1259px the home header wraps its nav onto a second
   56px row under the 84px top row (see .site-header--home above). */
@media not all and (max-width: 640px) {
  @media (max-width: 1259px) {
    .hero {
      --hero-header: 140px;
    }
  }
}
.hero__visual.auth__visual {
  display: block;
  position: absolute;
  inset: 0;
}
/* Ambient drift, hero only (the sign-in dialog's field stays still). The
   same colour field slowly wanders, turns and breathes: three independent
   transform properties on three unrelated periods, each easing back and
   forth (alternate), so the motion never visibly loops or jumps. The
   field's 15% overhang and its blur keep every edge out of view. */
.hero__visual .auth__field {
  animation:
    hero-drift 38s ease-in-out infinite alternate,
    hero-turn 57s ease-in-out infinite alternate,
    hero-breathe 46s ease-in-out infinite alternate;
  will-change: translate, rotate, scale;
}
@keyframes hero-drift {
  from {
    translate: -2.5% 1.5%;
  }
  to {
    translate: 2.5% -1.5%;
  }
}
@keyframes hero-turn {
  from {
    rotate: -3deg;
  }
  to {
    rotate: 3deg;
  }
}
@keyframes hero-breathe {
  from {
    scale: 1;
  }
  to {
    scale: 1.05;
  }
}
/* static field, as before (animation: none, not the global near-zero
   duration, which would flicker an infinite alternate loop) */
@media (prefers-reduced-motion: reduce) {
  .hero__visual .auth__field {
    animation: none;
  }
}
.hero-inner {
  position: relative;
  /* a flex item with auto side margins would shrink to its content */
  width: 100%;
  max-width: 1160px;
  margin: 0 auto;
}
.fade-up {
  animation: fadeUp 0.6s ease both;
}
.fade-up:nth-child(2) {
  animation-delay: 0.06s;
}
.fade-up:nth-child(3) {
  animation-delay: 0.12s;
}
.fade-up:nth-child(4) {
  animation-delay: 0.18s;
}
.fade-up:nth-child(5) {
  animation-delay: 0.24s;
}
@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
.eyebrow {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--action);
  text-transform: uppercase;
  margin: 0 0 14px;
}
.hero-copy {
  max-width: 1100px;
  margin: 0 auto;
}
.hero h1 {
  /* white on the auth visual, with the same soft shadow as its caption */
  color: #fff;
  text-shadow: 0 1px 12px rgba(16, 16, 16, 0.35);
  font-size: clamp(24px, 3.4vw, 38px);
  margin: 0 0 34px;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1.12;
}
/* Typing loop on the headline (core.js). The untyped remainder is still
   laid out — just transparent, and without the shadow, which would
   otherwise show — so the headline keeps its size and line breaks while
   letters come and go. The cursor comes after all the text, pinned to
   the headline's corner, and core.js moves it (transform) to the end of
   the typed part. It reuses the sign-in panel's blink (auth-cursor),
   solid while letters move and blinking at rest. */
.hero-type {
  position: relative;
}
.hero-type__rest {
  color: transparent;
  text-shadow: none;
}
.hero-type__cursor {
  position: absolute;
  top: 0;
  left: 0;
  width: 0.06em;
  height: 0.9em;
  border-radius: 1px;
  background: currentColor;
  box-shadow: 0 1px 12px rgba(16, 16, 16, 0.35);
  animation: auth-cursor 1.1s ease-in-out infinite;
}
.hero h1.is-typing .hero-type__cursor {
  animation: none;
}
@media (prefers-reduced-motion: reduce) {
  .hero-type__cursor {
    display: none;
  }
}
.hero .tagline {
  color: var(--text-muted);
  max-width: 560px;
  margin: 0 auto 28px;
  font-size: 16px;
  line-height: 1.6;
}

/* ---------- hero search ----------
   Same functional contract as .header-search (shares the class, so
   app.js's existing querySelectorAll(".header-search") wiring binds
   this form too — no JS changes needed), just restyled as the big
   resourceboy-style pill search bar that anchors the hero. */
.hero-search {
  display: flex;
  align-items: center;
  max-width: 620px;
  margin: 0 auto 22px;
  background: var(--card);
  border: 1px solid var(--grid-line);
  border-radius: 999px;
  padding: 6px 6px 6px 24px;
  transition: border-color 0.15s ease;
}
.hero-search:focus-within {
  border-color: var(--action);
}
.hero-search input {
  flex: 1 1 auto;
  width: auto;
  max-width: none;
  border: 0;
  background: transparent;
  padding: 13px 8px;
  font-family: inherit;
  font-size: 15px;
  color: var(--text);
}
.hero-search input::placeholder {
  color: var(--text-muted);
}
.hero-search input:focus {
  outline: none;
}
.hero-search__submit {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 0;
  background: var(--action);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
}
.hero-search__submit svg {
  width: 18px;
  height: 18px;
}
.hero-search__submit:hover {
  background: var(--action-hover);
}

.hero-actions {
  margin: 0 0 18px;
}
.hero-roadmap-link {
  color: var(--text-muted);
  font-size: 13.5px;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--grid-line);
}
.hero-roadmap-link:hover {
  color: var(--action);
}
.hero-stats {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  align-items: center;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  text-transform: uppercase;
}
.hero-stats strong {
  color: var(--text);
}
.hero-stats .dot {
  color: var(--cyan);
}

/* ---------- explore strip ----------
   Sits right under the hero search: two rows of compact links that
   drift in opposite directions (tools right-to-left, guide topics
   left-to-right), so the hero shows the whole platform at a glance
   instead of one guide category grid. The motion is pure CSS.

   The loop: each row holds its list three times (see index.html). Every
   copy animates by exactly its own width (-100%), and its right padding
   equals the gap between items, so copy 2 lands where copy 1 started
   and the restart is invisible. Three copies keep the row filled while
   one copy is narrower than the strip.

   Pausing: hover never touches the motion — the rows keep drifting
   under the pointer. Keyboard focus stops a row and snaps the
   real list back to its start, so a focused link is never parked
   off-screen to the left, where an overflow-hidden row cannot scroll;
   core.js then scrolls the row to keep each focused link in view. */
.explore {
  --explore-gap: 10px;
  --explore-fade: 56px;
  max-width: 1160px;
  margin: 48px auto 0;
}
.explore__rows {
  display: grid;
  gap: 10px;
}
.explore__row {
  display: flex;
  overflow: hidden;
  /* room for the 2px focus ring + 2px offset, which overflow would clip */
  padding: 4px 0;
  /* a link scrolled into view by keyboard focus stops clear of the fade */
  scroll-padding-inline: var(--explore-fade);
  -webkit-mask-image: linear-gradient(
    to right,
    transparent,
    #000 var(--explore-fade),
    #000 calc(100% - var(--explore-fade)),
    transparent
  );
  mask-image: linear-gradient(
    to right,
    transparent,
    #000 var(--explore-fade),
    #000 calc(100% - var(--explore-fade)),
    transparent
  );
}
.explore__list {
  flex: 0 0 auto;
  display: flex;
  gap: var(--explore-gap);
  padding-right: var(--explore-gap);
  animation: explore-left 44s linear infinite;
}
.explore__row--topics .explore__list {
  animation-name: explore-right;
  animation-duration: 52s;
}
.explore__row:has(:focus-visible) .explore__list {
  animation: none;
}
@keyframes explore-left {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-100%);
  }
}
@keyframes explore-right {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}
.explore-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border: 1px solid var(--grid-line);
  border-radius: 999px;
  background: var(--card);
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
  transition:
    color 0.15s ease,
    border-color 0.15s ease;
}
.explore-item:hover {
  color: var(--action);
  border-color: var(--action);
}
.explore-item__icon {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  color: var(--action);
}
/* Guide topics: lighter than the tools — no fill, a slash for a mark.
   Unfilled, they sit straight on the hero's dark visual, so they are
   drawn in white instead of the site's text colours. */
.explore__row--topics .explore-item {
  background: transparent;
  border-color: rgba(255, 255, 255, 0.35);
  color: #fff;
  font-weight: 500;
}
.explore__row--topics .explore-item:hover {
  color: #fff;
  border-color: #fff;
}
.explore-item__mark {
  color: rgba(255, 255, 255, 0.7);
  font-weight: 400;
}
.explore-item:hover .explore-item__mark {
  color: inherit;
}
/* Narrow screens: smaller items, and the rows run edge to edge (out
   through .hero's 24px side padding) so links drift off the screen
   edge, not an inner line. */
@media (max-width: 900px) {
  .explore {
    --explore-fade: 24px;
    margin-top: 36px;
  }
  .explore__rows {
    margin: 0 -24px;
  }
}
@media (max-width: 640px) {
  .explore {
    --explore-gap: 8px;
    margin-top: 28px;
  }
  .explore__rows {
    gap: 6px;
  }
  .explore-item {
    gap: 6px;
    padding: 7px 14px;
    font-size: 14px;
  }
  .explore-item__icon {
    width: 15px;
    height: 15px;
  }
}
/* Reduced motion: no drift. The copies go, and each row stays one
   still line — centered when it fits, otherwise scrollable by swipe,
   trackpad or keyboard focus (never wrapped, so the strip keeps its two
   rows). On desktop the edge fade stays as the "more this way" cue, with
   padding so the row's first item rests clear of it. (animation: none,
   not the global near-zero duration, which would spin an infinite loop.) */
@media (prefers-reduced-motion: reduce) {
  /* two classes, to outrank .explore__row--topics .explore__list */
  .explore__row .explore__list {
    animation: none;
    padding-right: 0;
  }
  .explore__list[aria-hidden="true"] {
    display: none;
  }
  .explore__row {
    overflow-x: auto;
    scrollbar-width: none;
    padding-inline: var(--explore-fade);
  }
  .explore__row::-webkit-scrollbar {
    display: none;
  }
  /* auto margins center a list that fits and drop to 0 when it
     overflows, so no item is pushed out of scroll reach */
  .explore__list:first-child {
    margin-inline: auto;
  }
}
@media (prefers-reduced-motion: reduce) and (max-width: 900px) {
  .explore__row {
    padding-inline: 24px;
    -webkit-mask-image: none;
    mask-image: none;
  }
}

/* ---------- filter controls (select-style fields) ---------- */
.field {
  position: relative;
  display: flex;
  align-items: center;
}
.select-field {
  flex: 1 1 190px;
}
.select-field select {
  width: 100%;
  appearance: none;
  border: none;
  border-radius: 0;
  padding: 4px 20px 4px 0;
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
  background-color: transparent;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%235c6b85' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right center;
  background-size: 11px 7px;
  cursor: pointer;
  transition: color 0.15s ease;
}
.select-field select:hover,
.select-field select:focus {
  color: var(--action);
  outline: none;
}
/* The colour shift above is the only focus cue otherwise, and a text-colour
   change alone is not a visible focus indicator (WCAG 2.4.7). Keyboard
   focus gets the site's standard ring back; a mouse click still doesn't. */
.select-field select:focus-visible {
  outline: 2px solid var(--action);
  outline-offset: 2px;
}
.select-field select option:disabled {
  color: var(--text-muted);
  font-style: italic;
}

/* ---------- guides section ----------
   Widens .wrap's 1160px cap: on wide screens the centered column was
   leaving large empty gutters on either side while the guide cards
   themselves stayed cramped into 5 columns. A much wider (but still
   capped, so cards don't balloon on ultra-wide monitors) container
   lets the grid actually fill the viewport the way resourceboy.com's
   does, while staying centered like the rest of .wrap. */
.guides-main {
  max-width: 1800px;
  padding: 44px 32px 88px;
  background-color: #ffffff;
}
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px 16px;
  margin-bottom: clamp(28px, 2vw + 12px, 44px);
  padding-bottom: 18px;
  border-bottom: 1px solid var(--grid-line);
}
/* Black everywhere, matching the home page's plain black, DM Sans body
   copy — used to be blue by default with a home-page-only override back
   to black; every page now gets the same black treatment directly. */
.section-label {
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--text);
  text-transform: uppercase;
}
.results-count {
  font-size: 12px;
  color: var(--text-muted);
}
.section-head__meta {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}
.level-filter {
  flex: 0 0 168px;
  padding-left: 16px;
  border-left: 1px solid var(--grid-line);
}
@media (max-width: 640px) {
  .section-head__meta {
    flex: 1 1 100%;
    justify-content: space-between;
  }
  .level-filter {
    flex: 0 0 150px;
  }
}
/* Fixed 5-column grid on desktop, matching resourceboy.com's card
   layout, stepping down at narrower widths instead of auto-fill's
   variable column count (which was only landing on 4 at this
   container's max-width). */
.grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 28px 20px;
}
@media (max-width: 960px) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
@media (max-width: 640px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 24px 16px;
  }
}
@media (max-width: 420px) {
  .grid {
    gap: 18px 10px;
  }
}

/* Minimal, resourceboy.com-style card: no border/box/shadow — just a
   plain rounded thumbnail with a title and a small muted meta line
   underneath. The whole tile is a single click target via .card-link's
   ::after (stretched over .content-card, which is the positioning
   context), so there's no separate "read guide" button to style.

   Phase 4 (D3): this is the site's universal card primitive — Guides
   and Palettes both render through it, on the homepage, /guides,
   /palettes and the category pages — so .content-card is now the
   canonical name. .guide-card is retained as an alias for the duration
   of the migration; every emitter in the repo now writes
   .content-card. .card-thumb / .card-body / .card-title / .card-meta /
   .card-link were already neutral and are unchanged. */
.content-card,
.guide-card {
  position: relative;
  display: flex;
  flex-direction: column;
  animation: cardIn 0.28s ease both;
}
@keyframes cardIn {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
/* Resource-type chip (GUIDE / PALETTE) on cards and search
   results. Uppercased here rather than in markup so the DOM text stays
   a normal word for assistive tech. */
.badge {
  font-family: var(--sans);
  font-size: 10.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--action);
  background: rgba(37, 99, 235, 0.08);
  border: 1px solid rgba(37, 99, 235, 0.22);
  padding: 4px 8px;
  border-radius: 2px;
  white-space: nowrap;
}

/* ---------- content-card thumbnail plate ---------- */
.card-thumb {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: 10px;
  background-color: #e5e5e7;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.card-thumb svg {
  width: 60%;
  height: auto;
  color: var(--action);
  opacity: 0.55;
  overflow: visible;
}
.card-thumb img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
/* A plate carrying artwork is a single surface. The grey plate and its
   blueprint motif are the fallback for a guide with no image (see
   thumbMediaHtml in src/shared/card.js); left visible under a lazy image,
   they showed as a second, different card until the artwork finished
   painting over them. While an image is present the plate takes the
   artwork's own pale edge tone and the motif stays hidden. An image that
   fails is hidden by its onerror handler (style="display: none"), which
   drops this rule and restores the full fallback. */
.card-thumb:has(> img:not([style*="none"])) {
  background-color: #f2f7fd;
}
.card-thumb:has(> img:not([style*="none"])) > svg {
  visibility: hidden;
}
.thumb-guide {
  stroke: currentColor;
  stroke-width: 1;
  stroke-dasharray: 2 3;
  opacity: 0.4;
}
.thumb-glyph {
  font-family: Georgia, "Times New Roman", serif;
  font-size: 58px;
  fill: currentColor;
  stroke: none;
}
.thumb-fill {
  fill: currentColor;
  stroke: none;
}
.thumb-handle {
  stroke: var(--cyan);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}
.thumb-handle-dot {
  stroke: var(--cyan);
  fill: var(--card);
  stroke-width: 1.4;
}
.thumb-dim-line {
  stroke: var(--cyan);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}
.thumb-dim-tick {
  stroke: var(--cyan);
  stroke-width: 1;
}
.thumb-dim-label {
  font-family: var(--sans);
  font-size: 9px;
  fill: var(--cyan);
  letter-spacing: 0.03em;
}
.card-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 2px 0;
}
.card-title {
  font-size: 15.5px;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: -0.005em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Covers the whole .content-card (its positioning context) so the entire
   tile is clickable from a single, accessible link — the image and
   meta text below aren't separately interactive. */
.card-link {
  color: inherit;
  text-decoration: none;
}
.card-link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.card-link:hover {
  color: var(--action);
}
/* Keyboard focus. The global :focus-visible outline lands on the link
   itself, which sits inside .card-title — and .card-title's overflow:hidden
   (the two-line clamp) clips it away completely, so tabbing through any
   card grid showed no focus at all (WCAG 2.4.7). The ::after overlay is
   positioned against .content-card, outside that clip, and already spans
   the whole tile, so the ring goes there. */
.card-link:focus-visible {
  outline: none;
  color: var(--action);
}
.card-link:focus-visible::after {
  outline: 2px solid var(--action);
  outline-offset: 2px;
}
.card-meta {
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--text-muted);
}
/* /search only: the result card's match explanation ("Matched in color
   name: Azure"), written by src/client/search.js. The search page's own
   .card-meta size and muted colour, so it reads as card metadata rather
   than a new block; a long colour name or HEX wraps instead of widening a
   2-up mobile card. */
.search-page .card-match {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.search-page .card-match__label {
  font-weight: 600;
}
/* The muted colour on the search page's <mark> tint is 4.3:1, under AA
   for 12px text; the matched part takes the full text colour instead. */
.search-page .card-match mark {
  color: var(--text);
}

/* ---------- category pages (/category/<slug>.html) ----------
   Minimal per-category archive page, resourceboy.com-style: a short
   title + one-line description, then the exact same
   .guides-main/.grid/.content-card markup as the homepage grid (see
   build-categories.js) so a category page reads as a filtered slice
   of the homepage rather than a separately-designed template.
   Deliberately NOT reusing guide-article.css's .guide-hero here — that
   stylesheet is article-only and isn't linked on these pages — this
   rule gives an equivalent title/dek treatment at the same type
   scale, scoped to this file instead. */
.category-hero {
  padding: var(--intro-space-top) 0 var(--intro-space-bottom);
}
.category-hero > .section-label {
  font-size: 13px;
  letter-spacing: 0.16em;
  margin-bottom: 20px;
}
.category-hero__title {
  font-size: var(--intro-title-hero);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
  margin: 0 0 var(--intro-title-gap);
  color: var(--text);
  overflow-wrap: break-word;
  text-wrap: balance;
}
.category-hero__dek {
  font-size: var(--intro-dek);
  line-height: 1.45;
  color: var(--text-muted);
  max-width: 800px;
  margin: 0;
}

.empty-state {
  display: none;
  text-align: center;
  padding: 64px 20px;
  border: 1px solid var(--grid-line);
  border-radius: 3px;
  color: var(--text-muted);
}
.empty-state[data-visible="true"] {
  display: block;
}
.empty-state p {
  margin-bottom: 14px;
  font-size: 14.5px;
}
.empty-state strong {
  color: var(--text);
}

/* ---------- roadmap ---------- */
.roadmap-section {
  padding: 68px 0 96px;
  border-top: 1px solid var(--grid-line);
}
/* On roadmap.html the section opens the page itself (there's no "← All
   guides" crumb above it anymore, nor the homepage's guide grid before
   it), so it carries its own top breathing room directly instead of
   relying on a now-removed sibling, and skips the divider border. */
.roadmap-section--page {
  padding-top: 44px;
  border-top: none;
}
@media (max-width: 640px) {
  .roadmap-section--page {
    padding-top: 32px;
  }
}
.roadmap-head {
  align-items: flex-start;
  gap: 28px 48px;
  padding-top: var(--intro-space-top);
  padding-bottom: clamp(28px, 3vw + 12px, 56px);
  margin-bottom: clamp(24px, 3vw, 48px);
}
.roadmap-head-text {
  max-width: 860px;
}
.roadmap-head .section-label {
  font-size: 13px;
  letter-spacing: 0.16em;
  margin-bottom: 20px;
}
.roadmap-title {
  font-size: var(--intro-title-section);
  font-weight: 800;
  letter-spacing: -0.025em;
  line-height: 1.04;
  margin: 0 0 var(--intro-title-gap);
  overflow-wrap: break-word;
  text-wrap: balance;
}
.roadmap-intro {
  font-size: var(--intro-dek);
  line-height: 1.45;
  color: var(--text-muted);
  max-width: 760px;
}
.roadmap-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  font-size: 13px;
  color: var(--text-muted);
  white-space: nowrap;
  flex-shrink: 0;
  padding-top: 4px;
}
.roadmap-summary strong {
  color: var(--text);
}
.roadmap-summary .dot {
  color: var(--cyan);
}

/* progress bar + reset control */
.roadmap-progress {
  margin: 30px 0 40px;
}
.roadmap-progress-track {
  height: 6px;
  border-radius: 3px;
  background: var(--grid-line);
  overflow: hidden;
}
.roadmap-progress-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--action), var(--cyan));
  border-radius: 3px;
  transition: width 0.25s ease;
}
.roadmap-progress-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 10px;
}
.roadmap-progress-label {
  font-size: 12px;
  color: var(--text-muted);
}
.roadmap-reset {
  border: 1px solid var(--grid-line);
  background: transparent;
  color: var(--text-muted);
  font-size: 12.5px;
  padding: 6px 10px;
  border-radius: 2px;
  flex-shrink: 0;
}
.roadmap-reset:hover {
  color: var(--action);
  border-color: var(--action);
}

/* stage track — a single dashed spine runs behind every stage card and
   only shows through the gaps between them, so the numbered markers
   read as one connected path rather than a plain list. */
.roadmap-track {
  position: relative;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 28px;
}
.roadmap-track::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 40px;
  width: 0;
  border-left: 2px dashed var(--grid-line);
  z-index: 0;
}
.roadmap-stage {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 20px;
  background: var(--card);
  border: 1px solid var(--grid-line);
  border-radius: 2px;
  padding: 22px 22px 22px 0;
}
.roadmap-stage-marker {
  flex-shrink: 0;
  width: 80px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 1px;
}
.roadmap-stage-num {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 2px;
  border: 2px solid var(--action);
  color: var(--action);
  background: var(--card);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.02em;
  transition:
    background 0.15s ease,
    color 0.15s ease;
}
.roadmap-stage.is-complete .roadmap-stage-num {
  background: var(--action);
  color: #fff;
}
.roadmap-stage-body {
  flex-grow: 1;
  min-width: 0;
}
.roadmap-stage-headline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}
.roadmap-stage-title {
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.005em;
}
.roadmap-stage-count {
  flex-shrink: 0;
  font-size: 13px;
  color: var(--text-muted);
}
.roadmap-stage-blurb {
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-muted);
  margin-bottom: 16px;
  max-width: 62ch;
}

/* stage steps — checkbox + guide link rows */
.roadmap-steps {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}
.roadmap-step {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 0;
  border-top: 1px solid var(--grid-line);
}
.roadmap-step:first-child {
  border-top: none;
  padding-top: 2px;
}
.roadmap-step-check {
  display: flex;
  flex-shrink: 0;
  position: relative;
  cursor: pointer;
}
.roadmap-step-check input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
}
.roadmap-step-box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: 1.5px solid var(--grid-line);
  border-radius: 2px;
  background: var(--paper);
  color: transparent;
  transition:
    background 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}
.roadmap-step-box svg {
  width: 11px;
  height: 9px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.roadmap-step-check input:focus-visible + .roadmap-step-box {
  outline: 2px solid var(--action);
  outline-offset: 2px;
}
.roadmap-step-check:hover .roadmap-step-box {
  border-color: var(--action);
}
.roadmap-step.is-complete .roadmap-step-box {
  background: var(--action);
  border-color: var(--action);
  color: #fff;
}
.roadmap-step-link {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px 16px;
  flex-grow: 1;
  min-width: 0;
  text-decoration: none;
  color: var(--text);
}
.roadmap-step-title {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.4;
  transition: color 0.15s ease;
}
.roadmap-step-link:hover .roadmap-step-title {
  color: var(--action);
}
.roadmap-step.is-complete .roadmap-step-title {
  color: var(--text-muted);
  text-decoration: line-through;
  text-decoration-color: var(--grid-line);
}
.roadmap-step-meta {
  flex-shrink: 0;
  font-size: 12.5px;
  color: var(--text-muted);
}

@media (max-width: 720px) {
  .roadmap-head {
    flex-direction: column;
  }
  .roadmap-stage {
    padding-right: 16px;
  }
  .roadmap-stage-marker {
    width: 56px;
  }
  .roadmap-track::before {
    left: 28px;
  }
  .roadmap-step-link {
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
  }
}

/* ---------- footer ----------
   Light editorial footer: four evenly distributed link columns (Tools /
   Discover / Guides / Company), each headed by a small muted uppercase label
   on the .section-label scale, then a quiet bottom row carrying the BPOZZ
   "B" mark, the copyright and the tagline. Structure comes from whitespace
   alone — no dividers, boxes or backgrounds — inside the standard 1160px
   .wrap. Colors are the existing tokens (--paper, --text, --text-muted)
   with --action as the hover accent. The white wordmark has no dark
   variant, so the brand sits in the bottom row as the existing blue app
   mark instead.
   4 columns >= 900px · 2 × 2 at 641–899px · stacked <= 640px */
.site-footer {
  background: var(--paper);
  padding-top: 56px;
}
/* Takes up any height left over in body's page-shell column, so the footer
   rests on the bottom edge of a short page; on a long page there is none
   left over and it simply follows the content. */
body > .site-footer {
  margin-top: auto;
}
.footer-main {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 48px;
  padding-bottom: 72px;
}
.footer-col__title {
  margin-bottom: 32px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.footer-col__list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.footer-col__list a {
  color: var(--text);
  font-size: 16px;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 5px;
  transition:
    color 0.15s ease,
    text-decoration-color 0.15s ease;
}
.footer-col__list a:hover {
  color: var(--action);
  text-decoration-color: currentColor;
}
.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding-bottom: 40px;
}
.footer-mark {
  display: block;
  flex-shrink: 0;
  border-radius: 5px;
}
.footer-mark img {
  display: block;
  width: 24px;
  height: 24px;
  border-radius: 5px;
}
.footer-copyright,
.footer-tagline {
  font-size: 13px;
  color: var(--text-muted);
}
.footer-tagline {
  margin-left: auto;
}
@media (max-width: 899px) {
  .footer-main {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 640px) {
  .site-footer {
    padding-top: 40px;
  }
  .footer-main {
    grid-template-columns: minmax(0, 1fr);
    gap: 32px;
    padding-bottom: 48px;
  }
  .footer-col__title {
    margin-bottom: 8px;
  }
  /* Taller hit areas in place of the list gap: ~42px per link row. */
  .footer-col__list {
    gap: 0;
  }
  .footer-col__list a {
    display: inline-block;
    padding: 9px 0;
  }
  .footer-bottom {
    padding-bottom: 32px;
  }
  .footer-tagline {
    flex-basis: 100%;
    margin-left: 0;
  }
}

/* ---------- toast ---------- */
.toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translate(-50%, 16px);
  background: var(--header-bg);
  color: #fff;
  font-size: 13.5px;
  padding: 12px 18px;
  border-radius: 3px;
  box-shadow: 0 12px 28px rgba(15, 42, 82, 0.3);
  opacity: 0;
  pointer-events: none;
  transition:
    opacity 0.2s ease,
    transform 0.2s ease;
  z-index: 60;
  max-width: 88vw;
  text-align: center;
}
.toast[data-visible="true"] {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ---------- page view ----------
         Every guide and every static page (about/contact/privacy/terms)
         has its own real, bookmarkable, shareable URL and renders here
         as an actual in-flow page — replacing the guide list rather
         than floating a modal dialog on top of it. */
#home-view[hidden] {
  display: none;
}
.page-view {
  padding-top: 44px;
  padding-bottom: 88px;
  animation: pageIn 0.18s ease both;
}
.page-view[hidden] {
  display: none;
}
@keyframes pageIn {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
.page-view-body {
  max-width: 760px;
}
@media (max-width: 640px) {
  .page-view {
    padding-top: 32px;
    padding-bottom: 64px;
  }
}

/* article view */

/* ---------- article hero image (mirrors .card-thumb) ---------- */
.article-hero {
  position: relative;
  aspect-ratio: 20 / 9;
  border: 1px solid var(--grid-line);
  background-color: var(--paper);
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 18px 18px;
  border-radius: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  margin-bottom: 26px;
}
.article-hero svg {
  width: 46%;
  height: auto;
  color: var(--action);
  overflow: visible;
}
.article-hero img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.article-hero .badge {
  position: absolute;
  top: 12px;
  left: 12px;
  background: var(--card);
}
@media (max-width: 640px) {
  .article-hero {
    aspect-ratio: 4 / 3;
  }
}

.article-eyebrow {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.article-title {
  font-size: clamp(24px, 3.4vw, 32px);
  font-weight: 800;
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin-bottom: 10px;
}
.article-dek {
  font-size: 15.5px;
  line-height: 1.65;
  color: var(--text-muted);
  margin-bottom: 28px;
  padding-bottom: 24px;
  border-bottom: 1px solid var(--grid-line);
}
.article-body h3 {
  font-size: 18px;
  font-weight: 700;
  margin: 30px 0 12px;
  letter-spacing: -0.005em;
}
.article-body h3:first-child {
  margin-top: 0;
}
.article-body p {
  font-size: 15px;
  line-height: 1.75;
  color: var(--text);
  margin-bottom: 14px;
}
.takeaways {
  margin-top: 30px;
  padding: 18px 20px;
  background: var(--paper);
  border: 1px solid var(--grid-line);
  border-radius: 2px;
}
.takeaways .section-label {
  display: block;
  margin-bottom: 10px;
}
.takeaways ul {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.takeaways li {
  font-size: 14px;
  line-height: 1.55;
  padding-left: 22px;
  position: relative;
}
.takeaways li::before {
  content: "✓";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--cyan);
  font-weight: 700;
}
.back-to-guides {
  margin-top: 32px;
}

/* ---------- guide page: single reading column + related-guides row ----------
   Every page under /guide/ wraps its reading content in
   .guide-layout > .guide-primary, with a sibling <section id="guide-rail">
   directly after it in the markup. This used to render as a sticky
   right-hand sidebar (a second grid column next to the article); it's
   now a plain block, so that same sibling simply falls below the
   article — no per-page markup changes needed, only this layout.
   .guide-primary keeps prose at a comfortable reading width and centers
   it, rather than stretching paragraphs across the full row now that
   there's no sidebar constraining it.
   The related-guides row itself ships empty in the static HTML; app.js's
   initRelatedGuides() fills it in at runtime from guides.json (matching
   first on category, then level) with up to 10 guides, reusing cardHtml()
   — the exact same markup/classes (.content-card/.card-thumb/.card-body)
   as the homepage grid — so "related guides" is the identical minimal
   card, not a bespoke bordered/bracketed rail treatment. */
.guide-layout {
  max-width: 1120px;
  margin: 0 auto;
}
.guide-primary {
  max-width: 760px;
  margin: 0 auto;
}
.guide-rail {
  margin-top: 72px;
  padding-top: 40px;
  border-top: 1px solid var(--grid-line);
}
.guide-rail:empty {
  display: none;
}
.guide-rail__label {
  display: block;
  margin-bottom: 22px;
  color: var(--action);
}
@media (max-width: 640px) {
  .guide-rail {
    margin-top: 52px;
    padding-top: 32px;
  }
}

/* ---------- 404 (404.html) ----------
   The Home Hero's backdrop (.auth__visual field and grid, further down),
   placed the same way as .hero__visual but with none of its drift: the
   page is static. Like the hero, it fills the first screen below the
   header (--notfound-header: the inner header's 84px top row, plus its
   70px nav row from 641px up), so the footer starts at the fold. The
   copy sits centred on top in white, as on the hero. */
.notfound {
  --notfound-header: 84px;
  position: relative;
  overflow: hidden;
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: calc(100vh - var(--notfound-header));
  min-height: calc(100svh - var(--notfound-header));
  padding: 72px 16px;
  text-align: center;
  color: #fff;
  text-shadow: 0 1px 12px rgba(16, 16, 16, 0.35);
}
@media not all and (max-width: 640px) {
  .notfound {
    --notfound-header: 154px;
  }
}
.notfound__visual.auth__visual {
  display: block;
  position: absolute;
  inset: 0;
}
.notfound__copy {
  position: relative;
}
.notfound__code {
  font-size: clamp(88px, 18vw, 168px);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
}
.notfound__title {
  margin-top: 12px;
  font-size: clamp(20px, 2.6vw, 28px);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

/* legal / static pages */
/* Standalone pages (about.html, contact.html, privacy.html,
         terms.html) use real heading levels — h1 for the page title,
         h2 for subheadings — via these classes, kept visually
         identical to the in-app h2/h3 treatment below without
         colliding with it. */
.legal-content .legal-title {
  font-size: 24px;
  font-weight: 800;
  margin-bottom: 6px;
  letter-spacing: -0.01em;
}
.legal-content .legal-subheading {
  font-size: 16px;
  font-weight: 700;
  margin: 24px 0 10px;
}
.legal-content .legal-subheading:first-of-type {
  margin-top: 0;
}
.legal-content h2 {
  font-size: 24px;
  font-weight: 800;
  margin-bottom: 6px;
  letter-spacing: -0.01em;
}
.legal-content .legal-updated {
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 26px;
}
.legal-content h3 {
  font-size: 16px;
  font-weight: 700;
  margin: 24px 0 10px;
}
.legal-content p {
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--text);
  margin-bottom: 12px;
}
.legal-content ul {
  margin: 0 0 12px;
  padding-left: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.legal-content li {
  font-size: 14.5px;
  line-height: 1.65;
  padding-left: 18px;
  position: relative;
  color: var(--text);
}
.legal-content li::before {
  content: "—";
  position: absolute;
  left: 0;
  color: var(--action);
}
.legal-content a {
  color: var(--action);
  text-decoration: underline;
}

/* ---------- tool pages: explanatory notes ----------
   The "About / How to use" block under the Colors, Palettes and Image
   Picker tools. Set off from the tool by the same hairline rule as
   .guide-rail, with the legal pages' text sizes, in a readable column. */
.tool-notes {
  max-width: 720px;
  margin-top: 72px;
  padding-top: 40px;
  border-top: 1px solid var(--grid-line);
}
.tool-notes h2 {
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 28px 0 10px;
}
.tool-notes h2:first-child {
  margin-top: 0;
}
.tool-notes p,
.tool-notes li {
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--text);
}
.tool-notes p {
  margin-bottom: 12px;
}
.tool-notes ul {
  list-style: none;
  margin: 0 0 12px;
  padding-left: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.tool-notes li {
  padding-left: 18px;
  position: relative;
}
.tool-notes li::before {
  content: "—";
  position: absolute;
  left: 0;
  color: var(--action);
}
.tool-notes a {
  color: var(--action);
  text-decoration: underline;
}
/* The site has one type family (var(--sans)); code terms stay in it. */
.tool-notes code {
  font-family: inherit;
  font-weight: 600;
}
@media (max-width: 640px) {
  .tool-notes {
    margin-top: 52px;
    padding-top: 32px;
  }
}

/* ---------- about page: founder signature ----------
   Modeled on the founder photo + name + title block at the end of
   resourceboy.com/about — a small centered portrait with a name and
   title underneath, set off from the copy above by a hairline rule. */
.about-founder {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  margin-top: 36px;
  padding-top: 30px;
  border-top: 1px solid var(--grid-line);
}
.about-founder__photo {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--grid-line);
  margin-bottom: 14px;
}
.about-founder__name {
  font-size: 15px;
  font-weight: 800;
  color: var(--text);
  letter-spacing: -0.01em;
  margin-bottom: 3px;
}
.about-founder__title {
  font-size: 13px;
  color: var(--text-muted);
}

/* ---------- contact page ----------
   Modeled on resourceboy.com/contact/: no visible field labels at
   all — placeholder text carries each field, labels are kept for
   screen readers only (.sr-only) — no boxed inputs, no fill, just a
   hairline rule under each field. Same "minimalism through spacing
   and type, not color blocking" language as .hero — just dialed up
   here with a bigger heading and a lot more room between fields.
   Scoped to .contact-content so about/privacy/terms (which share
   .legal-content/.legal-title) keep their original, smaller scale. */
.contact-content .legal-title {
  font-size: clamp(28px, 4vw, 40px);
  margin-bottom: 16px;
}
.contact-content .contact-lede {
  font-size: 17px;
  line-height: 1.7;
  color: var(--text-muted);
  max-width: 520px;
  margin: 0;
}
.contact-form {
  display: flex;
  flex-direction: column;
  gap: 38px;
  margin-top: 48px;
}
.form-row {
  display: flex;
  flex-direction: column;
}
.form-row input,
.form-row select,
.form-row textarea {
  width: 100%;
  border: none;
  border-bottom: 1px solid var(--grid-line);
  border-radius: 0;
  padding: 16px 2px 18px;
  font-size: 17px;
  font-family: inherit;
  color: var(--text);
  background: transparent;
  transition: border-color 0.15s ease;
}
.form-row input::placeholder,
.form-row textarea::placeholder {
  color: var(--text-muted);
}
.form-row select {
  appearance: none;
  padding-right: 20px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%231D5FD6' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 2px center;
  background-size: 11px 7px;
}
.form-row input:focus,
.form-row select:focus,
.form-row textarea:focus {
  border-color: var(--action);
  outline: none;
}
/* A 1px underline changing colour is a faint focus cue on its own; keyboard
   focus doubles it to 2px without shifting layout. */
.form-row input:focus-visible,
.form-row select:focus-visible,
.form-row textarea:focus-visible {
  box-shadow: 0 1px 0 0 var(--action);
}
.form-row textarea {
  resize: vertical;
  min-height: 150px;
}
.form-hint {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: -6px;
}
.form-error {
  font-size: 13px;
  color: #c0392b;
  min-height: 14px;
}
/* Scoped to .contact-form so the bigger scale here doesn't affect
   .btn-primary on the homepage / roadmap CTA links, which share the
   base .btn-primary class. */
.contact-form .btn-primary {
  padding: 16px 36px;
  font-size: 15px;
  margin-top: 6px;
}
.contact-content .contact-alt {
  margin-top: 40px;
  padding-top: 26px;
  border-top: 1px solid var(--grid-line);
  font-size: 14px;
  color: var(--text-muted);
}
.form-success {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  padding-top: 32px;
  border-top: 1px solid var(--grid-line);
  margin-top: 36px;
}
.form-success strong {
  font-size: 17px;
}
.form-success p {
  font-size: 14.5px;
  color: var(--text-muted);
  line-height: 1.6;
  margin: 0;
}
@media (max-width: 640px) {
  .contact-form {
    gap: 28px;
    margin-top: 36px;
  }
  .form-row input,
  .form-row select,
  .form-row textarea {
    padding: 14px 2px 16px;
  }
  .form-row textarea {
    min-height: 130px;
  }
}

/* ---------- color-preview card thumbnails ----------
   Two color-preview variants of the 4:3 .card-thumb plate: a row of
   swatches, or four stacked bars like the /palettes gallery. /search
   renders palette results with the bars variant (src/client/search.js).
   The homepage's generated resource sections used these too, until the
   section below the hero became the tool/resource cards in home.css. */
.card-thumb--swatches,
.card-thumb--bars {
  align-items: stretch;
  justify-content: stretch;
}
.card-thumb--bars {
  flex-direction: column;
}
.card-thumb--swatches span,
.card-thumb--bars span {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
}

/* ---------- /guides collection ----------
   Organised like /fonts/: the same container, hero, toolbar (category
   chips, search, select), count line, grid columns and gaps, and empty
   state. The values are transcribed from src/styles/fonts.css — which only
   the font pages load — and scoped to body.guides-page, so the other pages
   that share .guides-main, .grid and .empty-state are untouched.

   The card is NOT the font card: it stays the shared .content-card, whose
   .card-thumb keeps its own 4:3 ratio at every width (the font card's
   phone-width switch to 16:9 is deliberately not copied). */
.guides-page {
  --guides-radius: 8px;
  --guides-line: #e7e9ee;
  --guides-line-strong: #cfd4dd;
  --guides-ease: cubic-bezier(0.2, 0.6, 0.3, 1);
}
.guides-page .guides-main {
  max-width: 1160px;
  padding: 28px 24px 72px;
}

.guides-hero {
  padding: var(--intro-space-top) 0 clamp(28px, 2.4vw + 12px, 48px);
}
.guides-hero h1 {
  font-size: var(--intro-title-hero);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
}
.guides-hero p {
  max-width: 760px;
  margin-top: var(--intro-title-gap);
  color: var(--text-muted);
  font-size: var(--intro-dek);
  line-height: 1.45;
}

.guides-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px 24px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--guides-line);
  margin-bottom: 20px;
}
.guides-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.guides-filter {
  appearance: none;
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 7px 14px;
  border: 1px solid var(--guides-line);
  border-radius: 999px;
  background: #fff;
  color: var(--text);
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition:
    border-color 0.15s var(--guides-ease),
    background-color 0.15s var(--guides-ease),
    color 0.15s var(--guides-ease);
}
.guides-filter:hover {
  border-color: var(--guides-line-strong);
}
/* Selected = inverted; aria-pressed carries the same state to assistive
   tech, so it is never communicated by colour alone. */
.guides-filter[aria-pressed="true"],
.guides-filter[aria-pressed="true"]:hover {
  background: var(--text);
  border-color: var(--text);
  color: #fff;
}

.guides-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}
.guides-field {
  position: relative;
  display: flex;
  align-items: center;
}
.guides-field input,
.guides-field select {
  height: 40px;
  border: 1px solid var(--guides-line);
  border-radius: var(--guides-radius);
  background: #fff;
  color: var(--text);
  font-family: var(--sans);
  font-size: 14px;
  padding: 0 12px;
  min-width: 0;
}
.guides-field input::placeholder {
  color: #6b7485;
}
.guides-field input:hover,
.guides-field select:hover {
  border-color: var(--guides-line-strong);
}
.guides-icon {
  position: absolute;
  left: 11px;
  width: 17px;
  height: 17px;
  fill: none;
  stroke: #6b7485;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: none;
}
.guides-field--search input {
  width: 220px;
  padding-left: 34px;
}
.guides-field--level {
  gap: 8px;
}
.guides-field--level label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
}
.guides-field--level select {
  padding-right: 8px;
  cursor: pointer;
}

.guides-count {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 16px;
}

.guides-page .guides-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 28px 20px;
}
@media (max-width: 1023px) {
  .guides-page .guides-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (max-width: 767px) {
  .guides-page .guides-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 22px 14px;
  }
}
@media (max-width: 479px) {
  .guides-page .guides-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 22px;
  }
}

/* The listing's cards take the .font-card__preview plate edge — its radius
   (--guides-radius, transcribed from --font-radius) and its hairline border
   — and show the title alone. The meta line stays in the shared cardHtml()
   markup for the homepage, category, search and related-guides grids, which
   do not carry body.guides-page. */
.guides-page .guides-grid .card-thumb {
  border: 1px solid rgba(0, 0, 0, 0.05);
  border-radius: var(--guides-radius);
}
.guides-page .guides-grid .card-meta {
  display: none;
}

/* The /fonts/ empty state (no box, centred, muted), keeping the guide
   grid's query echo and reset button. Shown/hidden by [data-visible]. */
.guides-page .guides-empty {
  padding: 56px 0;
  border: 0;
}
.guides-page .guides-empty p {
  margin: 0;
}
.guides-page .guides-empty p + p {
  margin-top: 6px;
}
.guides-page .guides-empty .btn {
  margin-top: 20px;
}

@media (max-width: 767px) {
  /* nowrap: in a wrapping column flexbox each line is as wide as its widest
     item, so the single-row chip scroller below would widen the page. */
  .guides-toolbar {
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
  }
  .guides-toolbar > * {
    min-width: 0;
  }
  .guides-filters {
    flex-wrap: nowrap;
    overflow-x: auto;
    margin: 0 -24px;
    padding: 2px 24px;
    scrollbar-width: none;
  }
  .guides-filters::-webkit-scrollbar {
    display: none;
  }
  .guides-filter {
    flex: none;
  }
  .guides-controls {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 10px;
  }
  .guides-field--search {
    grid-column: 1 / -1;
  }
  .guides-field--search input {
    width: 100%;
  }
}

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

/* ---------- authentication dialog (src/client/auth.js) ----------
   Full-screen, not a centred modal: a native <dialog> opened with
   showModal(), so it sits in the top layer above the sticky header and the
   page behind it goes inert. Form panel left, visual panel right; below
   900px the visual folds away and the form takes the whole viewport.
   Everything reuses the site tokens (--paper, --text, --text-muted,
   --action, --radius, --sans). The only new colours are the gradient's own,
   scoped to .auth__visual below. */
html.auth-open {
  overflow: hidden;
}
.auth {
  position: fixed;
  inset: 0;
  width: 100%;
  max-width: none;
  height: 100vh;
  height: 100dvh;
  max-height: none;
  min-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--paper);
  color: var(--text);
  font-family: var(--sans);
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.22s ease;
}
.auth[open] {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.auth.is-open {
  opacity: 1;
}
.auth::backdrop {
  background: transparent;
}

/* Left: the form panel. It scrolls on its own if a short viewport (a phone
   in landscape) can't fit it; margin: auto centres the column when there is
   room and lets it start at the top when there isn't, so nothing is ever
   clipped above the fold. */
.auth__panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 24px 32px 32px;
  overflow-y: auto;
}
.auth__bar {
  display: flex;
}
.auth__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--grid-line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  transition: border-color 0.15s ease;
}
.auth__close:hover {
  border-color: var(--text-muted);
}
.auth__close svg {
  width: 18px;
  height: 18px;
}
.auth__body {
  width: 100%;
  max-width: 400px;
  margin: auto;
  padding: 40px 0 16px;
  transform: translateY(8px);
  transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.auth.is-open .auth__body {
  transform: none;
}
.auth__back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 20px -2px;
  padding: 4px 2px;
  border: 0;
  background: none;
  color: var(--text-muted);
  font-size: 14px;
  font-weight: 600;
}
.auth__back[hidden] {
  display: none;
}
.auth__back:hover {
  color: var(--text);
}
.auth__back svg {
  width: 16px;
  height: 16px;
}
.auth__title {
  font-size: clamp(32px, 1.4vw + 22px, 44px);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
}
/* Focused programmatically when a step changes, for screen readers; it is
   not an interactive control, so it draws no ring. */
.auth__title:focus {
  outline: none;
}
.auth__lede {
  margin-top: 12px;
  font-size: 16px;
  line-height: 1.55;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.auth__lede strong {
  color: var(--text);
  font-weight: 600;
}
.auth__notice {
  display: none;
  margin-top: 24px;
  padding: 12px 14px;
  border: 1px solid var(--grid-line);
  border-left: 3px solid var(--action);
  border-radius: var(--radius);
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
}
.auth__notice.is-visible {
  display: block;
}
.auth__step {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: 32px;
}
.auth__step[hidden] {
  display: none;
}
.auth__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  min-height: 48px;
  padding: 12px 20px;
  border: 1px solid rgba(16, 16, 16, 0.24);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  transition:
    border-color 0.15s ease,
    background 0.15s ease;
}
.auth__btn:hover {
  border-color: var(--text);
  background: rgba(37, 99, 235, 0.04);
}
.auth__btn:disabled {
  cursor: progress;
  opacity: 0.6;
}
.auth__icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}
.auth__or {
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.auth__or::before,
.auth__or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--grid-line);
}
.auth__form {
  gap: 10px;
}
.auth__label {
  font-size: 14px;
  font-weight: 600;
}
.auth__input {
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  border: 1px solid rgba(16, 16, 16, 0.32);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--text);
  font-family: inherit;
  font-size: 16px;
}
.auth__input:focus {
  border-color: var(--action);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
  outline: 2px solid transparent;
}
.auth__input[aria-invalid="true"] {
  border-color: #c0392b;
}
.auth__error {
  font-size: 13px;
  color: #c0392b;
}
.auth__error[hidden] {
  display: none;
}
.auth__submit {
  justify-content: center;
  min-height: 48px;
  margin-top: 6px;
  font-size: 15px;
}
.auth__switch {
  margin-top: 28px;
  font-size: 14px;
  color: var(--text-muted);
}
.auth__link {
  padding: 0;
  border: 0;
  background: none;
  color: var(--action);
  font-size: inherit;
  font-weight: 600;
}
.auth__link:hover {
  color: var(--action-hover);
  text-decoration: underline;
}
.auth__legal {
  margin-top: 12px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-muted);
}
.auth__legal a {
  color: var(--text);
  text-underline-offset: 2px;
}
.auth__legal a:hover {
  color: var(--action);
}

/* Right: the visual panel. An original gradient field (yellow, orange,
   pink, purple and the site's own --action blue), blurred so the hues bleed
   into each other, under a faint blueprint grid. The background is static:
   it settles from a slight scale on open and otherwise never moves. Over
   it, one short phrase at a time is typed and deleted (auth.js, TYPING). */
.auth__visual {
  --auth-yellow: #ffd23f;
  --auth-orange: #ff8a3d;
  --auth-pink: #ff4f9a;
  --auth-purple: #7b5cff;
  position: relative;
  overflow: hidden;
  background: #1a1d3a;
  color: #fff;
}
.auth__field {
  position: absolute;
  inset: -15%;
  background:
    radial-gradient(38% 42% at 24% 20%, var(--auth-yellow) 0%, transparent 70%),
    radial-gradient(44% 48% at 76% 26%, var(--auth-orange) 0%, transparent 70%),
    radial-gradient(48% 52% at 34% 68%, var(--auth-pink) 0%, transparent 72%),
    radial-gradient(52% 56% at 80% 78%, var(--auth-purple) 0%, transparent 72%),
    radial-gradient(60% 60% at 55% 50%, var(--action) 0%, transparent 78%);
  filter: blur(48px);
  transform: scale(1.06);
  transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.auth.is-open .auth__field {
  transform: none;
}
.auth__grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
  background-size: 48px 48px;
  background-position: center;
  -webkit-mask-image: radial-gradient(ellipse at center, #000 20%, transparent 75%);
  mask-image: radial-gradient(ellipse at center, #000 20%, transparent 75%);
}
/* The typed phrase. One grid cell holds every phrase invisibly (the
   sizers) and the live line on top, so the block is as wide as the widest
   phrase plus the cursor: centred once, it never shifts as letters come
   and go, and the text grows rightward from a fixed left edge. */
.auth__type {
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  margin: 0;
  transform: translate(-50%, -50%);
  font-size: clamp(32px, 4vw, 68px);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.02em;
  white-space: nowrap;
  text-shadow: 0 1px 12px rgba(16, 16, 16, 0.35);
}
.auth__type > * {
  grid-area: 1 / 1;
}
.auth__type-sizer {
  visibility: hidden;
  /* room for the cursor: its margin plus its width */
  padding-right: 0.13em;
}
.auth__cursor {
  display: inline-block;
  width: 0.06em;
  height: 0.92em;
  margin-left: 0.07em;
  vertical-align: -0.12em;
  border-radius: 1px;
  background: currentColor;
  animation: auth-cursor 1.1s ease-in-out infinite;
}
/* solid while letters are being typed or deleted, blinking at rest */
.auth__type-line.is-typing .auth__cursor {
  animation: none;
}
@keyframes auth-cursor {
  0%,
  45% {
    opacity: 1;
  }
  55%,
  95% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

@media (max-width: 899px) {
  .auth[open] {
    grid-template-columns: minmax(0, 1fr);
  }
  .auth__visual {
    display: none;
  }
}
@media (max-width: 640px) {
  .auth__panel {
    padding: 16px 20px 28px;
  }
  .auth__body {
    padding-top: 28px;
  }
  .auth__step {
    margin-top: 28px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .auth,
  .auth__body,
  .auth__field {
    transition: none;
  }
  .auth__body,
  .auth__field {
    transform: none;
  }
  /* auth.js does not run the typing loop; the cursor stays still too */
  .auth__cursor {
    animation: none;
  }
}
