/*
 * The public site's one stylesheet.
 *
 * It does not import the application's forty. That is the whole performance
 * argument in `docs/SEO.md`: a visitor reading the pricing page should not
 * download a parking management system to do it. The cost of that decision is
 * the token block below, which is a deliberate copy of the semantic tier of
 * `web/src/styles/tokens.css` — the same values, so the two halves of the site
 * look like one product, and no raw palette value appears anywhere beneath it.
 *
 * If a token changes there, change it here. Nothing else is shared.
 */

:root {
  /* Neutrals, warm (hue ~60): a pure grey beside this accent reads dirty. */
  --ink-900: #1E1B18;
  --ink-500: #5D554E;
  --ash-600: #786E66;
  --ash-300: #BFB5AE;
  --ash-200: #D9D1CB;
  --ash-150: #E8E3DE;
  --ash-100: #F3EEEB;
  --ash-050: #FAF7F5;

  /* 800 and 400 are declared here because the stylesheet already used
     them: `var(--paint-800)` with no fallback inside a gradient makes the
     whole declaration invalid at computed-value time, so the closing
     panel on every page was falling back to no background at all. */
  --paint-800: #7E2D00;
  --paint-700: #A23B01;
  --paint-600: #C84F0A;
  --paint-500: #E8641B;
  --paint-400: #F5854C;
  --paint-200: #FBC7AA;
  --paint-100: #FDE0CE;
  --paint-050: #FEF1E9;

  --display: 'Archivo', system-ui, -apple-system, sans-serif;
  --body: 'Public Sans', system-ui, -apple-system, sans-serif;

  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 26px; --sp-7: 40px; --sp-8: 64px; --sp-9: 96px;

  --radius-sm: 4px;
  --radius: 6px;
  --radius-lg: 10px;
  --radius-xl: 14px;

  --column: 1140px;
  --measure: 62ch;

  --dur-1: 110ms;
  --dur-2: 180ms;
  --ease-out: cubic-bezier(.2,.8,.3,1);

  /* One phone breakpoint and one tablet breakpoint — the thing L-33 asks the
     application for, which a new stylesheet may as well start out doing. */
  --bp-phone: 640px;
  --bp-tablet: 900px;

  /* The height every shaped band edge occupies. Declared once because the
     padding that keeps text off those shapes is derived from it. */
  --edge-h: clamp(44px, 5vw, 90px);
}

:root,
:root[data-theme='light'] {
  color-scheme: light;
  --bg: #FFFFFF;
  --bg-sunken: var(--ash-050);
  --bg-deep: var(--ash-100);
  --surface: #FFFFFF;
  --text: var(--ink-900);
  --text-muted: var(--ink-500);
  --text-subtle: var(--ash-600);
  --border-hairline: var(--ash-150);
  --border: var(--ash-200);
  --border-strong: var(--ash-300);
  --accent: var(--paint-500);
  --accent-solid: var(--paint-600);
  --accent-solid-hover: var(--paint-700);
  --accent-ink: var(--paint-700);
  /* The kicker's colour on a peach band only: --accent-ink is 4.37:1 on
     --tint-warmer, which fails 4.5:1 unrounded, and one rung down is
     6.10:1 (17-visual, 2026-10-23). In dark the tints are near-black and
     --accent-ink is already the light end of the ramp, so there it stays. */
  --kicker-on-tint: var(--paint-800);
  --accent-soft: var(--paint-050);
  --accent-edge: var(--paint-200);
  --on-accent: #FFFFFF;
  --on-mark: #FFFFFF;
  --shadow-1: 0 1px 2px rgba(30,27,24,.06), 0 1px 1px rgba(30,27,24,.04);
  --shadow-2: 0 10px 30px rgba(30,27,24,.10), 0 2px 6px rgba(30,27,24,.05);
  --ink-panel: #221E1B;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    color-scheme: dark;
    --bg: #15120F;
    --bg-sunken: #1B1714;
    --bg-deep: #120F0D;
    --surface: #211C19;
    --text: #E6E0DC;
    --text-muted: #B7ADA6;
    --text-subtle: #9C918A;
    --border-hairline: #2C2723;
    --border: #35302B;
    --border-strong: #463F3A;
    --accent: var(--paint-500);
    --accent-solid: var(--paint-500);
    --accent-solid-hover: var(--paint-400, #F5854C);
    --accent-ink: #FBA97E;
    --kicker-on-tint: #FBA97E;
    --accent-soft: #2A1C13;
    --accent-edge: #4A2C17;
    --on-accent: #1A1310;
    --on-mark: #15120F;
    --shadow-1: 0 1px 2px rgba(0,0,0,.4);
    --shadow-2: 0 14px 40px rgba(0,0,0,.5);
    --ink-panel: #0F0C0A;
  }
}

:root[data-theme='dark'] {
  color-scheme: dark;
  --bg: #15120F;
  --bg-sunken: #1B1714;
  --bg-deep: #120F0D;
  --surface: #211C19;
  --text: #E6E0DC;
  --text-muted: #B7ADA6;
  --text-subtle: #9C918A;
  --border-hairline: #2C2723;
  --border: #35302B;
  --border-strong: #463F3A;
  --accent-solid: var(--paint-500);
  --accent-solid-hover: #F5854C;
  --accent-ink: #FBA97E;
  --kicker-on-tint: #FBA97E;
  --accent-soft: #2A1C13;
  --accent-edge: #4A2C17;
  --on-accent: #1A1310;
  --on-mark: #15120F;
  --shadow-1: 0 1px 2px rgba(0,0,0,.4);
  --shadow-2: 0 14px 40px rgba(0,0,0,.5);
  --ink-panel: #0F0C0A;
}

/* ------------------------------------------------------------------ reset */

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

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

/*
 * Reduced motion: swap, do not kill.
 *
 * The blunt version — `transition-duration: .001ms !important` on every
 * element — also removes the hover feedback that tells somebody a control
 * is a control. What the preference asks for is less *movement*, so
 * movement goes and colour transitions stay. Nothing here can hide content:
 * `boot.js` never adds `js-reveal` under this preference, so no element is
 * ever in its hidden state to begin with.
 */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-property: opacity, color, background-color, border-color, box-shadow, fill !important;
    transition-duration: 160ms !important;
  }

  .tilt, .tilt-right, .tilt-flat { transform: none !important; }
}

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

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

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.02em;
  margin: 0;
  text-wrap: balance;
}

p { margin: 0; text-wrap: pretty; }

a { color: var(--accent-ink); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-solid-hover); }

:focus-visible {
  outline: 2px solid var(--accent-solid);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--surface);
  color: var(--text);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  z-index: 100;
}
.skip:focus { left: var(--sp-4); top: var(--sp-4); }

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

/* ------------------------------------------------------------------ layout */

/*
 * The gutter, and the notch in it.
 *
 * `viewport-fit=cover` (lib/template.mjs) asks iOS for the whole screen so
 * a full-bleed coloured band reaches the edge instead of sitting in a
 * letterbox. The cost is that the words reach the edge too, and on an
 * iPhone turned sideways the Dynamic Island is a black pill over the left
 * or the right of the page — it sat on the header and on the first words
 * of every band (operator, 2026-10-23).
 *
 * `max()` rather than an orientation media query, and no media query at
 * all: the condition is not "the phone is sideways", it is "this device
 * has something in the way", and only the device knows. Every screen
 * without an inset reports **0** — every desktop, every Android, an
 * iPhone in portrait, and the same iPhone in landscape on the edge the
 * island is not on — so this is the gutter it has always been everywhere
 * except the one place it was wrong.
 *
 * `.wrap` is the right place because every band's content is inside one,
 * including the sticky header's own row.
 */
.wrap {
  width: 100%;
  max-width: var(--column);
  margin-inline: auto;
  padding-inline:
    max(var(--sp-6), env(safe-area-inset-left))
    max(var(--sp-6), env(safe-area-inset-right));
}

@media (max-width: 640px) {
  .wrap {
    padding-inline:
      max(var(--sp-4), env(safe-area-inset-left))
      max(var(--sp-4), env(safe-area-inset-right));
  }
}

section { position: relative; }

.band { padding-block: var(--sp-9); }
.band-tight { padding-block: var(--sp-8); }
.band-sunken { background: var(--bg-sunken); }
.band-deep { background: var(--bg-deep); }

@media (max-width: 640px) {
  .band { padding-block: var(--sp-8); }
  .band-tight { padding-block: var(--sp-7); }
}

.rule { border: 0; border-top: 1px solid var(--border-hairline); margin: 0; }

/* A band that carries a shaped edge does not also get a hairline: a curve
   and a straight rule at the same join is the thing that looked unfinished. */
.has-decor .rule { display: none; }

/* ------------------------------------------------------------------- type */

.h-hero {
  font-size: clamp(2.4rem, 1.4rem + 3.6vw, 4.1rem);
  letter-spacing: -0.035em;
  line-height: 1.02;
}

.h-section {
  font-size: clamp(1.75rem, 1.2rem + 1.9vw, 2.6rem);
  letter-spacing: -0.028em;
}

.h-card {
  font-size: 1.125rem;
  letter-spacing: -0.012em;
  line-height: 1.25;
}

.lede {
  font-size: clamp(1.0625rem, 1rem + .4vw, 1.25rem);
  line-height: 1.55;
  color: var(--text-muted);
  max-width: var(--measure);
}

.kicker {
  font-family: var(--body);
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--text-subtle);
  margin: 0 0 var(--sp-3);
}

.prose { max-width: var(--measure); }
.prose > * + * { margin-top: var(--sp-5); }
.prose h2 { margin-top: var(--sp-8); font-size: 1.6rem; }
.prose h3 { margin-top: var(--sp-7); font-size: 1.22rem; }
.prose ul, .prose ol { padding-left: 1.3em; }
.prose li + li { margin-top: var(--sp-2); }
.prose table { width: 100%; border-collapse: collapse; }

.muted { color: var(--text-muted); }
.subtle { color: var(--text-subtle); font-size: .875rem; }

/* --------------------------------------------------------------- controls */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  /* 48px tall: the touch convention N-18 is a whole roadmap item about in the
     application, which a new surface has no excuse for getting wrong. */
  min-height: 48px;
  padding: 0 var(--sp-6);
  border-radius: var(--radius);
  border: 1px solid transparent;
  font: inherit;
  font-weight: 600;
  font-size: .9375rem;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}

.btn-primary {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--on-accent);
}
.btn-primary:hover { background: var(--accent-solid-hover); border-color: var(--accent-solid-hover); color: var(--on-accent); }

.btn-quiet {
  background: transparent;
  border-color: var(--border-strong);
  color: var(--text);
}
.btn-quiet:hover { background: var(--bg-sunken); border-color: var(--text-subtle); color: var(--text); }

.btn-sm { min-height: 40px; padding: 0 var(--sp-4); font-size: .875rem; }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
}

/* ----------------------------------------------------------------- header */

.sitehead {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in oklab, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--border-hairline);
}

.sitehead-in {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  min-height: 68px;
}

.brandlink {
  display: inline-flex;
  align-items: center;
  flex: none;
  text-decoration: none;
  color: var(--text);
}
.wordmark {
  display: block;
  /* 2356 × 600 — the aspect ratio of the shipped lockup, so the header does
     not reflow when the image arrives. */
  width: 102px;
  height: 26px;
  /*
   * The 408px copy, not the 2356px original.
   *
   * `/assets/brand/logo-primary.png` is 2356x600 and 99,952 bytes, and it
   * was being drawn into a 102x26 box on every page, above the fold — the
   * LCP element on the home page, and 88% of a typical page's weight
   * together with the two fonts. `build.mjs` derives a 408px copy (4x the
   * drawn width, so it stays crisp on a 3x screen) at 7,454 bytes, RMSE
   * 2.43/255 against the original. That is 92KB off every single page
   * view, which is more than every other delivery change put together.
   *
   * `/assets/brand/` keeps the full-size artwork: it is what email and
   * print use, and neither can resize.
   */
  background: url('/brand/wordmark.png') left center / contain no-repeat;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .wordmark {
    background-image: url('/brand/wordmark-dark.png');
  }
}

:root[data-theme='dark'] .wordmark {
  background-image: url('/brand/wordmark-dark.png');
}

.sitenav {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-inline-start: var(--sp-4);
}

.sitenav a {
  display: inline-flex;
  align-items: center;
  /* 44, not 40: the header's own links were the last controls under the
     floor after the sweep. */
  min-height: 44px;
  padding: 0 var(--sp-3);
  border-radius: var(--radius);
  color: var(--text-muted);
  text-decoration: none;
  font-size: .9375rem;
  font-weight: 500;
}
.sitenav a:hover { background: var(--bg-sunken); color: var(--text); }
.sitenav a[aria-current='page'] { color: var(--text); font-weight: 600; }

.sitehead-end {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-inline-start: auto;
}

.navtoggle { display: none; }

@media (max-width: 900px) {
  /*
   * The whole of the right-hand side goes into the menu.
   *
   * It used to keep "Get started" beside the logo, which on a phone put a
   * full-width-looking orange button between the wordmark and the hamburger
   * — three competing things in a 68px bar, none of them aligned to
   * anything. The operator's screenshot of it is why this rule exists. The
   * header is now the mark and one control, and both calls to action are
   * the first things in the opened menu, at full width where a thumb is.
   */
  .sitenav, .sitehead-end { display: none; }

  .navtoggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    margin-inline-start: auto;
    background: transparent;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
  }

  .sitehead-end { margin-inline-start: var(--sp-2); }

  /* The menu is a <details> element, so it opens with no JavaScript at all
     and closes on Escape in every browser that has ever shipped it. */
  .navmenu[open] .navpanel { display: block; }
}

.navmenu { display: contents; }
.navpanel { display: none; }

@media (max-width: 900px) {
  .navmenu { display: block; margin-inline-start: auto; }
  .navmenu > summary { list-style: none; }
  .navmenu > summary::-webkit-details-marker { display: none; }

  .navpanel {
    position: absolute;
    inset-inline: 0;
    top: 100%;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-2);
    /* Spans the viewport rather than sitting in a `.wrap`, so it carries
       the safe-area gutter itself — see the note on `.wrap`. */
    padding: var(--sp-4)
             max(var(--sp-6), env(safe-area-inset-right))
             var(--sp-6)
             max(var(--sp-6), env(safe-area-inset-left));
  }

  .navpanel a {
    display: block;
    min-height: 48px;
    line-height: 48px;
    border-bottom: 1px solid var(--border-hairline);
    color: var(--text);
    text-decoration: none;
    font-weight: 500;
  }

  .navpanel .btn-row { margin-top: var(--sp-5); }
  .navpanel .btn { flex: 1 1 auto; }
}

/* ------------------------------------------------------------------- hero */

.hero { position: relative; overflow: clip; padding-block: var(--sp-9) var(--sp-8); }

.bayfield {
  position: absolute;
  inset: -10% -20% auto -20%;
  width: 140%;
  height: 560px;
  color: var(--border);
  opacity: .5;
  pointer-events: none;
  z-index: 0;
}

.hero > .wrap { position: relative; z-index: 1; }

/* Copy on the left, the lot beside it, and the product below both. The
   right half of this band was empty in the first version, which is what
   made the hero look like a document rather than a page. */
.hero-top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .82fr);
  gap: var(--sp-8);
  align-items: center;
}

@media (max-width: 900px) {
  .hero-top { grid-template-columns: 1fr; gap: var(--sp-6); }
  /* On a phone the illustration is below the buttons and smaller; it is
     worth seeing, and not worth pushing the call to action off the screen. */
  .hero-aside { display: flex; justify-content: center; }
}

.hero-copy { max-width: 58ch; }
.hero-copy > * + * { margin-top: var(--sp-5); }
.hero-shot { margin-top: var(--sp-8); }

@media (max-width: 640px) {
  .hero-shot { margin-top: var(--sp-7); }
}
.hero-copy .btn-row { margin-top: var(--sp-7); }

.hero-note {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
  margin-top: var(--sp-5);
  color: var(--text-subtle);
  font-size: .875rem;
}
.hero-note span { display: inline-flex; align-items: center; gap: var(--sp-2); }
.hero-note .icon { color: var(--accent); }

/* ------------------------------------------------------------ screenshots */

/*
 * A screenshot is shown as a thing with an edge and a shadow, not inside a
 * drawn laptop. A fake device frame dates a page the moment the device does,
 * and it shrinks the only part anybody came to look at.
 */
.shot {
  margin: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  background: var(--surface);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}

.shot img { width: 100%; display: block; }

.shot-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border-hairline);
  background: var(--bg-sunken);
}

.shot-bar b {
  font: inherit;
  font-weight: 600;
  font-size: .8125rem;
  color: var(--text-muted);
}

.shot-bar .dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--border-strong);
  flex: none;
}

.shot figcaption,
.shotnote {
  color: var(--text-subtle);
  font-size: .8125rem;
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--border-hairline);
}

.shot-phone {
  max-width: 300px;
  border-radius: var(--radius-xl);
}

/* ----------------------------------------------------------------- panels */

.lead-in { max-width: 56ch; }
.lead-in > * + * { margin-top: var(--sp-4); }
.lead-in + * { margin-top: var(--sp-8); }

.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.28fr);
  gap: var(--sp-8);
  align-items: center;
}
.split-flip > :first-child { order: 2; }

@media (max-width: 900px) {
  .split, .split-flip { grid-template-columns: 1fr; gap: var(--sp-6); }
  .split-flip > :first-child { order: 0; }
}

.split + .split { margin-top: var(--sp-9); }

/* Heading against prose: the heading column is the narrower of the two, and
   it stays at the top of its row rather than floating in the middle of it. */
.split-text { grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); align-items: start; }
.split-text + .split-text { margin-top: var(--sp-8); }

@media (max-width: 900px) {
  .split-text { grid-template-columns: 1fr; gap: var(--sp-4); }
  .split-text + .split-text { margin-top: var(--sp-7); }
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--sp-5);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  padding: var(--sp-6);
}

.card > * + * { margin-top: var(--sp-3); }
.card .icon { color: var(--accent); }
.card p { color: var(--text-muted); font-size: .9375rem; }

a.card { text-decoration: none; color: inherit; display: block; transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out); }
a.card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-2); }
a.card .more { color: var(--accent-ink); font-weight: 600; font-size: .9375rem; display: inline-flex; align-items: center; gap: var(--sp-2); }

/* Lists marked with a bay rather than a disc. */
.ticks { list-style: none; margin: 0; padding: 0; }
.ticks li {
  position: relative;
  padding-left: 24px;
  color: var(--text-muted);
}
.ticks li + li { margin-top: var(--sp-3); }
.ticks .bullet { position: absolute; left: 0; top: .46em; color: var(--accent); }
.ticks b { color: var(--text); font-weight: 600; }

/* --------------------------------------------------------------- the band */

/*
 * No rules above and below.
 *
 * The band these sit in now has a curved edge cutting into the band below
 * it, and two straight hairlines running across a curve is what the
 * operator saw as messy: the swoosh reads as a shape, the rules read as a
 * table, and the two fight. Spacing separates them instead — which is the
 * V-4/V-5 rule the application already follows and the reason `.notice`
 * is a keyline rather than a box.
 */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--sp-7) var(--sp-6);
  /* The band's own `has-edge-top` padding clears the shape; this is just
     the figures' breathing room, and matching top to bottom left a third
     of an orange band empty under four short lines. */
  padding-block: var(--sp-4) var(--sp-5);
}

.fact > * + * { margin-top: var(--sp-1); }
.fact b {
  display: block;
  font-family: var(--display);
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1.05;
}
.fact span { color: var(--text-muted); font-size: .9375rem; }

/* ---------------------------------------------------------------- pricing */

.pricebox {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 0;
  border: 1px solid var(--tint-edge);
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

@media (max-width: 900px) { .pricebox { grid-template-columns: 1fr; } }

.pricebox-main { padding: var(--sp-8); }
.pricebox-side {
  padding: var(--sp-8);
  /* Peach rather than off-white: this is the half of the box that answers
     "what would it cost us", and off-white beside white is not a side. */
  background: var(--tint-warm);
  border-left: 1px solid var(--tint-edge);
}
@media (max-width: 900px) {
  .pricebox-side { border-left: 0; border-top: 1px solid var(--tint-edge); }
  .pricebox-main, .pricebox-side { padding: var(--sp-6); }
}

.bignum {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  font-family: var(--display);
  letter-spacing: -0.04em;
}
.bignum b { font-size: clamp(3rem, 2rem + 4vw, 4.25rem); font-weight: 700; line-height: 1; }
.bignum span { font-family: var(--body); font-size: 1rem; color: var(--text-muted); letter-spacing: 0; }

.calc {
  display: grid;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}
.calc table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.calc th, .calc td { text-align: left; padding: var(--sp-3) 0; border-bottom: 1px solid var(--border-hairline); font-size: .9375rem; }
.calc th { font-weight: 600; color: var(--text-muted); }
.calc td:last-child, .calc th:last-child { text-align: right; }
/* No bold last row: these are four examples, not a total, and the one
   at the bottom is not the answer to the ones above it. */
.calc tr:last-child td { border-bottom: 0; }

/* --------------------------------------------------------------- compare */

.cmp { width: 100%; border-collapse: collapse; font-size: .9375rem; }
.cmp caption { text-align: left; color: var(--text-subtle); font-size: .875rem; padding-bottom: var(--sp-3); }
.cmp th, .cmp td { text-align: left; padding: var(--sp-4) var(--sp-4); border-bottom: 1px solid var(--border-hairline); vertical-align: top; }
.cmp thead th { font-weight: 600; color: var(--text-muted); border-bottom: 1px solid var(--border); }
.cmp tbody th { font-weight: 600; width: 34%; }
.cmp td .icon { color: var(--accent); }

.scroller { overflow-x: auto; }

/* ------------------------------------------------------------------- FAQ */

.faq { border-top: 1px solid var(--border-hairline); }

.faq details {
  border-bottom: 1px solid var(--border-hairline);
}

.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-5) 0;
  cursor: pointer;
  font-family: var(--display);
  font-weight: 600;
  font-size: 1.0625rem;
  letter-spacing: -0.012em;
  list-style: none;
  min-height: 48px;
}
.faq summary::-webkit-details-marker { display: none; }

.faq summary::after {
  content: '';
  flex: none;
  width: 11px; height: 11px;
  border-right: 2px solid var(--text-subtle);
  border-bottom: 2px solid var(--text-subtle);
  transform: rotate(45deg) translateY(-3px);
  transition: transform var(--dur-2) var(--ease-out);
}
.faq details[open] summary::after { transform: rotate(-135deg) translateY(-3px); }

.faq .answer { padding-bottom: var(--sp-5); color: var(--text-muted); max-width: var(--measure); }
.faq .answer > * + * { margin-top: var(--sp-4); }

/* ---------------------------------------------------------- closing panel */

.closer {
  position: relative;
  overflow: clip;
  background: var(--ink-panel);
  color: #EDE7E3;
  border-radius: var(--radius-xl);
  padding: var(--sp-8) var(--sp-8);
  text-align: center;
}

.closer .bayfield { color: #4A423C; opacity: .5; inset: -30% -10% auto -10%; height: 420px; }

/* `:not(.bayfield)` is load-bearing. Written as `.closer > *` it set
   `position: relative` on the background artwork too, which took the SVG out
   of absolute positioning and put a 420-pixel-tall element into the flow —
   so every closing panel on the site was roughly twice the height of the
   three lines of text in it, with the extra space above them. */
.closer > *:not(.bayfield) { position: relative; z-index: 1; }
.closer h2 { color: #FFFFFF; }
.closer p { color: #C6BDB7; max-width: 52ch; margin-inline: auto; margin-top: var(--sp-4); }
.closer .btn-row { justify-content: center; margin-top: var(--sp-7); }
.closer .btn-quiet { border-color: #574E48; color: #EDE7E3; }
.closer .btn-quiet:hover { background: #2A2420; border-color: #786E66; color: #FFFFFF; }

@media (max-width: 640px) {
  .closer { padding: var(--sp-8) var(--sp-5); }
}

/* ----------------------------------------------------------------- footer */

.sitefoot {
  border-top: 1px solid var(--border-hairline);
  background: var(--bg-sunken);
  padding-block: var(--sp-8) var(--sp-7);
  /* No margin: the band above the footer already ends in its own 96px of
     padding, and the two together left a two-hundred-pixel band of nothing
     between the last words on the page and the first words of the footer. */
  margin-top: 0;
}

.footgrid {
  display: grid;
  grid-template-columns: minmax(0, 1.8fr) repeat(3, minmax(0, .7fr));
  gap: var(--sp-7) var(--sp-6);
  align-items: start;
}

@media (max-width: 900px) {
  .footgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .footbrand { grid-column: 1 / -1; }
}

@media (max-width: 520px) {
  .footgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.footgrid h2 {
  font-family: var(--body);
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--text-subtle);
  margin-bottom: var(--sp-4);
}

.footgrid ul { list-style: none; margin: 0; padding: 0; }
.footgrid li + li { margin-top: var(--sp-1); }
.footgrid a {
  display: inline-block;
  padding: var(--sp-2) 0;
  color: var(--text-muted);
  text-decoration: none;
  font-size: .9375rem;
}
.footgrid a:hover { color: var(--text); text-decoration: underline; }

.footbrand p { color: var(--text-subtle); font-size: .875rem; margin-top: var(--sp-4); max-width: 38ch; line-height: 1.55; }
.footbrand .wordmark { width: 118px; height: 30px; }

.footmail {
  display: inline-block;
  margin-top: var(--sp-4);
  color: var(--text-muted);
  font-size: .875rem;
  text-decoration: none;
  border-bottom: 1px solid var(--border-strong);
  padding-bottom: 1px;
}
.footmail:hover { color: var(--accent-ink); border-bottom-color: var(--accent); }

.footend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-6);
  align-items: center;
  justify-content: space-between;
  margin-top: var(--sp-8);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--border-hairline);
  color: var(--text-subtle);
  font-size: .8125rem;
}

/* The second tier: the pages that do not earn a column but should still be
   one click away. Quieter than the columns above, deliberately. */
.footend nav { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); }
.footend nav a { color: var(--text-subtle); text-decoration: none; }
.footend nav a:hover { color: var(--text); text-decoration: underline; }

/* ---------------------------------------------------------------- motion
 *
 * Everything here is an enhancement and none of it is load-bearing. The
 * `.js-reveal` class is set on <html> by `boot.js`, synchronously, and only
 * when IntersectionObserver exists and the visitor has not asked for reduced
 * motion — so a page with no JavaScript, a blocked script or a reduced-motion
 * preference is simply the finished page, immediately, with nothing hidden.
 *
 * That ordering matters more than the animation does. Content hidden by CSS
 * and revealed by script is content a crawler may never see and a person on
 * a flaky connection never sees at all; this way the hiding only ever happens
 * in a browser that has already proved it can do the revealing.
 */

/*
 * The reveal, recalibrated.
 *
 * It was 620ms over 14px on `cubic-bezier(.2,.8,.3,1)`. That curve is, to
 * within half a percent, the one Stripe uses most — and Stripe pairs it
 * with 300ms. Solved at the halfway mark ours is 93% complete, so 13 of the
 * 14 pixels were travelled in the first 310ms and the remaining 310ms moved
 * the element under one pixel. More than half the declared duration was
 * invisible: a 300ms animation wearing a 620ms label, with a stagger tuned
 * against the wrong number.
 *
 * Now: 20px over 760ms on `cubic-bezier(.25,.1,.25,1)`, which is only 80%
 * done at half time, so the tail is actually seen. A short blur is the
 * third channel — the thing that separates motion that reads as expensive
 * from motion that reads as a template — and it is on text only, never on a
 * 1140px screenshot, where blurring a raster that size is the one genuinely
 * costly thing available.
 */

.js-reveal [data-reveal] {
  opacity: 0;
  transform: translateY(20px);
}

.js-reveal [data-reveal][data-revealed] {
  opacity: 1;
  transform: none;
  transition:
    opacity 760ms cubic-bezier(.25, .1, .25, 1),
    transform 760ms cubic-bezier(.25, .1, .25, 1),
    filter 760ms cubic-bezier(.25, .1, .25, 1);
}

/* Text blocks get the blur; pictures do not. */
.js-reveal .lead-in[data-reveal],
.js-reveal .split[data-reveal],
.js-reveal .closer[data-reveal] { filter: blur(6px); }

.js-reveal .lead-in[data-revealed],
.js-reveal .split[data-revealed],
.js-reveal .closer[data-revealed] { filter: blur(0); }

/*
 * 90ms, and capped at four.
 *
 * Six at 70ms meant 350ms of lead-in before the last card even started,
 * which reads as a page still loading rather than a page arriving. Past the
 * fourth they all share the last delay.
 */
.js-reveal .cards > *,
.js-reveal .facts > *,
.js-reveal .cliprow > * {
  opacity: 0;
  transform: translateY(20px);
}

.js-reveal .cards[data-revealed] > *,
.js-reveal .facts[data-revealed] > *,
.js-reveal .cliprow[data-revealed] > * {
  opacity: 1;
  transform: none;
  transition:
    opacity 700ms cubic-bezier(.25, .1, .25, 1),
    transform 700ms cubic-bezier(.25, .1, .25, 1);
}

.js-reveal .cards[data-revealed] > *:nth-child(2),
.js-reveal .facts[data-revealed] > *:nth-child(2),
.js-reveal .cliprow[data-revealed] > *:nth-child(2) { transition-delay: 90ms; }

.js-reveal .cards[data-revealed] > *:nth-child(3),
.js-reveal .facts[data-revealed] > *:nth-child(3),
.js-reveal .cliprow[data-revealed] > *:nth-child(3) { transition-delay: 180ms; }

.js-reveal .cards[data-revealed] > *:nth-child(n+4),
.js-reveal .facts[data-revealed] > *:nth-child(n+4),
.js-reveal .cliprow[data-revealed] > *:nth-child(n+4) { transition-delay: 270ms; }

/* The middle card of a close-up row keeps its offset through the reveal. */
@media (min-width: 901px) {
  .js-reveal .cliprow > *:nth-child(even) { transform: translateY(56px); }
  .js-reveal .cliprow[data-revealed] > *:nth-child(even) { transform: translateY(22px); }
}

/*
 * The hero arrives on load, and waits a beat before it does.
 *
 * The pause is the part that reads as composure: Linear delays its first
 * hero line by 400ms and runs the rest at roughly 100ms apart. Starting at
 * zero is what makes an entrance look automatic.
 */
@media (prefers-reduced-motion: no-preference) {
  .hero-copy > * { animation: rise 820ms cubic-bezier(.25, .1, .25, 1) both; }
  .hero-copy > *:nth-child(1) { animation-delay: 240ms; }
  .hero-copy > *:nth-child(2) { animation-delay: 340ms; }
  .hero-copy > *:nth-child(3) { animation-delay: 440ms; }
  .hero-aside { animation: rise 1000ms cubic-bezier(.25, .1, .25, 1) 380ms both; }
  .hero-shot { animation: rise 1000ms cubic-bezier(.25, .1, .25, 1) 540ms both; }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: none; }
}

.hero-aside { display: flex; justify-content: flex-end; }

@media (max-width: 900px) {
  .hero-aside { justify-content: center; }
}








/* A tinted tile behind a card's mark, so the icon reads as deliberate rather
   than as a stray glyph above the heading. */
.card .icon {
  box-sizing: content-box;
  padding: 9px;
  border-radius: var(--radius);
  background: var(--accent-soft);
  border: 1px solid var(--accent-edge);
  color: var(--accent-ink);
}

/* ------------------------------------------------------------------ misc */

.crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
  font-size: .875rem;
  color: var(--text-subtle);
  margin-bottom: var(--sp-5);
}
.crumbs a { color: var(--text-muted); text-decoration: none; }
.crumbs a:hover { text-decoration: underline; }
.crumbs span[aria-hidden] { color: var(--border-strong); }

.pagehead { padding-block: var(--sp-8) var(--sp-7); }
.pagehead > * + * { margin-top: var(--sp-6); }
.pagehead .crumbs + * { margin-top: var(--sp-4); }

.note {
  border-left: 3px solid var(--accent-edge);
  background: var(--accent-soft);
  padding: var(--sp-4) var(--sp-5);
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--text-muted);
  font-size: .9375rem;
}

.steps { list-style: none; counter-reset: step; margin: 0; padding: 0; }
.steps li {
  position: relative;
  counter-increment: step;
  padding-left: 52px;
  min-height: 36px;
}
.steps li + li { margin-top: var(--sp-6); }
.steps li::before {
  content: counter(step);
  position: absolute;
  left: 0; top: -2px;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border: 1px solid var(--accent-edge);
  background: var(--accent-soft);
  color: var(--accent-ink);
  border-radius: 50%;
  font-family: var(--display);
  font-weight: 700;
  font-size: .9375rem;
}
.steps b { display: block; font-family: var(--display); font-size: 1.0625rem; letter-spacing: -.012em; }
.steps p { color: var(--text-muted); margin-top: var(--sp-2); max-width: 54ch; }

/* The permit illustration beside the phone screenshot on the permits page. */



/* ------------------------------------------------------------- close-ups */

/*
 * A crop of the real interface, shown bare. No window chrome and no title
 * bar: those belong to a screenshot of a whole screen, and around a single
 * widget they make it look like a thumbnail of something bigger.
 */
.clip {
  margin: 0;
  display: inline-block;
  max-width: 100%;
}

.clip img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-hairline);
  box-shadow: var(--shadow-2);
  background: var(--surface);
}

.clip figcaption {
  margin-top: var(--sp-3);
  color: var(--text-subtle);
  font-size: .8125rem;
  max-width: 34ch;
}

.cliprow {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--sp-6);
  align-items: start;
}

/* The middle one sits a little lower, so a row of three reads as three
   things rather than as one strip cut into pieces. */
@media (min-width: 901px) {
  .cliprow > *:nth-child(even) { transform: translateY(22px); }
  .js-reveal .cliprow[data-revealed] > *:nth-child(even) { transform: translateY(22px); }
  .js-reveal .cliprow > *:nth-child(even) { transform: translateY(36px); }
}

/* A wide, short crop — a table row — spans the column instead. */
.clip-wide { display: block; }
.clip-wide img { border-radius: var(--radius); }

/* ======================================================================
 * The decorative layer
 *
 * Abstract only: curves, arcs, grain, washes, and non-rectangular frames
 * for real screenshots. Nothing here depicts a product screen — that line
 * was drawn hard after an invented lot diagram went up and came straight
 * back down. These claim nothing, and they are what stops the page reading
 * as a document with pictures in it.
 * ==================================================================== */

/* ------------------------------------------------------------ band edges */

/*
 * The wrapper is what is positioned, not the shape inside it.
 *
 * `.edge-top`/`.edge-bottom` carried `top: 0` and `bottom: 0` on a STATIC
 * div, where those properties do nothing, and the absolutely positioned SVG
 * inside then fell back to its static position — which for a first child is
 * the band's top PADDING edge, ninety-six pixels down. So every shaped edge
 * on the site was drawn a band's worth of padding away from the join it was
 * supposed to be softening: the home page's slant cut straight through the
 * heading under it, and the hero's swoosh floated in the middle of the hero.
 * It is the kind of fault that looks like a design decision until you
 * measure it.
 */
.edge-top,
.edge-bottom {
  position: absolute;
  inset-inline: -1px;              /* -1px: a sub-pixel gap shows as a hairline */
  height: var(--edge-h);
  /* Above the washes and the hatching, below the words. */
  z-index: 1;
  pointer-events: none;
}

.edge-top    { top: 0; }
.edge-bottom { bottom: 0; }

.edge {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: var(--bg);
}

/*
 * A shape at the top of a band is the same shape upside down.
 *
 * Each path fills the area BELOW its curve, which is what a bottom edge
 * wants; at the top of a band the fill has to be above it, or the colour
 * of the band above appears as a stripe below the curve instead of a
 * wave coming down into this one.
 */
.edge-top > .edge { scale: 1 -1; }

/*
 * Every tone, declared once and globally.
 *
 * They were scoped under the band that owned them — `.band-brand .edge[
 * data-tone='brand']` — which is backwards: an edge is painted in the tone
 * of the band it is cutting INTO, so it always sits inside a band of a
 * different colour. The brand band's swoosh into the peach band below it
 * was therefore filled white, and the join showed as a white wave between
 * two coloured bands.
 */
.edge[data-tone='bg']     { fill: var(--bg); }
.edge[data-tone='sunken'] { fill: var(--bg-sunken); }
.edge[data-tone='deep']   { fill: var(--bg-deep); }
.edge[data-tone='ink']    { fill: var(--ink-deep); }
.edge[data-tone='warm']   { fill: var(--tint-warm); }
.edge[data-tone='peach']  { fill: var(--tint-warm); }
.edge[data-tone='warmer'] { fill: var(--tint-warmer); }
.edge[data-tone='brand']  { fill: var(--paint-700); }

/* An explicit flip is relative to the shape's natural direction, so a
   flipped TOP edge points back down again. `scale`, not `transform`. */
.edge.is-flipped { scale: 1 -1; }
.edge-top > .edge.is-flipped { scale: 1 1; }

/* A band that owns its own edges stops being a rectangle. */
.band, .band-tight { position: relative; }

/*
 * A band that carries a shaped edge reserves the room that shape takes.
 *
 * The operator, with a photograph of a phone: *"the text doesn't have enough
 * space on mobile."* A crest's curve reaches the whole of the edge's height
 * — ninety pixels on a desktop, forty-four on a phone — and the band under
 * it had the ordinary forty pixels of padding, so the last line of a caption
 * sat on the curve. The padding is the normal padding PLUS the shape's own
 * height, which is why both are written in terms of `--edge-h`: change the
 * shape's height and the clearance follows it. `build.mjs` refuses a page
 * where an edge appears on a band without these classes.
 */
.band.has-edge-top         { padding-top: calc(var(--sp-9) + var(--edge-h)); }
.band.has-edge-bottom      { padding-bottom: calc(var(--sp-9) + var(--edge-h)); }
.band-tight.has-edge-top    { padding-top: calc(var(--sp-8) + var(--edge-h)); }
.band-tight.has-edge-bottom { padding-bottom: calc(var(--sp-8) + var(--edge-h)); }
.hero.has-edge-bottom       { padding-bottom: calc(var(--sp-8) + var(--edge-h)); }
.pagehead.has-edge-bottom   { padding-bottom: calc(var(--sp-8) + var(--edge-h)); }

@media (max-width: 640px) {
  .band.has-edge-top         { padding-top: calc(var(--sp-8) + var(--edge-h)); }
  .band.has-edge-bottom      { padding-bottom: calc(var(--sp-8) + var(--edge-h)); }
  .band-tight.has-edge-top    { padding-top: calc(var(--sp-7) + var(--edge-h)); }
  .band-tight.has-edge-bottom { padding-bottom: calc(var(--sp-7) + var(--edge-h)); }
}

/* ---------------------------------------------------------------- washes */

.wash {
  position: absolute;
  width: min(760px, 90vw);
  aspect-ratio: 1;
  z-index: 0;
  pointer-events: none;
  opacity: .9;
}

.wash-tr { top: -22%; right: -14%; }
.wash-bl { bottom: -26%; left: -18%; }
.wash-c  { top: 50%; left: 50%; translate: -50% -50%; }

.arcs {
  position: absolute;
  width: min(620px, 80vw);
  color: var(--accent);
  opacity: .22;
  z-index: 0;
  pointer-events: none;
}

.grain {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  color: var(--text);
  opacity: .035;
  z-index: 0;
  pointer-events: none;
  /*
   * Faded out at both ends, so it can never reach a join.
   *
   * A band edge is a flat fill standing in for the band on the other side
   * of the join; any texture running up to that join makes the fill a
   * shade off, which is the whole family of "the background colour is
   * invading it". Dots at 3.5% are a small version of the same fault, and
   * a mask is cheaper than a rule about where grain may be used.
   */
  mask-image: linear-gradient(to bottom, transparent 0, #000 18%, #000 72%, transparent 96%);
}

/* Anything that has decoration behind it needs its content lifted over it,
   and the decoration clipped to the band. `:not(.edge)` so a band edge can
   still hang over the join it exists to soften. */
.band > .wrap, .band-tight > .wrap, .hero > .wrap, .pagehead > .wrap { position: relative; z-index: 2; }
.has-decor { overflow: clip; }

/* ------------------------------------------------------- drawn underline */

.marked { position: relative; white-space: nowrap; }

.scribble {
  position: absolute;
  left: -2%;
  bottom: -.22em;
  width: 104%;
  height: .42em;
  color: var(--accent);
  z-index: -1;
}

@media (prefers-reduced-motion: no-preference) {
  .scribble path {
    stroke-dasharray: 260;
    stroke-dashoffset: 260;
  }

  [data-revealed] .scribble path,
  .hero .scribble path {
    animation: draw 900ms var(--ease-out) 420ms forwards;
  }

  [data-revealed] .scribble path:last-child,
  .hero .scribble path:last-child { animation-delay: 620ms; }
}

@keyframes draw { to { stroke-dashoffset: 0; } }

/* --------------------------------------------------- non-standard frames
 *
 * Real screenshots, in shapes that are not rectangles. The picture is never
 * altered — only the window it is seen through.
 */

/*
 * No second corner radius on the picture itself.
 *
 * `.shot` already clips to `--radius-xl` and `.clip` to `--radius-lg`; a
 * second, different radius on the `img` inside produced two curves at every
 * corner, half a shadow between them, and the distinct look of a mistake.
 * The operator: *"cut-off corners which also doesn't look nice — make sure
 * every screenshot looks professional and intentionally taken or cropped."*
 *
 * So there is exactly one corner treatment per picture, it lives on the
 * container, and the image fills it edge to edge. Shaped frames are rings
 * around the picture (`.framed`), never cuts into it.
 */

/* ------------------------------------------------------- tilted devices
 *
 * A phone screenshot, set in space rather than squared to the page. The
 * transform is on a wrapper so the image itself stays unrotated for the
 * browser's image smoothing, and the perspective is on the parent so two
 * devices side by side share one vanishing point.
 */

.stage {
  perspective: 1600px;
  perspective-origin: 50% 40%;
  display: grid;
  justify-items: center;
}

.tilt {
  transform: rotateY(-14deg) rotateX(4deg) rotateZ(-2deg);
  transform-style: preserve-3d;
  transition: transform 700ms var(--ease-out);
  will-change: transform;
}

.tilt-right { transform: rotateY(14deg) rotateX(4deg) rotateZ(2deg); }
.tilt-flat  { transform: rotateZ(-3deg); }

/* It straightens as it comes into view: the page feels like it is settling
   rather than like something is permanently askew. */
@media (prefers-reduced-motion: no-preference) {
  .js-reveal .tilt { transform: rotateY(-22deg) rotateX(8deg) rotateZ(-4deg) translateY(18px); }
  .js-reveal [data-revealed] .tilt,
  .js-reveal .tilt[data-revealed] { transform: rotateY(-9deg) rotateX(3deg) rotateZ(-1.5deg); }

  .js-reveal .tilt-right { transform: rotateY(22deg) rotateX(8deg) rotateZ(4deg) translateY(18px); }
  .js-reveal [data-revealed] .tilt-right,
  .js-reveal .tilt-right[data-revealed] { transform: rotateY(9deg) rotateX(3deg) rotateZ(1.5deg); }
}

@media (max-width: 640px) {
  /* A thumb-sized screen is not the place for perspective. */
  .tilt, .tilt-right, .tilt-flat { transform: none !important; }
}

/* Two phones, overlapping, at different depths. */
.duo {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: center;
  gap: 0;
}

.duo > *:first-child { z-index: 2; }
.duo > *:last-child { margin-inline-start: -22%; margin-top: 14%; opacity: .96; }

@media (max-width: 640px) {
  .duo { grid-template-columns: 1fr; gap: var(--sp-5); }
  .duo > *:last-child { margin: 0; }
}

/* Where the hero's own decoration sits. Separate from `.wash`/`.arcs` so the
   positions can be tuned per band without touching the shapes themselves. */
.hero .wash-tr { top: -34%; right: -18%; width: min(880px, 100vw); }
.arcs-hero { top: -16%; right: -6%; }

@media (max-width: 900px) {
  /* Top-right on a phone too: at the hero's foot it sat directly above the
     band edge below, and a flat fill against three drawn arcs is a seam. */
  .arcs-hero { top: -8%; right: -34%; opacity: .16; }
  .hero .wash-tr { top: -18%; right: -40%; }
}

.arcs-mid { bottom: -22%; left: -14%; opacity: .16; }

/* In the empty half of a band whose content is one narrow column. */
.arcs-right { top: 50%; right: -8%; translate: 0 -50%; }

@media (max-width: 900px) {
  .arcs-right { top: auto; bottom: -14%; right: -34%; opacity: .16; }
}

/* The roster: everything the product does, named and not explained. */
.roster {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--sp-7) var(--sp-8);
  margin-top: var(--sp-8);
}

.roster h3 {
  font-family: var(--body);
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent-ink);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--accent-edge);
}

.roster ul { list-style: none; margin: var(--sp-4) 0 0; padding: 0; }

.roster li {
  position: relative;
  padding-left: 18px;
  color: var(--text-muted);
  font-size: .9375rem;
  line-height: 1.45;
}

.roster li + li { margin-top: var(--sp-3); }

.roster li::before {
  content: '';
  position: absolute;
  left: 0;
  top: .55em;
  width: 7px;
  height: 7px;
  border-radius: 2px;
  background: var(--accent);
  opacity: .55;
}

/* ======================================================================
 * Colour, and shaped frames
 *
 * Two complaints answered together: light mode was "completely white",
 * dark mode "completely black", and the clipped corner on the screenshots
 * looked wrong. So the bands now carry tone, and a screenshot sits inside
 * a frame rather than having a bite taken out of it.
 * ==================================================================== */

:root,
:root[data-theme='light'] {
  /* These were 6% tints and read as "white with a rumour of warmth". The
     operator, three times: the pages are either too white or too black.
     They are proper colours now — the accent ramp's own 100 and 200 rungs,
     which is peach you can actually see. */
  --tint-warm:   #FDE0CE;
  --tint-warmer: #FBC7AA;
  --tint-cool:   #E3EDF3;
  --tint-edge:   #F0A977;
  --ink-deep:    #221E1B;
  --on-ink:      #EDE7E3;
  --on-ink-muted:#B8ADA6;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --tint-warm:   #2E2017;
    --tint-warmer: #3D2A1C;
    --tint-cool:   #17242A;
    --tint-edge:   #7A4526;
    --ink-deep:    #0D0B09;
    --on-ink:      #EDE7E3;
    --on-ink-muted:#B8ADA6;
  }
}

:root[data-theme='dark'] {
  --tint-warm:   #2E2017;
  --tint-warmer: #3D2A1C;
  --tint-cool:   #17242A;
  --tint-edge:   #7A4526;
  --ink-deep:    #0D0B09;
  --on-ink:      #EDE7E3;
  --on-ink-muted:#B8ADA6;
}

/* ------------------------------------------------------------ band tones
 *
 * A page that alternates white and off-white is a document. These give the
 * sequence somewhere to go: warm, then white, then warm again, then ink.
 */

.band-warm { background: var(--tint-warm); }

.band-gradient {
  background: linear-gradient(168deg, var(--tint-warm) 0%, var(--bg) 62%);
}

.band-ink {
  background: var(--ink-deep);
  color: var(--on-ink);
}

.band-ink h1, .band-ink h2, .band-ink h3 { color: #FFFFFF; }
.band-ink p, .band-ink li, .band-ink .lede { color: var(--on-ink-muted); }
.band-ink .kicker { color: var(--accent); }
.band-ink .ticks b { color: #FFFFFF; }
.band-ink .btn-quiet { border-color: #574E48; color: var(--on-ink); }
.band-ink .btn-quiet:hover { background: #2A2420; border-color: var(--ash-600); color: #FFFFFF; }
.band-ink .card {
  background: #1A1613;
  border-color: #332C27;
}
.band-ink .card p { color: var(--on-ink-muted); }
.band-ink .card .icon { background: #2A1C13; border-color: #4A2C17; color: #FBA97E; }

.fact b { color: var(--accent-ink); }

/* ---------------------------------------------------------- shaped frames
 *
 * A frame that hugs the picture, and nothing behind it.
 *
 * The first version put a filled, tinted slab behind each screenshot,
 * rotated a degree, and the operator caught what that does on a coloured
 * band: the slab is a different colour from both the picture and the page,
 * so the picture sits in a pale halo that belongs to neither. On the dark
 * feature band it looked broken.
 *
 * The rule now: **an outline hugs the image exactly, and every layer
 * between the image and the page is transparent.** The band's own colour
 * runs uninterrupted right up to the edge of the picture, whatever that
 * colour happens to be — white, peach or near-black — because nothing is
 * painted there. `box-shadow` rings do this and `background` cannot.
 */

.framed { position: relative; display: block; }

.framed > figure,
.framed > .shot,
.framed > .clip { position: relative; z-index: 1; }

/*
 * Two rings, both translucent: a hairline of the accent on the edge of the
 * picture, and a wider soft one outside it. `color-mix` keeps them tied to
 * the accent rather than to a fixed colour, so they work on any band.
 */
.framed > figure,
.framed > .shot {
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--accent) 38%, transparent),
    0 0 0 9px color-mix(in oklab, var(--accent) 9%, transparent),
    var(--shadow-2);
  border-radius: var(--radius-xl);
}

/* Wider and softer, for the one picture per page that should stop you. */
.framed-wide > figure,
.framed-wide > .shot {
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--accent) 45%, transparent),
    0 0 0 14px color-mix(in oklab, var(--accent) 7%, transparent),
    0 0 0 15px color-mix(in oklab, var(--accent) 18%, transparent),
    var(--shadow-2);
}

/*
 * The one genuinely non-rectangular frame, and it deliberately does not
 * touch the picture. Three strokes offset from the corner, drawn as a
 * bracket rather than a box — no fill, so there is nothing to mismatch.
 */
.framed-bracket::before,
.framed-bracket::after {
  content: '';
  position: absolute;
  width: 74px;
  height: 74px;
  border: 2px solid var(--accent);
  opacity: .5;
  z-index: 0;
}

.framed-bracket::before {
  top: -20px;
  left: -20px;
  border-right: 0;
  border-bottom: 0;
  border-radius: var(--radius-xl) 0 0 0;
}

.framed-bracket::after {
  right: -20px;
  bottom: -20px;
  border-left: 0;
  border-top: 0;
  border-radius: 0 0 var(--radius-xl) 0;
}

@media (max-width: 640px) {
  .framed-bracket::before,
  .framed-bracket::after { display: none; }

  .framed > figure,
  .framed > .shot,
  .framed-wide > figure,
  .framed-wide > .shot {
    box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent) 32%, transparent), var(--shadow-2);
  }
}

/* ------------------------------------------------- more colour, less page
 *
 * The first pass was, in the operator's words, "completely white" in light
 * mode and "completely black" in dark. These push tone into the places a
 * flat page was showing through: the hero, the washes, the card marks and
 * the figures.
 */

.hero {
  background:
    radial-gradient(120% 90% at 88% -10%, var(--tint-warmer) 0%, transparent 58%),
    radial-gradient(90% 70% at -10% 10%, var(--tint-cool) 0%, transparent 55%),
    var(--bg);
}

/* The washes were barely there. */
.wash { opacity: 1; }
.arcs { opacity: .3; }

:root[data-theme='dark'] .arcs { opacity: .22; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .arcs { opacity: .22; }
}

/* A figure in the facts band is the loudest number on the page; let it be. */
.fact b {
  background: linear-gradient(135deg, var(--accent-ink), var(--accent));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Cards get a tinted mark and a warm hover rather than a grey one. */
a.card:hover {
  border-color: var(--accent-edge);
  box-shadow: 0 14px 34px rgba(200, 79, 10, .12), var(--shadow-1);
}

.band-ink a.card:hover {
  border-color: #6A4428;
  box-shadow: 0 14px 34px rgba(0, 0, 0, .5);
}

/* The steps' numerals, in the brand rather than in a grey circle. */
.steps li::before {
  background: linear-gradient(140deg, var(--accent-soft), var(--tint-warmer));
  border-color: var(--accent-edge);
  color: var(--accent-ink);
}

/* The closing panel gets its own light rather than being a black rectangle. */
.closer {
  background:
    radial-gradient(90% 120% at 15% 0%, #3A2A1E 0%, transparent 60%),
    radial-gradient(70% 90% at 95% 100%, #2B2320 0%, transparent 55%),
    var(--ink-panel);
}

/* ======================================================================
 * The brand, used as a surface
 *
 * Tinting at 6% was not colour, it was a rumour of colour, and the page
 * still read as white in one theme and black in the other. These bands
 * take the accent ramp as an actual background. White text sits on
 * `--paint-700`/`--paint-600` rather than `--paint-500`, because white on
 * 500 is 3.35:1 and fails SC 1.4.3 — the pair that carries words is
 * `--accent-solid` and `--accent-on`, and that rule does not bend for a
 * marketing page.
 * ==================================================================== */

.band-brand {
  background:
    radial-gradient(100% 140% at 10% 0%, var(--paint-600) 0%, transparent 60%),
    linear-gradient(135deg, var(--paint-700) 0%, var(--paint-600) 55%, var(--paint-700) 100%);
  color: #FFFFFF;
}

.band-brand h1,
.band-brand h2,
.band-brand h3 { color: #FFFFFF; }

.band-brand p,
.band-brand li,
.band-brand .lede,
.band-brand .fact span { color: rgba(255, 255, 255, .86); }

.band-brand .kicker { color: rgba(255, 255, 255, .72); }

/* The gradient-text trick is unreadable on a coloured ground. */
.band-brand .fact b {
  background: none;
  -webkit-text-fill-color: #FFFFFF;
  color: #FFFFFF;
}

.band-brand .btn-primary {
  background: #FFFFFF;
  border-color: #FFFFFF;
  color: var(--paint-700);
}
.band-brand .btn-primary:hover { background: #FFF1E7; border-color: #FFF1E7; color: var(--paint-800); }

.band-brand .btn-quiet { border-color: rgba(255, 255, 255, .5); color: #FFFFFF; }
.band-brand .btn-quiet:hover { background: rgba(255, 255, 255, .12); border-color: #FFFFFF; }

.band-brand .ticks .bullet { color: #FFFFFF; }
.band-brand .marked .scribble { color: rgba(255, 255, 255, .6); }

/* A full peach band — the accent at a weight you can see. */
.band-peach { background: var(--tint-warm); }
.band-peach .card { background: #FFFFFF; border-color: var(--tint-edge); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .band-peach .card { background: #241A14; }
}
:root[data-theme='dark'] .band-peach .card { background: #241A14; }

/* The closing panel is the brand, not a black rectangle. */
.closer {
  background:
    radial-gradient(90% 130% at 12% 0%, var(--paint-600) 0%, transparent 62%),
    linear-gradient(140deg, var(--paint-700) 0%, var(--paint-800) 100%);
}

.closer p { color: rgba(255, 255, 255, .88); }
.closer .btn-primary { background: #FFFFFF; border-color: #FFFFFF; color: var(--paint-700); }
.closer .btn-primary:hover { background: #FFF1E7; border-color: #FFF1E7; color: var(--paint-800); }
.closer .btn-quiet { border-color: rgba(255, 255, 255, .5); color: #FFFFFF; }
.closer .btn-quiet:hover { background: rgba(255, 255, 255, .14); border-color: #FFFFFF; color: #FFFFFF; }

/* The hero, with the accent actually present rather than implied. */
.hero {
  background:
    radial-gradient(90% 70% at 92% -16%, var(--tint-warmer) 0%, transparent 56%),
    radial-gradient(70% 60% at 4% 6%, var(--tint-cool) 0%, transparent 52%),
    linear-gradient(180deg, var(--tint-warm) 0%, var(--bg) 46%);
}

.arcs { opacity: .45; }
.wash { opacity: 1; }

/* A kicker that is the brand rather than grey. */
.kicker { color: var(--accent-ink); }
.band-ink .kicker { color: var(--accent); }

/* Section headings get a short brand rule above them. */
.lead-in .kicker::before {
  content: '';
  display: inline-block;
  width: 26px;
  height: 2px;
  margin-right: 10px;
  vertical-align: 3px;
  background: var(--accent);
}

/* ======================================================================
 * Touch targets
 *
 * From a sweep of 14 pages × 11 widths × 2 themes: no overflow, no text
 * under 12px, no console errors — and six controls under the 44px floor.
 * `N-18` on the roadmap is this exact fault in the application ("sixty
 * controls under 40px on /spots at 320"), and a new surface has no excuse
 * for repeating it.
 *
 * The padding is transparent and around the glyph, which is how the target
 * grows without the thing getting visually heavier (docs/DESIGN-2026 §1.7).
 * ==================================================================== */

/* The wordmark is 26–30px of artwork; the link around it is the target. */
.brandlink {
  min-height: 44px;
  align-items: center;
}

/* The footer's address and its second tier of links. */
.footmail {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  margin-top: var(--sp-3);
}

.footend nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-block: var(--sp-1);
}

/* Breadcrumbs: a row of small links is still a row of targets. */
.crumbs a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.crumbs { margin-block: calc(var(--sp-2) * -1) var(--sp-3); }

/* The small button variant was 40px, which is under every published floor
   — Apple's 44, Material's 48. It is only used in the header, where there
   is room for four more pixels. */
.btn-sm { min-height: 44px; }

/* The footer's column links were already 38px of line plus padding; this
   makes the box what it looks like. */
.footgrid a { min-height: 44px; display: inline-flex; align-items: center; }

/*
 * A figure that swaps to the phone photograph below 640px.
 *
 * The aspect ratio changes with the picture — 1440×900 becomes 390×844 —
 * so the figure must not keep the desktop proportions, and the phone shot
 * must not be stretched to the full column width or it is a 390px picture
 * blown up to 358px of very soft pixels. Capped and centred instead.
 */
/* The glossary's term links: a heading that is also the only control in its
   cell, so it carries the 44px floor like any other target. */
.termlink {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: inherit;
  text-decoration: none;
}

.termlink:hover { color: var(--accent-ink); text-decoration: underline; }

/* ======================================================================
 * Two things the reference set does that we had none of
 *
 * From the motion survey of fifteen current B2B SaaS sites: `mask-image`
 * is the actual house style of 2025–26, not animation — Attio uses it 316
 * times, Raycast 191, Clerk 144, Linear 121, Vercel 136. It is static, it
 * costs nothing, and it is most of why those pages look designed rather
 * than assembled. We had zero uses.
 * ==================================================================== */

/*
 * A tall screenshot fades at its lower edge instead of ending in a hard
 * line, so it reads as "this continues" rather than "this was cropped".
 * Only where the picture is genuinely taller than its frame — a fade on a
 * complete picture is a lie about there being more.
 */
.shot-tall img {
  mask-image: linear-gradient(to bottom, #000 72%, rgba(0, 0, 0, .35) 92%, transparent 100%);
}

/* Anything that scrolls sideways says so at the edge rather than cutting
   a word in half: the scroller's own content fades out where it continues. */
.scroller {
  mask-image: linear-gradient(to right, #000 0, #000 94%, transparent 100%);
}

@media (max-width: 640px) {
  .scroller { mask-image: linear-gradient(to right, #000 0, #000 88%, transparent 100%); }
}

/*
 * The figure holds while the text scrolls past it.
 *
 * A dense application screenshot that travels past at reading speed is a
 * grey rectangle for a second and a half. Pinned, somebody actually reads
 * it. Zero bytes of JavaScript, and only above the tablet breakpoint —
 * below it the grid is one column and sticky would be wrong.
 *
 * `top` clears the sticky header (68px) with room to breathe; without that
 * the figure parks underneath it.
 */
@media (min-width: 901px) {
  .split > *:last-child:has(.shot),
  .split > *:last-child:has(.framed) {
    position: sticky;
    top: calc(68px + var(--sp-6));
    align-self: start;
  }

  /* Not in the heading-and-prose variant, where the right column is text
     and pinning it would leave the heading stranded. */
  .split-text > *:last-child { position: static; }
}

/*
 * Already on screen at load: shown at once, with no transition.
 *
 * `boot.js` marks these before the observer runs, because measuring found
 * the reveal was delaying LCP by eight hundred milliseconds on the two
 * prose pages — the largest element was painted and then hidden, and a
 * browser cannot record a largest-contentful-paint for something at
 * `opacity: 0`. A first attempt did this with a `:first-of-type` selector
 * and missed, because the largest element is not always in the first
 * section. The script knows where the fold is; a stylesheet does not.
 */
.js-reveal [data-reveal][data-instant] {
  opacity: 1;
  transform: none;
  filter: none;
  transition: none;
}

.js-reveal [data-instant] .cards > *,
.js-reveal [data-instant] .facts > *,
.js-reveal [data-instant].cards > *,
.js-reveal [data-instant].facts > * {
  opacity: 1;
  transform: none;
  transition: none;
}

/*
 * The phone screenshot's reserved box must match what lands in it.
 *
 * `/features/digital-permits/` scored CLS 0.389 at 390px — a failing
 * score, deterministic, at first paint. `.shot-phone` capped the figure at
 * 300px while the image carried the intrinsic dimensions of the 2x phone
 * capture, so the box was laid out at one size and resized to another the
 * moment the picture arrived. Giving the figure the picture's own aspect
 * ratio makes the reserved space right before anything loads.
 */
.shot-phone {
  width: min(300px, 100%);
  aspect-ratio: 390 / 844;
}

.shot-phone img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: top center;
}

/* A phone shot inside a swapping figure keeps the swap's own cap. */
.shot-swaps.shot-phone { width: min(320px, 100%); }

/* ======================================================================
 * The drawn layer, second pass
 *
 * Three complaints, one answer. The swoosh appeared at two joins of the
 * home page and at a third on four other pages, which is a texture rather
 * than a motif; the flat fill of one of them sat against the gradient of
 * the band below and showed the seam ("the background color is invading
 * it"); and the pages between the coloured ones were still white with
 * words on them.
 *
 * So: band edges are used once per page and only between two FLAT bands,
 * and the decoration that carries a page is now inside the bands — a
 * ribbon that crosses one, hatching in the corner of another, a soft
 * field of the accent behind a third. Each of those, too, once per page,
 * and `build.mjs` fails the build on a second use.
 * ==================================================================== */

/*
 * Hatching. Opacity lives on the fill rather than on the element, because
 * the element's own opacity is what the reveal animates.
 */
.stripes {
  position: absolute;
  width: min(520px, 64vw);
  aspect-ratio: 1;
  color: var(--accent);
  opacity: .14;
  z-index: 0;
  pointer-events: none;
}

.stripes-tr { top: -10%; right: -8%; }
.stripes-bl { bottom: -12%; left: -10%; rotate: 180deg; }

:root[data-theme='dark'] .stripes { opacity: .2; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .stripes { opacity: .2; }
}

/* A field of the accent with no edge anywhere on it. */
.blob {
  position: absolute;
  width: min(820px, 96vw);
  aspect-ratio: 1;
  z-index: 0;
  pointer-events: none;
}

.blob-tr { top: -26%; right: -18%; }
.blob-bl { bottom: -28%; left: -20%; }
.blob-r  { top: 50%; right: -22%; translate: 0 -50%; }
.blob-l  { top: 50%; left: -24%; translate: 0 -50%; }

@media (max-width: 640px) {
  /* On a phone these are mostly off-screen anyway; pulled in so the part
     that shows is the middle of the shape rather than one corner of it. */
  .blob { width: 150vw; }
  .blob-tr { right: -40%; }
  .blob-bl { left: -40%; }
  .blob-r, .blob-l { top: 40%; }
  .stripes { width: 72vw; }
}

/* ======================================================================
 * Colour, third pass
 *
 * "Everything is either too black or either too white. We have the orange
 * color of the brand — why don't we implement that and take advantage of
 * it." Said three times, so this time it is not tints: it is the ramp, on
 * surfaces, on rules, on the type that should carry weight, and on every
 * page rather than only the home page.
 * ==================================================================== */

/*
 * The inner pages all began with a white box and a breadcrumb.
 *
 * `pageHead` is the first thing on every page that is not the home page —
 * features, segments, pricing, help, the glossary, the company pages — and
 * it had no colour at all, which is most of why those pages read as plain
 * next to the home page. It gets the hero's own light.
 */
.pagehead {
  position: relative;
  overflow: clip;
  background:
    radial-gradient(82% 120% at 88% -30%, var(--tint-warmer) 0%, transparent 58%),
    radial-gradient(60% 90% at -6% 0%, var(--tint-cool) 0%, transparent 56%),
    linear-gradient(180deg, var(--tint-warm) 0%, var(--bg) 78%);
  padding-block: var(--sp-8) var(--sp-8);
}

.pagehead > .wrap { position: relative; z-index: 2; }
.pagehead .lede { max-width: 64ch; }

/* The rule under a page head, in the brand, fading out — a line that stops
   the gradient ending in nothing. */
.pagehead::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--accent) 0%, color-mix(in oklab, var(--accent) 30%, transparent) 42%, transparent 88%);
  z-index: 1;
}

/* A stronger peach than `--tint-warm`, for one band on a page. */
.band-warmer { background: var(--tint-warmer); }
.band-warmer .card { background: var(--bg); border-color: var(--tint-edge); }

/*
 * A band that carries the brand as a 3px rule rather than as a surface.
 *
 * Colour where a whole orange band would be too much — which is most
 * places, because an orange band works once on a page and not twice.
 */
.band-rail { border-top: 3px solid transparent; border-image: linear-gradient(90deg, var(--accent-solid), var(--accent), color-mix(in oklab, var(--accent) 25%, transparent)) 1; }

/* The kicker's rule, and the kicker, carry the accent on every band. */
.kicker { color: var(--accent-ink); }

.lead-in .kicker::before {
  width: 30px;
  height: 3px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent-solid), var(--accent));
}

/* Headings get the brand in them rather than only above them. */
.lead-in .h-section em,
.h-hero em {
  font-style: normal;
  color: var(--accent-ink);
}

/*
 * The list marks, the window bar's dot and the FAQ's chevron.
 *
 * Three small grey things, on every page, and grey is what "too white"
 * is made of.
 */
.shot-bar .dot { background: var(--accent); }
.shot-bar b { color: var(--text-muted); }

.faq { border-top: 2px solid var(--accent-edge); }
.faq details { border-bottom: 1px solid var(--border-hairline); }
.faq summary::after { border-color: var(--accent); }
.faq summary:hover { color: var(--accent-ink); }

.faq details[open] {
  border-bottom-color: var(--accent-edge);
}

.faq details[open] summary { color: var(--accent-ink); }

/* A rule beside the answer, so an open question reads as opened. */
.faq .answer {
  border-left: 2px solid var(--accent-edge);
  padding-left: var(--sp-5);
  margin-left: 1px;
}

/* The comparison table's head, in the brand rather than in grey. */
.cmp thead th { color: var(--accent-ink); border-bottom-color: var(--accent-edge); }
.cmp caption { color: var(--text-muted); }

/* The big price, in the brand. */
.bignum b {
  background: linear-gradient(135deg, var(--accent-ink), var(--accent));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* The calculator's own figures are money, so they are set in the brand's
   ink rather than in body grey. */
.calc td:last-child { color: var(--accent-ink); font-weight: 600; }

/*
 * The footer was grey. It is the last thing on every page, and on a site
 * whose complaint is that it is too white, the last thing on every page
 * being off-white is not a small part of that.
 */
.sitefoot {
  background:
    radial-gradient(70% 140% at 88% 0%, color-mix(in oklab, var(--accent) 10%, transparent) 0%, transparent 60%),
    var(--bg-sunken);
  border-top: 2px solid transparent;
  border-image: linear-gradient(90deg, var(--accent-solid) 0%, var(--accent) 30%, transparent 80%) 1;
}

.footgrid h2 { color: var(--accent-ink); }
.footgrid a:hover { color: var(--accent-ink); }

/*
 * A split's picture column, with the brand behind one corner of it.
 *
 * Not a slab and nowhere near the edge of the picture: a 2px bracket in
 * the accent, offset well clear, which is the `framed-bracket` treatment
 * applied where a figure has no frame of its own.
 */
.split-rail > div:first-child {
  border-left: 2px solid var(--accent-edge);
  padding-left: var(--sp-6);
}

@media (max-width: 900px) {
  .split-rail > div:first-child { border-left: 0; padding-left: 0; border-top: 2px solid var(--accent-edge); padding-top: var(--sp-5); }
}

/* On the coloured bands the rail has to change with the ground. */
.band-ink .split-rail > div:first-child { border-color: #5A3A22; }
.band-brand .split-rail > div:first-child { border-color: rgba(255, 255, 255, .34); }

/* The ticks' marks were the accent already; the text beside them was not
   quite dark enough for a peach ground. */
.band-peach .ticks li,
.band-warmer .ticks li { color: color-mix(in oklab, var(--text) 82%, transparent); }

/*
 * And nor were the two other things that land on peach.
 *
 * Measured on 2026-10-23, unrounded, both in the light theme: `--text-subtle`
 * (#786E66) on `--tint-warm` is **3.95:1**, which is the pricing box's
 * right-hand half and the small print on every peach band, and `--accent-ink`
 * (`--paint-700`) on `--tint-warmer` is **4.37:1**, which is the kicker. Both
 * need 4.5:1 for text this size. The token is right where it was chosen — it
 * is 4.98:1 on white — and wrong on the ground these put it on, which is the
 * same shape of fault as the ticks above.
 *
 * `--text-muted` is 5.80:1 on `--tint-warm` and is lighter, not darker, in
 * dark, so the one rule serves both themes. The kicker cannot do the same:
 * `--paint-800` is 6.10:1 on peach in light and would be unreadable on the
 * dark tints, so it is set through a token the theme blocks define.
 */
.band-peach .subtle,
.band-warmer .subtle,
.pricebox-side .subtle { color: var(--text-muted); }

.band-peach .kicker,
.band-warmer .kicker { color: var(--kicker-on-tint); }

/* Quiet buttons on a peach band need a border that is visible on peach. */
.band-peach .btn-quiet,
.band-warmer .btn-quiet { border-color: var(--tint-edge); }
.band-peach .btn-quiet:hover,
.band-warmer .btn-quiet:hover { background: color-mix(in oklab, var(--accent) 12%, transparent); border-color: var(--accent); }

/* A screenshot on a coloured band keeps its own white surround; the window
   bar inside it must not read as a second, dirtier band. */
.band-peach .shot,
.band-warmer .shot { border-color: var(--tint-edge); }

/*
 * The glossary and the help index are lists of links, which is the one
 * shape on this site that genuinely was black text on white.
 */
.roster h3 { color: var(--accent-ink); border-bottom-color: var(--accent-edge); }
.roster li::before { opacity: .8; }

/* The step numerals, filled rather than outlined — a row of five rings in
   pale peach disappeared on a peach band. */
.steps li::before {
  background: linear-gradient(140deg, var(--accent-solid), var(--accent));
  border-color: transparent;
  color: #FFFFFF;
  box-shadow: 0 2px 8px color-mix(in oklab, var(--accent) 35%, transparent);
}

.band-ink .steps li::before,
.band-brand .steps li::before { box-shadow: none; }
.band-brand .steps li::before { background: #FFFFFF; color: var(--paint-700); }

/* The connecting line down a list of steps: it is a sequence, so it should
   look like one rather than like five separate paragraphs. */
.steps li:not(:last-child)::after {
  content: '';
  position: absolute;
  left: 17px;
  top: 40px;
  bottom: calc(var(--sp-6) * -1);
  width: 2px;
  background: linear-gradient(180deg, var(--accent-edge), transparent);
}

.band-ink .steps li:not(:last-child)::after { background: linear-gradient(180deg, #5A3A22, transparent); }
.band-brand .steps li:not(:last-child)::after { background: linear-gradient(180deg, rgba(255,255,255,.45), transparent); }

/*
 * The closing panel, with something drawn on it.
 *
 * In CSS rather than as another SVG, because the panel is on every page
 * and a shape element there would be the one thing on the site used
 * fourteen times. Two sets of hairlines at opposing angles, barely
 * there — enough that the panel is a surface rather than a rectangle of
 * orange.
 */
.closer {
  background-image:
    repeating-linear-gradient(118deg, rgba(255, 255, 255, .055) 0 2px, transparent 2px 15px),
    repeating-linear-gradient(-118deg, rgba(255, 255, 255, .035) 0 1px, transparent 1px 26px),
    radial-gradient(90% 130% at 12% 0%, var(--paint-600) 0%, transparent 62%),
    linear-gradient(140deg, var(--paint-700) 0%, var(--paint-800, #7E2D00) 100%);
}

/* The page you are on, said in the brand rather than in bold alone. */
.sitenav a[aria-current='page'] {
  color: var(--accent-ink);
  background: var(--accent-soft);
}

/*
 * A card lifts, and draws a line under itself as it does.
 *
 * The whole of the interaction vocabulary on this site is: things rise
 * slightly when you point at them, and the brand appears. Both are
 * cheap — transform and opacity only, no layout — and both are off
 * under `prefers-reduced-motion`, which the blanket rule at the top of
 * this file already takes care of by dropping `transform` out of the
 * transition list.
 */
a.card {
  position: relative;
  overflow: hidden;
  transition:
    border-color var(--dur-1) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out),
    transform var(--dur-2) var(--ease-out);
}

a.card::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--accent-solid), var(--accent));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 320ms var(--ease-out);
}

a.card:hover { transform: translateY(-3px); }
a.card:hover::after { transform: scaleX(1); }

/* A button rises by a pixel. Any more and it reads as a toy. */
.btn { transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out), box-shadow var(--dur-2) var(--ease-out); }
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 18px color-mix(in oklab, var(--accent) 34%, transparent); }
.btn-quiet:hover { transform: translateY(-1px); }

/*
 * Tiles: a list of short things, each in its own surface.
 *
 * The glossary was twenty-four definitions printed straight onto a peach
 * band, which at full width is a wall of pale orange with text on it. A
 * tile per term gives the colour something to sit behind.
 */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
  gap: var(--sp-4);
}

/* Fewer, longer ones: two across rather than four, so a paragraph in a
   tile is not a column of three-word lines. */
.tiles-wide { grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: var(--sp-5); }

@media (max-width: 860px) {
  .tiles-wide { grid-template-columns: 1fr; }
}

.tile {
  background: var(--bg);
  border: 1px solid var(--border-hairline);
  border-left: 3px solid var(--accent-edge);
  border-radius: var(--radius);
  padding: var(--sp-5);
  transition: border-left-color var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}

.tile h2, .tile h3 { font-size: 1rem; letter-spacing: -.01em; }
.tile p { color: var(--text-muted); font-size: .9375rem; margin-top: var(--sp-2); }
.tile:hover { border-left-color: var(--accent); transform: translateY(-2px); box-shadow: var(--shadow-1); }

.band-peach .tile,
.band-warmer .tile { border-color: var(--tint-edge); border-left-color: var(--accent); }

/*
 * The tile is the link, not the two words inside it.
 *
 * A heading wrapped in an anchor is a 108x20 target, which is under every
 * published floor and was the only fault in the sweep of 14 pages x 11
 * widths x 2 themes. Making the tile itself the anchor gives a 258x100
 * target and removes the "only the title is clickable" surprise with it.
 */
a.tile {
  display: block;
  text-decoration: none;
  color: inherit;
}

a.tile h2, a.tile h3 { color: var(--text); }
a.tile:hover h2, a.tile:hover h3 { color: var(--accent-ink); }

/*
 * A mark above a heading, in a tinted tile.
 *
 * `/about/` and `/security/` are the two pages with nothing to
 * photograph — five headings and five paragraphs each, and in the
 * operator's words the site is "too black or too white". A single
 * drawn mark per section is the smallest thing that gives a page of
 * prose some colour without inventing a picture of a feature.
 */
.markbox {
  display: inline-grid;
  place-items: center;
  width: 42px;
  height: 42px;
  margin-bottom: var(--sp-4);
  border-radius: var(--radius);
  background: linear-gradient(140deg, var(--accent-soft), color-mix(in oklab, var(--accent) 16%, transparent));
  border: 1px solid var(--accent-edge);
  color: var(--accent-ink);
}

.band-ink .markbox { background: #2A1C13; border-color: #4A2C17; color: #FBA97E; }
.band-peach .markbox,
.band-warmer .markbox { background: var(--bg); border-color: var(--tint-edge); }

/* Prose lists on a marketing page take the brand for their markers. */
.prose ul li::marker { color: var(--accent); }
.prose ol li::marker { color: var(--accent-ink); font-weight: 600; }

/* The kicker's rule draws itself when its section arrives. */
@media (prefers-reduced-motion: no-preference) {
  .js-reveal .lead-in[data-reveal]:not([data-instant]) .kicker::before { transform: scaleX(0); transform-origin: left; }
  .js-reveal .lead-in[data-revealed] .kicker::before { transform: scaleX(1); transition: transform 620ms var(--ease-out) 160ms; }
}

/* ======================================================================
 * The band that inverts
 *
 * Operator: *"add animations where the background and text color
 * completely invert into place when scrolled onto the screen."* So a
 * marked band is drawn in the page's own colours and turns into their
 * opposite as it arrives — a dark panel on a light page, a light panel on
 * a dark one. Literally inverted, in both themes, rather than "goes dark"
 * in one of them and barely moves in the other.
 *
 * Every colour it lands on is a token, so the ruleset below is written
 * once and reads correctly in either theme. Nothing here hides content:
 * without JavaScript the band simply stays in the page's colours, which is
 * a finished band and not a broken one, and under `prefers-reduced-motion`
 * the global rule at the top of this file keeps the colour transition and
 * drops the movement — the state arrives without travelling.
 * ==================================================================== */

:root,
:root[data-theme='light'] {
  --flip-bg:     #221E1B;
  --flip-head:   #FFFFFF;
  --flip-text:   #B8ADA6;
  --flip-strong: #FFFFFF;
  --flip-card:   #1A1613;
  --flip-line:   #332C27;
  --flip-mark:   #2A1C13;
  --flip-mark-edge: #4A2C17;
  --flip-accent: #FBA97E;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --flip-bg:     #F3EEEB;
    --flip-head:   #1E1B18;
    --flip-text:   #5D554E;
    --flip-strong: #1E1B18;
    --flip-card:   #FFFFFF;
    --flip-line:   #E2DAD4;
    --flip-mark:   #FEF1E9;
    --flip-mark-edge: #FBC7AA;
    --flip-accent: #A23B01;
  }
}

:root[data-theme='dark'] {
  --flip-bg:     #F3EEEB;
  --flip-head:   #1E1B18;
  --flip-text:   #5D554E;
  --flip-strong: #1E1B18;
  --flip-card:   #FFFFFF;
  --flip-line:   #E2DAD4;
  --flip-mark:   #FEF1E9;
  --flip-mark-edge: #FBC7AA;
  --flip-accent: #A23B01;
}

/* 900ms: long enough to read as a change of state rather than a flicker,
   short enough that a fast scroll does not leave it half-way. */
.band-flip,
.band-flip h2,
.band-flip h3,
.band-flip p,
.band-flip li,
.band-flip .lede,
.band-flip .kicker,
.band-flip .card,
.band-flip .tile,
.band-flip .more,
.band-flip .btn-quiet,
.band-flip .card .icon,
.band-flip .faq,
.band-flip .faq details,
.band-flip .faq summary,
.band-flip .faq .answer,
.band-flip .steps li::before,
.band-flip .roster h3 {
  transition:
    background-color 900ms var(--ease-out),
    color 900ms var(--ease-out),
    border-color 900ms var(--ease-out);
}

.band-flip[data-flipped] { background: var(--flip-bg); color: var(--flip-text); }

.band-flip[data-flipped] h2,
.band-flip[data-flipped] h3 { color: var(--flip-head); }

.band-flip[data-flipped] p,
.band-flip[data-flipped] li,
.band-flip[data-flipped] .lede { color: var(--flip-text); }

.band-flip[data-flipped] .kicker { color: var(--flip-accent); }
.band-flip[data-flipped] .ticks b,
.band-flip[data-flipped] b,
.band-flip[data-flipped] strong { color: var(--flip-strong); }

.band-flip[data-flipped] .card,
.band-flip[data-flipped] .tile {
  background: var(--flip-card);
  border-color: var(--flip-line);
}

.band-flip[data-flipped] .card p { color: var(--flip-text); }
.band-flip[data-flipped] .card .icon {
  background: var(--flip-mark);
  border-color: var(--flip-mark-edge);
  color: var(--flip-accent);
}

.band-flip[data-flipped] .btn-quiet { border-color: var(--flip-line); color: var(--flip-head); }
.band-flip[data-flipped] .btn-quiet:hover { background: color-mix(in oklab, var(--flip-head) 10%, transparent); border-color: var(--flip-head); }

.band-flip[data-flipped] .faq { border-top-color: var(--flip-mark-edge); }
.band-flip[data-flipped] .faq details { border-bottom-color: var(--flip-line); }
.band-flip[data-flipped] .faq summary { color: var(--flip-head); }
.band-flip[data-flipped] .faq summary::after { border-color: var(--flip-accent); }
.band-flip[data-flipped] .faq .answer { color: var(--flip-text); border-left-color: var(--flip-mark-edge); }
.band-flip[data-flipped] .faq a { color: var(--flip-accent); }

.band-flip[data-flipped] .roster h3 { color: var(--flip-accent); border-bottom-color: var(--flip-mark-edge); }
.band-flip[data-flipped] .roster li { color: var(--flip-text); }

.band-flip[data-flipped] a.card:hover { border-color: var(--flip-mark-edge); }
.band-flip[data-flipped] .more { color: var(--flip-accent); }

@media (max-width: 640px) {
  /*
   * On a phone, a phone screenshot is CROPPED, not shrunk.
   *
   * A 390x844 capture at the column's full width is seven hundred pixels
   * tall: one screenshot, one screenful, and the reader has to scroll past
   * a picture of a screen to get back to the page. Operator: *"they're
   * taking up way too much space on mobile — fit them nicely and
   * professionally without just shrinking them."* Shrinking is the other
   * failure and it is worse: at sixty per cent the type stops being
   * readable and the point of showing the real product goes with it.
   *
   * So the picture keeps its scale and the WINDOW gets shorter. Four
   * windows, not one, because the same crop twelve times is a template —
   * see `shot()` in `lib/sections.mjs` for which is which. The aspect
   * ratio is on the image, so the box is the right size before anything
   * loads and nothing shifts.
   */
  .shot-swaps { max-width: 360px; margin-inline: auto; }
  .hero-shot .shot-swaps { max-width: 340px; }

  .shot-swaps img,
  .shot-phone img {
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 5;
    object-fit: cover;
    object-position: top center;
  }

  /* A clean square, cut at the frame's own edge. No fade: the frame has a
     border and a shadow, so the cut reads as the edge of a window. */
  .crop-card img { aspect-ratio: 1 / 1; }

  /* Tall, and dissolving at its foot. The only one that says "there is
     more below this", so it goes on the one picture where that is the
     point — the dashboard. */
  .crop-fade img {
    aspect-ratio: 4 / 5;
    -webkit-mask-image: linear-gradient(to bottom, #000 72%, rgba(0, 0, 0, .4) 92%, transparent 100%);
            mask-image: linear-gradient(to bottom, #000 72%, rgba(0, 0, 0, .4) 92%, transparent 100%);
  }

  /*
   * The head of a screen and its first row: short, wide, and obviously a
   * detail rather than a whole screen.
   *
   * A hard cut lands wherever the screenshot happens to put a control, so
   * the foot of this one carries a rule in the brand — which turns the cut
   * into an edge somebody drew. It is the deliberate version of the same
   * gesture the fade makes softly, and the point is that they are not the
   * same gesture.
   */
  .crop-strip img { aspect-ratio: 3 / 2; }
  .crop-strip { border-bottom: 3px solid var(--accent); }

  /* The middle of the screen instead of the top of it — the rows rather
     than the chrome, which is the half worth reading on a list. */
  .crop-lower img { aspect-ratio: 4 / 5; object-position: center 36%; }

  /* The figure no longer holds the ratio — the picture does — so the box
     is whatever the cropped picture needs plus its own window bar. */
  .shot-phone,
  .shot-swaps.shot-phone {
    width: min(340px, 100%);
    aspect-ratio: auto;
  }

  /* Two phones become one column, and they are cropped differently, so
     they read as two screens rather than one screen twice. */
  .duo > * { width: min(320px, 100%); margin-inline: auto; }
}
