/* ── Fallback faces that occupy the same space as the real ones ───────────
 *
 * ── The jump this removes ──
 * The Google Fonts link carries `display=swap`, so the headline paints in a
 * fallback and is replaced when the webfont arrives. The fallback for
 * Bricolage Grotesque -- a grotesque display face -- was **Georgia**, a serif,
 * measured here at 20.6% wider. At the hero's 89.6px that is not a subtle
 * difference in colour, it is a different number of lines: the headline wrapped
 * one line further in Georgia, so the block was taller, and when the real font
 * landed it collapsed and the title dropped into position. Reported as "the
 * title drops as soon as the page loads", and correctly -- it was invisible to
 * a layout-shift measurement taken with the font already cached, which is why
 * it survived this long.
 *
 * ── How the numbers were arrived at ──
 * Measured in the browser with `measureText` over a representative string
 * rather than one word, comparing each real face against Arial:
 *
 *     size-adjust      = target width / fallback width
 *     ascent-override  = target ascent / size-adjust
 *     descent-override = target descent / size-adjust
 *
 * Measured against the strings this page actually sets -- the headline and the
 * hero paragraph -- not a synthetic alphabet. A first pass tuned on a sample
 * string left the sans 4.1% narrow, which on an eight-line paragraph is a
 * whole extra line and moved the buttons under it by 32px.
 *
 * That is the standard construction, and it means the fallback lays out to the
 * same width and the same line box as the face it stands in for. The swap then
 * changes the letterforms and moves nothing.
 *
 * Arial rather than Georgia or system-ui: it is on every platform that matters
 * and its metrics are stable, which is what makes an override worth computing.
 * If either webfont is ever changed, these numbers have to be re-measured --
 * they describe those two faces and nothing else. */
@font-face {
  font-family: 'Bricolage Fallback';
  src: local('Arial'), local('Helvetica'), local('Liberation Sans');
  size-adjust: 93.06%;
  ascent-override: 99.94%;
  descent-override: 29.01%;
  line-gap-override: 0%;
}
@font-face {
  font-family: 'Grotesk Fallback';
  src: local('Arial'), local('Helvetica'), local('Liberation Sans');
  size-adjust: 108.54%;
  ascent-override: 90.29%;
  descent-override: 26.72%;
  line-gap-override: 0%;
}

:root {
  --bg: #0a0a0c;
  --bg-2: #101015;
  --surface: rgba(255, 255, 255, 0.025);
  --surface-2: rgba(255, 255, 255, 0.04);
  --border: rgba(255, 255, 255, 0.09);
  --border-hi: rgba(255, 255, 255, 0.18);
  --text: #ECEAE3;
  --muted: #9b9b92;
  --dim: #6a6a63;
  --accent: #ff7a2f;
  --accent-soft: rgba(255, 122, 47, 0.12);
  --live: #57e3b0;

  /* ── Status has its own colours ──
     Before these, --accent painted the primary button, every badge, every
     "on" state and every link. When one colour means "do this", "this is
     fine" and "this is broken" at once, it means none of them — and a page
     where everything is orange has no way left to say something is wrong.

     Four roles, and nothing paints outside its role: --accent is reserved for
     the one action a view is for. See docs/ui-research.md. */
  /* Muted on purpose. The first pass took these straight off a default palette
     and the result was neon mint and highlighter amber, which is the exact
     saturation every generated interface uses and reads as one. These still
     clear WCAG AA comfortably on the dark page (7.2:1, 8.2:1, 6.4:1) while
     looking like colours somebody chose. */
  --ok: #6aa88c;
  --ok-soft: rgba(106, 168, 140, 0.12);
  --warn: #c9a05a;
  --warn-soft: rgba(201, 160, 90, 0.13);
  --danger: #d8776c;
  --danger-soft: rgba(216, 119, 108, 0.12);
  --info: #7fa3c9;
  --info-soft: rgba(127, 163, 201, 0.12);
  /* The text colour on an accent-filled surface: buttons, tags, selection.
     A variable rather than a literal because the moment a theme lightens
     --accent, dark-on-dark text is what a hardcoded #120a03 produces, and
     every primary button on the site becomes unreadable at once. */
  --on-accent: #120a03;
  /* The nav's own backdrop. Derived from --bg by hand rather than by
     colour-mix, which Safari only learned recently, so a theme that changes
     --bg does not leave a dark bar floating over a pale page. */
  --nav-bg: rgba(10, 10, 12, 0.72);
  --nav-bg-open: rgba(10, 10, 12, 0.96);
  --serif: 'Bricolage Grotesque', 'Bricolage Fallback', Arial, sans-serif;
  --sans: 'Space Grotesk', 'Grotesk Fallback', system-ui, -apple-system, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --max: 1120px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Tunable from the CMS. Defaults reproduce the look exactly, so an
     unconfigured site renders as it always has. */
  --hero-grayscale: 0.5;
  --hero-contrast: 1.05;
  --hero-brightness: 1.04;
  --hero-tint: var(--accent);
  /* The second hue-replacer, and the one that survived the first fix. It is
     the same `mix-blend-mode: color` as `.pb-tint` but scoped to the hero, so
     de-tinting the page background turned the whole site green-and-real
     EXCEPT the one screen everybody lands on, which stayed sepia and looked
     broken next to the rest. Both were set to match the palm loop this hero
     no longer has. Off, same as the page; the CMS field still works. */
  --hero-tint-opacity: 0;
  --hero-blend: color;
  --bg-tint: var(--accent);
  /* 0, and it used to be 0.45. `.pb-tint` blends with `mix-blend-mode: color`,
     which does not tint -- it REPLACES the hue of every pixel underneath. At
     0.45 over a photograph of foliage that threw away the photograph: no
     green, no depth, just an orange field with shapes in it, and the same
     orange as the accent, the buttons and the links. The page read as one
     colour because it very nearly was one colour.
     The photograph is the only real texture on the site. It is allowed to be
     its own colour now; the CMS field can still put the tint back. */
  --bg-tint-opacity: 0;
  /* Lifted with it. 0.72 of black was hiding what the tint had not already
     flattened, and was set when there was a hue wash to cut through. */
  --bg-scrim: 0.55;
  --bg-brightness: 0.8;
  /* 1 keeps the page photograph and the hero's video; 0 takes both off.
     Declared here rather than living only as a `var()` fallback so the default
     is a stated value somebody can find, and so the themes test — which checks
     that every allowlisted variable is actually consumed — has something to
     match. */
  --bg-media: 1;
}

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

html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; background: var(--bg); }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  font-family: var(--sans);
  background: transparent;
  color: var(--text);
  font-size: 16px;
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* ── Where the lines break ────────────────────────────────────────────────
   The platform already does this for its customers: `.pb-hero-title` and
   `.pb-hero-sub`, the blocks a customer's published page is built from, both
   carry `text-wrap: balance`. Javier's own headings did not — the same
   asymmetry as the server rendering, where customer pages were rendered for
   crawlers and /work and /resume were not. The builder was more carefully
   typeset than the site selling it.

   ── What each one is for, because they are not interchangeable ──
   `balance` evens the ragged edge across a small number of lines. It is for
   headings, and browsers cap it at a handful of lines precisely because it is
   expensive — applied to a paragraph it either does nothing or costs layout
   time for a result nobody reads as different.

   `pretty` is the paragraph one. It leaves the line count alone and works on
   the last line, so a paragraph does not end on a single orphaned word. That
   is the failure that actually shows on this site: a 62ch measure and a long
   compound noun leaves "infrastructure." alone on its own line.

   Both are ignored by a browser that does not know them, which is why this is
   four declarations rather than the 132 KB of line-breaking library already
   vendored in this repository. That library exists here for a job CSS cannot
   do at all — naming every line for an SVG export, in Japanese, with kinsoku
   rules — and shipping it to a marketing page to do what one property does
   natively would be the opposite of craft. */
.hero h1, .page-head h1, .section-head h2, h2, h3,
.price-card h3, .work-card h3, .faq-wrap h2 {
  text-wrap: balance;
}

.hero p, .page-head p, .section-head p, .faq-wrap p, .pricing-note,
.price-summary, .blurb {
  text-wrap: pretty;
}

/* ── Light mode ───────────────────────────────────────────────────────────
   A visitor's own choice, kept in their browser, stamped on <html> before the
   page paints. It is not the CMS theme and it is not the theme playground: it
   is the one switch a reader expects to find on a dark site at eleven at
   night, and the site did not have it.

   Only the palette is redefined. Everything downstream already reacts, because
   `markScheme()` reads the *resolved* --bg back out of the page and stamps
   `data-scheme="light"` from its luminance — so the rules further down this
   file that hide the photograph, drop the hero video and halve the grain on a
   light page all fire without knowing this block exists.

   Values, not guesses: --text on --bg is 15.9:1 and --muted is 5.2:1, both
   past AA. The accent darkens because #ff7a2f on white is 2.4:1 and unreadable
   as a link colour, which is the mistake every naive light mode makes. */
:root[data-mode="light"] {
  --bg: #f7f6f3;
  --bg-2: #ffffff;
  --surface: rgba(20, 20, 16, 0.035);
  --surface-2: rgba(20, 20, 16, 0.055);
  --border: rgba(20, 20, 16, 0.12);
  --border-hi: rgba(20, 20, 16, 0.24);
  --text: #16150f;
  --muted: #5d5c55;
  --dim: #7d7c74;
  --accent: #c2470d;
  --accent-soft: rgba(194, 71, 13, 0.10);
  --on-accent: #ffffff;
  /* ── The bar that stayed dark ──
     `#nav` is tokenised, and these two were the only tokens in the base
     palette still holding a literal dark value. So on a light page the links
     correctly turned dark and the bar behind them did not: dark text on a dark
     bar, with "Launcher", "Tools" and "Sign in" effectively invisible.
     Reported from a screenshot of /services, and the reason a palette should
     be checked against the token list rather than by eye. */
  --nav-bg: rgba(247, 246, 243, 0.78);
  --nav-bg-open: rgba(247, 246, 243, 0.97);
}

/* Plain and light together: the off-white accent that reads as considered on a
   dark page is invisible on a light one, so plain keeps its restraint by going
   dark instead of pale. Without this the primary button is off-white on
   off-white and disappears entirely. */
:root[data-mode="light"][data-plain] {
  --accent: #2e2c25;
  --accent-soft: rgba(46, 44, 37, 0.08);
  --on-accent: #f7f6f3;
}

/* ── The switch, as a sweep across the page ───────────────────────────────
   Same-document view transitions are cross-browser (Chrome 111, Firefox 133,
   Safari 18) and `:active-view-transition` is Baseline, so this is the native
   mechanism rather than a library.

   ── The constraint that decides the whole design ──
   During a transition the browser is not showing the page. It is showing two
   snapshots, `::view-transition-old(root)` and `::view-transition-new(root)`,
   in a layer above everything. So an animation applied to a real heading while
   the transition runs is invisible — the heading is not on screen, a picture
   of it is.

   The way to move something individually is to give it its own name, which
   takes it out of the root snapshot and into a pair of its own. That is what
   the `hw-*` names below are: up to twelve headings, each with its own
   snapshot, each starting a beat after the one to its left. The colour arrives
   as a circle opening from the button, and the headings lift and settle in its
   wake.

   Twelve because each name is a separate snapshot and a separate layer, and
   the point is a flourish rather than a slideshow. Only headings actually on
   screen are named. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) {
    /* The old page does not fade. It sits still and is covered, which is what
       makes the new colour read as arriving rather than as blending. */
    animation: none;
  }
  /* The cheap reveal, for a machine that told us it is modest. A `circle()`
     clip is the shape every theme toggle uses and the one browsers optimise;
     the polygon below is the same idea with forty-eight points and a repaint
     of the whole snapshot on every frame. */
  :root[data-vt-plain] ::view-transition-new(root) {
    animation: vt-circle .5s cubic-bezier(.4, 0, .2, 1) both;
  }
  @keyframes vt-circle {
    from { clip-path: circle(0% at var(--vt-x, 85%) var(--vt-y, 4%)); }
    to   { clip-path: circle(150% at var(--vt-x, 85%) var(--vt-y, 4%)); }
  }

  ::view-transition-new(root) {
    /* A wave rather than the usual circle. The circle from the button is
       the pattern every theme toggle uses; this arrives from the same place
       and crosses the whole page, which is the thing worth looking at. */
    animation: vt-wave .58s cubic-bezier(.36, 0, .18, 1) both;
    mix-blend-mode: normal;
  }

  /* Generated. 48 samples down the viewport, which is the number that
     matters: at 26 the segments between points were 30px of straight
     line against a 100px amplitude, and the "wave" was a zigzag. Every
     keyframe carries the same point count, because clip-path interpolates
     between polygons only when their lengths match — a mismatch does not
     error, it snaps, which reads as a bug rather than a choice.

     Two sine terms rather than one. A single frequency reads as the wave
     symbol on a weather app; the smaller second harmonic makes the crest
     uneven enough to read as water.

     The edge starts past the right of the screen and ends past the left,
     so the new theme arrives from the switch and crosses the page. */
  @keyframes vt-wave {
    0% { clip-path: polygon(122.00% 0.00%, 124.84% 2.08%, 127.20% 4.17%, 128.74% 6.25%, 129.29% 8.33%, 128.92% 10.42%, 127.86% 12.50%, 126.45% 14.58%, 125.02% 16.67%, 123.77% 18.75%, 122.77% 20.83%, 121.93% 22.92%, 121.06% 25.00%, 120.02% 27.08%, 118.73% 29.17%, 117.28% 31.25%, 115.91% 33.33%, 114.96% 35.42%, 114.74% 37.50%, 115.47% 39.58%, 117.17% 41.67%, 119.66% 43.75%, 122.53% 45.83%, 125.32% 47.92%, 127.55% 50.00%, 128.91% 52.08%, 129.29% 54.17%, 128.76% 56.25%, 127.62% 58.33%, 126.19% 60.42%, 124.78% 62.50%, 123.58% 64.58%, 122.61% 66.67%, 121.77% 68.75%, 120.89% 70.83%, 119.80% 72.92%, 118.47% 75.00%, 117.02% 77.08%, 115.70% 79.17%, 114.85% 81.25%, 114.79% 83.33%, 115.71% 85.42%, 117.58% 87.50%, 120.16% 89.58%, 123.06% 91.67%, 125.77% 93.75%, 127.87% 95.83%, 129.06% 97.92%, 129.25% 100.00%, 130% 100%, 130% 0%); }
    20% { clip-path: polygon(103.29% 0.00%, 103.63% 2.08%, 103.00% 4.17%, 101.68% 6.25%, 100.02% 8.33%, 98.36% 10.42%, 96.94% 12.50%, 95.84% 14.58%, 94.98% 16.67%, 94.18% 18.75%, 93.29% 20.83%, 92.21% 22.92%, 91.02% 25.00%, 89.92% 27.08%, 89.22% 29.17%, 89.23% 31.25%, 90.15% 33.33%, 91.98% 35.42%, 94.53% 37.50%, 97.41% 39.58%, 100.13% 41.67%, 102.24% 43.75%, 103.43% 45.83%, 103.58% 47.92%, 102.81% 50.00%, 101.40% 52.08%, 99.71% 54.17%, 98.08% 56.25%, 96.72% 58.33%, 95.67% 60.42%, 94.83% 62.50%, 94.03% 64.58%, 93.10% 66.67%, 92.00% 68.75%, 90.80% 70.83%, 89.75% 72.92%, 89.16% 75.00%, 89.33% 77.08%, 90.42% 79.17%, 92.40% 81.25%, 95.04% 83.33%, 97.93% 85.42%, 100.57% 87.50%, 102.53% 89.58%, 103.53% 91.67%, 103.50% 93.75%, 102.59% 95.83%, 101.10% 97.92%, 99.40% 100.00%, 130% 100%, 130% 0%); }
    40% { clip-path: polygon(73.38% 0.00%, 71.48% 2.08%, 69.62% 4.17%, 68.06% 6.25%, 66.90% 8.33%, 66.05% 10.42%, 65.36% 12.50%, 64.64% 14.58%, 63.80% 16.67%, 62.87% 18.75%, 62.05% 20.83%, 61.61% 22.92%, 61.83% 25.00%, 62.91% 27.08%, 64.84% 29.17%, 67.42% 31.25%, 70.26% 33.33%, 72.88% 35.42%, 74.84% 37.50%, 75.83% 39.58%, 75.75% 41.67%, 74.72% 43.75%, 73.05% 45.83%, 71.13% 47.92%, 69.31% 50.00%, 67.82% 52.08%, 66.72% 54.17%, 65.92% 56.25%, 65.24% 58.33%, 64.50% 60.42%, 63.64% 62.50%, 62.71% 64.58%, 61.93% 66.67%, 61.59% 68.75%, 61.96% 70.83%, 63.20% 72.92%, 65.27% 75.00%, 67.93% 77.08%, 70.77% 79.17%, 73.30% 81.25%, 75.10% 83.33%, 75.89% 85.42%, 75.62% 87.50%, 74.45% 89.58%, 72.71% 91.67%, 70.79% 93.75%, 69.01% 95.83%, 67.59% 97.92%, 66.56% 100.00%, 130% 100%, 130% 0%); }
    60% { clip-path: polygon(39.15% 0.00%, 37.95% 2.08%, 37.16% 4.17%, 36.61% 6.25%, 36.10% 8.33%, 35.51% 10.42%, 34.86% 12.50%, 34.31% 14.58%, 34.11% 16.67%, 34.53% 18.75%, 35.73% 20.83%, 37.73% 22.92%, 40.30% 25.00%, 43.07% 27.08%, 45.56% 29.17%, 47.34% 31.25%, 48.11% 33.33%, 47.78% 35.42%, 46.50% 37.50%, 44.59% 39.58%, 42.46% 41.67%, 40.48% 43.75%, 38.90% 45.83%, 37.78% 47.92%, 37.05% 50.00%, 36.52% 52.08%, 36.00% 54.17%, 35.39% 56.25%, 34.75% 58.33%, 34.24% 60.42%, 34.13% 62.50%, 34.68% 64.58%, 36.04% 66.67%, 38.16% 68.75%, 40.80% 70.83%, 43.56% 72.92%, 45.95% 75.00%, 47.56% 77.08%, 48.13% 79.17%, 47.61% 81.25%, 46.19% 83.33%, 44.21% 85.42%, 42.08% 87.50%, 40.16% 89.58%, 38.66% 91.67%, 37.63% 93.75%, 36.95% 95.83%, 36.43% 97.92%, 35.90% 100.00%, 130% 100%, 130% 0%); }
    80% { clip-path: polygon(7.95% 0.00%, 7.67% 2.08%, 7.35% 4.17%, 6.98% 6.25%, 6.69% 8.33%, 6.72% 10.42%, 7.30% 12.50%, 8.61% 14.58%, 10.63% 16.67%, 13.16% 18.75%, 15.82% 20.83%, 18.15% 22.92%, 19.73% 25.00%, 20.26% 27.08%, 19.69% 29.17%, 18.16% 31.25%, 16.03% 33.33%, 13.71% 35.42%, 11.60% 37.50%, 9.96% 39.58%, 8.87% 41.67%, 8.25% 43.75%, 7.89% 45.83%, 7.61% 47.92%, 7.28% 50.00%, 6.91% 52.08%, 6.66% 54.17%, 6.77% 56.25%, 7.49% 58.33%, 8.93% 60.42%, 11.06% 62.50%, 13.65% 64.58%, 16.28% 66.67%, 18.50% 68.75%, 19.91% 70.83%, 20.24% 72.92%, 19.47% 75.00%, 17.81% 77.08%, 15.61% 79.17%, 13.30% 81.25%, 11.26% 83.33%, 9.72% 85.42%, 8.73% 87.50%, 8.17% 89.58%, 7.84% 91.67%, 7.56% 93.75%, 7.21% 95.83%, 6.85% 97.92%, 6.65% 100.00%, 130% 100%, 130% 0%); }
    100% { clip-path: polygon(-22.78% 0.00%, -22.81% 2.08%, -22.58% 4.17%, -21.85% 6.25%, -20.48% 8.33%, -18.47% 10.42%, -16.02% 12.50%, -13.51% 14.58%, -11.36% 16.67%, -10.01% 18.75%, -9.71% 20.83%, -10.53% 22.92%, -12.29% 25.00%, -14.62% 27.08%, -17.10% 29.17%, -19.30% 31.25%, -20.96% 33.33%, -21.99% 35.42%, -22.48% 37.50%, -22.63% 39.58%, -22.65% 41.67%, -22.70% 43.75%, -22.79% 45.83%, -22.80% 47.92%, -22.49% 50.00%, -21.65% 52.08%, -20.16% 54.17%, -18.05% 56.25%, -15.56% 58.33%, -13.08% 60.42%, -11.05% 62.50%, -9.87% 64.58%, -9.78% 66.67%, -10.79% 68.75%, -12.68% 70.83%, -15.08% 72.92%, -17.53% 75.00%, -19.65% 77.08%, -21.20% 79.17%, -22.12% 81.25%, -22.53% 83.33%, -22.63% 85.42%, -22.66% 87.50%, -22.72% 89.58%, -22.81% 91.67%, -22.77% 93.75%, -22.38% 95.83%, -21.43% 97.92%, -19.82% 100.00%, 130% 100%, 130% 0%); }
  }

  /* The wake. Each named heading rises a few pixels and settles, delayed by
     its distance from the switch, so the movement reads as one thing crossing
     the page rather than twelve things twitching. */
  @keyframes hw-rise {
    from { transform: translateY(9px); opacity: 0; }
    to   { transform: none; opacity: 1; }
  }
  ::view-transition-old(hw-0), ::view-transition-old(hw-1),
  ::view-transition-old(hw-2), ::view-transition-old(hw-3),
  ::view-transition-old(hw-4), ::view-transition-old(hw-5),
  ::view-transition-old(hw-6), ::view-transition-old(hw-7),
  ::view-transition-old(hw-8), ::view-transition-old(hw-9),
  ::view-transition-old(hw-10), ::view-transition-old(hw-11) { animation: none; opacity: 0; }

  ::view-transition-new(hw-0)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .04s; }
  ::view-transition-new(hw-1)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .08s; }
  ::view-transition-new(hw-2)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .12s; }
  ::view-transition-new(hw-3)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .16s; }
  ::view-transition-new(hw-4)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .20s; }
  ::view-transition-new(hw-5)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .24s; }
  ::view-transition-new(hw-6)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .28s; }
  ::view-transition-new(hw-7)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .32s; }
  ::view-transition-new(hw-8)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .36s; }
  ::view-transition-new(hw-9)  { animation: hw-rise .42s var(--ease) both;  animation-delay: .40s; }
  ::view-transition-new(hw-10) { animation: hw-rise .42s var(--ease) both;  animation-delay: .44s; }
  ::view-transition-new(hw-11) { animation: hw-rise .42s var(--ease) both;  animation-delay: .48s; }
}

/* Somebody who asked for less motion gets the colour and none of the theatre.
   `startViewTransition` is skipped entirely rather than given a zero duration,
   so there is no snapshot and no layer work at all. */

/* A word lifted out of a heading for the length of the switch. `inline-block`
   so it is one box the browser can capture; the rest is inherited so the type
   is unchanged while it is in pieces. */
.vt-w { display: inline-block; will-change: transform; }

/* ── The switch itself ──
   A half moon: filled on the dark side, hollow on the light. One glyph that
   states the current mode rather than the one you would move to, which is the
   ambiguity every moon-and-sun pair has. */
.mode-btn {
  background: transparent; border: 1px solid var(--border); border-radius: 999px;
  color: var(--muted); cursor: pointer; width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; transition: color .15s var(--ease), border-color .15s var(--ease);
}
.mode-btn:hover { color: var(--text); border-color: var(--border-hi); }
.mode-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.mode-btn svg { width: 15px; height: 15px; display: block; }
li.nav-mode { display: flex; align-items: center; }

/* ── On a phone the switch sits beside the burger ──
   Inside the collapsed menu it is a control you have to open a menu to reach,
   which is the wrong shape for a two-state toggle. `#nav ul` is `display:none`
   at this width and a `position:fixed` child of a hidden parent is still not
   rendered, so the button is *moved* out of the list by the script rather than
   positioned out of it. This is what it looks like once it is there. */
@media (max-width: 780px) {
  /* Applied to the <li> holding the theme switch and the avocado, so the pair
     travels as one flex item. It used to go on the button itself, which was
     fine while there was only one. */
  .nav-mode-loose { margin-left: auto; margin-right: 10px; }
  li.nav-mode.nav-mode-loose { gap: 6px; }
}

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

/* ── Plain mode ───────────────────────────────────────────────────────────
   One switch in the CMS, and the site drops its photography, its video and
   its colour. Not a second stylesheet and not a second set of components:
   `--accent` is read 171 times through `var(--accent)`, so redefining it once
   here carries every button, badge, link and focus ring with it. That is the
   whole reason this is four rules rather than a theme.

   It exists because the decorated version is not right for every audience. A
   plants photograph behind the text and an orange call to action reads as a
   studio; some work wants to read as a practice. The content, the layout and
   the type do not change — only what is painted behind them.

   Stamped on <html> by applyTheme, so it also cannot be reached by a page
   that has not loaded the CMS. */
:root[data-plain] {
  /* A warm off-white, one step down from --text so a primary button still
     separates from body copy. Dark ink on it, which is what makes it read as
     considered rather than as a missing colour. */
  --accent: #dcd8cf;
  --accent-soft: rgba(220, 216, 207, 0.10);
  --on-accent: #16150f;
  /* The photograph's tint and scrim have nothing to tint. Zeroed rather than
     left set, so a stray value in the CMS cannot paint a haze over a page
     that is meant to be plain. */
  --bg-tint-opacity: 0;
  --bg-scrim: 0;
}

/* The grain is the one decorative thing that survives at half strength. It is
   what stops a large flat dark area reading as an unpainted div, and at 2% it
   is texture rather than decoration. */
:root[data-plain] body::after { opacity: 0.02; }

/* Belt and braces for the two elements applyTheme removes. If the script that
   removes them ever fails, or runs late, the page must not flash a photograph
   and a video at somebody who asked for neither. */
:root[data-plain] #page-bg,
:root[data-plain] #heroMedia { display: none !important; }

/* The hero borrows its height from the media it no longer has. Without this
   the opening screen is a tall empty box above the first line of text. */
:root[data-plain] .hero { min-height: 0; }

/* Film-grain / texture overlay (anti-"AI slop") */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

a { color: inherit; }
::selection { background: var(--accent); color: var(--on-accent); }

.wrap { max-width: var(--max); margin: 0 auto; padding: 0 28px; width: 100%; }

/* ── Payments are not real right now ─────────────────────────────────
   Only ever drawn when the server says the next payment would go to a
   sandbox — see renderPayMode() for why it is site-wide rather than inside
   the checkout, and why absence is read as live.

   Above the nav in the stacking order and above it on the page, because the
   nav is fixed too: a bar underneath would be covered by the thing it is
   trying to warn past. The root class pushes the nav down by the bar's own
   height rather than a number typed twice, so wrapping on a phone cannot
   leave the menu sitting on top of the warning. */
.pay-mode-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 300;
  background: var(--accent); color: var(--on-accent);
  font-family: var(--sans); font-size: 13.5px; line-height: 1.5;
  padding: 9px 20px; text-align: center;
}
.pay-mode-bar strong {
  font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; margin-right: 9px;
}
:root.pay-test #nav { top: var(--pay-bar-h, 40px); }
:root.pay-test { scroll-padding-top: calc(var(--pay-bar-h, 40px) + 70px); }
@media (max-width: 560px) {
  .pay-mode-bar { font-size: 12.5px; padding: 8px 14px; }
}

/* ── Nav ─────────────────────────────────────────────────────────── */
#nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 200;
  transition: background .3s var(--ease), border-color .3s var(--ease);
  border-bottom: 1px solid transparent;
}
#nav.scrolled, #nav.solid {
  background: var(--nav-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}
#nav .inner {
  max-width: var(--max); margin: auto;
  display: flex; align-items: center; justify-content: space-between;
  height: 66px; padding: 0 28px;
}
.brand {
  font-family: var(--mono);
  font-size: 13px; font-weight: 500;
  letter-spacing: 0.02em; text-decoration: none;
  display: inline-flex; align-items: center; gap: 9px;
}
.brand .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--live); box-shadow: 0 0 0 0 rgba(87, 227, 176, .5);
  animation: pulse 2.4s infinite;
}
@keyframes pulse {
  0% { box-shadow: 0 0 0 0 rgba(87, 227, 176, .45); }
  70% { box-shadow: 0 0 0 7px rgba(87, 227, 176, 0); }
  100% { box-shadow: 0 0 0 0 rgba(87, 227, 176, 0); }
}
/* ── Even spacing, and why the gap is small ──
   A dropdown is a `<button>` carrying `padding: 6px 9px`; a plain link was bare
   text. With a 30px gap that made the space beside a menu 30+9+9 and the space
   between two links 30 — so "Editor" and "Account" looked pushed apart while
   the rest sat close, which is exactly what it was.

   Every item gets the same box instead, and the gap comes down to match. The
   call to action keeps its own padding: it is a button and is meant to sit
   slightly apart from the links. */
#nav ul { list-style: none; display: flex; gap: 12px; align-items: center; }
#nav ul > li > a:not(.cta) { padding: 6px 9px; display: inline-block; }
#nav ul a {
  font-size: 13px; color: var(--muted); text-decoration: none;
  letter-spacing: .01em; transition: color .18s;
}
#nav ul a:hover, #nav ul a.active { color: var(--text); }
#nav ul a.active { position: relative; }
#nav ul a.cta {
  color: var(--text); border: 1px solid var(--border-hi);
  padding: 7px 15px; border-radius: 99px; transition: all .2s var(--ease);
}
#nav ul a.cta:hover { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.nav-toggle { display: none; background: none; border: 0; color: var(--text); cursor: pointer; }
@media (max-width: 780px) {
  #nav ul { display: none; }
  #nav ul.open {
    display: flex; flex-direction: column; position: absolute;
    top: 66px; left: 0; right: 0;
    /* Opaque, not translucent. `--nav-bg-open` is a 97% wash meant for a
       shallow dropdown over a desktop page; a full-height drawer over a phone
       leaves the page legible through the menu, and two documents on top of
       each other is hard to read even at a few per cent. */
    background: var(--bg-2);
    backdrop-filter: blur(14px); padding: 22px 28px; gap: 20px;
    border-bottom: 1px solid var(--border);
    /* ── The drawer has to be able to scroll ──
       It is absolutely positioned, so it does not grow the page: with Services,
       Launcher and Tools all expanded it is taller than a phone, and everything
       past the fold was simply unreachable — no scrollbar, nothing to drag,
       because the document behind it had not got any longer.

       `svh` rather than `vh`: on iOS Safari `vh` is the height with the address
       bar hidden, so a `100vh - 66px` menu is still taller than the screen at
       the exact moment somebody opens it. */
    /* ── The one line that makes the drawer a list ──
       `#nav ul` sets `align-items: center` for the horizontal bar, which is
       right there and wrong here: in a column the cross axis is the width, so
       every row shrank to its own text. The Services group is full width
       because it carries `.nav-menu` directly on the `li`; the three built by
       app.js wrap their contents in a div, so their `li` shrank and their
       sub-lists started at 114px, 115px and 154px — four little cards at four
       different left edges instead of one indented list. */
    align-items: stretch;
    max-height: calc(100svh - 66px);
    overflow-y: auto;
    /* Stops the page behind scrolling when the drawer reaches its end. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  .nav-toggle { display: block; }
}

main { flex: 1 0 auto; }

/* Page background (plants), tinted orange to match the hero. CMS-toggleable. */
#page-bg { position: fixed; inset: 0; z-index: -1; overflow: hidden; }
#page-bg .pb-img {
  position: absolute; inset: 0; background-size: cover; background-position: center;
  filter: brightness(var(--bg-brightness));
}
#page-bg .pb-tint { position: absolute; inset: 0; background: var(--bg-tint); mix-blend-mode: color; opacity: var(--bg-tint-opacity); display: none; }
#page-bg.tinted .pb-tint { display: block; } /* orange tint only on the home page */
#page-bg .pb-scrim { position: absolute; inset: 0; background: rgb(0 0 0 / var(--bg-scrim)); }

/* ── Hero (home) ─────────────────────────────────────────────────── */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex; align-items: center;
  padding: 120px 0 60px;
  overflow: hidden;
  /* Set from JS as the hero scrolls away. On the whole section rather than the
     video alone, so the headline, buttons and the dark gradient go with it and
     what shows through is the page background rather than a flat colour.
     Unset means fully opaque, so this looks right before any script runs. */
  opacity: var(--hero-fade, 1);
}
#edge-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  z-index: 0; display: block;
}
/* Once the palm gif's box, now a window.

   `background: var(--bg)` was correct while this held an opaque loop: the box
   was always full, so the solid fill never showed. With the loop gone it was
   painting flat page-colour over `#page-bg` for the whole height of the hero
   -- so the one screen everybody sees first was the one screen with no
   photograph behind it, and the 3D object floated in a void. Transparent, so
   the hero stands on the same background the rest of the page does. */
.hero-media {
  position: absolute; inset: 0; z-index: 0; overflow: hidden;
}
.hero-media img,
.hero-media video {
  width: 100%; height: 100%; object-fit: cover; object-position: center;
  filter: grayscale(var(--hero-grayscale)) contrast(var(--hero-contrast)) brightness(var(--hero-brightness));
}
.hero-media::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: var(--hero-tint); mix-blend-mode: var(--hero-blend); opacity: var(--hero-tint-opacity);
}
.hero::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    /* Neutral the whole way across. The last two stops were rgba(12,8,4) and
       rgba(20,10,4) -- warm browns, laid over an already-orange background, on
       top of an orange-hue-replaced photograph. Three warm layers multiplying
       into the muddy brown the hero had. The gradient is here to keep the
       headline readable over whatever is behind it, which is a job for black,
       not for a colour. */
    linear-gradient(100deg, rgba(10,10,12,0.94) 0%, rgba(10,10,12,0.80) 34%, rgba(10,10,12,0.52) 64%, rgba(10,10,12,0.26) 100%),
    linear-gradient(to bottom, rgba(10,10,12,0.35) 0%, transparent 30%, transparent 55%, var(--bg) 100%);
}
.hero .wrap { position: relative; z-index: 2; }

.eyebrow {
  font-family: var(--mono);
  font-size: 12px; letter-spacing: 0.06em;
  color: var(--accent);
  display: inline-flex; align-items: center; gap: 10px;
  margin-bottom: 26px;
}
.eyebrow::before { content: ""; width: 26px; height: 1px; background: var(--accent); opacity: .6; }
.hero h1 {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(2.7rem, 7.5vw, 5.6rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  /* 8.91em, not 15ch, and that is the fix for the headline dropping into
     place on load.

     `ch` is the advance width of the font's "0" glyph, so a box measured in it
     RESIZES when the webfont arrives: Bricolage's zero is 0.594em, the
     fallback's is not, so 15ch was 804px with the real font and 683px without
     it. The headline rewrapped, the block changed height, and everything under
     it moved -- which reads as the title dropping, because that is what it did.

     `em` depends on font-size alone and nothing about the glyphs, so the box is
     the same width before and after the swap. 8.91em is what 15ch measured to
     in Bricolage, so the line breaks are unchanged from the design.

     Metric-matched fallbacks cannot fix this on their own: `size-adjust` can
     match a font's average width or its "0" glyph, not both, and `ch` only
     cares about the second. They are still worth having for the text inside
     the box -- see the @font-face pair at the top of this file. */
  max-width: 8.91em;
  margin-bottom: 26px;
}
.hero h1 .rot {
  font-style: italic; color: var(--accent); display: inline-block;
  transition: opacity .5s var(--ease), filter .5s var(--ease), transform .5s var(--ease);
}
.hero h1 .rot.swap { opacity: 0; filter: blur(6px); transform: translateY(6px); }
.hero .lead {
  font-size: clamp(1rem, 1.5vw, 1.18rem);
  color: var(--muted);
  /* Same reasoning as the headline above. Space Grotesk's zero is 0.641em, so
     56ch is 35.89em -- and unlike the headline this one sits directly under it,
     so the two were moving together. */
  max-width: 35.89em; line-height: 1.72;
  margin-bottom: 34px;
}
.hero-actions { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-start; }
/* flex-basis:100% forces the notes onto their own line, so the buttons above
   stay side by side instead of being pushed apart by the note's width. */
/* flex:0 0 100% rather than flex-basis alone: with the default shrink factor
   the row squeezes back onto the buttons' line instead of wrapping. The width
   cap lives on the child so it cannot affect the line break. */
.hero-note-row {
  flex: 0 0 100%; display: flex; flex-direction: column;
  align-items: flex-start; gap: 8px; margin-top: 2px;
}
.hero-note-row .hero-pay-note { max-width: 46ch; }
.hero-pay { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 9px; max-width: 330px; }
.hero-pay-note {
  font-size: 12px; line-height: 1.55; color: var(--muted);
}
.hero-pay .pay-cards { height: 22px; width: auto; opacity: .85; align-self: flex-start; }

.btn {
  font-family: var(--sans); font-size: 14px; font-weight: 500;
  text-decoration: none; padding: 13px 26px; border-radius: 99px;
  display: inline-flex; align-items: center; gap: 9px;
  transition: all .22s var(--ease); cursor: pointer; border: 1px solid transparent;
  background: transparent; /* reset the UA default on <button> */
  -webkit-appearance: none; appearance: none;
}
.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 30px -8px rgba(255,122,47,.5); }
.btn-ghost { background: transparent; border-color: var(--border-hi); color: var(--text); }
.btn-ghost:hover { border-color: var(--accent); background: var(--surface-2); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ── Subpage header ──────────────────────────────────────────────── */
.page-head {
  position: relative;
  padding: 140px 0 40px;
  border-bottom: 1px solid var(--border);
}
.page-head h1 {
  font-family: var(--serif); font-weight: 300;
  font-size: clamp(2.2rem, 5.5vw, 4rem); line-height: 1.05;
  letter-spacing: -0.02em; margin-bottom: 16px; max-width: 18ch;
}
.page-head p { color: var(--muted); max-width: 62ch; }
.back-link {
  font-family: var(--mono); font-size: 12px; color: var(--dim);
  text-decoration: none; display: inline-flex; align-items: center; gap: 7px;
  margin-bottom: 22px; transition: color .18s;
}
.back-link:hover { color: var(--accent); }

/* ── Section scaffolding ─────────────────────────────────────────── */
section.block { padding: 90px 0; position: relative; }
.section-head { margin-bottom: 48px; max-width: 760px; }
.section-kicker {
  font-family: var(--mono); font-size: 12px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent);
  display: flex; align-items: center; gap: 12px; margin-bottom: 18px;
}
.section-kicker span.num { color: var(--dim); }
.cc-trigger { cursor: pointer; transition: opacity .16s; }
.cc-trigger:hover { opacity: 0.7; }
.section-head h2 {
  font-family: var(--serif); font-weight: 300;
  font-size: clamp(1.9rem, 4vw, 3rem); line-height: 1.08; letter-spacing: -0.02em;
}
.section-head p { color: var(--muted); margin-top: 16px; max-width: 60ch; }

/* ── Work ────────────────────────────────────────────────────────── */
.work-list { display: flex; flex-direction: column; gap: 22px; }
.work-card {
  border: 1px solid var(--border); border-radius: 16px;
  background: var(--surface); padding: 34px;
  transition: border-color .3s var(--ease), background .3s var(--ease);
}
.work-card:hover { border-color: var(--border-hi); background: var(--surface-2); }
.work-card.flagship {
  background: radial-gradient(120% 140% at 100% 0%, var(--accent-soft) 0%, transparent 55%), var(--surface);
  border-color: rgba(255,122,47,.28);
}
.work-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; flex-wrap: wrap; margin-bottom: 16px; }
.work-top .title-wrap { display: flex; flex-direction: column; gap: 5px; }
.flag {
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent);
  border: 1px solid rgba(255,122,47,.35); border-radius: 99px;
  padding: 3px 10px; align-self: flex-start; margin-bottom: 4px;
}
.work-card h3 { font-family: var(--serif); font-weight: 500; font-size: 1.7rem; letter-spacing: -.01em; }
.work-card .subtitle { font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
.work-card .year { font-family: var(--mono); font-size: 12px; color: var(--dim); white-space: nowrap; }
.work-card .blurb { color: var(--muted); max-width: 68ch; margin-bottom: 20px; }
.work-card ul.hl { list-style: none; display: flex; flex-direction: column; gap: 9px; margin-bottom: 22px; }
.work-card ul.hl li { position: relative; padding-left: 24px; font-size: 14.5px; color: var(--muted); line-height: 1.6; }
.work-card ul.hl li::before { content: "→"; position: absolute; left: 0; top: 0; color: var(--accent); font-family: var(--mono); }
.work-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; justify-content: space-between; }
.tags { display: flex; flex-wrap: wrap; gap: 7px; }
.tag {
  font-family: var(--mono); font-size: 11px; color: var(--muted);
  background: var(--surface-2); border: 1px solid var(--border);
  padding: 4px 10px; border-radius: 6px;
}
.work-links { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; }
.work-links a {
  font-size: 13.5px; color: var(--text); text-decoration: none;
  display: inline-flex; align-items: center; gap: 7px;
  border-bottom: 1px solid var(--border-hi); padding-bottom: 2px; transition: color .18s, border-color .18s;
}
.work-links a:hover { color: var(--accent); border-color: var(--accent); }
.value-pill {
  font-family: var(--mono); font-size: 11.5px; color: var(--live);
  border: 1px solid rgba(87,227,176,.3); border-radius: 99px; padding: 5px 12px;
}

/* ── Launcher ────────────────────────────────────────────────────── */
.launch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.launch-item {
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface); overflow: hidden;
  text-decoration: none; transition: all .25s var(--ease); position: relative;
}
.launch-item:hover { border-color: var(--accent); background: var(--surface-2); transform: translateY(-3px); }
.launch-shot {
  display: block; aspect-ratio: 16 / 10; overflow: hidden;
  background: var(--surface-2); border-bottom: 1px solid var(--border); position: relative;
}
.launch-shot img {
  width: 100%; height: 100%; object-fit: cover; object-position: top center;
  display: block; transition: transform .5s var(--ease);
}
.launch-item:hover .launch-shot img { transform: scale(1.04); }
.launch-shot .ph-dom { display: none; }
.launch-shot.ph {
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, rgba(255,122,47,.16), rgba(255,122,47,.03));
}
.launch-shot.ph img { display: none; }
.launch-shot.ph .ph-dom {
  display: block; font-family: var(--mono); font-size: 15px; color: var(--accent);
  letter-spacing: .02em; padding: 0 14px; text-align: center; word-break: break-word;
}
.launch-body { display: flex; flex-direction: column; gap: 7px; padding: 16px 20px; }
.launch-item.nopreview .launch-body { padding: 20px 22px; }
.launch-item .cat { font-family: var(--mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--dim); }
.launch-item .name {
  font-size: 16px; font-weight: 500; color: var(--text);
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.launch-item .name .arw { color: var(--accent); transition: transform .25s var(--ease); }
.launch-item:hover .name .arw { transform: translate(3px, -3px); }
.launch-item .note { font-size: 13px; color: var(--muted); }

/* ── Capabilities ────────────────────────────────────────────────── */
.caps { display: flex; flex-wrap: wrap; gap: 10px; }
.cap {
  font-family: var(--sans); font-size: 14px; color: var(--text);
  border: 1px solid var(--border); border-radius: 99px; padding: 9px 18px;
  background: var(--surface); transition: all .22s var(--ease);
  cursor: pointer; display: inline-flex; align-items: center; gap: 9px;
}
.cap:hover { border-color: var(--accent); color: var(--accent); }
.cap:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.cap-plus {
  width: 9px; height: 9px; position: relative; flex: none; opacity: .55;
  transition: transform .25s var(--ease), opacity .25s var(--ease);
}
.cap-plus::before, .cap-plus::after {
  content: ""; position: absolute; background: currentColor; border-radius: 2px;
}
.cap-plus::before { left: 0; right: 0; top: 4px; height: 1.5px; }
.cap-plus::after { top: 0; bottom: 0; left: 4px; width: 1.5px; transition: opacity .25s var(--ease); }
.cap:hover .cap-plus { opacity: 1; }
.cap.active {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 500;
}
.cap.active .cap-plus { transform: rotate(90deg); opacity: 1; }
.cap.active .cap-plus::after { opacity: 0; }

/* ── The capabilities as a document ───────────────────────────────────────
   What the server renders into #caps, and what a reader with no JavaScript
   keeps. `renderCaps()` replaces it with the pills above the moment app.js
   runs, so on a normal visit this is on screen for a few hundred milliseconds
   and has to look deliberate for those -- a flash of unstyled paragraphs is
   worse than the empty div it replaced.

   It shares the `.caps` flex container with the pills rather than needing its
   own, so the cards tile into the same row and the swap does not move the rest
   of the page. */
.cap-doc {
  flex: 1 1 300px; min-width: 0;
  border: 1px solid var(--border); border-radius: 14px;
  background: var(--surface); padding: 18px 20px;
}
.cap-doc-title {
  font-family: var(--serif); font-size: 1.05rem; font-weight: 500;
  letter-spacing: -.01em; margin: 0 0 8px;
}
.cap-doc-body {
  color: var(--muted); font-size: 14px; line-height: 1.7; margin: 0;
}

.cap-panel {
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height .32s var(--ease), opacity .28s var(--ease), margin-top .32s var(--ease);
  border-left: 2px solid var(--accent);
  margin-top: 0; padding-left: 0;
}
/* The height is a starting point only: renderCaps() measures the text and
   sets its own ceiling inline, because one number cannot hold both a line of
   desktop prose and the same words wrapped three times on a phone. This is
   what shows if that measurement never runs. */
.cap-panel.open {
  max-height: 460px; opacity: 1; margin-top: 22px; padding: 4px 0 4px 20px;
}
.cap-panel-title {
  font-family: var(--serif); font-size: 1.25rem; font-weight: 500;
  letter-spacing: -.01em; margin-bottom: 8px;
}
.cap-panel-body { color: var(--muted); max-width: 68ch; font-size: 15px; line-height: 1.75; }
.cap-panel-close {
  margin-top: 14px; background: none; border: 0; cursor: pointer;
  font-family: var(--mono); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--dim); transition: color .16s;
}
.cap-panel-close:hover { color: var(--accent); }
@media (prefers-reduced-motion: reduce) {
  .cap-panel { transition: none; }
}
/* Under the pill that opened it, rather than under the whole group — see the
   note in renderCaps() for why. The panel is moved into the pill row itself,
   so it has to claim a whole line of the flex container; it is moved back out
   as soon as it closes, which is why the closed state needs nothing here. */
@media (max-width: 780px) {
  .caps > .cap-panel { flex: 0 0 100%; width: 100%; }
  .caps > .cap-panel.open { margin-top: 6px; margin-bottom: 6px; }
}

/* ── How payments work (/payments) ───────────────────────────────────
   A table is the right shape here and nearly the only one on the site, so the
   rules are local rather than a global `table` style that would then have to
   be undone everywhere else.

   The wrapper scrolls rather than the page: four columns and a notes column
   do not fit a phone, and a table that widens the document breaks every other
   section on it. */
.pay-table-wrap { overflow-x: auto; margin: 22px 0 28px; -webkit-overflow-scrolling: touch; }
.pay-table {
  width: 100%; min-width: 540px; border-collapse: collapse;
  font-size: 15px; line-height: 1.6;
}
.pay-table caption {
  caption-side: bottom; text-align: left; color: var(--dim);
  font-size: 13px; line-height: 1.6; padding-top: 12px;
}
.pay-table th, .pay-table td {
  text-align: left; vertical-align: top; padding: 11px 14px 11px 0;
  border-bottom: 1px solid var(--border);
}
.pay-table thead th {
  font-family: var(--mono); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--dim); font-weight: 400;
}
.pay-table tbody th { font-weight: 500; white-space: nowrap; }
/* Named rather than "the last cell in the row". The notes column stopped
   being last the moment one of them spanned two rows: `td:last-child` then
   matched Affirm's *availability* cell instead, and quietly greyed out the one
   word on the table that has to stand out. */
.pay-table td.pay-note { color: var(--muted); font-size: 14px; }
/* Said in words as well as in colour, because a red cell that reads "No" to
   somebody who can see it reads as nothing at all to somebody who cannot. */
.pay-no { color: var(--accent); font-weight: 500; }
.pay-note-cell { border-bottom: 0; }

/* Spacing this page and not /faq, which shares `.faq-wrap`. The global reset
   zeroes every margin and nothing puts one back for body copy, which the FAQ
   gets away with because its paragraphs come in ones and twos under a heading.
   This page runs several together and needs the air. Scoped rather than added
   to `.faq-wrap`, because changing how an existing page reads is not part of
   adding a new one. */
.pay-wrap > p + p { margin-top: 14px; }
.pay-wrap > h2 { margin-top: 42px; }
.pay-wrap > h2:first-child { margin-top: 0; }

.pay-live { color: var(--muted); }
.pay-demo { margin: 22px 0 4px; }
.pay-demo-form { margin-top: 18px; max-width: 520px; }
.pay-demo-note, .pay-demo-err {
  color: var(--muted); font-size: 14px; line-height: 1.7; margin-top: 12px; max-width: 60ch;
}
.pay-demo-err { color: var(--accent); }

/* ── Pricing ─────────────────────────────────────────────────────── */
.pricing-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); gap: 18px;
}
.price-card {
  position: relative; display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: 16px;
  background: var(--surface); padding: 28px 26px;
  transition: border-color .28s var(--ease), background .28s var(--ease), transform .28s var(--ease);
}
.price-card:hover { border-color: var(--border-hi); background: var(--surface-2); transform: translateY(-3px); }
.price-card.featured {
  border-color: rgba(255, 122, 47, .38);
  background: radial-gradient(120% 130% at 100% 0%, var(--accent-soft) 0%, transparent 58%), var(--surface);
}
.price-flag {
  position: absolute; top: -10px; left: 26px;
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--on-accent); background: var(--accent); border-radius: 99px; padding: 3px 11px;
}
.price-card h3 { font-family: var(--serif); font-weight: 500; font-size: 1.35rem; letter-spacing: -.01em; }
.price-amount {
  font-family: var(--serif); font-size: 2.1rem; font-weight: 500;
  color: var(--accent); margin: 10px 0 4px; line-height: 1;
}
.price-unit { font-family: var(--mono); font-size: 11.5px; color: var(--dim); letter-spacing: .04em; }
.price-summary { color: var(--muted); font-size: 14px; margin-bottom: 18px; }
.price-feats { list-style: none; display: flex; flex-direction: column; gap: 9px; margin-bottom: 22px; }
.price-feats li {
  position: relative; padding-left: 22px; font-size: 13.8px; color: var(--muted); line-height: 1.6;
}
.price-feats li::before {
  content: ""; position: absolute; left: 0; top: 8px;
  width: 7px; height: 7px; border-radius: 50%; border: 1.5px solid var(--accent);
}
.price-cta { margin-top: auto; justify-content: center; width: 100%; font-size: 13.5px; padding: 11px 20px; }

/* ── Collapsed pricing cards ──────────────────────────────────────────
   Enough in the closed state to choose from — name, price, one line — and
   the rest a click away. */
.price-groups { display: flex; flex-direction: column; gap: 42px; }
.price-group-label {
  font-family: var(--serif); font-weight: 500; font-size: 1.15rem;
  letter-spacing: -.01em; color: var(--text); margin-bottom: 16px;
  padding-bottom: 11px; border-bottom: 1px solid var(--border);
}
/* Closed cards are short, so they may sit wider without looking sparse.
   align-items:start matters more than it looks: grid rows stretch to their
   tallest item by default, so opening one card would inflate the two beside
   it into tall empty boxes. */
.pricing-grid.is-collapsed {
  grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
  align-items: start;
}
.price-card.pc-collapsible { padding: 0; overflow: hidden; }
.price-card.pc-collapsible:hover { transform: none; }   /* a list should not bounce */
.price-head {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 26px 24px; cursor: pointer; user-select: none;
}
.price-head-main { flex: 1; min-width: 0; }
.price-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; border-radius: 15px; }
.price-head .price-summary { margin-bottom: 0; }
.price-chev {
  flex: none; margin-top: 4px; width: 11px; height: 11px;
  border-right: 1.7px solid var(--dim); border-bottom: 1.7px solid var(--dim);
  transform: rotate(45deg); transition: transform .25s var(--ease), border-color .25s var(--ease);
}
.price-card.is-open .price-chev { transform: rotate(-135deg); border-color: var(--accent); }
.price-head:hover .price-chev { border-color: var(--accent); }
.price-body { padding: 0 24px 26px; }
.price-body:not([hidden]) { animation: priceReveal .22s var(--ease); }
@keyframes priceReveal {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .price-body:not([hidden]) { animation: none; }
  .price-chev { transition: none; }
}
/* The flag sits on the card edge, which now starts at the header. */
.price-card.pc-collapsible .price-flag { top: -10px; left: 24px; }
.price-card.pc-collapsible .price-cta { margin-top: 0; }
/* Care plans use the same pattern, so they inherit the card rules and only
   need the grid told not to stretch its rows. */
.care-grid.is-collapsed { align-items: start; }
.care-card.pc-collapsible .care-tagline,
.care-card.pc-collapsible .care-who { margin-bottom: 0; }
.care-card.pc-collapsible .price-head-main > * + * { margin-top: 8px; }
.price-pay {
  margin-top: 9px; justify-content: center; width: 100%;
  font-size: 13.5px; padding: 11px 20px;
}
.pay-hosted { margin-top: 10px; min-height: 46px; }
.pay-hosted:empty { min-height: 0; }
.price-pay-note {
  display: block; margin-top: 8px; font-size: 11.5px; line-height: 1.5;
  color: var(--dim); text-align: center;
}
/* Not fine print. A refund disclosure nobody can read does not count as one,
   and this is what a card network looks at when a payment is disputed. */
.pay-refund {
  display: block; margin-top: 8px; font-size: 11.75px; line-height: 1.55;
  color: var(--dim); opacity: .92; text-align: center;
}
.pay-refund a { color: var(--accent); text-decoration: none; white-space: nowrap; }
.pay-refund a:hover, .pay-refund a:focus-visible { text-decoration: underline; }
.pricing-note {
  margin-top: 26px; font-size: 13px; color: var(--dim); max-width: 70ch;
  border-left: 2px solid var(--border); padding-left: 16px;
}

/* ── Care plans ──────────────────────────────────────────────────── */
.care-block { border-top: 1px solid var(--border); }
.care-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); gap: 18px; align-items: start; }
.care-card {
  position: relative; display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: 16px;
  background: var(--surface); padding: 30px 26px;
  transition: border-color .28s var(--ease), background .28s var(--ease), transform .28s var(--ease);
}
.care-card:hover { border-color: var(--border-hi); background: var(--surface-2); transform: translateY(-3px); }
.care-card.featured {
  border-color: rgba(255, 122, 47, .45);
  background: radial-gradient(130% 120% at 50% 0%, var(--accent-soft) 0%, transparent 60%), var(--surface);
  box-shadow: 0 18px 50px -26px rgba(255, 122, 47, .5);
}
.care-card h3 { font-family: var(--serif); font-weight: 500; font-size: 1.5rem; letter-spacing: -.01em; }
.care-tagline {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--accent); margin-top: 6px;
}
.care-who {
  font-size: 13.5px; color: var(--muted); line-height: 1.65;
  padding-bottom: 16px; margin-bottom: 16px; border-bottom: 1px solid var(--border);
}
.care-inherits {
  font-family: var(--mono); font-size: 11px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--text); margin-bottom: 14px;
}
.care-card .price-feats { margin-bottom: 24px; }

/* ── What the client provides ────────────────────────────────────── */
.provides-block { border-top: 1px solid var(--border); }
.provides-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
.provide-item {
  border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
  padding: 22px 24px; transition: border-color .25s var(--ease), background .25s var(--ease);
}
.provide-item:hover { border-color: var(--border-hi); background: var(--surface-2); }
.provide-item h3 {
  font-size: 15px; font-weight: 600; margin-bottom: 8px;
  display: flex; align-items: center; gap: 9px;
}
.provide-item h3::before {
  content: ""; width: 7px; height: 7px; border-radius: 2px;
  background: var(--accent); flex: none;
}
.provide-item p { color: var(--muted); font-size: 13.8px; line-height: 1.7; }

/* ── PayPal subscription block ───────────────────────────────────── */
.pay-block { margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border); }
.pay-terms {
  font-size: 12.5px; line-height: 1.6; color: var(--muted); margin-bottom: 12px;
}
.pay-consent {
  display: flex; align-items: flex-start; gap: 9px; cursor: pointer;
  font-size: 12px; line-height: 1.55; color: var(--muted); margin-bottom: 14px;
  border-radius: 8px; padding: 2px;
}
.pay-consent input {
  margin-top: 2px; flex: none; width: 15px; height: 15px;
  accent-color: var(--accent); cursor: pointer;
}
.pay-consent a { color: var(--accent); }
.pay-consent.shake { animation: payshake .5s; }
@keyframes payshake {
  0%,100% { transform: translateX(0); }
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}
@media (prefers-reduced-motion: reduce) { .pay-consent.shake { animation: none; outline: 2px solid var(--accent); } }
.pay-btn { opacity: .45; filter: grayscale(1); transition: opacity .25s var(--ease), filter .25s var(--ease); }
.pay-btn.ready { opacity: 1; filter: none; }

/* ── Consultation & hourly rates ─────────────────────────────────── */
.rates-block { border-top: 1px solid var(--border); }
.rates-list { display: flex; flex-direction: column; }
.rate-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 28px;
  padding: 24px 0; border-top: 1px solid var(--border);
  transition: border-color .25s var(--ease);
}
.rate-row:first-child { border-top: 0; padding-top: 4px; }
.rate-row:hover { border-color: var(--border-hi); }
.rate-main h3 { font-size: 16.5px; font-weight: 600; margin-bottom: 6px; }
.rate-main p { color: var(--muted); font-size: 14px; line-height: 1.7; max-width: 62ch; }
.rate-price {
  font-family: var(--serif); font-size: 1.7rem; font-weight: 500; color: var(--accent);
  line-height: 1; white-space: nowrap; text-align: right; flex: none;
  display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
}
.rate-unit {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--dim);
}
@media (max-width: 560px) {
  .rate-row { flex-direction: column; gap: 12px; }
  .rate-price { align-items: flex-start; text-align: left; flex-direction: row; align-items: baseline; gap: 10px; }
}

/* ── Useful links ────────────────────────────────────────────────── */
.useful-block { border-top: 1px solid var(--border); }
.useful-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
.useful-item {
  display: flex; flex-direction: column; gap: 5px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); padding: 16px 18px; text-decoration: none;
  transition: all .22s var(--ease);
}
.useful-item:hover { border-color: var(--accent); background: var(--surface-2); transform: translateY(-2px); }
.useful-name {
  font-size: 14.5px; font-weight: 500; color: var(--text);
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.useful-name .arw { color: var(--accent); transition: transform .22s var(--ease); }
.useful-item:hover .useful-name .arw { transform: translate(3px, -3px); }
.useful-note { font-size: 12.5px; color: var(--muted); }

/* ── Legal pages ─────────────────────────────────────────────────── */
.legal-wrap { max-width: 820px; }
.legal-toc {
  display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 48px;
  padding-bottom: 26px; border-bottom: 1px solid var(--border);
}
.legal-toc a {
  font-family: var(--mono); font-size: 12px; color: var(--muted); text-decoration: none;
  border: 1px solid var(--border); border-radius: 99px; padding: 6px 14px;
  transition: all .2s var(--ease);
}
.legal-toc a:hover { border-color: var(--accent); color: var(--accent); }
.legal-sec { margin-bottom: 60px; scroll-margin-top: 90px; }
.legal-sec h2 {
  font-family: var(--serif); font-weight: 300; font-size: clamp(1.6rem, 3vw, 2.2rem);
  letter-spacing: -.02em; margin-bottom: 18px; padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.legal-sec h3 {
  font-size: 15px; font-weight: 600; color: var(--text);
  margin: 28px 0 10px; letter-spacing: .01em;
}
.legal-sec p { color: var(--muted); margin-bottom: 14px; line-height: 1.78; font-size: 15px; }
.legal-sec ul { list-style: none; display: flex; flex-direction: column; gap: 9px; margin: 0 0 16px; }
.legal-sec li {
  position: relative; padding-left: 20px; color: var(--muted); font-size: 14.5px; line-height: 1.7;
}
.legal-sec li::before { content: ""; position: absolute; left: 0; top: 9px; width: 6px; height: 1.5px; background: var(--accent); }
.legal-sec a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.legal-sec strong { color: var(--text); font-weight: 600; }
.legal-sec code {
  font-family: var(--mono); font-size: 12.5px; background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; color: var(--text);
}

/* ── Footer social ───────────────────────────────────────────────── */
.footer-social { display: flex; flex-wrap: wrap; gap: 14px; justify-content: center; }
.footer-social a {
  display: inline-flex; align-items: center; gap: 7px; color: var(--muted);
  text-decoration: none; transition: color .18s;
}
.footer-social a:hover { color: var(--accent); }
.footer-social svg { width: 17px; height: 17px; flex: none; }
.footer-legal { color: var(--muted); text-decoration: none; transition: color .18s; }
.footer-legal:hover { color: var(--accent); }

/* ── Résumé ──────────────────────────────────────────────────────── */
.resume-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 46px; }
@media (max-width: 820px) { .resume-grid { grid-template-columns: 1fr; gap: 40px; } }
.col-label { font-family: var(--mono); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); margin-bottom: 24px; }
.job { padding: 22px 0; border-top: 1px solid var(--border); }
.job:first-of-type { border-top: 0; padding-top: 0; }
.job-head { display: flex; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 8px; }
.job-head h4 { font-size: 16px; font-weight: 600; }
.job-head .org { font-size: 13px; color: var(--accent); }
.job-head .date { font-family: var(--mono); font-size: 11.5px; color: var(--dim); white-space: nowrap; }
.job ul { list-style: none; display: flex; flex-direction: column; gap: 6px; margin: 10px 0; }
.job ul li { font-size: 14px; color: var(--muted); padding-left: 16px; position: relative; }
.job ul li::before { content: "·"; position: absolute; left: 3px; color: var(--accent); }
.vol-item { padding: 16px 0; border-top: 1px solid var(--border); }
.vol-item:first-of-type { border-top: 0; padding-top: 0; }
.vol-item .vt { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
.vol-item .vd { font-size: 13.5px; color: var(--muted); }
.edu-card { border: 1px solid var(--border); border-radius: 12px; padding: 20px; background: var(--surface); margin-bottom: 30px; }

/* ── Contact ─────────────────────────────────────────────────────── */
.contact-block { text-align: center; padding: 96px 0 90px; }
.contact-block h2 { font-family: var(--serif); font-weight: 300; font-size: clamp(2.2rem, 5vw, 3.6rem); letter-spacing: -.02em; margin-bottom: 20px; }
.contact-block p { color: var(--muted); max-width: 48ch; margin: 0 auto 34px; }
.rotword {
  color: var(--accent); font-style: italic; display: inline-block;
  transition: opacity .4s var(--ease), transform .4s var(--ease), filter .4s var(--ease);
}
.rotword.swap { opacity: 0; transform: translateY(6px); filter: blur(5px); }
.contact-row { display: flex; flex-wrap: wrap; gap: 14px 26px; justify-content: center; margin-top: 10px; }
.contact-row a { display: inline-flex; align-items: center; gap: 9px; font-size: 14px; color: var(--muted); text-decoration: none; transition: color .18s; }
.contact-row a:hover { color: var(--accent); }
.contact-row svg { width: 17px; height: 17px; flex: none; }
/* The one contact route that does not send the reader out to another app. */
.contact-cta { margin-top: 26px; }

/* The analytics opt-out on /legal. Rendered by assets/track.js rather than
   written into the CMS copy, because it is a control rather than prose and it
   has to exist even when the tracker itself has switched itself off. */
.optout { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border); }
.optout-note { color: var(--muted); font-size: 13px; line-height: 1.6; margin-top: 10px; max-width: 60ch; }

.resume-link {
  display: inline-block; margin-top: 28px; font-size: 14px; color: var(--muted);
  text-decoration: none; transition: color .16s;
}
.resume-link:hover { color: var(--accent); }

footer {
  flex-shrink: 0;
  border-top: 1px solid var(--border); text-align: center;
  padding: 30px 28px; font-family: var(--mono); font-size: 12px; color: var(--dim);
}
.site-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px 18px; }
.footer-link {
  color: var(--accent); text-decoration: none; font-weight: 500;
  border: 1px solid rgba(255, 122, 47, 0.4); border-radius: 99px; padding: 4px 14px;
  transition: all .18s var(--ease);
}
.footer-link:hover { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }

/* ── Reveal animation (JS-gated, reduced-motion safe) ────────────── */
.js .reveal { opacity: 0; transform: translateY(22px); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.js .reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .js .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
  .hero h1 .rot.swap { opacity: 1; filter: none; transform: none; }
  .brand .dot { animation: none; }
}

/* ── Contact modal (vCard QR) ────────────────────────────────────── */
.modal-overlay {
  position: fixed; inset: 0; z-index: 500;
  display: flex; align-items: flex-start; justify-content: center;
  padding: 24px; background: rgba(5, 5, 7, 0.72);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  opacity: 0; pointer-events: none; transition: opacity .25s var(--ease);
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
.modal-overlay.open { opacity: 1; pointer-events: auto; }
.contact-card {
  background: var(--bg-2); border: 1px solid var(--border-hi); border-radius: 18px;
  padding: 34px 30px 30px; max-width: 360px; width: 100%; text-align: center; position: relative;
  margin: auto;
  transform: translateY(12px) scale(.98); transition: transform .25s var(--ease);
  box-shadow: 0 30px 80px -20px rgba(0, 0, 0, .6);
}
.modal-overlay.open .contact-card { transform: none; }
.cc-close {
  position: absolute; top: 12px; right: 12px; width: 32px; height: 32px; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--muted);
  cursor: pointer; font-size: 12px; transition: all .18s var(--ease);
}
.cc-close:hover { color: var(--text); border-color: var(--border-hi); }
.cc-name { font-family: var(--serif); font-size: 1.6rem; font-weight: 500; letter-spacing: -.01em; }
.cc-role { font-family: var(--mono); font-size: 11.5px; color: var(--accent); margin-top: 5px; letter-spacing: .02em; }
.cc-rows { display: flex; flex-direction: column; gap: 8px; margin: 18px 0 4px; }
.cc-row { font-size: 13.5px; color: var(--muted); text-decoration: none; transition: color .16s; }
.cc-row:hover { color: var(--accent); }
.qr-box {
  width: 200px; height: 200px; margin: 20px auto 12px; background: #fff; border-radius: 12px;
  padding: 12px; display: flex; align-items: center; justify-content: center;
}
.qr-box svg { width: 100%; height: 100%; display: block; }
.qr-load { font-family: var(--mono); font-size: 12px; color: #333; }
.cc-hint { font-family: var(--mono); font-size: 11px; color: var(--dim); letter-spacing: .04em; margin-bottom: 18px; }
.cc-actions { display: flex; flex-direction: column; gap: 9px; }
.cc-save, .cc-book, .cc-msg { width: 100%; justify-content: center; }

/* ── Enquiry form modal ──────────────────────────────────────────── */
.inq-card {
  background: var(--bg-2); border: 1px solid var(--border-hi); border-radius: 18px;
  padding: 34px 32px 30px; max-width: 520px; width: 100%; position: relative;
  transform: translateY(12px) scale(.98); transition: transform .25s var(--ease);
  box-shadow: 0 30px 80px -20px rgba(0, 0, 0, .6);
  margin: auto;
}
.modal-overlay.open .inq-card { transform: none; }
.inq-card h2 {
  font-family: var(--serif); font-weight: 400; font-size: 1.7rem;
  letter-spacing: -.01em; padding-right: 40px;
}
.inq-sub { color: var(--muted); font-size: 14px; margin: 8px 0 22px; }
.inq-form { display: flex; flex-direction: column; gap: 15px; }
.inq-form label {
  display: flex; flex-direction: column; gap: 7px;
  font-size: 12.5px; font-weight: 500; color: var(--text); letter-spacing: .01em;
}
.inq-form label span[aria-hidden] { color: var(--accent); }
.inq-form label .opt {
  color: var(--dim); font-weight: 400; font-family: var(--mono); font-size: 10.5px;
}
.inq-form input, .inq-form select, .inq-form textarea {
  font-family: var(--sans); font-size: 14.5px; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 9px; padding: 11px 13px; width: 100%;
  transition: border-color .18s var(--ease), background .18s var(--ease);
  -webkit-appearance: none; appearance: none;
}
.inq-form select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%239b9b92' stroke-width='1.6' d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 13px center; padding-right: 34px;
}
.inq-form textarea { resize: vertical; min-height: 96px; line-height: 1.6; }
.inq-form input:focus, .inq-form select:focus, .inq-form textarea:focus {
  outline: none; border-color: var(--accent); background: var(--surface);
}
.inq-row { display: grid; grid-template-columns: 1fr 1fr; gap: 13px; }
@media (max-width: 520px) { .inq-row { grid-template-columns: 1fr; } }
.inq-hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.inq-error { color: #ff7a7a; font-size: 13px; min-height: 1em; }
.inq-error:empty { min-height: 0; }
.inq-submit { width: 100%; justify-content: center; margin-top: 2px; }
.inq-submit:disabled { opacity: .6; cursor: wait; }
.inq-alt { font-size: 12.5px; color: var(--dim); text-align: center; margin-top: 2px; }
.inq-alt a { color: var(--accent); text-decoration: none; }
/* Inline card fields. Each box is a PayPal iframe, so it is styled from the
   outside only: the border and background are ours, the input inside is theirs
   and is themed through the SDK's style object. */
.pay-or {
  display: flex; align-items: center; gap: 12px;
  margin: 20px 0 16px; color: var(--dim);
  font-family: var(--sans); font-size: 0.8125rem; text-transform: none; letter-spacing: 0;
}
.pay-or::before, .pay-or::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}
/* ── Stripe's Payment Element ─────────────────────────────────────────────
   One iframe holding every method Stripe offers — card, wallets, bank, the
   buy-now-pay-later options — so the styling here is the frame around it. The
   inside is themed through the Appearance API in app.js, which reads the very
   custom properties below rather than a second copy of the palette: the site
   has a theme editor, and a hex pasted in here would be correct until somebody
   changed the theme and then the checkout would be the last thing still
   wearing the old one. */
.pay-el { margin: 4px 0 16px; min-height: 40px; }
/* Details, then methods. The group gets one margin rather than each element
   carrying its own, so the two read as one block of "your information". */
.pay-el-details:not(:empty) { margin: 14px 0 20px; }
.pay-el-addr:not(:empty) { margin-top: 14px; }

/* The wallet buttons, and the divider under them.
   Both only appear once the Express Checkout Element has told us it actually
   has something to show — a visitor with no wallet configured gets the card
   form with no empty gap and no divider dividing nothing. */
.pay-el-express { margin-bottom: 0; }
.pay-el.has-express .pay-el-express { margin-bottom: 18px; }
.pay-el.has-express .pay-el-express::after {
  content: 'or pay another way';
  display: block; margin-top: 18px; text-align: center;
  color: var(--dim); font-family: var(--sans);
  font-size: 0.8125rem; text-transform: none; letter-spacing: 0;
}
.pay-el .book-hint { text-align: center; padding: 18px 0; }
.pay-stripe-btn { width: 100%; justify-content: center; }
/* Why the Pay button is not clickable yet. Klarna, Affirm and Cash App Pay
   show no fields of their own, so without this a disabled button reads as
   broken rather than as incomplete. */
.pay-el-why {
  margin: 0 0 10px; text-align: center;
  font-size: 12.5px; color: var(--dim);
}
.pay-stripe-btn:disabled { opacity: .55; cursor: not-allowed; }

.pay-cf-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 13px; }
.pay-cf-field { display: flex; flex-direction: column; gap: 7px; }
.pay-cf-field.span2 { grid-column: 1 / -1; }
.pay-cf-label { font-size: 12.5px; font-weight: 500; color: var(--text); letter-spacing: .01em; }
.pay-cf-field > div {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 9px; padding: 2px 11px; height: 46px;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.pay-cf-field > div.pp-focus { border-color: var(--accent); background: var(--surface); }
.pay-cf-field > div.pp-invalid { border-color: #ff6b5e; }
.pay-cf-field iframe { height: 100%; width: 100%; display: block; }
.pay-cf-submit { width: 100%; justify-content: center; margin-top: 16px; }
.pay-cf-submit:disabled { opacity: .6; cursor: wait; }
.pay-receipt { margin-top: 18px; text-align: left; }
/* Waiting on the tick box. Dimmed, never click-blocked: the buttons have to
   receive the click in order to say why they refused it. */
#payPane.pay-locked #payButtons,
#payPane.pay-locked #payCard { opacity: .45; transition: opacity .2s var(--ease); }
.pay-terms-wrap .pay-consent { margin: 0 0 16px; }
.pay-terms-wrap .pay-refund { margin: 0 0 16px; }
/* The refund terms on a checkout screen. Deliberately readable rather than
   fine print: a disclosure nobody can read is not a disclosure. Distinct from
   .pay-terms above, which is the subscription auto-renewal line. */
.pay-terms-box {
  display: block; margin: 0 0 16px; padding: 11px 13px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 9px;
  font-size: 12px; line-height: 1.6; color: var(--dim); text-align: left;
}
.book-summary + .pay-terms-box { margin: 14px 0 16px; text-align: left; }
@media (max-width: 420px) { .pay-cf-grid { grid-template-columns: 1fr; } }

html.modal-open, html.modal-open body { overflow: hidden; }

/* `hidden` must win over the flex/block display rules below it. */
[hidden] { display: none !important; }
.inq-done { text-align: center; padding: 18px 0 6px; }
.inq-done-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.inq-done-actions .btn { flex: 1 1 auto; justify-content: center; min-width: 150px; }
.inq-countdown {
  font-family: var(--mono); font-size: 11px; color: var(--dim);
  letter-spacing: .05em; margin-top: 16px; min-height: 1em;
}
.inq-tick {
  width: 54px; height: 54px; margin: 0 auto 18px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; font-size: 26px;
  color: var(--live); border: 1.5px solid rgba(87, 227, 176, .4);
}
.inq-done h3 { font-family: var(--serif); font-weight: 500; font-size: 1.4rem; margin-bottom: 8px; }
.inq-done p { color: var(--muted); font-size: 14px; margin-bottom: 22px; }

/* ── Booking ─────────────────────────────────────────────────────── */
.booking { max-width: 940px; }
.book-loading, .book-hint, .book-empty {
  color: var(--dim); font-size: 13.5px; margin: 0; padding: 10px 0;
}
.book-empty .linkish {
  background: none; border: 0; padding: 0; font: inherit;
  color: var(--accent); cursor: pointer; text-decoration: underline;
}

.book-types { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; margin-bottom: 26px; }
.book-type {
  display: flex; flex-direction: column; gap: 6px; text-align: left; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  padding: 16px 17px; color: var(--text); font-family: inherit;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.book-type:hover { border-color: var(--dim); }
.book-type.on { border-color: var(--accent); background: var(--surface-2); }
.bt-name { font-weight: 600; font-size: 15px; }
.bt-meta { font-family: var(--mono); font-size: 11px; color: var(--accent); letter-spacing: .02em; }
.bt-desc { font-size: 12.5px; color: var(--dim); line-height: 1.55; }

.book-cols { display: grid; grid-template-columns: minmax(0, 320px) minmax(0, 1fr); gap: 30px; align-items: start; }
@media (max-width: 720px) { .book-cols { grid-template-columns: 1fr; gap: 22px; } }

.book-cal { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 16px; }
.book-cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.book-month { font-weight: 600; font-size: 14.5px; }
.book-nav {
  background: transparent; border: 1px solid var(--border); border-radius: 8px;
  color: var(--text); width: 30px; height: 30px; cursor: pointer; font-size: 17px; line-height: 1;
}
.book-nav:hover { border-color: var(--accent); color: var(--accent); }
.book-dow, .book-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.book-dow span {
  text-align: center; font-family: var(--mono); font-size: 10px;
  color: var(--dim); padding-bottom: 6px; letter-spacing: .04em;
}
.bd {
  aspect-ratio: 1; display: grid; place-items: center; border-radius: 8px;
  font-size: 13px; font-family: var(--mono); border: 1px solid transparent; background: transparent;
}
.bd.empty { visibility: hidden; }
.bd.off { color: #4a4a52; }
.bd.open { color: var(--text); cursor: pointer; background: var(--surface-2); border-color: var(--border); }
.bd.open:hover { border-color: var(--accent); color: var(--accent); }
.bd.open.sel { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }

.book-day { font-weight: 600; font-size: 15px; margin: 0 0 4px; }
.book-tz { font-family: var(--mono); font-size: 10.5px; color: var(--dim); margin: 0 0 12px; }
.book-times { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; }
.bslot {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 9px;
  color: var(--text); font-family: var(--mono); font-size: 12.5px; padding: 10px 6px; cursor: pointer;
  transition: border-color .16s var(--ease), color .16s var(--ease);
}
.bslot:hover { border-color: var(--accent); color: var(--accent); }
.bslot.sel { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }

.book-form-wrap { margin-top: 26px; }
.book-form { display: flex; flex-direction: column; gap: 15px; max-width: 520px; }
.book-form label { display: flex; flex-direction: column; gap: 7px; font-size: 12.5px; font-weight: 500; color: var(--text); }
/* Text fields only. A checkbox is an input too, and this rule used to catch
   it: `width: 100%`, a 9px radius, a surface fill and `appearance: none` turned
   the refund-consent tick box into a 516x24 empty rounded rectangle that looked
   exactly like an unlabelled text field.

   That was not a cosmetic bug. `refundAgreed()` gates the deposit button on
   that checkbox being ticked, and nobody can tick a control they cannot see —
   so "Pay $60.00 by card" did nothing at all, on Stripe *and* on PayPal, which
   rejects in `onClick` for the same reason. Paid booking could not be
   completed by anyone.

   Both selectors are (0,1,1), so this rule simply won by coming later in the
   file than `.pay-consent input`. Excluded by type here rather than fought with
   specificity, because the next checkbox added to this form would have hit the
   same wall. */
.book-form input:not([type="checkbox"]):not([type="radio"]),
.book-form textarea {
  font-family: var(--sans); font-size: 14.5px; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 9px; padding: 11px 13px; width: 100%;
  -webkit-appearance: none; appearance: none;
}
.book-form textarea { resize: vertical; min-height: 84px; line-height: 1.6; }
.book-form input:not([type="checkbox"]):not([type="radio"]):focus,
.book-form textarea:focus { outline: none; border-color: var(--accent); background: var(--surface); }
/* The tick box keeps a visible focus ring, since the rule above no longer
   gives it one and it is the control the whole payment is gated on. */
.book-form .pay-consent input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.book-form .hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.book-summary { font-size: 13.5px; color: var(--dim); margin: 0; }
.book-summary strong { color: var(--text); }
.book-err { color: #ff6b5e; font-size: 12.5px; margin: 0; }

.book-done { padding: 30px 0; max-width: 56ch; }
.book-done h2 { margin: 0 0 10px; }
.book-done p { color: var(--dim); margin: 0 0 14px; }
.book-when { font-family: var(--mono); font-size: 13.5px; color: var(--accent) !important; }
.book-paypal { margin: 6px 0 4px; min-height: 46px; }
.book-hold { font-family: var(--mono); font-size: 10.5px; color: var(--dim); margin: 0; }

/* ── ACCOUNT PAGES ────────────────────────────────────────────────────────
   Signup, sign in and the profile. Built as a plate rather than a centred card
   floating on a glow: the same composition the CMS login uses, so the two read
   as one system, and deliberately not the shape every generated auth screen
   arrives in. */
.auth-wrap { max-width: 520px; margin: 0 auto; padding: 0 20px 80px; }
.auth-plate {
  position: relative; overflow: hidden;
  border: 1px solid var(--border); border-radius: 3px;
  background: var(--surface);
  padding: clamp(26px, 5vw, 40px);
}
/* One saturated line across the top edge, the way an instrument shows power. */
.auth-plate::before {
  content: ''; position: absolute; inset: 0 0 auto; height: 2px;
  background: linear-gradient(90deg, var(--accent) 0%, var(--accent) 26%,
              var(--accent-soft) 26%, var(--accent-soft) 100%);
}
.auth-eyebrow {
  display: block; margin-bottom: 12px;
  font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--dim);
}
.auth-plate h1 {
  font-family: var(--serif); font-weight: 500;
  font-size: clamp(1.7rem, 4vw, 2.2rem); line-height: 1; letter-spacing: -0.03em;
  margin-bottom: 10px;
}
.auth-plate > p { color: var(--muted); font-size: 0.92rem; margin-bottom: 26px; }

.auth-field { margin-bottom: 18px; }
/* The label on every form field on the site — sign-in, sign-up, the page
   editor, billing. Tracked uppercase mono made "Display name" and "Email"
   the least legible text on a panel whose entire job is saying which box is
   which. Sentence case, per the same rule as every other control. */
.auth-field label {
  display: block; margin-bottom: 7px;
  font-family: var(--sans); font-size: 0.8125rem; font-weight: 500;
  letter-spacing: 0; text-transform: none; color: var(--muted);
}
/* Underlined rather than boxed, matching the CMS. A boxed field with an icon
   tucked inside is the other half of the generated-auth look. */
.auth-field input {
  width: 100%; background: transparent; border: none;
  border-bottom: 1px solid var(--border-hi);
  color: var(--text); font-family: var(--sans); font-size: 1rem;
  padding: 10px 2px; outline: none; transition: border-color 0.2s var(--ease);
}
.auth-field input::placeholder { color: var(--dim); }
.auth-field input:focus { border-bottom-color: var(--accent); }

.auth-terms {
  display: flex; gap: 11px; align-items: flex-start;
  margin: 22px 0 4px; font-size: 0.82rem; color: var(--muted); line-height: 1.6;
}
.auth-terms input { margin-top: 3px; accent-color: var(--accent); flex: none; width: 15px; height: 15px; }
.auth-terms a { border-bottom: 1px solid rgba(255,122,47,0.4); }

.auth-btn {
  width: 100%; margin-top: 24px; cursor: pointer;
  background: var(--accent); color: var(--on-accent);
  border: 1px solid var(--accent); border-radius: 2px; padding: 13px 16px;
  font-family: var(--sans); font-size: 0.9375rem; font-weight: 500;
  letter-spacing: 0; text-transform: none;
  transition: opacity 0.18s var(--ease);
}
.auth-btn:hover { opacity: 0.88; }
.auth-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.auth-btn:disabled { opacity: 0.45; cursor: not-allowed; }

.auth-alt {
  margin-top: 22px; padding-top: 20px; border-top: 1px solid var(--border);
  font-size: 0.85rem; color: var(--muted);
}
.auth-alt a { border-bottom: 1px solid rgba(255,122,47,0.4); }

/* A square marker rather than an icon: the message is already the signal. */
.auth-msg {
  display: none; position: relative; margin-top: 18px; padding-left: 15px;
  font-family: var(--mono); font-size: 0.76rem; line-height: 1.65;
}
.auth-msg::before {
  content: ''; position: absolute; left: 0; top: 0.5em;
  width: 4px; height: 4px; background: currentColor;
}
.auth-msg.bad { color: #ff6b5e; display: block; }
.auth-msg.good { color: var(--live); display: block; }

/* ── Profile ── */
.acct-row {
  display: flex; align-items: center; gap: 14px;
  padding: 15px 0; border-bottom: 1px solid var(--border);
}
.acct-row:last-child { border-bottom: none; }
.acct-row .k {
  font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--dim); min-width: 110px; flex: none;
}
.acct-row .v { color: var(--text); font-size: 0.92rem; word-break: break-word; }
.acct-actions { margin-top: 26px; display: flex; gap: 10px; flex-wrap: wrap; }
/* The original button, restyled rather than replaced.
   Every label in the markup is already written in sentence case — "Change my
   password", "I do not remember it", "Download my data" — and it was this rule
   that shouted them. Tracked-out uppercase mono on a control is the single
   loudest generated-interface tell we had, and it was on every button on the
   site. Changing it here fixes all of them without touching a line of HTML.

   Matches .btn-secondary exactly, so the two can be used side by side while
   the pages migrate, and .acct-btn can eventually just become an alias. */
.acct-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  cursor: pointer; background: none; border: 1px solid var(--border-hi);
  border-radius: 6px; color: var(--text); height: 40px; padding: 0 16px;
  font-family: var(--sans); font-size: 0.875rem; font-weight: 500;
  line-height: 1; letter-spacing: 0; text-transform: none; white-space: nowrap;
  transition: background 0.15s var(--ease), border-color 0.15s var(--ease),
              color 0.15s var(--ease), opacity 0.15s var(--ease);
}
.acct-btn:hover { border-color: var(--text); background: var(--surface-2); }
.acct-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.acct-btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* A password you cannot see is a password you cannot check, and a typo in a
   field that hides itself is only discovered at the next screen. */
.pw-wrap { position: relative; }
.pw-wrap input { padding-right: 44px; }
.pw-eye {
  position: absolute; right: 2px; bottom: 6px;
  background: none; border: none; cursor: pointer; padding: 6px;
  color: var(--dim); line-height: 0; transition: color 0.18s var(--ease);
}
.pw-eye:hover { color: var(--accent); }
.pw-eye svg { width: 18px; height: 18px; }

/* ── Theme playground ─────────────────────────────────────────────────────
   Built from the same variables as everything else, so the editor is styled by
   the theme being edited. That is deliberate: a theme whose text is illegible
   should be illegible here too, immediately and on the screen where it can be
   fixed, rather than looking fine in the editor and wrong everywhere else.

   The one exception is #themeBar, which is pinned to fixed colours. It carries
   the way out of a preview, so it is the one element that has to stay readable
   under a theme that has made everything else unreadable. */

/* The standing marker that what you are looking at is not the site. A preview
   with no visible state and no way out is a trap, and a share link opens on
   somebody who never asked for one. */
#themeBar {
  position: fixed; z-index: 9998; left: 50%; transform: translateX(-50%);
  bottom: 18px; display: flex; align-items: center; gap: 16px;
  padding: 10px 12px 10px 18px; border-radius: 99px;
  background: rgba(18, 18, 22, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.16);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
  max-width: calc(100vw - 32px);
}
#themeBar .tb-name {
  font-family: var(--mono); font-size: 0.72rem; color: #ECEAE3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#themeBar .tb-stop {
  flex: none; cursor: pointer; border-radius: 99px;
  background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);
  color: #ECEAE3; padding: 6px 14px;
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.12em;
  text-transform: uppercase; transition: background 0.18s var(--ease);
}
#themeBar .tb-stop:hover { background: rgba(255, 255, 255, 0.2); }

/* ── A draft of your own ──
   The primary way out of an unsaved edit is finishing it, not throwing it
   away, so Finish is the filled control and Discard steps back to a quiet
   one. Same fixed colours as the rest of the bar and for the same reason: this
   has to stay legible under a theme that has made everything else unreadable,
   which rules out var(--accent) here. */
#themeBar .tb-go {
  flex: none; text-decoration: none; border-radius: 99px;
  background: #ECEAE3; color: #17171a; padding: 6px 15px;
  font-family: var(--sans); font-size: 0.76rem; font-weight: 500;
  transition: opacity 0.18s var(--ease);
}
#themeBar .tb-go:hover { opacity: 0.85; }
#themeBar.tb-draft .tb-stop {
  background: none; border-color: transparent;
  color: rgba(236, 234, 227, 0.62); text-transform: none;
  font-family: var(--sans); font-size: 0.76rem; letter-spacing: 0; padding: 6px 8px;
}
#themeBar.tb-draft .tb-stop:hover { background: none; color: #ff6b5e; }

.tp-wrap { max-width: 900px; margin: 0 auto; padding: 0 20px 90px; }

/* Two columns on a desktop: the list of saved themes beside the editor. One
   column below, with the editor first — on a phone the editor is the page. */
.tp-cols { display: grid; gap: 22px; grid-template-columns: 1fr; }
@media (min-width: 900px) { .tp-cols { grid-template-columns: 260px 1fr; } }
@media (min-width: 900px) { .tp-side { order: -1; } }

.tp-card {
  border: 1px solid var(--border); border-radius: 3px;
  background: var(--surface); padding: 20px;
}
.tp-card h2 {
  font-family: var(--sans); font-size: 0.9375rem; font-weight: 600;
  letter-spacing: -0.01em; text-transform: none; color: var(--text);
  margin-bottom: 14px;
}

/* Each saved theme and preset as a row with its own colours on it, so the
   list is legible without opening anything. */
.tp-item {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 9px 10px; margin-bottom: 5px; cursor: pointer; text-align: left;
  background: transparent; border: 1px solid transparent; border-radius: 2px;
  color: var(--muted); font-family: var(--sans); font-size: 0.85rem;
  transition: 0.15s var(--ease);
}
.tp-item:hover { border-color: var(--border); color: var(--text); }
.tp-item.on { border-color: var(--border-hi); color: var(--text); background: var(--surface-2); }
.tp-item .sw {
  flex: none; width: 26px; height: 18px; border-radius: 2px;
  border: 1px solid var(--border-hi); position: relative; overflow: hidden;
}
.tp-item .sw i { position: absolute; inset: 0 40% 0 0; }
.tp-item .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tp-item .sh {
  flex: none; font-family: var(--mono); font-size: 0.56rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--live);
}
.tp-empty { color: var(--dim); font-size: 0.82rem; line-height: 1.6; }

/* The swatch grid. A label, a colour well and the literal value — the value
   is shown because somebody choosing a palette here wants to paste it into a
   design file, and a picker that hides its own hex is useless for that. */
.tp-vars { display: grid; gap: 3px; grid-template-columns: 1fr; }
@media (min-width: 620px) { .tp-vars { grid-template-columns: 1fr 1fr; gap: 3px 22px; } }
.tp-var {
  display: flex; align-items: center; gap: 11px;
  padding: 7px 0; border-bottom: 1px solid var(--border);
}
.tp-var label {
  flex: 1; min-width: 0; font-family: var(--mono); font-size: 0.68rem;
  color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tp-var .hex {
  font-family: var(--mono); font-size: 0.62rem; color: var(--dim);
  flex: none; width: 74px; text-align: right;
}
/* The native colour input, stripped of its chrome so it reads as a swatch. */
.tp-var input[type="color"] {
  flex: none; width: 30px; height: 22px; padding: 0; cursor: pointer;
  background: transparent; border: 1px solid var(--border-hi); border-radius: 2px;
}
.tp-var input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
.tp-var input[type="color"]::-webkit-color-swatch { border: none; border-radius: 1px; }
.tp-var input[type="color"]::-moz-color-swatch { border: none; border-radius: 1px; }
.tp-var input[type="range"] { flex: none; width: 96px; accent-color: var(--accent); cursor: pointer; }

/* A row governed by a switch that is currently off. Dimmed and inert rather
   than removed — a control that disappears reads as breakage, and a group that
   changes height every time a switch moves is hard to aim at. */
.tp-var.off { opacity: 0.38; pointer-events: none; }

/* ── The switch ──
   A real two-state control, because the alternative in this grid was a range
   input with two stops, and a slider is the wrong shape for a yes/no.
   The checkbox stays in the DOM and keeps its own semantics and keyboard
   behaviour; only its painting is replaced. */
.tp-switch { flex: none; display: inline-flex; cursor: pointer; }
.tp-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.tp-switch-track {
  width: 38px; height: 22px; border-radius: 99px; position: relative;
  background: var(--surface-2); border: 1px solid var(--border-hi);
  transition: background 0.18s var(--ease), border-color 0.18s var(--ease);
}
.tp-switch-track::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--muted);
  transition: transform 0.18s var(--ease), background 0.18s var(--ease);
}
.tp-switch input:checked + .tp-switch-track {
  background: var(--accent-soft); border-color: var(--accent);
}
.tp-switch input:checked + .tp-switch-track::after {
  background: var(--accent); transform: translateX(16px);
}
/* Focus goes on the track, since the input it belongs to is not painted. */
.tp-switch input:focus-visible + .tp-switch-track {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

.tp-actions { display: flex; gap: 9px; flex-wrap: wrap; margin-top: 20px; }
.tp-name {
  width: 100%; background: transparent; border: none;
  border-bottom: 1px solid var(--border-hi); color: var(--text);
  font-family: var(--serif); font-size: 1.35rem; letter-spacing: -0.02em;
  padding: 6px 2px; outline: none; margin-bottom: 18px;
}
.tp-name:focus { border-bottom-color: var(--accent); }

/* ── Public profile (/u/handle) ───────────────────────────────────────────
   Served from a Function with no script at all, so everything here has to work
   from markup alone. Deliberately narrow and centred: it is a card people open
   from a phone, usually once. */
.pf-wrap {
  max-width: 520px; margin: 0 auto; padding: 64px 20px 40px;
  min-height: 100vh; display: flex; flex-direction: column;
}
.pf-card {
  border: 1px solid var(--border); border-radius: 3px; background: var(--surface);
  padding: clamp(24px, 6vw, 40px); text-align: center; position: relative; overflow: hidden;
}
.pf-card::before {
  content: ''; position: absolute; inset: 0 0 auto; height: 2px;
  background: linear-gradient(90deg, var(--accent) 0%, var(--accent) 26%,
              var(--accent-soft) 26%, var(--accent-soft) 100%);
}
.pf-mark {
  width: 64px; height: 64px; margin: 4px auto 16px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--on-accent);
  font-family: var(--mono); font-size: 1.15rem; font-weight: 500; letter-spacing: 0.02em;
}
.pf-name {
  font-family: var(--serif); font-weight: 500; letter-spacing: -0.02em;
  font-size: clamp(1.4rem, 5vw, 1.9rem); line-height: 1.1; color: var(--text);
}
.pf-handle {
  font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.08em;
  color: var(--dim); margin-top: 6px;
}
.pf-bio {
  color: var(--muted); font-size: 0.9rem; line-height: 1.7;
  margin: 16px auto 0; max-width: 40ch;
}
.pf-links { display: flex; flex-direction: column; gap: 9px; margin-top: 26px; }
.pf-link {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 14px 18px; border-radius: 2px; text-decoration: none;
  background: var(--surface-2); border: 1px solid var(--border);
  transition: border-color 0.18s var(--ease), background 0.18s var(--ease);
}
.pf-link:hover { border-color: var(--accent); background: var(--accent-soft); }
.pf-label { color: var(--text); font-size: 0.92rem; text-align: left; }
/* The destination, shown rather than hidden. A link labelled one thing and
   going somewhere else should say so before it is clicked. */
.pf-host {
  font-family: var(--mono); font-size: 0.64rem; color: var(--dim);
  flex: none; overflow: hidden; text-overflow: ellipsis; max-width: 40%;
}
/* ── A site's furniture ───────────────────────────────────────────────────
   The header and footer a customer builds, shown on every page. They hold the
   same sections a page does, so they inherit the block styles and need only
   the frame around them.

   Full width, because a header that sat inside the 940px reading column would
   be a header that stopped short of the edges on exactly the wide screens a
   business site is looked at on. The column is restored inside, so the content
   still lines up with the page beneath it. */
.pf-furn { width: 100%; }
.pf-furn > .pb-blocks { max-width: 940px; margin: 0 auto; padding: 0 20px; }
/* The blocks module puts vertical rhythm between sections. Furniture is one
   band, not a stack with a page in it, so it sets its own. */
.pf-header > .pb-blocks { padding-top: 14px; padding-bottom: 14px; }
.pf-footer > .pb-blocks { padding-top: 34px; padding-bottom: 20px; }
.pf-header {
  position: sticky; top: 0; z-index: 900;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}
.pf-footer { border-top: 1px solid var(--border); margin-top: 48px; }
/* A canvas page paints its own ground across the document, so the furniture
   sits over it rather than in a column of its own. */
.cv-doc > .pf-furn { position: relative; z-index: 900; }

/* ── A site's own navigation ──────────────────────────────────────────────
   Drawn only when an account has more than one page, so every page built
   before pages existed renders exactly as it did.

   Above the content rather than inside the card, because it belongs to the
   site and not to the page: a canvas page replaces the card entirely, and a
   navigation that lived inside it would disappear on exactly the layout most
   likely to need one.

   Sticky, and that is the one decision here worth defending. A canvas page can
   be four thousand pixels tall with no other way back, and a navigation you
   have to scroll to the top to reach is one people stop using. */
.pf-nav {
  position: sticky; top: 0; z-index: 900;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}
.pf-nav-in {
  display: flex; gap: 4px; align-items: center;
  max-width: 940px; margin: 0 auto; padding: 10px 20px;
  /* A site with a lot of pages scrolls its menu rather than wrapping it into a
     second row that pushes the page down. */
  overflow-x: auto; -webkit-overflow-scrolling: touch;
}
.pf-nav a {
  flex: none; white-space: nowrap;
  padding: 7px 11px; border-radius: 7px;
  color: var(--muted); text-decoration: none;
  font-size: 0.9rem; line-height: 1;
}
.pf-nav a:hover { color: var(--text); background: var(--surface); }
/* The attribute rather than a class: it is what says "this one" to a screen
   reader, and the styling may as well hang off the thing that already means it. */
.pf-nav a[aria-current="page"] { color: var(--text); background: var(--surface-2); font-weight: 500; }
.pf-nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* A canvas page paints its own background across the whole document, so the
   navigation sits over it rather than in a column. */
.cv-doc > .pf-nav + main,
.pf-nav + .pf-wrap { margin-top: 0; }

.pf-foot {
  margin-top: auto; padding-top: 32px; text-align: center;
  font-family: var(--mono); font-size: 0.64rem; color: var(--dim);
}
.pf-foot a { color: var(--dim); text-decoration: none; }
.pf-foot a:hover { color: var(--muted); }
.pf-sep { margin: 0 8px; }

/* ── The profile editor ── */
.pf-handle-row {
  display: flex; align-items: baseline; gap: 2px;
  border-bottom: 1px solid var(--border-hi); padding-bottom: 8px; flex-wrap: wrap;
}
.pf-prefix { font-family: var(--mono); font-size: 0.82rem; color: var(--dim); flex: none; }
.pf-handle-row input {
  flex: 1; min-width: 120px; background: transparent; border: none; outline: none;
  color: var(--text); font-family: var(--mono); font-size: 0.95rem; padding: 2px 0;
}
.pf-handle-row:focus-within { border-bottom-color: var(--accent); }

.pf-bio-input {
  width: 100%; background: transparent; border: none;
  border-bottom: 1px solid var(--border-hi); color: var(--text);
  font-family: var(--sans); font-size: 1rem; line-height: 1.6;
  padding: 10px 2px; outline: none; resize: vertical;
  transition: border-color 0.2s var(--ease);
}
.pf-bio-input:focus { border-bottom-color: var(--accent); }
.pf-bio-input::placeholder { color: var(--dim); }

.pf-select {
  width: 100%; background: var(--bg-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 2px;
  padding: 10px 12px; font-family: var(--sans); font-size: 0.9rem; outline: none;
}
.pf-select:focus { border-color: var(--accent); }

.pf-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--border);
}
.pf-row:last-child { border-bottom: none; }
.pf-move { display: flex; flex-direction: column; gap: 2px; flex: none; }
.pf-move button {
  background: transparent; border: 1px solid var(--border); border-radius: 2px;
  color: var(--muted); cursor: pointer; width: 22px; height: 16px;
  font-size: 9px; line-height: 1; padding: 0; transition: 0.15s var(--ease);
}
.pf-move button:hover:not(:disabled) { border-color: var(--border-hi); color: var(--text); }
.pf-move button:disabled { opacity: 0.3; cursor: default; }
.pf-row-text { flex: 1; min-width: 0; }
.pf-row-text strong {
  display: block; color: var(--text); font-size: 0.9rem; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-row-text span {
  display: block; font-family: var(--mono); font-size: 0.66rem; color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-del {
  flex: none; background: transparent; border: none; cursor: pointer;
  color: var(--dim); font-family: var(--sans); font-size: 0.8125rem;
  letter-spacing: 0; text-transform: none; transition: color 0.15s var(--ease);
}
.pf-del:hover { color: var(--danger); }

.pf-add { display: flex; gap: 9px; margin-top: 16px; flex-wrap: wrap; }
.pf-add input {
  flex: 1; min-width: 130px; background: transparent; color: var(--text);
  border: 1px solid var(--border); border-radius: 2px; padding: 9px 11px;
  font-family: var(--sans); font-size: 0.86rem; outline: none;
}
.pf-add input:focus { border-color: var(--accent); }
.pf-add input::placeholder { color: var(--dim); }

/* Which address is theirs. Both work regardless, so this reads as a choice
   between two good options rather than a switch that disables one. */
.pf-domains { display: flex; flex-direction: column; gap: 8px; margin-top: 18px; }

/* Louder than the hints around it, because it is the only one that describes
   something breaking. Amber rather than red: nothing is wrong yet, and this is
   a consequence to weigh rather than an error to fix. */
.pf-domain-warn {
  color: var(--warn) !important; background: var(--warn-soft);
  border: 1px solid var(--warn); border-radius: 6px;
  padding: 10px 12px; margin-top: 10px; line-height: 1.55;
}
.pf-domain {
  display: flex; gap: 11px; align-items: flex-start; cursor: pointer;
  padding: 12px 14px; border-radius: 2px;
  border: 1px solid var(--border); background: var(--bg-2);
  transition: border-color 0.16s var(--ease);
}
.pf-domain:hover { border-color: var(--border-hi); }
.pf-domain.on { border-color: var(--accent); background: var(--accent-soft); }
.pf-domain input { margin-top: 3px; accent-color: var(--accent); width: 15px; height: 15px; flex: none; }
.pf-domain strong {
  display: block; color: var(--text); font-family: var(--mono);
  font-size: 0.78rem; font-weight: 500; word-break: break-all;
}
.pf-domain em {
  display: block; color: var(--muted); font-size: 0.76rem;
  font-style: normal; margin-top: 3px;
}

.pf-publish { display: flex; gap: 12px; align-items: flex-start; cursor: pointer; }
.pf-publish input { margin-top: 3px; accent-color: var(--accent); width: 16px; height: 16px; flex: none; }
.pf-publish strong { display: block; color: var(--text); font-size: 0.92rem; font-weight: 500; }
.pf-publish em { display: block; color: var(--muted); font-size: 0.8rem; font-style: normal; margin-top: 3px; }

/* ── The generator ──
   The one control that matters, given room to look like it. */
.tp-seed-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.tp-seed-well {
  display: inline-flex; align-items: center; gap: 11px; cursor: pointer;
  font-family: var(--mono); font-size: 0.8rem; color: var(--text);
}
.tp-seed-well input[type="color"] {
  width: 54px; height: 40px; padding: 0; cursor: pointer;
  background: transparent; border: 1px solid var(--border-hi); border-radius: 3px;
}
.tp-seed-well input[type="color"]::-webkit-color-swatch-wrapper { padding: 3px; }
.tp-seed-well input[type="color"]::-webkit-color-swatch { border: none; border-radius: 2px; }
.tp-seed-well input[type="color"]::-moz-color-swatch { border: none; border-radius: 2px; }

.tp-modes { display: inline-flex; border: 1px solid var(--border); border-radius: 99px; overflow: hidden; }
.tp-mode {
  cursor: pointer; background: transparent; border: none; color: var(--muted);
  padding: 8px 16px; font-family: var(--sans); font-size: 0.8125rem; font-weight: 500;
  letter-spacing: 0; text-transform: none; transition: 0.18s var(--ease);
}
.tp-mode:hover { color: var(--text); }
.tp-mode.on { background: var(--accent); color: var(--on-accent); }

.tp-tint { display: inline-flex; align-items: center; gap: 10px; }
.tp-tint span {
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--dim);
}
.tp-tint input[type="range"] { width: 110px; accent-color: var(--accent); cursor: pointer; }
.tp-hint { font-size: 0.72rem; color: var(--dim); line-height: 1.5; }

/* ── The preview canvas ──
   A page in miniature, built from the same variables as the real one. It
   exists because judging a palette from swatches is judging the wrong thing:
   what matters is body text on a panel over a background, and that is only
   visible when the three are on screen together. */
.tp-canvas {
  border: 1px solid var(--border); border-radius: 3px; overflow: hidden;
  background: var(--bg);
}
.tpc-nav {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px; background: var(--nav-bg);
  border-bottom: 1px solid var(--border);
}
.tpc-brand { font-family: var(--mono); font-size: 0.72rem; color: var(--text); }
.tpc-links { display: flex; align-items: center; gap: 14px; }
.tpc-links i { font-style: normal; font-size: 0.72rem; color: var(--muted); }
.tpc-links i.tpc-cta {
  color: var(--text); border: 1px solid var(--border-hi);
  padding: 4px 11px; border-radius: 99px;
}

.tpc-hero { padding: 26px 20px 22px; background: var(--bg-2); }
.tpc-hero h3 {
  font-family: var(--serif); font-weight: 500; letter-spacing: -0.02em;
  font-size: clamp(1.2rem, 3vw, 1.6rem); color: var(--text); margin: 4px 0 8px;
}
.tpc-hero p { color: var(--muted); font-size: 0.85rem; line-height: 1.65; max-width: 48ch; }
.tpc-btns { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
.tpc-btns .btn { font-size: 12.5px; padding: 9px 18px; }

.tpc-grid { display: grid; gap: 12px; padding: 14px; grid-template-columns: 1fr; }
@media (min-width: 640px) { .tpc-grid { grid-template-columns: 1fr 1fr; } }
.tpc-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 3px; padding: 15px;
}
/* The name of the thing being sold. It was set as 0.58rem tracked uppercase
   mono in the dim tone — smaller and quieter than the price beneath it — so
   "Short address" and "Your own subdomain" read as filing labels rather than
   as what you are buying. */
.tpc-kicker {
  display: block; margin-bottom: 8px;
  font-family: var(--sans); font-size: 0.875rem; font-weight: 600;
  letter-spacing: -0.01em; text-transform: none; color: var(--text);
}
.tpc-card strong { display: block; color: var(--text); font-size: 0.9rem; font-weight: 500; }
.tpc-card p { color: var(--muted); font-size: 0.8rem; line-height: 1.6; margin: 6px 0 10px; }
.tpc-card .auth-field input { font-size: 0.85rem; pointer-events: none; }
.tpc-meta {
  display: flex; align-items: center; gap: 8px; margin-top: 14px;
  font-family: var(--mono); font-size: 0.66rem; color: var(--muted);
}
.tpc-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--live); flex: none; }

/* ── The manual controls, folded away ── */
.tp-fine summary {
  cursor: pointer; list-style: none;
  font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--dim); transition: color 0.18s var(--ease);
}
.tp-fine summary::-webkit-details-marker { display: none; }
.tp-fine summary::before { content: '+ '; }
.tp-fine[open] summary::before { content: '– '; }
.tp-fine summary:hover { color: var(--text); }

/* The share link, once there is one. Selectable in full, because the only
   thing anybody does with it is copy it. */
.tp-share {
  margin-top: 16px; padding: 12px 14px; border-radius: 2px;
  background: var(--bg-2); border: 1px solid var(--border);
  font-family: var(--mono); font-size: 0.68rem; color: var(--muted);
  word-break: break-all; line-height: 1.6;
}

/* ── Page blocks ──────────────────────────────────────────────────────────
   The content a visitor builds on their own page. Typed blocks rendered by
   the server — see functions/api/account/_blocks.js for why none of this can
   carry markup. Styled to sit inside .pf-card, so a page reads as one object
   rather than a stack of unrelated widgets. */
.pb-blocks { margin-top: 26px; text-align: left; }
.pb-blocks > * + * { margin-top: 18px; }
.pb-h {
  font-family: var(--serif); font-weight: 500; letter-spacing: -0.02em;
  color: var(--text); line-height: 1.2;
}
h2.pb-h { font-size: 1.25rem; }
h3.pb-h { font-size: 1.05rem; }
.pb-text { color: var(--muted); font-size: 0.92rem; line-height: 1.75; }
.pb-text strong { color: var(--text); font-weight: 600; }
.pb-text a { color: var(--accent); text-decoration: none; border-bottom: 1px solid var(--accent-soft); }

/* ── Shape in a paragraph ──
   Text blocks used to be one slab with line breaks in it, because the
   formatter handled emphasis and links but not structure. A list of opening
   hours or of what is included is the commonest thing anybody writes on a
   business page, and there was no way to write one. */
.pb-text p { margin-bottom: 0.9em; }
.pb-text p:last-child { margin-bottom: 0; }
.pb-text > .pb-list { margin: 0 0 0.9em; padding-left: 1.15em; }
.pb-text > .pb-list:last-child { margin-bottom: 0; }
.pb-list li { margin-bottom: 0.42em; }
.pb-list li:last-child { margin-bottom: 0; }
/* Bullets in the accent, which is the one place a list should carry any, and
   numbers left as the browser draws them. */
.pb-text ul.pb-list { list-style: none; padding-left: 1.1em; }
.pb-text ul.pb-list li { position: relative; }
.pb-text ul.pb-list li::before {
  content: ""; position: absolute; left: -1.1em; top: 0.62em;
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent); opacity: 0.6;
}
.pb-text code {
  font-family: var(--mono); font-size: 0.88em;
  background: var(--surface-2); border-radius: 3px; padding: 1px 5px;
}
.pb-text a:hover { border-bottom-color: var(--accent); }
.pb-hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; }

/* ── The sections the real sites turned out to need ────────────────────────
   Read off pages already built and running rather than invented: a wellness
   practice, a restaurant, two estate firms. All four open the same way, put a
   small word above every section heading, and close with one line and one
   button. Pages built without these read as a list of headings, which is what
   "looks nothing like the examples" meant.

   Everything below is drawn from the same variables as the rest of the page, so
   it follows the chosen background, the chosen font, and the plain light theme
   without a second set of rules. */

/* The small word above a heading. Mono and letterspaced, so it reads as a label
   rather than as a very short sentence. */
.pb-eyebrow {
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--accent); margin: 0 0 9px;
}
.pb-head > .pb-h { margin: 0; }

/* ── The hero ── */
.pb-hero {
  position: relative; overflow: hidden; border-radius: 4px;
  border: 1px solid var(--border); padding: 44px 26px;
  text-align: center; background: var(--surface);
}
/* A photograph if there is one. The URL arrives as a custom property set from
   an already-escaped, already-owned media key — never as free text. */
.pb-hero[style*="--cover"]::before {
  content: ''; position: absolute; inset: 0;
  background-image: var(--cover); background-size: cover; background-position: center;
  opacity: 0.34;
}
.pb-hero-in { position: relative; }
.pb-hero-t {
  font-family: var(--serif); font-weight: 500; line-height: 1.08;
  letter-spacing: -0.03em; color: var(--text);
  font-size: clamp(1.7rem, 6vw, 2.4rem); margin: 0; text-wrap: balance;
}
.pb-hero-sub {
  color: var(--muted); font-size: 1.02rem; line-height: 1.55;
  margin: 12px auto 0; max-width: 46ch; text-wrap: balance;
}
.pb-hero-x {
  color: var(--dim); font-size: 0.92rem; line-height: 1.75;
  margin: 14px auto 0; max-width: 58ch;
}
.pb-hero .pb-btns { justify-content: center; margin-top: 22px; }
.pb-hero .pb-btn { flex: 0 1 auto; }

/* The named washes. Six words rather than a colour picker, because whatever
   goes behind that text becomes CSS and an allowlist is the only version of
   that which cannot become a declaration somebody typed. Each is built from the
   page's own accent, so it follows a chosen palette instead of fighting it. */
.pb-cover-warm {
  background:
    radial-gradient(ellipse 70% 60% at 62% 45%, var(--accent-soft) 0%, transparent 70%),
    var(--surface);
}
.pb-cover-cool {
  background:
    radial-gradient(ellipse 65% 70% at 30% 20%, var(--surface-2) 0%, transparent 72%),
    radial-gradient(ellipse 50% 50% at 85% 90%, var(--accent-soft) 0%, transparent 66%),
    var(--surface);
}
.pb-cover-deep {
  background:
    linear-gradient(160deg, var(--surface-2) 0%, transparent 62%),
    var(--surface);
}
.pb-cover-dusk {
  background:
    radial-gradient(ellipse 90% 70% at 50% 110%, var(--accent-soft) 0%, transparent 68%),
    var(--surface);
}
.pb-cover-sand {
  background:
    linear-gradient(180deg, var(--accent-soft) 0%, transparent 48%),
    var(--surface);
}

/* ── A picture beside words ──
   One column until there is room for two, and the picture stays first in the
   source either way: on a phone the image belongs above the paragraph it
   illustrates regardless of which side it sits on at width. */
.pb-split { display: grid; gap: 18px; align-items: center; }
@media (min-width: 640px) {
  .pb-split { grid-template-columns: 1fr 1fr; gap: 28px; }
  .pb-split-r .pb-split-img { order: 2; }
}
.pb-split-solo { grid-template-columns: 1fr !important; }
.pb-split-img img {
  width: 100%; height: 100%; max-height: 340px; object-fit: cover;
  display: block; border-radius: 3px; border: 1px solid var(--border);
}
.pb-split-t {
  font-family: var(--serif); font-weight: 500; letter-spacing: -0.02em;
  color: var(--text); line-height: 1.2; font-size: 1.2rem; margin: 0 0 10px;
}
.pb-split .pb-btns { margin-top: 16px; }
.pb-split .pb-btn { flex: 0 1 auto; }

/* ── Numbers ──
   The number is what is being read, so it is the size of a heading and the
   label under it is a caption. A cards block with the number as its title
   produced a row of boxes, which is not what any of these sites do. */
.pb-stats {
  display: grid; gap: 14px; margin: 0;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.pb-stat {
  border-top: 1px solid var(--border); padding-top: 13px;
}
.pb-stat-v {
  font-family: var(--serif); font-weight: 500; letter-spacing: -0.03em;
  color: var(--text); font-size: clamp(1.6rem, 4.5vw, 2.1rem); line-height: 1;
  font-variant-numeric: tabular-nums;
}
.pb-stat-l {
  margin: 7px 0 0; color: var(--dim);
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.13em;
  text-transform: uppercase; line-height: 1.5;
}

/* ── The closing line ── */
.pb-banner {
  border: 1px solid var(--border); border-radius: 4px;
  padding: 30px 24px; text-align: center; background: var(--surface);
}
.pb-banner-t {
  font-family: var(--serif); font-weight: 500; letter-spacing: -0.025em;
  color: var(--text); line-height: 1.15; margin: 0;
  font-size: clamp(1.3rem, 4vw, 1.75rem); text-wrap: balance;
}
.pb-banner-x {
  color: var(--muted); font-size: 0.95rem; line-height: 1.6;
  margin: 10px auto 0; max-width: 48ch;
}
.pb-banner .pb-btns { justify-content: center; margin-top: 18px; }
.pb-banner .pb-btn { flex: 0 1 auto; }

.pb-fig { margin: 0; }
.pb-img {
  display: block; width: 100%; height: auto; border-radius: 3px;
  border: 1px solid var(--border); background: var(--surface-2);
}
.pb-grid { display: grid; gap: 8px; grid-template-columns: repeat(2, 1fr); }
@media (min-width: 520px) { .pb-grid { grid-template-columns: repeat(3, 1fr); } }
.pb-grid .pb-img { aspect-ratio: 1; object-fit: cover; }
.pb-fig figcaption {
  margin-top: 8px; color: var(--dim); font-size: 0.76rem; line-height: 1.5; text-align: center;
}

.pb-btns { display: flex; flex-wrap: wrap; gap: 9px; }
.pb-btn {
  flex: 1 1 auto; text-align: center; text-decoration: none;
  background: var(--accent); color: var(--on-accent);
  border-radius: 99px; padding: 11px 20px;
  font-size: 0.88rem; font-weight: 500;
  transition: opacity 0.16s var(--ease);
}
.pb-btn:hover { opacity: 0.88; }

/* ── What a block chose to look like ───────────────────────────────────────
   Every rule here is opt-in. A block that chose nothing carries no class and
   is untouched, which is what lets these ship without changing a single page
   anybody has already built. */

.pb-a-left { text-align: left; }
.pb-a-center { text-align: center; }
.pb-a-right { text-align: right; }

/* Buttons stretch to fill their row by default, and always have. That makes
   alignment meaningless: there is no spare space to align into. So choosing an
   alignment also stops them stretching, because "align these right" and "make
   these fill the width" are contradictory instructions and the alignment is
   the one that was asked for. */
.pb-btns.pb-a-left,
.pb-btns.pb-a-center,
.pb-btns.pb-a-right { text-align: initial; }
.pb-btns.pb-a-left { justify-content: flex-start; }
.pb-btns.pb-a-center { justify-content: center; }
.pb-btns.pb-a-right { justify-content: flex-end; }
.pb-btns.pb-a-left .pb-btn,
.pb-btns.pb-a-center .pb-btn,
.pb-btns.pb-a-right .pb-btn { flex: 0 1 auto; }

/* Shape. `pill` is the default and has no class. */
.pb-btns.pb-btn-round .pb-btn { border-radius: 8px; }
.pb-btns.pb-btn-square .pb-btn { border-radius: 0; }

/* Size. `medium` is the default and has no class. */
.pb-btns.pb-btn-small .pb-btn { padding: 8px 15px; font-size: 0.8rem; }
.pb-btns.pb-btn-large .pb-btn { padding: 15px 28px; font-size: 1rem; }

/* Outline. Keeps the same footprint as solid by paying for the border out of
   the padding, so switching between them does not move anything around it. */
.pb-btns.pb-btn-outline .pb-btn {
  background: none; color: var(--accent);
  border: 1px solid var(--accent); padding: 10px 19px;
}
.pb-btns.pb-btn-outline.pb-btn-small .pb-btn { padding: 7px 14px; }
.pb-btns.pb-btn-outline.pb-btn-large .pb-btn { padding: 14px 27px; }
.pb-btns.pb-btn-outline .pb-btn:hover { background: var(--accent); color: var(--on-accent); opacity: 1; }

/* The SVG that holds a page's duotone filters. It must define them and
   contribute nothing else.

   Sized to nothing and taken out of flow rather than given `display: none`.
   That is not superstition: a filter inside a `display: none` subtree has
   historically stopped resolving in WebKit, which renders every picture
   referencing it as a blank rectangle. Zero size is the arrangement that has
   always worked. */
.cv-defs {
  position: absolute; width: 0; height: 0; overflow: hidden;
  pointer-events: none;
}

/* ── Rolling text ─────────────────────────────────────────────────────────
   A line that travels across the page and never stops.

   The track holds the content twice and moves by exactly -50% of its own
   width, so at the end of the pass the second copy is where the first one
   started and the loop has no seam. Animating by a pixel amount instead would
   need to know the text's width, which CSS cannot ask for.

   Full width of the window rather than the reading column, because a ticker
   that stops at 940px is a ticker that looks like a mistake. The pair
   `left:50%` and `margin-left:-50vw` is the version that survives a container
   with padding, which is why the design bands use it too. */
.pb-mq {
  position: relative; left: 50%; width: 100vw; margin-left: -50vw;
  overflow: hidden;
  /* Its own rhythm. A marquee between two sections is a band, not a paragraph. */
  padding: 18px 0;
  color: var(--mq-ink, inherit);
  /* Nothing here is selectable text somebody wants to grab, and a drag-select
     across a moving line is a mess. */
  user-select: none;
}
.pb-mq-tinted { background: var(--mq-bg); }

/* Which edges fence it. A ticker between two sections usually wants both; one
   at the top of a page usually wants only the bottom, because the edge of the
   window is already a line. */
.pb-mq-b-both { border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.pb-mq-b-top { border-top: 1px solid var(--border); }
.pb-mq-b-bottom { border-bottom: 1px solid var(--border); }

/* ── One pass ──
   Not "crosses and is gone", which leaves an empty strip on the page for as
   long as somebody is looking at it. It slides in and *rests*, so the band ends
   up holding readable text — an entrance rather than a departure.

   `forwards` is what makes it hold. Without it the line would snap back to
   where it started the instant the animation ended, which reads as a glitch. */
.pb-mq-once .pb-mq-track {
  animation-name: pb-mq-in;
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
.pb-mq-once.pb-mq-r .pb-mq-track { animation-name: pb-mq-in-r; animation-direction: normal; }
@keyframes pb-mq-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
@keyframes pb-mq-in-r { from { transform: translateX(-100%); } to { transform: translateX(0); } }
.pb-mq-track {
  display: flex; width: max-content;
  animation: pb-mq var(--mq, 24s) linear infinite;
  will-change: transform;
}
.pb-mq-r .pb-mq-track { animation-direction: reverse; }
.pb-mq-run {
  display: inline-flex; align-items: center; gap: 0.5em;
  padding-right: 0.5em; white-space: nowrap;
  font-family: var(--serif); line-height: 1.1;
}
.pb-mq-sep { opacity: 0.45; }
.pb-mq-small  .pb-mq-run { font-size: clamp(1.1rem, 2.4vw, 1.6rem); }
.pb-mq-medium .pb-mq-run { font-size: clamp(1.6rem, 4vw, 2.6rem); }
.pb-mq-large  .pb-mq-run { font-size: clamp(2.2rem, 6vw, 4rem); }
.pb-mq-huge   .pb-mq-run { font-size: clamp(3rem, 10vw, 7rem); }

@keyframes pb-mq { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* Stopped for somebody who asked for less motion, and it stops showing the
   line rather than showing nothing — the resting state is the true state, the
   same rule the canvas animations follow. */
@media (prefers-reduced-motion: reduce) {
  .pb-mq-track { animation: none; transform: none; }
  /* Without the animation the doubled track would overflow silently, so the
     duplicate is hidden and the first copy is simply a line of text. */
  .pb-mq-track > [aria-hidden="true"] { display: none; }
  .pb-mq { overflow-x: auto; }
}

/* ── A button that opens a menu ───────────────────────────────────────────
   `<details>` and `<summary>`, which is the only answer available under
   `script-src 'none'` and is the right one regardless: it opens on click and
   on Enter or Space, it is announced as expandable, and it works with a
   finger. A `:hover` menu is unusable on every touch device ever made.

   The marker is removed in both spellings. WebKit still needs the
   `-webkit-details-marker` pseudo-element; everything else honours
   `list-style`, and omitting either leaves a triangle next to the label on
   some browsers and not others. */
.pb-menu { position: relative; }
.pb-menu > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: 7px; }
.pb-menu > summary::-webkit-details-marker { display: none; }
.pb-menu > summary::marker { content: ''; }
.pb-menu-c { flex: none; width: 10px; height: 10px; opacity: 0.6; transition: transform 0.15s ease; }
.pb-menu[open] > summary .pb-menu-c { transform: rotate(180deg); }

.pb-menu-p {
  position: absolute; z-index: 60; top: calc(100% + 6px); left: 50%;
  transform: translateX(-50%);
  min-width: 190px; max-width: min(280px, 90vw);
  display: flex; flex-direction: column;
  padding: 5px;
  background: var(--bg-2); border: 1px solid var(--border-hi);
  border-radius: 10px; box-shadow: 0 18px 44px -16px #000;
  /* A menu longer than the screen scrolls rather than running off the bottom. */
  max-height: 70vh; overflow-y: auto;
}
.pb-menu-p a {
  display: block; padding: 9px 11px; border-radius: 7px;
  color: var(--muted); text-decoration: none;
  font-size: 0.9rem; line-height: 1.35; text-align: left;
  white-space: normal;
}
.pb-menu-p a:hover { color: var(--text); background: var(--surface); }
.pb-menu-p a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Near the right edge the panel would hang off the page. Anchored to the
   button's right instead, which is what every menu at the end of a bar does. */
.pb-btns.pb-a-right .pb-menu-p { left: auto; right: 0; transform: none; }
.pb-btns.pb-a-left .pb-menu-p { left: 0; transform: none; }

/* An open menu must not be clipped by the row it is in, which is a flex
   container and would otherwise scroll it. */
.pb-btns { overflow: visible; }

/* Ghost. Only the words, which is what a navigation is: a row of solid pills
   across the top of a page reads as five calls to action rather than as a way
   of getting around. Keeps the solid footprint so switching moves nothing. */
.pb-btns.pb-btn-ghost .pb-btn {
  background: none; color: var(--muted);
  border: 1px solid transparent; padding: 10px 19px;
}
.pb-btns.pb-btn-ghost.pb-btn-small .pb-btn { padding: 7px 14px; }
.pb-btns.pb-btn-ghost.pb-btn-large .pb-btn { padding: 14px 27px; }
.pb-btns.pb-btn-ghost .pb-btn:hover { color: var(--text); background: var(--surface); opacity: 1; }

/* A menu's entries, indented under the button that opens them, so the nesting
   is visible in the editor as well as on the page. */
.pb-menu-edit {
  margin: 0 0 12px 18px; padding: 10px 0 10px 14px;
  border-left: 2px solid var(--border);
}
.pb-menu-edit .tp-hint { margin-bottom: 8px; }

/* ── The block editor ── */
.pb-block {
  position: relative;
  border: 1px solid var(--border); border-radius: 3px;
  background: var(--bg-2); padding: 14px; margin-bottom: 10px;
  transition: border-color 0.16s var(--ease);
}
/* The selected card, which is the same section the preview has outlined.
   A left rail rather than a full ring: the cards sit in a narrow column where a
   ring on all four sides reads as a box inside a box, and the rail lines up
   down the list so which one is live is legible while scrolling past. */
.pb-block.on { border-color: var(--border-hi); }
.pb-block.on::before {
  content: ''; position: absolute; left: -1px; top: -1px; bottom: -1px;
  width: 2px; background: var(--accent); border-radius: 3px 0 0 3px;
}
.pb-block-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-bottom: 12px;
}
.pb-block-head > span {
  font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--dim);
}
.pb-tools { display: flex; align-items: center; gap: 5px; }
/* ── A fixed width, on buttons that are not all one width ──────────────
   This was `width: 24px`, sized for the two arrows. "Reuse" is a five-letter
   word in the same row and inherited the same 24px box with `overflow`
   visible, so its label spilled out of its own border and collided with
   REMOVE next to it — ten times over on a page with ten sections, reading as
   one smudged control rather than two.

   `min-width` keeps the arrows square and lets a word size to its word, so the
   next button with a label in it cannot reintroduce this. */
.pb-tools button {
  background: transparent; border: 1px solid var(--border); border-radius: 2px;
  color: var(--muted); cursor: pointer; min-width: 24px; height: 20px;
  font-size: 10px; line-height: 1; padding: 0 5px; transition: 0.15s var(--ease);
}
.pb-tools button:hover:not(:disabled) { border-color: var(--border-hi); color: var(--text); }
.pb-tools button:disabled { opacity: 0.3; cursor: default; }
/* ── A section that is kept but not published ─────────────────────────────
   Dimmed rather than hidden in the list: the whole point of the flag is that
   the work is still there, so the row has to stay legible and editable. The
   dashed edge is what stops a faded card reading as "loading". */
.pb-block.is-hidden { opacity: .55; border-style: dashed; }
.pb-block.is-hidden:hover, .pb-block.is-hidden.on { opacity: 1; }

/* States, not actions. A button labelled "Hide" on a section that is already
   hidden is a coin toss, so it says which way round things are and the title
   says what pressing it does. */
.pb-eye { font-family: var(--mono); letter-spacing: .04em; }
.pb-eye.off { color: var(--warn); border-color: var(--warn); }

.pb-x {
  background: transparent; border: none; cursor: pointer; color: var(--dim);
  font-family: var(--mono); font-size: 0.58rem; letter-spacing: 0.1em;
  text-transform: uppercase; width: auto !important; height: auto !important;
  padding: 4px 6px !important;
}
.pb-x:hover { color: #ff6b5e; }

.pb-field { display: block; margin-bottom: 10px; }
/* A form label. It was 0.58rem tracked uppercase mono in the dim tone, which
   made "Display name" and "Bio" the least legible text on a panel whose whole
   job is telling you which box is which. */
.pb-field > span {
  display: block; margin-bottom: 6px;
  font-family: var(--sans); font-size: 0.8125rem; font-weight: 500;
  letter-spacing: 0; text-transform: none; color: var(--muted);
}
.pb-field input, .pb-field textarea, .pb-field select {
  width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 2px; padding: 9px 11px;
  font-family: var(--sans); font-size: 0.88rem; outline: none; resize: vertical;
}
.pb-field textarea { line-height: 1.65; }
.pb-field input:focus, .pb-field textarea:focus, .pb-field select:focus { border-color: var(--accent); }

/* Label, what it does, and the thing itself, stacked.

   A button now says what it is before it says where it goes, so the row grew
   from two fields to three. The first attempt gave it columns above a 1200px
   *viewport*, which is the wrong thing to measure: this row lives in a fixed
   330px panel, so a wide window widened nothing and simply squeezed three
   fields into 300px, truncating "Open a link" to "Ope" and the address to
   "https:/". The panel does not get wider, so the row stacks, always. */
.pb-btn-row { display: grid; gap: 8px; grid-template-columns: 1fr; align-items: end; }
.pb-btn-row .pb-x { justify-self: start; }

/* The look controls, under the content fields and visibly a different kind of
   thing: content above the line, appearance below it. */
.pb-look {
  /* auto-fit measures the container, so this one is correct at any panel
     width without a breakpoint: two across in the panel, more if it grows. */
  display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);
}
.pb-look .pb-field { margin-bottom: 0; }
.pb-add { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }
.pb-add .acct-btn { padding: 7px 12px; font-size: 0.6rem; }

/* A button that is a sentence, not a control. Used where a hint points at
   something elsewhere in the editor. */
.pb-linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: var(--accent);
  border-bottom: 1px solid var(--accent-soft);
}
.pb-linkish:hover { border-bottom-color: var(--accent); }
/* The one way to add a section from the end of the list, so it reads as the
   primary action of this pane rather than as one of eight equal buttons. */
.pb-add-main { width: 100%; justify-content: center; padding: 12px !important; }

/* ── Adding a section between two others ──
   Invisible until the row is approached, because a page of eight sections has
   seven of these and drawing them all would be a ladder of plus signs down the
   middle of the editor. Kept at full opacity for a keyboard, which cannot
   hover — a control only reachable by pointer is not reachable. */
.pb-insert {
  position: relative; height: 10px; margin: 0 0 4px;
  display: flex; align-items: center; justify-content: center;
}
.pb-insert::before {
  content: ''; position: absolute; left: 0; right: 0; height: 1px;
  background: var(--accent); opacity: 0; transition: opacity 0.15s var(--ease);
}
.pb-insert button {
  position: relative; width: 20px; height: 20px; padding: 0; line-height: 1;
  border-radius: 50%; border: 1px solid var(--border); background: var(--bg);
  color: var(--dim); cursor: pointer; font-size: 13px;
  opacity: 0; transition: 0.15s var(--ease);
}
.pb-insert:hover::before, .pb-insert:focus-within::before { opacity: 0.5; }
.pb-insert:hover button, .pb-insert:focus-within button { opacity: 1; }
.pb-insert button:hover { border-color: var(--accent); color: var(--accent); }
.pb-insert button:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 2px; }

/* Images are chosen from what has been uploaded, never typed as a key. */
.pb-pick, .pb-lib {
  display: grid; gap: 7px; margin-bottom: 12px;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
}
.pb-lib { margin: 14px 0 0; }
.pb-thumb {
  padding: 0; cursor: pointer; background: none; border-radius: 2px; overflow: hidden;
  border: 2px solid transparent; aspect-ratio: 1; transition: border-color 0.15s var(--ease);
}
.pb-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pb-thumb:hover { border-color: var(--border-hi); }
.pb-thumb.on { border-color: var(--accent); }
.pb-libitem { position: relative; aspect-ratio: 1; border-radius: 2px; overflow: hidden; border: 1px solid var(--border); }
.pb-libitem img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pb-libdel {
  position: absolute; top: 3px; right: 3px; width: 20px; height: 20px;
  border-radius: 50%; border: none; cursor: pointer; line-height: 1;
  background: rgba(10, 10, 12, 0.78); color: #fff; font-size: 14px;
}
.pb-libdel:hover { background: #ff6b5e; }
.pb-pick-hint {
  grid-column: 1 / -1; margin: 0 0 2px;
  font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--dim);
}
.pb-pick-hint.warn { color: var(--accent); }

/* ── Pages & hosting ──────────────────────────────────────────────────────
   The public page describing the paid tiers. Prices come from the CMS through
   /api/account/config, so this page and the account page cannot disagree about
   what a subscription costs. */
.hosting-wrap { max-width: var(--max); margin: 0 auto; padding: 0 28px 90px; }
.hosting-lede {
  color: var(--muted); font-size: 1.02rem; line-height: 1.75;
  max-width: 58ch; margin-top: 14px;
}

.host-tiers {
  display: grid; gap: 16px; margin-top: 46px; grid-template-columns: 1fr;
  align-items: start;
}
/* On /hosting the paid tiers are appended straight into the grid; on /pricing
   they arrive inside their own container, because the free tier is written
   into the markup beside them. `display: contents` makes that container carry
   no layout of its own, so its children are grid items either way and the two
   pages lay out identically from one renderer. */
#paidTiers { display: contents; }
@media (min-width: 860px) { .host-tiers { grid-template-columns: repeat(3, 1fr); } }

.host-tier {
  position: relative; overflow: hidden;
  border: 1px solid var(--border); border-radius: 3px;
  background: var(--surface); padding: 26px 22px 22px;
  display: flex; flex-direction: column; height: 100%;
}
/* The paid tier is the one being sold, so it gets the saturated edge the rest
   of the site uses to mean "this one". */
.host-tier.featured { border-color: var(--border-hi); background: var(--surface-2); }
.host-tier.featured::before {
  content: ''; position: absolute; inset: 0 0 auto; height: 2px;
  background: linear-gradient(90deg, var(--accent) 0%, var(--accent) 40%,
              var(--accent-soft) 40%, var(--accent-soft) 100%);
}
.host-eyebrow {
  display: block; font-family: var(--mono); font-size: 0.62rem;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--dim);
}
.host-price {
  font-family: var(--serif); letter-spacing: -0.03em; color: var(--text);
  margin-top: 12px; line-height: 1;
}
.host-price strong { font-size: 2.3rem; font-weight: 500; }
.host-per { font-family: var(--sans); font-size: 0.9rem; color: var(--muted); margin-left: 4px; }
.host-alt { color: var(--accent); font-size: 0.82rem; margin-top: 8px; }
.host-addr { margin-top: 14px; }
.host-addr code {
  font-family: var(--mono); font-size: 0.72rem; color: var(--muted);
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: 2px; padding: 5px 9px; display: inline-block; word-break: break-all;
}
.host-feats { list-style: none; margin: 20px 0 0; padding: 0; }
.host-feats li {
  position: relative; padding-left: 20px; margin-bottom: 9px;
  color: var(--muted); font-size: 0.88rem; line-height: 1.55;
}
.host-feats li::before {
  content: ''; position: absolute; left: 4px; top: 0.55em;
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent); opacity: 0.75;
}
.host-cta { margin-top: auto; justify-content: center; text-align: center; }
.host-tier .host-cta { margin-top: 22px; }
.host-note { color: var(--dim); font-size: 0.74rem; line-height: 1.6; margin-top: 12px; }

.host-faq { margin-top: 64px; }
.host-faq h2 {
  font-family: var(--serif); font-weight: 400; letter-spacing: -0.02em;
  font-size: clamp(1.4rem, 3vw, 1.8rem); color: var(--text); margin-bottom: 22px;
}
.host-faq dl { display: grid; gap: 0; }
.host-faq dt {
  color: var(--text); font-size: 0.95rem; font-weight: 500;
  padding-top: 18px; border-top: 1px solid var(--border);
}
.host-faq dd {
  color: var(--muted); font-size: 0.9rem; line-height: 1.7;
  margin: 7px 0 18px; max-width: 62ch;
}
.host-foot {
  margin-top: 48px; padding-top: 22px; border-top: 1px solid var(--border);
  color: var(--dim); font-size: 0.8rem; line-height: 1.7;
}
.host-foot a { color: var(--muted); border-bottom: 1px solid var(--border-hi); text-decoration: none; }
.host-foot a:hover { color: var(--text); }

/* ── The plan page ── */
/* Three plans, so a two-column cap left the third one orphaned on its own row
   next to an empty cell — and inside the wide shell each card came out about
   250px wide with 750px of nothing beside them.

   auto-fit rather than a column count: three across when there is room for
   three, two when there is not, one on a phone, and it stays right if a fourth
   plan is ever added. */
.plan-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  align-items: stretch;
}
/* Full height, so the row is a row rather than three cards of different
   lengths — and `margin-top: auto` on the CTA below can then put every Pay
   button on the same line whatever the feature list above it does. */
.plan-card { display: flex; flex-direction: column; height: 100%; }
.plan-card .host-cta { margin-top: auto; }
.plan-card .host-price { margin-top: 10px; }
.plan-card .host-price strong { font-size: 1.9rem; }
/* PayPal renders an iframe here. It needs a light backing of its own or the
   silver button disappears into a dark theme. */
.plan-slot {
  margin-top: auto; padding-top: 16px; min-height: 52px;
}
/* The correction that has to be read before the feature list, not after it. */
.host-clarify {
  margin-top: 14px; padding: 11px 13px; border-radius: 2px;
  background: var(--bg-2); border: 1px solid var(--border);
  color: var(--muted); font-size: 0.8rem; line-height: 1.6;
}
.host-clarify strong { color: var(--text); }
.host-feats code {
  font-family: var(--mono); font-size: 0.78em; color: var(--text);
}

/* Test mode has to be impossible to miss — the whole risk is somebody thinking
   they paid when they did not, or thinking they did not when they have. */
.sandbox-banner {
  display: flex; flex-direction: column; gap: 4px;
  margin-bottom: 22px; padding: 13px 16px; border-radius: 2px;
  background: rgba(232, 163, 60, 0.1); border: 1px solid rgba(232, 163, 60, 0.4);
}
.sandbox-banner strong {
  font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: #e8a33c;
}
.sandbox-banner span { color: var(--muted); font-size: 0.82rem; line-height: 1.6; }
.twofa-gate { border-color: var(--border-hi); margin-bottom: 16px; }
/* A real photo where the monogram was. Same size and shape, so a page does not
   jump when somebody adds one. */
.pf-photo {
  width: 72px; height: 72px; margin: 4px auto 16px; display: block;
  border-radius: 50%; object-fit: cover;
  border: 1px solid var(--border-hi); background: var(--surface-2);
}
.look-row { display: flex; gap: 16px; align-items: center; }
.look-photo { flex: none; width: 72px; height: 72px; }
.look-photo img {
  width: 72px; height: 72px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--border-hi);
}
.look-controls { flex: 1; min-width: 0; }
.look-grid { display: grid; gap: 14px; margin-top: 16px; grid-template-columns: 1fr; }
@media (min-width: 560px) { .look-grid { grid-template-columns: 1fr 1fr; } }
.look-grid input[type="color"] {
  width: 46px; height: 34px; padding: 0; cursor: pointer;
  background: transparent; border: 1px solid var(--border-hi); border-radius: 2px;
}



/* ── "What this means" ────────────────────────────────────────────────────
   A <details>, so it works with JavaScript off and needs no tooltip library.
   A tooltip would also be the wrong shape: this is several paragraphs, and the
   custom-domain one exists because that tier was being read as "a domain for
   $11 a month". */
.host-info { margin: 16px 0 0; }
.host-info summary {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  list-style: none; color: var(--muted);
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em;
  text-transform: uppercase; transition: color 0.16s var(--ease);
}
.host-info summary::-webkit-details-marker { display: none; }
.host-info summary:hover { color: var(--text); }
.host-i {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%; flex: none;
  border: 1px solid var(--border-hi); color: var(--muted);
  font-family: var(--serif); font-size: 0.7rem; font-style: italic;
  text-transform: none; letter-spacing: 0;
}
.host-info summary:hover .host-i { border-color: var(--accent); color: var(--accent); }
.host-info-body {
  margin-top: 12px; padding: 14px; border-radius: 2px;
  background: var(--bg-2); border: 1px solid var(--border);
}
.host-info-body p { color: var(--muted); font-size: 0.82rem; line-height: 1.7; }
.host-info-body p + p { margin-top: 10px; }
.host-info-body strong { color: var(--text); }
.host-info-body code {
  font-family: var(--mono); font-size: 0.76em; color: var(--text);
  background: var(--surface-2); border-radius: 2px; padding: 1px 5px;
}


/* A tier card carries its own monthly/yearly switch, so the page shows three
   things to buy rather than six. */
.plan-card .tp-modes { margin: 14px 0 4px; align-self: flex-start; }
.plan-card .tp-mode { padding: 6px 14px; }
.plan-card .host-feats { margin-top: 14px; }
.plan-card .host-feats li { font-size: 0.84rem; }

/* ── The account menu ──────────────────────────────────────────────────────
   "Account" in the nav was a link to one page, which meant every other part of
   an account — the plan, the security settings, the page builder — was
   reachable only by landing on that page first and reading a list of buttons.

   So it is a disclosure instead. Click opens it, click closes it, and it stays
   open in between: the items are destinations rather than a menu being skimmed,
   and one that collapses on the first stray pointer move loses the click that
   was on its way. Escape and an outside click close it, because a panel with no
   way out but the button that opened it is a trap on a touchscreen.

   Built as a real <button> with aria-expanded rather than a styled div. The
   state is announced rather than only drawn, and the keyboard gets it free. */
.nav-menu { position: relative; display: inline-flex; }

.nav-menu-btn {
  display: inline-flex; align-items: center; gap: 7px;
  background: transparent; border: 1px solid transparent; border-radius: 2px;
  color: var(--muted); cursor: pointer; padding: 6px 9px;
  font-family: var(--sans); font-size: 13px; letter-spacing: .01em;
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
.nav-menu-btn:hover { color: var(--text); }
.nav-menu-btn[aria-expanded="true"] { color: var(--text); border-color: var(--border); }

/* Rotates to point at the panel it opened. The only motion here, and it is
   doing a job: it is the one part of the button that says "still open". */
.nav-menu-btn .chev {
  width: 9px; height: 9px; flex: none;
  transition: transform .22s var(--ease);
}
.nav-menu-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .nav-menu-btn .chev { transition: none; } }

.nav-menu-panel {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 300;
  min-width: 194px; padding: 5px;
  background: var(--nav-bg-open);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--border); border-radius: 3px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .5);
  display: flex; flex-direction: column;
}
.nav-menu-panel[hidden] { display: none; }

/* Rectangles, deliberately. A rounded pill reads as a button you press once;
   these are rows in a list of places, and the square corners say so. */
.nav-menu-panel a, .nav-menu-panel button {
  display: block; width: 100%; text-align: left;
  background: transparent; border: 0; border-radius: 2px; cursor: pointer;
  color: var(--muted); text-decoration: none;
  font-family: var(--sans); font-size: .875rem; letter-spacing: 0;
  text-transform: none; padding: 9px 12px;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.nav-menu-panel a:hover, .nav-menu-panel button:hover,
.nav-menu-panel a:focus-visible, .nav-menu-panel button:focus-visible {
  background: var(--surface-2); color: var(--text); outline: none;
}
/* Where you already are. Marked rather than removed: a menu whose contents
   change depending on the page is a menu you have to re-read every time. */
.nav-menu-panel a[aria-current="page"] { color: var(--text); background: var(--surface); }
.nav-menu-panel a[aria-current="page"]::before {
  content: ''; display: inline-block; vertical-align: middle;
  width: 3px; height: 3px; margin-right: 8px; margin-top: -2px;
  background: var(--accent);
}
/* Signing out is not navigation, and the hairline says so before the label is
   read. */
/* `.nav-grown` and `.nav-menu-label` used to live here.

   `.nav-grown` hid a nav item that JavaScript had replaced with a menu — the
   Launcher link and the Arcade link, both left in the markup so `renderPages`
   could still find and hide them. Nothing is replaced at runtime any more: the
   menus are in the markup, so there is no second copy to hide. (The empty
   Arcade `<li>` that class left behind is also why every page carried a 24px
   hole in the nav; `#nav ul` has `gap: 12px` and an emptied item still takes
   one.)

   `.nav-menu-label` was a group heading inside a panel — "Pages", "Live sites",
   "Free tools", "See what it can do". The last of those was the Tools menu
   apologising for containing the lab. With the lab moved to Built and the
   Launcher menu no longer reprinting /launcher, no panel needs headings: the
   separators carry the grouping and every panel is short enough to read. */

.nav-menu-sep { height: 1px; background: var(--border); margin: 5px 3px; }

@media (max-width: 780px) {
  /* Inside the open burger the nav is already a vertical stack, so a floating
     panel over it would be a second layer for no gain. It becomes an indented
     part of the same list. */
  .nav-menu { display: block; width: 100%; }
  .nav-menu-btn { padding: 0; font-size: 13px; }
  .nav-menu-btn[aria-expanded="true"] { border-color: transparent; }
  .nav-menu-panel {
    position: static; min-width: 0; margin-top: 12px; padding: 0 0 0 13px;
    background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none;
    border: 0; border-left: 1px solid var(--border); border-radius: 0;
    box-shadow: none; gap: 2px;
  }
  /* ── Something a thumb can hit ──
     `padding: 9px 0` on 13px text is a target about 31px tall. The figure
     everyone quotes is 44, and these are the rows somebody is trying to hit
     while holding a phone in one hand — the sub-items of a menu, which are
     also the narrowest. The gap between them stays small so the group still
     reads as one list rather than as six separate buttons. */
  /* ── Every group open, not just the one written in HTML ──
     Services is static markup and follows the rule above. Launcher, Tools and
     Account are built by app.js through `wireMenu`, which starts them
     `hidden` — so in the drawer one group was sprawled open and three were
     collapsed behind a tap, which is the inconsistency that made the menu feel
     broken. The rule was already decided three hundred lines up; these did not
     know about it.

     Done in script rather than here. The first attempt was
     `#nav ul.open .nav-menu-panel[hidden] { display: flex }`, which loses to
     the global `[hidden] { display: none !important }` on line 1235 — and
     winning it with another `!important` would have been worse, because
     `hidden` is semantic: the links would have been on screen and still hidden
     from a screen reader. The attribute is removed instead, so they are
     genuinely not hidden. See `navToggle` in app.js.

     A chevron that never changes state is furniture. */
  #nav ul.open .nav-menu-btn .chev { display: none; }

  /* ── One list, one indent ──
     The base `.nav-menu-panel` is a floating dropdown: shrink-to-fit, its own
     background, a 44px drop shadow. The mobile block above flattens it, and
     the JS-built groups kept the shadow and their shrink-to-fit width anyway —
     so the drawer showed four little cards at four different left edges (28,
     115, 115, 154) instead of one indented list.

     Scoped to the open drawer and stated in full rather than patched, because
     the base rule is a floating panel and this is not one. */
  #nav ul.open .nav-menu { display: block; width: 100%; }
  #nav ul.open .nav-menu-panel {
    position: static; width: auto; min-width: 0; max-width: none;
    background: transparent; box-shadow: none;
    backdrop-filter: none; -webkit-backdrop-filter: none;
    border: 0; border-left: 1px solid var(--border); border-radius: 0;
    margin: 6px 0 0; padding: 0 0 0 14px; gap: 0;
  }

  .nav-menu-panel a, .nav-menu-panel button {
    min-height: 44px; display: flex; align-items: center; padding: 0;
  }
  .nav-menu-panel a:hover, .nav-menu-panel button:hover { background: transparent; }

  /* The top-level rows too. They were relying on the 20px gap for spacing,
     which is space between targets rather than target. */
  /* Not `> li >`: the JS-built groups wrap their trigger in a `.nav-menu`
     div, so a child selector matched Work and Contact at 44px and left
     Launcher, Tools and Account at 26. */
  #nav ul.open li > a,
  #nav ul.open .nav-menu-btn {
    min-height: 44px; display: flex; align-items: center;
  }
  #nav ul.open > li > a.cta { display: inline-flex; justify-content: center; }
  .nav-menu-sep { display: none; }
}

/* ── The account pages ─────────────────────────────────────────────────────
   Profile, Billing and Settings. They share a shape: a page header, then cards
   of the same kind the theme playground and the page editor already use, so
   the account area reads as one place rather than four screens that happened
   to be built by the same person.

   Everything here is built from the existing tokens. There is no new colour
   and no new radius — the account is not a different product. */
.ac-wrap { max-width: 660px; margin: 0 auto; padding: 0 20px 90px; }

/* The masthead: a mark, the name, and the one line that says what this is. */
.ac-head { display: flex; gap: 20px; align-items: center; margin-bottom: 32px; flex-wrap: wrap; }

/* Square, not round. A circle reads as a person's face; this is as often a
   business, and a business mark that has been cropped to a circle has usually
   been cropped badly. */
.ac-logo {
  width: 84px; height: 84px; flex: none; position: relative;
  border: 1px solid var(--border); border-radius: 3px;
  background: var(--bg-2); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.ac-logo img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The fallback is initials rather than a silhouette icon. It is specific to
   the account, so it stops looking like a missing image. */
.ac-logo .mono {
  font-family: var(--mono); font-size: 1.5rem; letter-spacing: 0.04em;
  color: var(--dim);
}
.ac-head-text { min-width: 0; flex: 1; }
.ac-head-text h1 {
  font-family: var(--serif); font-weight: 500;
  font-size: clamp(1.6rem, 4vw, 2.1rem); line-height: 1.08;
  letter-spacing: -0.03em; margin: 6px 0 6px; word-break: break-word;
}
.ac-head-text p { color: var(--muted); font-size: 0.9rem; }

/* A tier badge. Small, and the only place on these pages an accent fill
   appears — so it is the thing the eye lands on, which is correct: what you
   are paying for is the question the page answers. */
.ac-tag {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.13em;
  text-transform: uppercase; padding: 4px 10px; border-radius: 99px;
  border: 1px solid var(--border); color: var(--muted);
}
.ac-tag.paid { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.ac-tag.warn { border-color: rgba(255, 107, 94, 0.35); color: #ff6b5e; }
.ac-tag .dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }

/* A section. Same plate as .tp-card, named separately so the two can diverge
   later without one dragging the other with it. */
.ac-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 3px; padding: 22px 24px; margin-top: 18px;
}
/* A section heading is a heading. It was set as tracked uppercase mono in the
   dim tone — the treatment Cloudflare's docs use exactly once, on a throwaway
   "was this helpful?" label — which made Password, Two-factor and Your data
   read as metadata rather than as the names of the things below them. The
   markup already says "Password"; only this rule was shouting it. */
.ac-card > h2 {
  font-family: var(--sans); font-size: 0.9375rem; font-weight: 600;
  letter-spacing: -0.01em; text-transform: none; color: var(--text);
  margin-bottom: 6px;
}
.ac-card > h2 + .ac-sub { margin-bottom: 16px; }
.ac-sub { color: var(--muted); font-size: 0.84rem; line-height: 1.6; margin-bottom: 14px; }

/* A read-only fact with an edit affordance beside it. The row is the same
   .acct-row used on the old account page, extended so the value can carry a
   control without the label moving. */
.ac-row {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 0; border-bottom: 1px solid var(--border);
}
.ac-row:last-child { border-bottom: none; }
.ac-row .k {
  font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--dim); min-width: 116px; flex: none;
}
.ac-row .v { color: var(--text); font-size: 0.9rem; word-break: break-word; flex: 1; min-width: 0; }
/* Absent is a state, not an error. Said in the dim tone and in words that
   offer the next step rather than reporting a gap. */
.ac-row .v.none { color: var(--dim); }
.ac-row .act { flex: none; }

/* An inline edit: the field replaces the value in place rather than opening a
   dialog, because these are one-line facts and a modal for a one-line fact is
   three clicks to change six characters. */
.ac-field {
  display: flex; gap: 9px; align-items: center; flex-wrap: wrap; width: 100%;
}
.ac-field input {
  flex: 1; min-width: 150px; background: var(--bg-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 2px; padding: 9px 11px;
  font-family: var(--sans); font-size: 0.88rem; outline: none;
}
.ac-field input:focus { border-color: var(--accent); }
.ac-field input::placeholder { color: var(--dim); }

.ac-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }

/* A quiet link out of a card — "manage this elsewhere". Not a button, because
   it is not the thing the card is for. */
.ac-more {
  display: inline-block; margin-top: 14px; color: var(--muted);
  font-size: 0.82rem; text-decoration: none;
  border-bottom: 1px solid rgba(255, 122, 47, 0.35);
}
.ac-more:hover { color: var(--accent); }

/* ── Billing ──
   The cycle, drawn rather than described. A bar is the only part of a billing
   page somebody reads without effort, and "how much of what I paid for is
   left" is the question they came with. */
.ac-cycle { margin: 4px 0 6px; }
.ac-cycle-bar {
  height: 5px; border-radius: 99px; background: var(--surface-2);
  overflow: hidden; margin: 12px 0 10px;
}
.ac-cycle-bar i { display: block; height: 100%; background: var(--accent); border-radius: 99px; }
/* Running out is worth saying in the colour as well as the number. */
.ac-cycle-bar.low i { background: #ff6b5e; }
.ac-cycle-ends {
  display: flex; justify-content: space-between; gap: 12px;
  font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--dim);
}

/* Add-ons. Listed as unavailable rather than hidden, because a paid tier with
   nothing to show reads as a broken page — and knowing what is coming is part
   of what the tier is. */
.ac-addons { display: grid; gap: 10px; grid-template-columns: 1fr; margin-top: 4px; }
@media (min-width: 560px) { .ac-addons { grid-template-columns: 1fr 1fr; } }
.ac-addon {
  border: 1px solid var(--border); border-radius: 3px; padding: 14px 15px;
  background: var(--bg-2);
}
.ac-addon strong { display: block; color: var(--text); font-size: 0.88rem; font-weight: 500; }
.ac-addon span { display: block; color: var(--muted); font-size: 0.78rem; line-height: 1.55; margin-top: 4px; }
.ac-addon em {
  display: inline-block; margin-top: 9px; font-style: normal;
  font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--dim);
}
.ac-addon.soon { opacity: 0.62; }

/* A card the page has just scrolled somebody to. Emphasis for a moment, not a
   selected state: they may well want the longer pass this time, and a card
   that stays highlighted reads as one that has already been chosen. */
.plan-card.pass-focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 0 36px -8px rgba(255, 122, 47, 0.4);
  transition: box-shadow 0.4s var(--ease), border-color 0.4s var(--ease);
}
@media (prefers-reduced-motion: reduce) { .plan-card.pass-focus { transition: none; } }

/* An address on a plan this account does not have. Shown rather than hidden —
   an upgrade nobody can see is an upgrade nobody buys — but visibly out of
   reach, and carrying the price rather than the word "locked".

   Dimmed part by part rather than with `opacity` on the row. Opacity creates a
   stacking context and composites the whole subtree, so the "Get it" button
   could not have been brighter than its container however it was styled — and
   that button is the one thing on the row that must stay legible, because it
   is the way out of the state the row is in. */
.pf-domain.locked { cursor: default; background: transparent; }
.pf-domain.locked:hover { border-color: var(--border); }
.pf-domain.locked strong { color: var(--dim); }
.pf-domain.locked em { color: var(--muted); }
.pf-domain.locked input { opacity: 0.35; }
.pf-domain-buy {
  flex: none; align-self: center; text-decoration: none;
  border: 1px solid var(--accent); border-radius: 2px; color: var(--accent);
  padding: 6px 12px; font-family: var(--mono); font-size: 0.6rem;
  letter-spacing: 0.12em; text-transform: uppercase; white-space: nowrap;
  transition: 0.16s var(--ease);
}
.pf-domain-buy:hover { background: var(--accent); color: var(--on-accent); }

/* ── A light palette on a site drawn for a dark one ────────────────────────
   Everything decorative here was made for #0a0a0c: the plants photograph sits
   under a 72% black scrim, and the grain overlay is dark noise at 4%. Both are
   invisible on the default background and both are a grey film over a white
   one — which is exactly the complaint somebody has after choosing a light
   theme precisely because they wanted a plain page.

   `data-scheme` is stamped on <html> by app.js from the *resolved* background
   colour, so this reacts to any light theme — a preset, a shared link, or one
   somebody built themselves in the playground — rather than to a list of
   theme names that would go out of date on the first save. */

/* The photograph goes. Not dimmed, not re-tinted: on a light page it is a
   large dark rectangle behind the text, and there is no opacity at which that
   becomes an improvement. */
:root[data-scheme="light"] #page-bg { display: none !important; }

/* And so does the hero's video, for exactly the same reason — which the rule
   above should have covered from the start and did not.
   `#page-bg` is the photograph behind the *inner* pages; the home page's motion
   loop is a separate element, so choosing a light theme dropped one and left
   the other playing. That is the "the gif still plays behind the front page"
   report: half the imagery went, which reads as a bug rather than a choice.

   `display: none` rather than `visibility` or opacity: a hidden `<video>` that
   is still laid out keeps decoding frames, and the point of a plain page is
   partly that it is not doing that. app.js pauses the element as well. */
:root[data-scheme="light"] .hero-media { display: none !important; }

/* The scrim goes with it.
   `.hero::before` is two hardcoded near-black gradients whose entire job is
   making white text legible over moving video. With no video under it, it is a
   dark sheet over a light page — and it was the other half of "the appearance
   tab does not actually change the whole page": the palette changed everywhere
   except the one screen most people land on.
   Keyed to the imagery rather than to the palette, because that is the actual
   dependency: no picture, nothing to scrim. */
:root[data-scheme="light"] .hero::before,
:root[data-media="off"] .hero::before { display: none; }

/* Driven by the custom property as well as by the stamped attribute, and that
   redundancy is the point: the inline script in every page's <head> applies a
   saved theme's variables *before first paint*, but `data-media` is stamped by
   app.js, which is deferred. Without this the imagery would paint and then
   vanish a moment later — which is precisely the flash the inline script exists
   to prevent. Opacity hides it on the first frame; the attribute rule below
   removes it from layout and stops the video decoding. */
.hero-media, #page-bg, .hero::before { opacity: var(--bg-media); }

/* ── Imagery off, on any palette ──
   The rules above key off the *resolved background*, which is the right
   default — a light page and a dark photograph do not go together. But it
   makes "I want a plain page" reachable only by also wanting a light one, and
   those are two different preferences.

   `--bg-media: 0` in a theme stamps `data-media="off"`, and that turns the
   imagery off at any brightness. app.js does the stamping, from the same
   resolved-value probe it uses for the scheme. */
:root[data-media="off"] #page-bg,
:root[data-media="off"] .hero-media { display: none !important; }

/* With nothing behind it the hero has to hold the top of the page on its own,
   so it stops reserving room for a picture that is not there. */
:root[data-media="off"] .hero { min-height: auto; padding-top: 96px; padding-bottom: 72px; }

/* The grain is kept and halved. It is what stops large flat areas looking like
   an untextured export, and at 2% on white it reads as paper rather than as
   dirt. Removing it entirely made the light themes look unfinished next to the
   dark ones. */
:root[data-scheme="light"] body::after { opacity: 0.02; }

/* The nav's blur sits over content rather than over a photograph now, so it
   needs a real edge to separate it — on a dark page the shadowed backdrop did
   that job on its own. */
:root[data-scheme="light"] #nav.scrolled,
:root[data-scheme="light"] #nav.solid { border-bottom-color: var(--border-hi); }

/* The account menu's drop shadow was tuned against a near-black page. At the
   same strength on white it reads as a smudge. */
:root[data-scheme="light"] .nav-menu-panel {
  box-shadow: 0 14px 34px rgba(20, 18, 16, 0.14);
}

/* Colour inputs and the like get a visible chrome outline in light mode; this
   keeps the site's own hairline in charge of the edge. */
:root[data-scheme="light"] .ac-logo,
:root[data-scheme="light"] .ac-card,
:root[data-scheme="light"] .tp-card { border-color: var(--border); }

/* Announced as well as drawn, so form controls, scrollbars and the browser's
   own focus rings come from the light set rather than the dark one. Without
   this a white page still gets a dark native date picker and a dark
   scrollbar.

   ── Only for the light theme, and that is not an oversight ──
   The matching `color-scheme: dark` on the default theme is deliberately
   absent. Declaring it broke the PayPal checkout: when an embedding document
   declares a colour scheme, the browser refuses to leave a cross-origin iframe
   transparent if that iframe's own document does not support the same scheme —
   it paints an opaque canvas behind it instead, to guarantee the content stays
   readable. PayPal's button frame is light-only, so the result was a white slab
   behind the buttons inside a dark modal.

   The dark theme never needed the declaration: this site painted its own dark
   surfaces long before any of this existed, and the native controls it uses are
   styled directly. So the rule that buys nothing and costs a checkout is the
   one that goes. */
:root[data-scheme="light"] { color-scheme: light; }

/* Belt and braces for the same problem, in case a colour scheme is ever
   declared again further up. Payment frames are third-party, light-only, and
   must be left to composite themselves. */
#ppButtons, #ppButtons iframe,
.book-paypal, .book-paypal iframe,
.pay-cf-field, .pay-cf-field iframe { color-scheme: light; }

/* ── Dragging a section into place ─────────────────────────────────────────
   The grip is the only draggable part of a block. The card itself is full of
   text inputs, and a card you can drag by its body is a card where selecting a
   word moves the section instead. */
.pb-grip {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; margin-right: 2px; padding: 0;
  background: transparent; border: 0; border-radius: 2px;
  color: var(--dim); cursor: grab;
  /* Without this the browser claims vertical drags for scrolling and the
     gesture never reaches the pointer handlers on a touchscreen. */
  touch-action: none;
  transition: color 0.15s var(--ease), background 0.15s var(--ease);
}
.pb-grip:hover { color: var(--text); background: var(--surface-2); }
.pb-grip:active { cursor: grabbing; }
.pb-grip svg { width: 10px; height: 16px; pointer-events: none; }
.pb-grip:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }

/* The card in flight. Lifted off the page and following the pointer, with
   pointer-events off so it cannot swallow the moves that are positioning it. */
.pb-block.dragging {
  position: relative; z-index: 20; cursor: grabbing;
  pointer-events: none;
  border-color: var(--accent);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
  /* No transition on this one: it must track the pointer exactly. */
  transition: none;
}

/* The cards making room. These do animate — the movement is what explains
   where the dragged card is about to land. */
.dragging-list .pb-block { transition: transform 0.18s var(--ease); }
.dragging-list .pb-block.dragging { transition: none; }
/* Stops the drag from selecting the text it passes over. */
.dragging-list { user-select: none; }

@media (prefers-reduced-motion: reduce) {
  .dragging-list .pb-block { transition: none; }
}

/* ── Cards and quotes, on the public page ──────────────────────────────────
   Two block types the templates needed. Every template worth having lists
   three services and carries one testimonial, and expressing that as six
   alternating heading and text blocks made the editor unusable and the page
   wrong. */
.pb-cards {
  display: grid; gap: 12px; grid-template-columns: 1fr; margin: 4px 0;
}
/* Two columns, not three. A user page is a narrow column and three cards in it
   are three slivers; two hold a readable line length at every width the page
   actually gets. */
@media (min-width: 560px) { .pb-cards { grid-template-columns: 1fr 1fr; } }
.pb-card {
  border: 1px solid var(--border); border-radius: 3px;
  padding: 15px 16px; background: var(--surface);
}
.pb-card strong {
  display: block; color: var(--text); font-size: 0.94rem;
  font-weight: 600; margin-bottom: 5px;
}
.pb-card p { color: var(--muted); font-size: 0.87rem; line-height: 1.65; }
.pb-card a { color: var(--accent); text-decoration: none; border-bottom: 1px solid var(--accent-soft); }

/* A real blockquote with a real cite. The accent rule down the side does the
   work a pair of quotation marks would, without the glyphs turning up in the
   middle of somebody's sentence when they quote something themselves. */
.pb-quote {
  margin: 6px 0; padding: 4px 0 4px 18px;
  border-left: 2px solid var(--accent);
}
.pb-quote blockquote {
  color: var(--text); font-size: 1rem; line-height: 1.7;
  font-family: var(--serif); font-style: italic;
}
.pb-quote figcaption {
  margin-top: 9px; color: var(--dim);
  font-family: var(--mono); font-size: 0.68rem;
  letter-spacing: 0.11em; text-transform: uppercase;
}
.pb-quote a { color: var(--accent); text-decoration: none; }

/* ── Cards, in the editor ── */
.pb-card-row {
  display: grid; gap: 9px; align-items: start;
  grid-template-columns: 1fr; padding: 11px 0;
  border-bottom: 1px solid var(--border);
}
@media (min-width: 620px) {
  .pb-card-row { grid-template-columns: 1fr 1.6fr auto; align-items: end; }
}
.pb-card-row:last-of-type { border-bottom: none; }

/* ── Starting points ──────────────────────────────────────────────────────
   Shown only while the page is empty. A template is useful when there is
   nothing to lose and clutter the moment there is. */
.pb-templates {
  display: grid; gap: 10px; grid-template-columns: 1fr; margin-bottom: 22px;
}
@media (min-width: 560px) { .pb-templates { grid-template-columns: 1fr 1fr; } }
.pb-template {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: var(--bg-2); border: 1px solid var(--border); border-radius: 3px;
  padding: 15px 16px; font-family: var(--sans);
  transition: border-color 0.16s var(--ease), background 0.16s var(--ease);
}
.pb-template:hover { border-color: var(--accent); background: var(--accent-soft); }
.pb-template:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
.pb-template:disabled { opacity: 0.5; cursor: default; }
.pb-template strong {
  display: block; color: var(--text); font-size: 0.92rem; font-weight: 500;
}
.pb-template span {
  display: block; color: var(--muted); font-size: 0.8rem;
  line-height: 1.55; margin-top: 4px;
}
.pb-template em {
  display: block; margin-top: 9px; font-style: normal;
  font-family: var(--mono); font-size: 0.6rem;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--dim);
}

/* The templates, folded away once a page has content. Styled as a real
   disclosure rather than a link so it is obvious there is something behind it. */
.pb-templates-toggle {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--sans); font-size: 0.875rem; letter-spacing: 0;
  text-transform: none; color: var(--muted); margin-bottom: 12px;
  transition: color 0.15s var(--ease);
}
.pb-templates-toggle::-webkit-details-marker { display: none; }
.pb-templates-toggle::before { content: '+'; font-size: 0.9rem; line-height: 1; }
details[open] > .pb-templates-toggle::before { content: '–'; }
.pb-templates-toggle:hover { color: var(--text); }

/* A template on a plan this account does not have. Same treatment as a locked
   address: visible, dimmed part by part rather than with opacity on the box,
   and it links to where you would go to unlock it. */
.pb-template.locked { text-decoration: none; display: block; }
.pb-template.locked strong { color: var(--muted); }
.pb-template.locked span { color: var(--dim); }
.pb-template.locked em { color: var(--accent); }
.pb-template.locked:hover { border-color: var(--accent); background: var(--accent-soft); }

/* ── The other shape a page can be ─────────────────────────────────────────
   A bio link and a business site are not the same document, and rendering both
   inside the same 520px card is why applying a "Restaurant" template produced
   a linktree with restaurant words in it. The blocks were right; the shape was
   wrong, and the shape is what somebody sees.

   Everything below overrides the card rather than replacing it. One renderer,
   two shapes — the markup is identical, so there is still exactly one place
   that decides what a block becomes. */
.pf-wrap.pf-page { max-width: 940px; padding: 56px 24px 48px; }

/* The card stops being a card. No border, no plate, no centring: a document
   reads left-aligned, and a 940px column of centred text does not read at all. */
.pf-page .pf-card {
  border: 0; background: transparent; border-radius: 0;
  padding: 0; text-align: left; overflow: visible;
}
/* The accent rule along the top belongs to a card. Without a card it is a
   line across the top of somebody's website for no reason. */
.pf-page .pf-card::before { display: none; }

/* The identity becomes a masthead rather than a profile photo. Still the same
   elements in the same order — only the arrangement changes. */
.pf-page .pf-photo,
.pf-page .pf-mark {
  margin: 0 0 20px; width: 64px; height: 64px;
}
.pf-page .pf-name {
  font-size: clamp(2rem, 5.5vw, 3.1rem); line-height: 1.04;
  letter-spacing: -0.035em; margin-bottom: 6px;
}
/* The handle is a bio-link convention. On a business page it is noise beside
   the name, so it drops to a quiet line rather than sitting under the title. */
.pf-page .pf-handle { font-size: 0.7rem; opacity: 0.55; }
.pf-page .pf-bio {
  font-size: 1.05rem; line-height: 1.7; max-width: 62ch; margin: 14px 0 0;
}

/* Sections, with room to be sections. */
.pf-page .pb-blocks { margin-top: 48px; display: flex; flex-direction: column; gap: 30px; }
.pf-page .pb-h { font-size: clamp(1.5rem, 3.4vw, 2.05rem); letter-spacing: -0.02em; }
.pf-page h3.pb-h { font-size: clamp(1.15rem, 2.4vw, 1.4rem); }
.pf-page .pb-text { font-size: 1rem; line-height: 1.8; max-width: 68ch; }
.pf-page .pb-hr { margin: 14px 0; }

/* Three across at this width, where the narrow layout only ever fits two. */
@media (min-width: 700px) {
  .pf-page .pb-cards { grid-template-columns: repeat(3, 1fr); gap: 16px; }
}
.pf-page .pb-card { padding: 20px 22px; }

/* On a page the hero escapes the column's padding exactly, so it reads as a
   band across the top rather than as a large card inside the text. The number
   matches .pf-wrap.pf-page's own padding; nothing else here depends on it. */
/* On a page the hero escapes the column's padding exactly, so it reads as a
   band across the top of the document rather than as a large card sitting
   inside the text. The 24px matches .pf-wrap.pf-page's own padding and nothing
   else depends on it.

   It keeps all four borders. Dropping the side ones was meant to suggest a
   full-bleed band, but the column is centred in the viewport — so the edges
   stopped short of the window anyway and the result read as a rectangle
   missing two of its sides. A true bleed would need viewport-width margins,
   which risks a horizontal scrollbar on the page for a decorative gain. */
.pf-page .pb-hero {
  margin-inline: -24px; border-radius: 0;
  padding: clamp(52px, 9vw, 86px) 24px;
}
.pf-page .pb-hero-t { font-size: clamp(2.1rem, 6.5vw, 3.4rem); }
.pf-page .pb-hero-sub { font-size: 1.15rem; }
.pf-page .pb-hero-x { font-size: 0.98rem; }

.pf-page .pb-split { gap: 34px; }
.pf-page .pb-split-t { font-size: clamp(1.3rem, 3vw, 1.7rem); }
.pf-page .pb-split-img img { max-height: 420px; }

.pf-page .pb-stats { gap: 22px; }
.pf-page .pb-stat { padding-top: 18px; }

.pf-page .pb-banner { padding: clamp(34px, 6vw, 56px) 28px; }

.pf-page .pb-quote { padding-left: 24px; border-left-width: 3px; }
.pf-page .pb-quote blockquote { font-size: 1.2rem; }

/* Buttons read as calls to action rather than as a list of links. */
.pf-page .pb-btns { gap: 12px; }
.pf-page .pb-btn { padding: 13px 26px; font-size: 0.92rem; }

/* The links list is a bio-link idea. On a page it belongs at the end, as a
   footer of sorts, and in a row rather than a stack. */
.pf-page .pf-links {
  margin-top: 52px; padding-top: 28px; border-top: 1px solid var(--border);
  flex-direction: row; flex-wrap: wrap; gap: 10px;
}
.pf-page .pf-link { flex: 1 1 240px; }
.pf-page .pf-foot { text-align: left; }

/* ── The page editor ───────────────────────────────────────────────────────
   Controls on the left, the real page on the right. It was ten stacked cards
   and no picture of the result: you filled in forms, pressed Save, and opened
   another tab to find out what happened.

   The panel never collapses and the preview can. That is deliberate and comes
   straight from the block-editor handbook: nothing needed for basic operation
   may live somewhere a person can dismiss. Editing is the operation; the
   preview is the luxury. */
.ed {
  padding-top: 78px; height: 100vh; height: 100dvh;
  display: flex; flex-direction: column; overflow: hidden;
}

/* ── The bar ── */
.ed-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 16px; background: var(--bg-2);
  border-bottom: 1px solid var(--border); flex: none;
}
.ed-bar-gap { flex: 1; }
.ed-addr {
  font-family: var(--mono); font-size: 0.72rem; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 99px; padding: 5px 13px; max-width: 46vw;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The bar's controls run shorter than the 40px default — it is a toolbar, not
   a form, and eight full-height buttons would own the top of the editor. */
.ed-bar .acct-btn { height: 34px; padding: 0 13px; }
.ed-bar .btn { height: 34px; }

.ed-seg { display: inline-flex; border: 1px solid var(--border); border-radius: 99px; overflow: hidden; }
.ed-seg button {
  cursor: pointer; background: transparent; border: 0; color: var(--dim);
  font-family: var(--sans); font-size: 0.8125rem; font-weight: 500; letter-spacing: 0;
  text-transform: none; padding: 6px 14px; transition: 0.16s var(--ease);
}
.ed-seg button.on { background: var(--surface-2); color: var(--text); }
.ed-seg button:hover { color: var(--text); }

/* Says what has happened to your work. Never empty for long — an editor that
   goes quiet is an editor people assume has lost something. */
.ed-state {
  font-family: var(--mono); font-size: 0.58rem; letter-spacing: 0.11em;
  text-transform: uppercase; color: var(--dim); white-space: nowrap;
}
.ed-state.dirty { color: var(--accent); }
.ed-state.saved { color: var(--live); }

.ed-undo { display: inline-flex; gap: 6px; }
.ed-undo .acct-btn:disabled { opacity: 0.35; cursor: default; }

.ed-publish {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  border: 1px solid var(--border); border-radius: 99px; padding: 5px 13px 5px 10px;
}
.ed-publish input { accent-color: var(--live); width: 14px; height: 14px; }
.ed-publish span {
  font-family: var(--sans); font-size: 0.8125rem; font-weight: 500;
  letter-spacing: 0; text-transform: none; color: var(--muted);
}
.ed-publish:has(input:checked) { border-color: var(--live); }
.ed-publish:has(input:checked) span { color: var(--live); }

/* Offered, never applied on its own. */
.ed-restore {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; flex: none;
  padding: 11px 16px; background: var(--accent-soft);
  border-bottom: 1px solid var(--accent); font-size: 0.86rem; color: var(--text);
}
.ed-restore[hidden] { display: none; }

/* ── The two panes ── */
.ed-body { flex: 1; display: grid; grid-template-columns: 1fr; min-height: 0; }
@media (min-width: 900px) { .ed-body { grid-template-columns: 340px 1fr; } }

.ed-panel {
  position: relative;
  border-right: 1px solid var(--border); background: var(--bg);
  display: flex; flex-direction: column; min-height: 0;
}

/* ── The section chooser ──
   Covers the panel and nothing else. The preview stays visible beside it,
   because what somebody is choosing is a thing that will appear over there —
   covering it to make the choice would hide the subject of the choice. */
.ed-palette {
  position: absolute; inset: 0; z-index: 4; background: var(--bg);
  display: flex; flex-direction: column;
}
.ed-palette[hidden] { display: none; }
.ed-palette-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 13px 16px; border-bottom: 1px solid var(--border); flex: none;
}
.ed-palette-head > span {
  font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--muted);
}
.ed-palette-grid {
  flex: 1; min-height: 0; overflow-y: auto; padding: 14px;
  display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  align-content: start;
}
.ed-kind {
  display: grid; grid-template-columns: 22px 1fr; gap: 2px 11px; text-align: left;
  padding: 13px; cursor: pointer; border-radius: 3px;
  border: 1px solid var(--border); background: var(--bg-2); color: var(--text);
  transition: 0.15s var(--ease);
}
.ed-kind:hover { border-color: var(--accent); background: var(--surface-2); }
.ed-kind:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ed-kind-icon { grid-row: span 2; color: var(--muted); }
.ed-kind:hover .ed-kind-icon { color: var(--accent); }
.ed-kind-icon svg { width: 22px; height: 22px; display: block; }
.ed-kind strong { font-size: 0.88rem; font-weight: 500; }
.ed-kind-about { font-size: 0.76rem; line-height: 1.45; color: var(--dim); }

/* ── Choosing a picture ── */
.ed-drawer-grid {
  flex: 1; min-height: 0; overflow-y: auto; padding: 14px;
  display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
  align-content: start;
}
.ed-drawer-grid .pb-thumb { width: 100%; }
/* The upload tile, first in the grid rather than a button somewhere above it:
   needing a picture and not having one is the same moment. */
.ed-uptile {
  aspect-ratio: 1; display: flex; flex-direction: column; gap: 3px;
  align-items: center; justify-content: center; cursor: pointer;
  border: 1px dashed var(--border-hi); border-radius: 2px;
  background: transparent; color: var(--dim); transition: 0.15s var(--ease);
}
.ed-uptile span { font-size: 19px; line-height: 1; }
.ed-uptile em {
  font-style: normal; font-family: var(--mono); font-size: 0.55rem;
  letter-spacing: 0.12em; text-transform: uppercase;
}
.ed-uptile:hover { border-color: var(--accent); color: var(--accent); }

/* ── What a picture looks like inside its section card ──
   A thumbnail and a button. The card used to hold the whole library, three
   times over on a page with three image sections. */
.pb-slot {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 10px;
}
.pb-slot-strip { display: flex; align-items: center; gap: 4px; }
.pb-slot-strip img {
  width: 42px; height: 42px; object-fit: cover; display: block;
  border-radius: 2px; border: 1px solid var(--border);
}
.pb-slot-more, .pb-slot-empty {
  font-family: var(--mono); font-size: 0.58rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--dim);
}
.pb-slot-empty {
  display: flex; align-items: center; height: 42px; padding: 0 10px;
  border: 1px dashed var(--border); border-radius: 2px;
}
.pb-slot .acct-btn { padding: 8px 13px; font-size: 0.6rem; }
.ed-tabs { display: flex; border-bottom: 1px solid var(--border); flex: none; }
.ed-tabs button {
  flex: 1; cursor: pointer; background: transparent; border: 0;
  border-bottom: 2px solid transparent; color: var(--dim);
  font-family: var(--sans); font-size: 0.875rem; font-weight: 500; letter-spacing: 0;
  text-transform: none; padding: 12px 6px; transition: 0.16s var(--ease);
}
.ed-tabs button:hover { color: var(--muted); }
.ed-tabs button.on { color: var(--text); border-bottom-color: var(--accent); }
.ed-tabs button:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }
.ed-tabs button[disabled] { opacity: 0.5; cursor: default; }

/* ── The same bar, in the designer's header ───────────────────────────────
   The sections editor stacks it above a panel, so its tabs stretch. The
   designer's header is a single row of controls, so they sit at their natural
   width beside everything else and lose the divider that would otherwise cut
   the header in half. The bar is the same element in both, which is the point:
   two documents that read as one editor. */
.ed2-bar #edTabsSlot .ed-tabs { border-bottom: 0; }
.ed2-bar #edTabsSlot .ed-tabs button {
  flex: none; padding: 8px 11px; font-size: 0.82rem;
  border-bottom-width: 2px; border-radius: 6px 6px 0 0;
}
/* A slim rule under the whole group rather than under each tab, so the current
   one reads as selected rather than as underlined. */
.ed2-bar #edTabsSlot { display: flex; align-self: stretch; align-items: flex-end; }

/* ── A page whose sections nobody can see ─────────────────────────────────
   Loud, because the symptom it explains is "I saved and published and nothing
   changed", and somebody in that state has already tried everything obvious.
   Sits at the top of the tab it is about, with the fix attached rather than
   described. */
.ed-warn {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin: 0 0 18px; padding: 12px 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: 8px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  font-size: 0.86rem; line-height: 1.45;
}
.ed-warn[hidden] { display: none; }
.ed-warn strong { flex: 0 0 100%; color: var(--text); }
.ed-warn span { flex: 1; min-width: 200px; color: var(--muted); }

/* The panel scrolls; the page does not. Losing your place in a long form
   because the whole document moved is the thing this replaces. */
.ed-tab { padding: 16px; overflow-y: auto; flex: 1; min-height: 0; }
.ed-tab[hidden] { display: none; }
/* Inside a panel the cards are already on a panel, so they lose their plate. */
.ed-tab .tp-card { background: transparent; border: 0; padding: 0 0 22px; }
.ed-tab .tp-card + .tp-card {
  border-top: 1px solid var(--border); padding-top: 22px; margin-top: 0;
}

/* ── The preview ── */
.ed-view {
  background: var(--bg-2); overflow: auto; padding: 22px;
  display: flex; justify-content: center; align-items: flex-start; min-height: 0;
}
.ed-frame {
  position: relative; width: 100%; height: 100%; max-width: 100%;
  border: 1px solid var(--border); border-radius: 4px; overflow: hidden;
  background: var(--bg); transition: max-width 0.3s var(--ease);
}
/* A real phone width rather than a gesture at one, so what you see is what
   the person on a phone sees. */
.ed-frame.narrow { max-width: 420px; }
.ed-frame iframe { width: 100%; height: 100%; border: 0; display: block; background: var(--bg); }
.ed-frame-empty {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: var(--dim); font-family: var(--mono); font-size: 0.62rem;
  letter-spacing: 0.12em; text-transform: uppercase; pointer-events: none;
  background: var(--bg); transition: opacity 0.2s var(--ease);
}
.ed-frame-empty[hidden] { opacity: 0; }

/* ── On a phone ──
   The two panes stack and the bar gets a switch. Half of these pages will be
   built on the device they are read on, so the editor has to work there. */
@media (max-width: 899px) {
  .ed { height: auto; overflow: visible; }
  .ed-body { grid-template-rows: auto; }
  .ed-panel { border-right: 0; border-bottom: 1px solid var(--border); }
  .ed-tab { overflow: visible; }
  .ed-view { height: 70vh; position: sticky; top: 66px; }
  .ed-body.show-preview .ed-panel { display: none; }
  .ed-body:not(.show-preview) .ed-view { display: none; }
  .ed-addr { max-width: 100%; order: -1; width: 100%; }
  .ed-back { display: none; }
}
@media (min-width: 900px) { .ed-mobile-switch { display: none; } }

/* ── Your own domain ──
   Tier 3 of the address ladder, in the Address tab of the page editor.

   The whole card is really one question — "is it working yet" — and the layout
   answers it before anything else: a state line at the top, then the records
   that produce that state. Somebody who is waiting reads the first line and
   leaves; somebody who is stuck reads the second half. */
.cd-state {
  display: flex; align-items: center; gap: 9px;
  font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 6px;
}
.cd-state .dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: currentColor;
}
/* Only two states get a colour: live, and broken. Waiting is the normal case
   and colouring it makes every setup look like an alarm for ten minutes. */
.cd-state.is-active { color: var(--accent); }
.cd-state.is-failed { color: #ff6b5e; }
/* The pulse is the only thing on the card that says work is happening without
   anybody having to press something. */
.cd-state.is-verifying .dot { animation: cd-pulse 1.6s var(--ease) infinite; }
@keyframes cd-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }

.cd-host {
  font-family: var(--mono); font-size: 0.95rem; color: var(--text);
  word-break: break-all; margin-bottom: 4px;
}

/* ── The pre-flight ───────────────────────────────────────────────────────
   What is true about a domain before anybody changes anything, because the
   question that stops people is "will this break what I already have" and the
   product used to answer it only by letting them find out. */
.cd-check:not(:empty) {
  margin-top: 16px; padding-top: 4px;
  border-top: 1px solid var(--border);
}
.cd-fact {
  margin: 8px 0 0; padding-left: 20px; position: relative;
  font-size: 0.875rem; line-height: 1.6; color: var(--muted);
}
.cd-fact::before {
  content: "·"; position: absolute; left: 6px; color: var(--dim);
}
.cd-fact.is-good::before { content: "\2713"; color: #57c07a; }
.cd-fact.is-warn::before { content: "!"; color: var(--accent); font-weight: 700; }
.cd-fact.is-bad::before  { content: "\00d7"; color: #ff6b5e; font-weight: 700; }
.cd-fact.is-good, .cd-fact.is-warn, .cd-fact.is-bad { color: var(--text); }

/* The secondary of a pair of buttons. Checking changes nothing and adding
   does, so the safe one is the one that looks pressable. */
.acct-btn.is-quiet {
  background: transparent; color: var(--muted);
  border: 1px solid var(--border);
}
.acct-btn.is-quiet:hover { color: var(--text); border-color: var(--border-hi); }

/* The other half of a pair, with its own state. A pair where one half quietly
   stopped is the failure the pair exists to prevent, so it gets its own dot
   rather than being folded into the primary's. */
.cd-partner {
  margin-top: 18px; padding-top: 16px;
  border-top: 1px solid var(--border);
}

/* ── The setup plan ───────────────────────────────────────────────────────
   What to do at *this* customer's DNS host, which the server works out from
   the domain's nameservers. Most of the time it is one record and four steps;
   the interesting case is an apex domain on a host that cannot point one here,
   where there is no record to show and a choice to make instead. */
.cd-setup-t {
  font-size: 1rem; font-weight: 650; color: var(--text);
  margin: 18px 0 6px;
}
.cd-steps {
  margin: 12px 0 0; padding-left: 20px;
  color: var(--muted); font-size: 0.9rem; line-height: 1.7;
}
.cd-steps li { margin-bottom: 6px; }
.cd-steps li::marker { color: var(--dim); font-variant-numeric: tabular-nums; }

/* Two ways round a blocked apex. The recommended one is marked rather than
   merely first, because a page that offers two routes and prefers neither
   leaves somebody to guess at exactly the moment they are least able to. */
.cd-opt {
  border: 1px solid var(--border); border-radius: 4px;
  padding: 14px 16px; margin-top: 12px;
}
.cd-opt.is-rec { border-color: var(--border-hi); background: var(--surface); }
.cd-opt-t {
  font-size: 0.95rem; font-weight: 650; color: var(--text); margin: 0 0 2px;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.cd-tag {
  font-family: var(--mono); font-size: 0.56rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--on-accent); background: var(--accent);
  border-radius: 2px; padding: 3px 7px;
}
/* Not styled as an error: nothing has gone wrong. It is the one consequence on
   this screen that is worse than the domain not working, so it is the one
   sentence that gets a colour. */
.cd-warn {
  margin: 12px 0 0; padding: 10px 12px;
  border-left: 2px solid var(--accent); background: var(--surface-2);
  color: var(--text); font-size: 0.85rem; line-height: 1.6;
}

/* One DNS record, laid out as the registrar's own form asks for it: type,
   name, value. Copy sits on the value because that is the field people get
   wrong by retyping. */
.cd-rec {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: 3px; padding: 12px 14px; margin-top: 10px;
}
.cd-rec .f { min-width: 0; }
.cd-rec .f.grow { flex: 1; min-width: 160px; }
.cd-rec .k {
  display: block; font-family: var(--mono); font-size: 0.56rem;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--dim);
  margin-bottom: 4px;
}
.cd-rec .v {
  font-family: var(--mono); font-size: 0.82rem; color: var(--text);
  word-break: break-all;
}
.cd-copy {
  flex: none; background: none; border: 1px solid var(--border);
  color: var(--muted); border-radius: 5px; padding: 6px 12px; cursor: pointer;
  font-family: var(--sans); font-size: 0.8125rem; font-weight: 500; letter-spacing: 0;
  text-transform: none; transition: border-color 0.2s var(--ease), color 0.2s var(--ease);
}
.cd-copy:hover { border-color: var(--accent); color: var(--accent); }

/* The add form. Same shape as the handle row so the two halves of the address
   ladder look like one thing rather than two features. */
.cd-add { display: flex; gap: 9px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
.cd-add input {
  flex: 1; min-width: 180px; background: var(--bg-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 2px; padding: 10px 12px;
  font-family: var(--mono); font-size: 0.88rem; outline: none;
}
.cd-add input:focus { border-color: var(--accent); }
.cd-add input::placeholder { color: var(--dim); font-family: var(--sans); }

.cd-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }

/* ── Finding a name ──
   Folded away, because it is the second question. Somebody who already owns a
   domain should see the box that takes it, not a search they do not need. */
.cd-find { margin-top: 18px; border-top: 1px solid var(--border); padding-top: 16px; }
.cd-find summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 8px;
  font-size: 0.875rem; color: var(--muted); margin-bottom: 10px;
  transition: color 0.15s var(--ease);
}
.cd-find summary::-webkit-details-marker { display: none; }
.cd-find summary::before {
  content: "+"; font-family: var(--mono); color: var(--dim);
}
.cd-find[open] summary::before { content: "\2212"; }
.cd-find summary:hover { color: var(--text); }

.cd-results { margin-top: 12px; }
.cd-result {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 0; border-bottom: 1px solid var(--border);
}
.cd-result:last-of-type { border-bottom: none; }
/* The name gets its own line, always.

   Sharing a row with the badge and two buttons left about 100px for it inside
   the editor's 330px panel, so "dulcesmiamibakery.com" broke mid-word across
   two lines and "Use this" wrapped underneath on its own. A domain is the
   thing being read here, so it takes the full width and the controls sit
   beneath it. Same shape at every width, which also means no breakpoint. */
.cd-result { flex-direction: column; align-items: stretch; gap: 7px; }
.cd-result .nm {
  font-family: var(--mono); font-size: 0.875rem; color: var(--text);
  overflow-wrap: anywhere;
}
/* No badge here at all.

   A list of domains with a coloured capsule beside each one is a registrar
   search wearing a dashboard's clothes, and it was the loudest thing on the
   panel. Every real registrar states it as a word. So: "Available" in the
   ordinary text colour, "Taken" dimmed, and the buttons carry the emphasis
   because they are the only things you can press. */
.cd-result-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.cd-state-word { font-size: 0.8125rem; color: var(--muted); }
.cd-state-word.is-free { color: var(--text); }
.cd-state-word.is-gone { color: var(--dim); }
/* Removing a domain takes a site off the internet. It stays a quiet link
   rather than a button, and asks before it does it. */
.cd-remove {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--dim); font-size: 0.78rem; text-decoration: underline;
  text-underline-offset: 3px;
}
.cd-remove:hover { color: #ff6b5e; }

/* ══════════════════════════════════════════════════════════════════════════
   THE PRODUCT UI
   Buttons, the app shell, and the card grid the account pages use.

   Why this block exists, in one paragraph: the account area was a 660px
   centred column on a 1600px screen — 59% of the window empty — holding a
   stack of full-width cards, each with one badge, one sentence and one
   outlined button. Every button was the same outlined uppercase-mono pill,
   so "Change my password" and "I do not remember it" were pixel-identical.
   The measurements, the four products they came from, and the rules five
   design systems agree on are in docs/ui-research.md. This is that document
   as CSS.
   ═════════════════════════════════════════════════════════════════════════ */

/* ── Buttons ───────────────────────────────────────────────────────────────
   Five roles, and nothing paints outside its role.

   The hierarchy is the whole point and it is what we did not have. Vercel,
   GitHub, IBM, Atlassian and Adobe all ship the same five weights under
   slightly different names, and all of them say the same thing about the top
   one: **at most one primary per view.** Two primaries is no primary.

   Sentence case, active verb, names the outcome — "Upgrade", not "BILLING".
   Uppercase tracked mono is metadata, not a label somebody presses. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--sans); font-size: 0.875rem; font-weight: 500;
  line-height: 1; letter-spacing: 0; text-transform: none; text-decoration: none;
  /* 40px tall. The medium default every system recommends, and comfortably
     over the 44px touch target once the 8px gap between controls is counted. */
  height: 40px; padding: 0 16px;
  border-radius: 6px; border: 1px solid transparent;
  background: none; color: var(--text);
  cursor: pointer; white-space: nowrap;
  transition: background 0.15s var(--ease), border-color 0.15s var(--ease),
              color 0.15s var(--ease), opacity 0.15s var(--ease);
}
.btn-sm { height: 32px; padding: 0 12px; font-size: 0.8125rem; border-radius: 5px; }
.btn-lg { height: 48px; padding: 0 22px; font-size: 0.9375rem; }

/* The one action a view is for. Reserved — this is why --accent stopped
   painting badges, links and "on" states when the status tokens landed. */
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn-primary:hover { opacity: 0.88; }
.btn-primary:active { opacity: 0.78; }

/* Everything else that is a real action. The workhorse. */
.btn-secondary { border-color: var(--border-hi); color: var(--text); }
.btn-secondary:hover { border-color: var(--text); background: var(--surface-2); }

/* Present but not competing: "Learn more", "Cancel", a third option in a row. */
.btn-ghost { color: var(--muted); }
.btn-ghost:hover { background: var(--surface-2); color: var(--text); }

/* Destructive. Outlined rather than filled, so a red slab is not the loudest
   thing on a page whose main job is something else — it fills on hover, at
   the moment the pointer is actually on it. */
.btn-danger { border-color: var(--danger); color: var(--danger); }
.btn-danger:hover { background: var(--danger); border-color: var(--danger); color: #1a0d0c; }

.btn-warning { border-color: var(--warn); color: var(--warn); }
.btn-warning:hover { background: var(--warn); color: #241a04; }

/* Focus is on every variant and is never removed. `:focus-visible` rather
   than `:focus` so a mouse click does not leave a ring behind, and the
   keyboard still gets one. */
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.45; cursor: not-allowed; pointer-events: none;
}

/* Async work, said in place. A button that silently does nothing for two
   seconds gets pressed twice. */
.btn[data-busy] { pointer-events: none; opacity: 0.7; }
.btn[data-busy]::before {
  content: ""; width: 13px; height: 13px; flex: none;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: btn-spin 0.7s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .btn[data-busy]::before { animation-duration: 2.4s; } }

/* A row of them. 10px keeps adjacent targets past the 8px minimum. */
.btn-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
/* Pushes what follows to the far end — "Save / Cancel … Delete". */
.btn-row .spacer { margin-left: auto; }

/* ── Status ────────────────────────────────────────────────────────────────
   A badge is a fact, not an action, so it never wears the accent. */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--sans); font-size: 0.75rem; font-weight: 500;
  padding: 3px 9px; border-radius: 5px;
  border: 1px solid var(--border); color: var(--muted); background: var(--surface);
}
/* A 5px dot rather than 6, and a hairline rather than a filled capsule. A
   badge with a coloured fill, a coloured border and a coloured dot is saying
   the same thing three times, and three times at full saturation is the look
   being avoided. The border carries it; the fill is gone. */
.pill::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.pill.is-ok     { color: var(--ok);     border-color: rgba(106, 168, 140, 0.4); }
.pill.is-warn   { color: var(--warn);   border-color: rgba(201, 160, 90, 0.4); }
.pill.is-danger { color: var(--danger); border-color: rgba(216, 119, 108, 0.4); }
.pill.is-info   { color: var(--info);   border-color: rgba(127, 163, 201, 0.4); }
/* No dot when there is no state to show — a plain label, not a status. */
.pill.is-plain::before { display: none; }

/* ── The app shell ─────────────────────────────────────────────────────────
   Every product we measured — Vercel, Linear, Shopify, Cloudflare — puts a
   fixed nav rail between 240 and 292px against content that fills the rest of
   the window. None of them centres a column. Ours was 660px wide with the
   marketing site's nav, its 120px of top padding, and its footer on the
   bottom, which is chrome a landing page needs and a tool does not.

   Opted into with `class="app"` on <body> rather than applied to every page
   under /account, so a page can stay a document when it should be one — the
   sign-in and sign-up pages are documents, and putting a product sidebar
   beside a password field would be worse. */

/* Two opt-ins, because two shapes of page need it.

   `app`      the dashboard shape: chrome plus a 240px rail beside content.
   `app-full` the same chrome with no rail — for the page editor, which is
              already a full-height two-pane tool with its own bar. Figma,
              Webflow and Framer all drop the global navigation inside the
              editor, and for the same reason: the canvas is the page, and
              240px spent on links is 240px taken off the thing being made.

   Everything below that says `body.app, body.app-full` is chrome. Everything
   that says `body.app` alone is the rail and the layout beside it. */
body.app, body.app-full { --app-nav: 240px; --app-head: 62px; }
body.app-full { --app-nav: 0px; }

/* The header stops pretending to be a marketing bar: full width instead of
   centred at --max, and 62px rather than 66 with 120 below it. */
body.app #nav .inner, body.app-full #nav .inner { max-width: none; height: var(--app-head); padding: 0 20px; }
body.app #nav, body.app-full #nav { background: var(--bg-2); border-bottom: 1px solid var(--border); }
/* Always solid. The transparent-until-scrolled treatment exists so a hero
   photograph shows through it; there is no hero here. */
body.app #nav.scrolled, body.app #nav.solid,
body.app-full #nav.scrolled, body.app-full #nav.solid { background: var(--bg-2); backdrop-filter: none; }

/* A tool does not need a copyright line and a legal link at the bottom of
   every screen. Reachable from the sidebar's own footer instead. */
body.app .site-footer, body.app-full .site-footer { display: none; }

/* And it does not need the photograph. The page background is atmosphere for a
   landing page; behind a settings form it is a picture somebody has to read
   through, and the account screens were the worst case — a palm frond running
   under four cards of small grey text. */
body.app #page-bg, body.app-full #page-bg { display: none !important; }

/* The marketing links go too. Work, Pricing, Launcher, Arcade and Contact are
   the site selling itself; somebody already inside their account is past that,
   and five links they will not press are five things between them and the
   account menu they will. The wordmark still goes home, and the rail carries an
   explicit way back.

   Matched on `data-match`, which every marketing link already carries for the
   CMS page-visibility script, so no new markup is needed to identify them. */
body.app #navMenu > li:has(a[data-match]),
body.app #navMenu > li:has(a.cta),
body.app-full #navMenu > li:has(a[data-match]),
body.app-full #navMenu > li:has(a.cta) { display: none; }

/* Small, because a compact header is the point. The account pages opened with
   a 64px avatar tile beside a marketing-sized headline. */
body.app .app-head .ac-logo { width: 44px; height: 44px; flex: none; border-radius: 8px; }
body.app .app-head .ac-logo .mono { font-size: 0.8rem; }

/* The rail. `position: fixed` so a long settings page scrolls under it rather
   than dragging it along — which is what every product in the reference set
   does and is the main thing that makes a page feel like an app. */
.app-nav {
  position: fixed; z-index: 150;
  top: var(--app-head); bottom: 0; left: 0; width: var(--app-nav);
  display: flex; flex-direction: column;
  padding: 18px 12px 16px;
  background: var(--bg-2); border-right: 1px solid var(--border);
  overflow-y: auto; overscroll-behavior: contain;
}
.app-nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; margin-bottom: 1px; border-radius: 6px;
  font-size: 0.875rem; color: var(--muted); text-decoration: none;
  transition: background 0.15s var(--ease), color 0.15s var(--ease);
}
.app-nav a:hover { background: var(--surface-2); color: var(--text); }
/* Where you are, said once and quietly. A filled accent block for the current
   page would be the loudest thing on screen, which is the wrong place for the
   emphasis — you already know where you are. */
.app-nav a[aria-current="page"] { background: var(--surface-2); color: var(--text); font-weight: 500; }
.app-nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The glyph column. Fixed width so the labels line up whether or not a given
   item has one. */
.app-nav .ico { flex: none; width: 16px; height: 16px; opacity: 0.75; }
.app-nav a[aria-current="page"] .ico { opacity: 1; }

.app-nav-group {
  font-family: var(--sans); font-size: 0.6875rem; font-weight: 600;
  letter-spacing: 0.04em; text-transform: none; color: var(--dim);
  padding: 16px 10px 6px;
}
.app-nav-foot { margin-top: auto; padding-top: 14px; border-top: 1px solid var(--border); }
.app-nav-foot .who {
  padding: 4px 10px 10px; font-size: 0.75rem; color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The content beside it. */
body.app main { margin-left: var(--app-nav); padding-top: var(--app-head) !important; }
/* Wide, with a ceiling. Capping the *grid* at ~1400 keeps a card from becoming
   a metre long on an ultrawide; prose is capped separately, on the prose. */
body.app .ac-wrap,
body.app .tp-wrap { max-width: 1400px; margin: 0; padding: 30px 32px 80px; }

/* The one place a narrow measure is right, and the reason the 660px column
   felt defensible: it is correct for reading and wrong for a dashboard. */
body.app .app-prose { max-width: 68ch; }

/* The editor fills what is left under the header. Its 78px was written for the
   66px marketing bar plus its shadow; the app header is 62. */
body.app-full .ed { padding-top: var(--app-head); }

/* ── Settings-shaped pages ──
   Not every account page is a dashboard. A settings list is read and then
   acted on one row at a time, so it wants a capped column — GitHub's is about
   900px, Cloudflare's prose column 765px — where the grid pages want the
   window. Full width here produced 1266px-wide bars with a sentence of text
   in the top-left corner of each, which is worse than the 660px it replaced.

   So: cap the *prose-shaped* page, fill with the *grid-shaped* one. That is
   house rule 2, and this is the rule that applies it. */
body.app .ac-wrap.app-narrow { max-width: 880px; }

/* The eyebrow above each page title said SETTINGS, BILLING, APPEARANCE — which
   was useful when the only way to know where you were was to read it. The rail
   now says it, highlighted, permanently, two inches to the left. Saying it
   twice makes the second one noise. */
body.app .auth-eyebrow { display: none; }

/* And the title comes down to a page heading. These are set inline at
   clamp(1.7rem, 4.5vw, 2.2rem) — a landing-page size, which is what they were
   when these pages opened with a marketing hero. */
body.app .ac-wrap h1,
body.app .tp-wrap h1 {
  font-size: 1.75rem !important; font-weight: 400 !important;
  margin: 0 0 6px !important; letter-spacing: -0.02em !important;
}

/* A message to a person, set as tracked mono, which made every ordinary
   sentence look like a stack trace. The marker square earns its keep; the
   typewriter does not. */
body.app .auth-msg {
  font-family: var(--sans); font-size: 0.875rem; padding-left: 16px;
}
body.app .auth-msg::before { top: 0.55em; width: 5px; height: 5px; border-radius: 1px; }

/* The page's own title row, replacing the marketing-sized hero the account
   pages opened with. */
.app-head {
  display: flex; align-items: flex-start; gap: 20px; flex-wrap: wrap;
  margin-bottom: 26px;
}
.app-head-text { flex: 1; min-width: 240px; }
.app-head h1 {
  font-family: var(--serif); font-weight: 400;
  font-size: 1.75rem; letter-spacing: -0.02em; line-height: 1.15; margin-bottom: 5px;
}
.app-head p { color: var(--muted); font-size: 0.9rem; max-width: 68ch; }
.app-head .btn-row { flex: none; }

/* ── The card grid ────────────────────────────────────────────────────────
   auto-fit rather than a fixed column count: 2 up at ~900px, 3 at ~1200, 4 at
   ~1560, and one on a phone, with no breakpoint written for any of them. The
   stack of full-width bars this replaces is the shape that made four facts
   take three screens. */
.app-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
/* For a card that earns two columns — a table, a chart, a form. */
.app-grid .wide { grid-column: span 2; }
@media (max-width: 719px) { .app-grid .wide { grid-column: auto; } }

/* A card in the grid. Same plate as .ac-card, but it fills its cell and puts
   its actions on the floor, so a row of them lines up along the bottom
   whatever length the text above happens to be. */
.app-card {
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; padding: 18px 18px 16px;
}
.app-card h2 {
  font-family: var(--sans); font-size: 0.9375rem; font-weight: 600;
  letter-spacing: -0.01em; color: var(--text); margin-bottom: 8px;
}
.app-card .app-card-body { flex: 1; }
.app-card p { color: var(--muted); font-size: 0.84rem; line-height: 1.55; margin-bottom: 8px; }
.app-card .btn-row { margin-top: 14px; }
/* Data reads as data: hostnames, addresses, amounts, ids. This is what mono
   is for, and the only thing it is for. */
.app-card .val { font-family: var(--mono); font-size: 0.82rem; color: var(--text); word-break: break-all; }

/* ── On a narrow screen ───────────────────────────────────────────────────
   The rail becomes a scrolling strip under the header. A drawer behind a
   hamburger would hide six links behind a tap on a screen where the whole
   point is that there are only six. */
@media (max-width: 899px) {
  body.app { --app-nav: 0px; }
  /* Sticky under the header rather than static, and offset by its height.

     `position: static` put it at the top of the body flow — which is *behind*
     the fixed 62px header, so on a phone the whole rail was invisible. Sticky
     also keeps it reachable after scrolling, which on a screen with no room
     for a permanent rail is the thing that makes it usable at all. */
  .app-nav {
    position: sticky; top: var(--app-head); margin-top: var(--app-head);
    width: auto; flex-direction: row; gap: 4px;
    padding: 8px 12px; overflow-x: auto; overflow-y: hidden;
    background: var(--bg-2);
    border-right: 0; border-bottom: 1px solid var(--border);
    -webkit-overflow-scrolling: touch;
  }
  .app-nav a { margin-bottom: 0; white-space: nowrap; }
  /* The group headings and the footer are a column's furniture. In a row they
     are two more things to scroll past to reach Settings. */
  .app-nav .app-nav-group, .app-nav .app-nav-foot { display: none; }
  /* The rail now carries the header's offset, so main must not add it again. */
  body.app main { margin-left: 0; padding-top: 0 !important; }
  body.app .ac-wrap, body.app .tp-wrap { padding: 22px 18px 64px; }
  .app-head h1 { font-size: 1.5rem; }
}

/* ── A canvas page ─────────────────────────────────────────────────────────
   The second page mode: boxes at coordinates the designer chose, rather than a
   stack of sections that lay themselves out. Everything here is the part that
   is the same on every canvas page. The per-widget geometry is generated per
   page by functions/api/account/_canvas.js and arrives in a <style> in the
   head, because it is different for every page and belongs to the document
   rather than to the site.

   Nothing in this block positions anything. That separation is deliberate: if
   a rule here set a coordinate, a page would depend on this file's version to
   look right, and a stylesheet cache-bust would move somebody's design. */

.cv-doc {
  position: relative;
  min-height: 100vh;
  /* The document paints its own background so a full-bleed one covers the
     window rather than the stage, which is narrower on a wide screen. */
  background-color: var(--bg);
  /* A canvas is a drawing, not a column of prose. The reading-width and
     paragraph-rhythm rules that make a block page readable would all be
     wrong here, so the stage inherits none of them. */
  overflow-x: clip;
}

.cv-stage {
  position: relative;
  width: 100%;
  /* Height comes from the generated stylesheet, per viewport. It cannot come
     from the content: every widget is out of flow, so an auto height collapses
     to zero and the whole page renders one screen tall with the design
     spilling out of the bottom of it. */
  margin: 0 auto;
}

.cv-w {
  position: absolute;
  box-sizing: border-box;
  /* The centre is the anchor for rotation and scale, which is why the model
     stores a widget's centre rather than a corner. Stated rather than left to
     the default so a change to the default cannot move every page. */
  transform-origin: center center;
}

.cv-text {
  display: flex;
  flex-direction: column;
  /* Vertical alignment is `justify-content` in a column, and the generated
     rule sets it per widget. */
  white-space: normal;
  overflow-wrap: break-word;
}
.cv-text > * { margin: 0; }
.cv-text > * + * { margin-top: 0.6em; }
.cv-text .pb-list { padding-left: 1.15em; }
/* A heading inside a text widget is a heading for the *outline*, not a request
   for the site's heading styles. On a canvas the size, weight and face are the
   designer's, set per widget in the generated stylesheet, so the tag has to
   carry none of its own or choosing h1 would silently resize the text. */
.cv-text h1, .cv-text h2, .cv-text h3 {
  font: inherit; letter-spacing: inherit; color: inherit; margin: 0;
}

.cv-picture { overflow: hidden; }
.cv-picture img {
  position: absolute;
  display: block;
  /* The crop rectangle is expressed as a percentage blow-up of the source, so
     the image is routinely wider than its box. `max-width: none` is what stops
     the site's global image rule shrinking it back and undoing the crop. */
  max-width: none;
}

/* ── A design band inside a block page ─────────────────────────────────────
   The join between the two halves of the builder. A band is the same document,
   the same geometry and the same generated rules as a canvas page; it is simply
   positioned against itself rather than against the page.

   Full-bleed, and that is the point of it. The block page is a 940px column,
   and a band designed at 1200 would be letterboxed inside it — so the band
   breaks out to the window's width, which is what makes a designed hero read as
   a hero rather than as a picture in a column.

   `left: 50%` plus `margin-left: -50vw` rather than `width: 100vw` alone: the
   band has to be centred on the window regardless of how wide its container is
   or how far it is indented, and that pair is the only version that holds when
   the container has padding. */
.cv-band {
  position: relative;
  left: 50%;
  width: 100vw;
  margin-left: -50vw;
  /* Clipped for the same reason `.cv-doc` is: a widget placed 900px left of
     centre must not give a narrow window a horizontal scrollbar. */
  overflow: hidden;
}

/* A band is a section, so it sits in the stack's rhythm rather than against its
   neighbours. The negative margins cancel the column's own gap, because a band
   that is full-bleed horizontally should be flush vertically too. */
.pb-blocks > .cv-band { margin-top: 0; }
.pb-blocks > .cv-band + * { margin-top: 30px; }

/* ── A page that opens with a band opens with the band ──
   The rule above makes a band flush against its *neighbours*, which leaves the
   first one sitting below the stack's own top margin and the page wrapper's
   padding — a hundred pixels of empty page above a full-bleed hero, which
   reads as a mistake rather than as breathing room. A designed opening should
   meet whatever is above it. */
.pb-blocks:has(> .cv-band:first-child) { margin-top: 0; }
.pf-wrap:has(.pb-blocks > .cv-band:first-child) { padding-top: 0; }

/* ── The clip goes on the body, and never on an ancestor of the band ──
   The page must not scroll sideways: `100vw` counts the scrollbar's width on
   most browsers, so a band is a dozen pixels wider than the space it has and
   would add a horizontal scrollbar to every page that used one.

   The first version put `overflow-x: clip` on `.pf-wrap`, and that was worse
   than the problem. `.pf-wrap` is the 940px column the band exists to escape,
   so clipping there clipped the band back to 940 — full-bleed markup, full-
   bleed geometry, and a band that came out exactly as wide as the column it
   was supposed to break out of. Nothing in the numbers showed it: the element
   really was 100vw, and only the paint was cut.

   `body` is above the column, so it clips the dozen pixels of scrollbar
   overhang and nothing else. The rule is the same one the fixed layer already
   follows: never clip on an ancestor that something is meant to escape. */
body:has(.cv-band) { overflow-x: clip; }

/* ── The fixed layer ──
   Fixed widgets are rendered here rather than in the stage, because the stage
   clips horizontally and `overflow: clip` clips fixed descendants too. See the
   note in _canvas.js. The layer itself is transparent to the pointer so that an
   empty one, or the gaps in a full one, do not swallow clicks meant for the
   page underneath. */
.cv-fixed {
  position: fixed;
  inset: 0;
  pointer-events: none;
  /* Above the stage without competing with it: widget z-indexes inside this
     layer are resolved against each other, not against the stage's. */
  z-index: 900;
}
.cv-fixed .cv-w { pointer-events: auto; }

/* ── Icons ──
   Inline SVG, sized to its own box and coloured by `currentColor`, so an icon
   inside a button follows the button's colour without being told. */
.cv-icon { display: block; color: var(--text); }
.cv-icon svg { display: block; width: 100%; height: 100%; }

/* ── Buttons ──
   The widget *is* the button: it fills its own box and centres what is in it,
   which is why a row of them lines up because they were placed rather than
   because their labels happened to match. */
.cv-button {
  display: flex; align-items: center; justify-content: center; gap: 0.55em;
  text-align: center; line-height: 1.2; box-sizing: border-box;
  padding: 0 1.1em; overflow: hidden;
  /* Not a plain colour change: a button is the one thing on a canvas somebody
     expects to react, and a page whose buttons do nothing on hover reads as a
     picture of a page. Overridden the moment a real hover animation is added,
     because that is a later declaration on the same element. */
  transition: filter .18s ease, transform .18s ease;
}
.cv-button:hover { filter: brightness(1.08); }
.cv-button:active { transform: translateY(1px); }
.cv-button .cv-btn-i {
  flex: 0 0 auto; width: 1.15em; height: 1.15em; display: block;
}
.cv-button .cv-btn-i svg { display: block; width: 100%; height: 100%; }
/* The anchor inherits the button's own layout rather than nesting a second
   flex container inside it. */
.cv-a-btn {
  display: flex; align-items: center; justify-content: center; gap: inherit;
  width: 100%; height: 100%; color: inherit; text-decoration: none;
}

.cv-shape { pointer-events: none; }
.cv-shape:has(.cv-a) { pointer-events: auto; }

.cv-a { color: inherit; text-decoration: none; }
.cv-a-fill { position: absolute; inset: 0; }

/* A widget animated to nothing must not keep catching clicks. Not used yet —
   nothing animates until phase 5 — and it is here because it is the same rule
   the reference implementation applies, and the bug it prevents reads as "the
   button does not work sometimes", which is the kind nobody ever reports
   accurately. */
.cv-w.cv-out { pointer-events: none; }

/* The footer is the one piece of site furniture a canvas page keeps, and it
   sits under the design rather than over it. */
.cv-doc .pf-foot { position: relative; z-index: 1000; padding: 28px 20px 40px; }

@media (prefers-reduced-motion: reduce) {
  /* Every animation on a canvas page is authored so that its final step is the
     page's true state — see docs/interactions-spec.md §7.2, where the same
     property is what lets scroll animations degrade to nothing in browsers
     without scroll timelines. That rule is what makes this one line a complete
     answer rather than a partial one. */
  .cv-w, .cv-w * { animation: none !important; transition: none !important; }
}


/* ── The free tools page ──────────────────────────────────────────────────
   A grid of what we have opened up. Deliberately plain: these are utilities,
   and a utility that arrives with a marketing page in front of it is a utility
   somebody closes. */
.page { max-width: var(--max); margin: 0 auto; padding: 90px 28px 120px; }
/* ── Scoped to the tools pages, which is where it was always meant to be ──
   Written `.page-head { max-width: 60ch }` inside this section, with only the
   comment above saying it was for the tools pages. A comment is not a
   selector: it applied to every `.page-head` on the site and beat the
   site-wide rule 3,800 lines above, capping the whole header section at 615px
   with no auto margin.

   The visible effect was every page heading sitting flush against the left
   edge while the content under it centred at 196px — worst on /services,
   where the two are a screen apart. It read as a broken grid and was reported
   as one. It also had nothing to do with the theme; the dark background
   photograph simply hid the misalignment for long enough that nobody looked.

   The measure belongs on the text, which is what `.page-head p` at the top of
   this file already does. */
.page .page-head { max-width: 60ch; margin-bottom: 54px; }
.page-head .eyebrow {
  font-family: var(--mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 14px;
}
.page-head h1 { font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -.02em; margin-bottom: 16px; }
.page-head .lede { color: var(--muted); font-size: 1.05rem; line-height: 1.7; }

/* ── The lab, as a mosaic ─────────────────────────────────────────────────
 *
 * ── What this replaced ──
 * `repeat(auto-fill, minmax(300px, 1fr))` with an 18px gap: twelve identical
 * bordered boxes, each a heading over a paragraph, four of them running a live
 * demo at `opacity: .18` behind the text where nobody could see it. A page
 * whose entire argument is "these run, they are not screenshots" was showing
 * twelve equal rectangles of prose.
 *
 * ── The shape ──
 * Six columns, tiles spanning two to four of them and one or two rows, on a
 * 2px gap so they read as one interlocked surface rather than as cards laid
 * out on a page. Asymmetry is the point: equal weight forces the reader to
 * read every heading to find out what matters, and a mosaic answers that
 * before they read anything.
 *
 * The spans are assigned by nth-child in a repeating rhythm rather than tile
 * by tile, so adding or removing an experiment cannot leave a hole — a
 * hand-placed grid is a grid that breaks the next time the list changes.
 *
 * ── The demo is the tile ──
 * Each live demo fills its tile and the words sit on top of it, which is the
 * inversion this page needed: the thing being demonstrated is the content, and
 * the sentence explaining it is the annotation. Kept legible by a scrim under
 * the text rather than by holding the demo at 18% — a demo dim enough to be
 * safely ignored is a demo not worth mounting. */
.tool-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-auto-rows: 150px;
  gap: 2px;
  /* The gap shows the page through, so the tiles touch along a hairline
     instead of floating. */
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: 14px;
  overflow: hidden;
}

/* ── Which tiles get the room ──
   Assigned by whether the tile has something to SHOW, not by its position.
   An nth-child rhythm was the first attempt and it put a four-column slot
   under the seventh tile, which happens to be an outbound link with no demo
   in it — a large empty box in the most prominent position on the page. Size
   follows content here, so that cannot happen however the list is reordered.

   `dense` lets a later small tile backfill a hole a big one left, which is
   what stops the last row breaking up. */
/* ── Sizes that add up to six ──
   `grid-auto-flow: dense` plus spans assigned by `:has()` left holes: six
   demo tiles asking for 4+3+3+3+3+3 columns across a six-column grid never
   lands on a row boundary, so the packer back-filled where it could and left
   gaps where it could not. The bottom of the page was a half-width tile with
   an empty field beside it.

   The spans are declared on the tiles now and they tile exactly:

     row 1-2   wide 4 + narrow 2      = 6
     row 3-4   narrow 2 + wide 4      = 6
     row 5-6   half 3 + half 3        = 6
     row 7     three posters, 2 each  = 6
     row 8     three posters, 2 each  = 6

   No dense packing, because nothing needs back-filling when every row is
   full — and dense was also what made the layout depend on document order,
   which is how reordering the list used to move the big tile onto an
   outbound link with nothing to show. */
.tool { grid-column: span 2; grid-row: span 1; }
.tool.t-wide { grid-column: span 4; grid-row: span 2; }
.tool.t-half { grid-column: span 3; grid-row: span 2; }
.tool.t-tall { grid-column: span 2; grid-row: span 2; }

@media (max-width: 900px) {
  .tool-grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 150px; }
  .tool, .tool.t-wide, .tool.t-half, .tool.t-tall { grid-column: span 1; grid-row: span 1; }
  .tool:has(.demo-stage) { grid-column: span 2; grid-row: span 2; }
}
@media (max-width: 560px) {
  .tool-grid { grid-template-columns: 1fr; }
  .tool, .tool.t-wide, .tool.t-half, .tool.t-tall { grid-column: span 1; grid-row: span 1; }
  .tool:has(.demo-stage) { grid-row: span 2; }
}

.tool {
  position: relative; overflow: hidden; isolation: isolate;
  background: var(--bg-2);
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 16px 18px;
  transition: background .25s var(--ease);
}
.tool:hover { background: var(--surface); }

/* The live demo, filling the tile. Never a pointer target: the whole tile is
   a link, and a canvas that swallowed the click would break it. */
/* ── Drawn big, shown small ──
   These demos lay out real paragraphs at real reading size. Dropped straight
   into a 300px tile that is not a preview of a page of type, it is a corner
   of one: eight words per line, three lines visible, the rest clipped. That
   is what the tiles were showing — a wall of broken sentences with the tile's
   own title sitting on top of it.

   So the stage is given a box 1.8x the tile and scaled back down into it. The
   demo still measures a full-width column and still breaks its lines around
   the moving shape; the reader sees that column as a miniature, which is the
   thing worth previewing. The type is small on purpose — it is texture at
   this size, and the page it links to is where it is meant to be read. */
.tool {
  --demo-scale: .56;
}
.tool .demo-stage {
  position: absolute; top: 0; left: 0; z-index: 0;
  width: calc(100% / var(--demo-scale));
  height: calc(100% / var(--demo-scale));
  transform: scale(var(--demo-scale));
  transform-origin: 0 0;
  overflow: hidden;
  opacity: .5; pointer-events: none;
  transition: opacity .3s var(--ease);
}
/* The big anchor tile has room for a bigger miniature. */
.tool:has(.demo-stage).t-wide { --demo-scale: .68; }
.tool:hover .demo-stage { opacity: .85; }
/* The old rule held the stage at 0 until a pointer arrived and .demo-live was
   set. The stage now fills the tile and is the point of it, so it fades in as
   soon as it has something in it. */
.tool.demo-live .demo-stage { opacity: .6; }
.tool.demo-live:hover .demo-stage { opacity: .92; }

/* ── Legibility without dimming the demo ──
   A scrim from the bottom edge only, so the top two thirds of the tile is the
   demo at full strength and the words still have something to sit on.

   Painted as a layer on the tile's own background rather than as a pseudo-
   element, because BOTH pseudo-elements on `.tool` are already spoken for:
   `::before` is the "hover" badge and `::after` is the surface for tiles with
   no demo. Putting the scrim on `::before` silently replaced the badge with a
   gradient, which is exactly what shipped a moment ago. */
.tool {
  background-image: linear-gradient(to top,
    var(--bg) 0%, color-mix(in srgb, var(--bg) 72%, transparent) 26%, transparent 62%);
  background-repeat: no-repeat;
}
@supports not (background: color-mix(in srgb, red, blue)) {
  .tool { background-image: linear-gradient(to top, var(--bg) 0%, rgba(10,10,12,.72) 26%, transparent 62%); }
}


/* ── Why these are NOT `position: relative` ──
   They were, and it made the whole tile unclickable except on the three
   words of the heading. The stretched link below is `h2 a::after` with
   `inset: 0`, which resolves against the nearest POSITIONED ancestor — and
   `position: relative` here made that the <h2> itself, so the hit area was
   the heading's own box rather than the tile.

   These are flex items of `.tool`, and a flex item takes `z-index` without
   being positioned. So the stacking still works, `.tool` stays the
   containing block, and clicking anywhere on the tile goes where the tile
   says it goes. The demo stage sets its own `position: absolute` and is
   excluded rather than fought with. */
.tool > *:not(.demo-stage):not(.tool-frame) { position: static; z-index: 2; }

/* ── The tiles with nothing to run ──
   The outbound experiments live on another domain and cannot mount here, and
   `X-Frame-Options: DENY` means our own lab pages cannot be iframed either —
   that header protects the account and admin areas and is not worth weakening
   for a thumbnail.

   So rather than a screenshot to fetch and keep current, they get a surface
   built from the same accent the page already has: two offset radial washes,
   seeded off the tile's position so no two are identical. No request, nothing
   to go stale, and it reads as texture rather than as a missing image. */
/* ── The layer every tile paints its still on ──
   Declared for BOTH kinds of tile. It used to exist only on tiles with no
   demo, which meant a demo tile could carry a `--poster` and never draw it:
   no `content`, no pseudo-element, nothing. That mattered once the demo tiles
   got stills of their own, because only three demos run at a time and the
   other three were bare black rectangles with a title on them — which is most
   of what "the lab page is basic" was. */
.tool:not(:has(.demo-stage))::after,
.tool[style*="--poster"]::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: .55; transition: opacity .3s var(--ease);
}
.tool:not(:has(.demo-stage))::after {
  background:
    radial-gradient(120% 100% at 15% 0%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 60%),
    radial-gradient(90% 80% at 90% 100%, color-mix(in srgb, var(--accent) 7%, transparent), transparent 55%);
}

/* ── A frame of the real thing ──
   The outbound experiments run on another domain and cannot mount here, so
   these five tiles carry a still of the actual page instead of a gradient.
   Captured from the live sites at 1280px and cut to 640x400 at quality 72 —
   all five together are 92 KB, because a tile is 300px wide behind a scrim
   and anything sharper is detail nobody can see.

   Set as a custom property on the tile rather than as an <img>, so the markup
   stays a list of links and the picture stays presentation. A tile with no
   `--poster` falls through to the gradient above with nothing to guard. */
.tool[style*="--poster"]::after {
  background-image: var(--poster);
  background-size: cover; background-position: center;
  /* Higher than the gradient tiles sit at, because a photograph carries its
     own darkness and the scrim is over it as well — .42 read as "an image
     failed to load" rather than as a still. */
  opacity: .62; filter: saturate(.9);
}
.tool[style*="--poster"]:hover::after { opacity: .9; filter: saturate(1.05); }
.tool:not(:has(.demo-stage)):hover::after { opacity: 1; }
/* ── The rule that was erasing half the posters ──
   This paints the alternate gradient, and it must never touch a tile that
   declared a `--poster`. It used to, and the way it did is worth keeping
   written down: it sets `background`, the SHORTHAND, which resets
   background-image — and `.tool:nth-child(even):not(:has(…))::after` is
   (0,3,1) against the poster rule's (0,2,1), so it won on specificity AND
   came later in the file.

   The result was that every EVEN tile silently lost its still: Avocados,
   Ducks and The arcade rendered as empty black rectangles with a title,
   while the odd ones beside them showed their picture. Three of the twelve
   tiles on the page, and the cause was a shorthand. */
.tool:nth-child(even):not(:has(.demo-stage)):not([style*="--poster"])::after {
  background:
    radial-gradient(110% 90% at 85% 10%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 58%),
    radial-gradient(80% 70% at 10% 95%, color-mix(in srgb, var(--accent) 6%, transparent), transparent 52%);
}
@supports not (background: color-mix(in srgb, red, blue)) {
  .tool:not(:has(.demo-stage))::after { display: none; }
}

/* ── A whole other site, running in the tile ──
   Sits over the poster rather than instead of it: the still is on screen at
   the first paint and the frame fades in on top once it has actually loaded,
   so the tile never blanks and a slow network degrades to a picture.

   `pointer-events: none` because the tile is a link. A frame that took the
   click would mean the reader could drive somebody else's demo and never
   reach the page it belongs to. */
.tool-frame {
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%; border: 0;
  pointer-events: none;
  opacity: 0; transition: opacity .6s var(--ease);
  /* The demos are built for a full window. At tile size that is a scene shown
     through a letterbox, which is the wrong crop but a live one — scaling the
     frame up and down instead would make the type inside it unreadable. */
  background: transparent;
}
.tool.frame-live .tool-frame { opacity: .55; }
.tool.frame-live:hover .tool-frame { opacity: .9; }
/* Once the real thing is running, the still underneath is only in the way —
   and two pictures of the same demo at half opacity each is a smear. */
.tool.frame-live::after,
.tool.demo-live::after { opacity: 0; }

/* ── The tile shows the thing; it does not describe it ──
   The paragraph is taken off the screen entirely rather than revealed on
   hover. This page's whole argument is that these run — a tile that explains
   a technique in two sentences while the technique is running behind the
   sentences is arguing with itself, and the explanation wins because words
   are what an eye lands on first.

   Not `display: none`. The text stays in the document for search, for a
   screen reader, and for the crawler that will never see the canvas — it is
   the accessible description of a demo that is otherwise pure pixels. This is
   the standard visually-hidden clip, which keeps it in the accessibility tree
   while removing it from the layout. */
.tool p {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

.tool h2 { font-size: .96rem; letter-spacing: -.01em; line-height: 1.25; }
/* The anchor tile gets the bigger type, because it is the one carrying
   the row and should read first. */
.tool.t-wide h2 { font-size: 1.35rem; }
.tool h2 a { color: var(--text); text-decoration: none; }

/* The whole card is the target, not the three words in the heading.
   Done by stretching the heading's own anchor over the card rather than by
   wrapping everything in one: the link keeps its real text, so it still reads
   correctly to a screen reader and still right-clicks and opens in a new tab.
   A card with nothing to link to (the "soon" ones) has no anchor and so gets
   none of this, which is why every rule below is guarded on :has(). */
.tool h2 a::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
}
.tool:has(h2 a) { cursor: pointer; }
/* No transform. The tiles touch along a 2px hairline inside a clipped grid,
   so lifting one by 2px slid it over its neighbour and left the old edge
   showing through — a stray line of tile hanging in the gap, which is what
   "random ghosts" were. A tile that is already a live demo does not need to
   move to say it is interactive; brightening the edge says it without
   disturbing anything around it. `border-color` alone did nothing here
   either, because `.tool` has no border — the hairline is the grid's
   background showing through the gap. */
.tool:has(h2 a):hover {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
}
.tool:has(h2 a):hover h2 a { color: var(--accent); }
/* Keyboard users get the same signal the mouse gets. The outline goes on the
   card because the anchor's own box is just the heading text. */
.tool:has(h2 a:focus-visible) {
  /* Inset, because the grid clips: an outset ring on a tile touching the
     clip edge is half a ring. */
  outline: 2px solid var(--accent); outline-offset: -2px;
}
.tool h2 a:focus-visible { outline: none; }
@media (prefers-reduced-motion: reduce) {
  .tool { transition: none; }
}
.tool p { color: var(--muted); font-size: .92rem; line-height: 1.65; }
.tool-note { color: var(--dim); font-size: .8rem; margin-top: auto; }

/* ── A lab card that runs its own technique ───────────────────────────────
   /lab was twelve boxes of prose about things that move, and it was the one
   page here that did not move. A card with `data-demo` mounts its own engine
   while the pointer is on it — see assets/lab-demos.js for the budget that
   allows (one at a time, imported on demand, destroyed on leave).

   The demo is a BACKGROUND. It runs behind the writing rather than replacing
   it, so the card still reads as a card, still links where it linked, and
   still says what the technique is. A preview that hides the explanation is a
   worse card, not a better one. */
.demo-stage {
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;            /* the card-wide link stays clickable */
  overflow: hidden;
  /* `border-radius: inherit` was right while the cards were rounded boxes.
     The mosaic tiles are square and butt against each other, so inheriting a
     radius drew a visible rounded rectangle floating inside each sharp tile. */
  border-radius: 0;
  opacity: 0;
  transition: opacity .45s var(--ease);
}
/* ── Held well back, and then held back further ──
   The first version ran at .42 and was a legibility failure: the demo sets its
   own column of type, so a card ended up with two paragraphs of white text
   crossing each other and neither could be read. Seen on screen, not reasoned
   about — the prediction in this comment was already here and it still shipped
   too strong, which is the argument for looking at it.

   .18 is the level where the movement still registers in peripheral vision but
   the card reads as a card. The effect is texture, not content. */
.tool.demo-live .demo-stage canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%; display: block;
}

/* The writing sits above whatever is running. `position: relative` alone is
   enough — both are in the same stacking context and the stage is z-index 0.

   The shadow is a scrim, not a style: moving type behind stationary type is the
   hardest thing to read on this page, and a few pixels of the card's own
   background bled around each glyph separates the two planes without drawing a
   box. It costs nothing while no demo is running, because it is the same colour
   as what is behind it. */
.tool[data-demo] > h2,
.tool[data-demo] > p {
  position: relative; z-index: 2;
  text-shadow: 0 1px 14px var(--bg), 0 0 30px var(--bg);
}

/* Says "live", not "hover". The tile nearest the middle of the screen now
   mounts on its own as you scroll, so instructing the reader to hover was
   describing the previous behaviour. Hidden while running, because by then
   the tile is its own label. */
.tool[data-demo]::before {
  content: "live"; position: absolute; z-index: 2; top: 12px; right: 12px;
  font-family: var(--mono); font-size: 9px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--dim);
  border: 1px solid var(--border); border-radius: 99px; padding: 3px 8px;
  opacity: .75; transition: opacity .3s var(--ease);
}
.tool[data-demo].demo-live::before { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  /* Nothing ever mounts, so the badge would be a promise the page does not
     keep. See the reduced-motion guard in lab-demos.js. */
  .tool[data-demo]::before { display: none; }
    .tool .demo-stage { transition: none; }
}
/* No hover means the badge is a lie: on a touch screen the card in the middle
   of the screen runs on its own. */
@media (hover: none) {
  .tool[data-demo]::before { display: none; }
}
/* ── The tool pages ───────────────────────────────────────────────────────
   One stage on the left, one panel of controls on the right, and an
   explanation underneath. Shared by all three because the three tools are the
   same shape: put a thing in, push some sliders, take a thing out. */
.tp .page-head { margin-bottom: 32px; }
.tp .page-head .eyebrow a { color: var(--accent); text-decoration: none; }
.tp .page-head .eyebrow a:hover { text-decoration: underline; }

.tp-grid {
  display: grid; gap: 18px; align-items: start;
  grid-template-columns: minmax(0, 1fr) 280px;
}
@media (max-width: 860px) { .tp-grid { grid-template-columns: minmax(0, 1fr); } }

.tp-stage {
  position: relative; overflow: hidden;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--bg-2);
  min-height: 420px; aspect-ratio: 4 / 3;
  cursor: pointer;
}
/* Nothing to drop onto, so nothing that suggests it — until the family that
   wants a photograph is the one showing. */
.tp-stage-flat { cursor: default; aspect-ratio: 16 / 9; min-height: 300px; }
.tp-stage.tp-droppable { cursor: pointer; }

/* `hidden` has to actually hide. The attribute's own `display: none` comes
   from the user-agent sheet and loses to any class rule that sets `display`,
   which `.tp-empty` does — so the invitation stayed on screen over the
   pattern preview until this said otherwise. */
.tp-empty[hidden] { display: none; }
.tp-stage.over { border-color: var(--accent); background: var(--surface-2); }

.tp-view { position: absolute; inset: 0; }
/* A texture is judged repeated, so the preview repeats it. */
.tp-view-tiled { background-repeat: repeat; }
/* A cutout is judged whole, so it is contained and centred. */
.tp-view-fit { background-repeat: no-repeat; background-position: center; background-size: contain; }

/* ── The backdrop a cutout is checked against ──
   On the stage rather than on the view, and that is forced rather than
   stylistic: the view carries the cutout as its own `background-image`, and a
   pseudo-element is always painted above its own element's background — so a
   checkerboard drawn on the view would cover the very thing being checked.
   Putting it on the parent puts it underneath, where a backdrop belongs.

   Checks by default because both ways a cutout goes wrong (a halo of leftover
   background, a pale fringe from the old one) are invisible against a plain
   colour, and white hides the commoner of the two. */
.tp-stage.on-checks {
  --c: rgba(255,255,255,.07);
  background-color: #17171a;
  background-image:
    linear-gradient(45deg, var(--c) 25%, transparent 25%, transparent 75%, var(--c) 75%),
    linear-gradient(45deg, var(--c) 25%, transparent 25%, transparent 75%, var(--c) 75%);
  background-size: 20px 20px;
  background-position: 0 0, 10px 10px;
}
.tp-stage.on-light { background-color: #ffffff; background-image: none; }
.tp-stage.on-dark { background-color: #000000; background-image: none; }

/* The invitation, which gets out of the way once there is a picture. */
.tp-empty {
  position: absolute; inset: 0; display: flex; gap: 12px;
  flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 24px; pointer-events: none;
}
.tp-empty > * { pointer-events: auto; }
.tp-stage.has-image .tp-empty { display: none; }
.tp-empty-lead { color: var(--muted); font-size: 1rem; }
.tp-fine { color: var(--dim); font-size: .78rem; line-height: 1.6; }

.tp-panel {
  border: 1px solid var(--border); border-radius: 12px; padding: 18px;
  background: var(--bg-2); display: flex; flex-direction: column; gap: 12px;
}
.tp-sep { height: 1px; background: var(--border); margin: 2px 0; }

.tp-row { display: flex; flex-direction: column; gap: 6px; }
.tp-row label {
  display: flex; justify-content: space-between; align-items: baseline;
  color: var(--muted); font-size: .8rem;
}
.tp-val { color: var(--dim); font-family: var(--mono); font-size: .72rem; }
.tp-row input[type="range"] { width: 100%; accent-color: var(--accent); }
.tp-row input[type="color"] {
  width: 100%; height: 32px; padding: 0; cursor: pointer;
  background: none; border: 1px solid var(--border); border-radius: 8px;
}

.tp-seg { display: flex; gap: 6px; flex-wrap: wrap; }
.tp-b {
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); color: var(--muted);
  padding: 7px 11px; font: inherit; font-size: .82rem; cursor: pointer;
  transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.tp-b:hover { border-color: var(--border-hi); color: var(--text); }
.tp-b.on { border-color: var(--accent); color: var(--text); background: var(--surface-2); }
.tp-b.primary { background: var(--accent); border-color: var(--accent); color: #0a0a0c; }
.tp-b.primary:hover { filter: brightness(1.08); color: #0a0a0c; }
.tp-b[disabled] { opacity: .45; cursor: not-allowed; filter: none; }
.tp-save { margin-top: 4px; }
.tp-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.tp-note { color: var(--dim); font-size: .78rem; line-height: 1.55; min-height: 2.4em; }

.tp-code {
  font-family: var(--mono); font-size: .68rem; line-height: 1.5;
  color: var(--muted); background: var(--surface); border-radius: 8px;
  padding: 10px; margin: 0; max-height: 180px;
  overflow: auto; white-space: pre-wrap; word-break: break-all;
}

/* The gallery. Every swatch is drawn with the live settings, so choosing one
   is choosing something already seen at the colour and scale in use. */
.tp-swatches {
  display: grid; gap: 12px; margin-top: 18px;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.tp-swatch {
  display: flex; flex-direction: column; gap: 0; padding: 0; cursor: pointer;
  border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
  background: var(--bg-2); font: inherit;
  transition: border-color .15s ease, transform .15s ease;
}
.tp-swatch:hover { border-color: var(--border-hi); transform: translateY(-2px); }
.tp-swatch.on { border-color: var(--accent); }
.tp-swatch-face { display: block; height: 78px; background-color: var(--bg); }
.tp-swatch-name {
  padding: 7px 9px; color: var(--muted); font-size: .74rem;
  font-family: var(--mono); text-align: left;
}
.tp-swatch.on .tp-swatch-name { color: var(--text); }

.tp-about { margin-top: 44px; max-width: 68ch; display: grid; gap: 14px; }
.tp-about h2 { font-size: 1.05rem; letter-spacing: -.01em; }
.tp-about p { color: var(--muted); font-size: .92rem; line-height: 1.7; }
.tp-about strong { color: var(--text); font-weight: 500; }

@media (prefers-reduced-motion: reduce) {
  .tp-swatch:hover { transform: none; }
}

/* Not yet built, and saying so rather than linking to nothing. */
.tool.soon { opacity: .62; }
.tool.soon h2::after {
  content: "soon"; margin-left: 10px; vertical-align: middle;
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dim);
  border: 1px solid var(--border); border-radius: 99px; padding: 3px 8px;
}

/* ── One convention for every menu in the nav ─────────────────────────────
   Services used to be the odd one out. It was static markup so it could work
   with JavaScript off, and hover was the only way to open a panel with no
   script — so the bar had two kinds of dropdown: Services on hover, and
   Launcher, Tools and Account on click. The same chevron, two behaviours,
   depending on which word it sat under. Its trigger was also an <a href>, so
   pressing it navigated while pressing the others only disclosed.

   Click wins. Hover does not exist on a touchscreen, and a menu that opens
   itself as the pointer crosses the bar is a menu that opens by accident.

   ── The no-JS promise survives it ──
   Every page stamps `.js` on <html> synchronously in the <head>, before
   anything paints. So:

     no script   the panels are plain open lists, indented under their heading.
                 Every link reachable, nothing to press. This is BETTER than
                 the hover fallback it replaces, which a phone could not use.
     script      collapsed from the first paint by the rule below, then opened
                 and closed by `wireStaticMenus` in app.js.

   Keyed on `aria-expanded`, which the button carries in the shipped markup, so
   the collapse happens at paint rather than when the deferred script runs —
   nothing flashes open. app.js also sets `hidden` once it wires the panel; the
   two agree, and `hidden` is only ever present when there is script to remove
   it, which is what keeps the attribute honest for a screen reader. */
html.js .nav-menu-btn[aria-expanded="false"] + .nav-menu-panel { display: none; }

/* Scripting off: every panel is an open, indented list rather than a floating
   dropdown pinned over the page. The same flattening the burger drawer already
   does, applied for the same reason. */
html:not(.js) #nav ul { align-items: flex-start; }
html:not(.js) .nav-menu { display: block; }
html:not(.js) .nav-menu-panel {
  position: static; min-width: 0; margin-top: 4px;
  padding: 0 0 4px 13px; gap: 0;
  background: transparent; border: 0; border-left: 1px solid var(--border);
  border-radius: 0; box-shadow: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
/* A chevron that cannot be pressed is furniture. */
html:not(.js) .nav-menu-btn .chev { display: none; }
html:not(.js) .nav-menu-btn { cursor: default; }

/* ── Ask a question ───────────────────────────────────────────────────────
   A button in the corner and a panel above it. Deliberately not a chat bubble
   that opens itself: a panel springing open uninvited lands on somebody who is
   reading, and it is the most disliked pattern on the web. This waits. */
.ask-fab {
  position: fixed; right: 20px; bottom: 20px; z-index: 400;
  background: var(--accent); color: var(--on-accent, #10100c);
  border: 0; border-radius: 24px; padding: 11px 18px;
  font: 500 14px/1 var(--sans); cursor: pointer;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .34);
  transition: transform .16s var(--ease), box-shadow .16s var(--ease);
}
.ask-fab:hover { transform: translateY(-1px); box-shadow: 0 14px 32px rgba(0,0,0,.42); }
.ask-fab:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }

.ask-panel {
  position: fixed; right: 20px; bottom: 74px; z-index: 400;
  width: min(370px, calc(100vw - 40px));
  display: flex; flex-direction: column;
  background: var(--bg-2, #131316); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: 0 22px 60px rgba(0, 0, 0, .5);
  overflow: hidden;
}
.ask-panel[hidden] { display: none; }

.ask-head {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px; border-bottom: 1px solid var(--border);
}
.ask-title { flex: 1; font: 500 14px/1 var(--sans); color: var(--text); }
.ask-x {
  background: transparent; border: 0; color: var(--dim);
  font-size: 20px; line-height: 1; cursor: pointer; padding: 2px 6px; border-radius: 5px;
}
.ask-x:hover { color: var(--text); background: var(--surface); }

.ask-log {
  padding: 12px 14px; display: flex; flex-direction: column; gap: 9px;
  max-height: min(46vh, 380px); overflow-y: auto;
}
.ask-msg {
  font: 13px/1.5 var(--sans); padding: 9px 12px; border-radius: 12px;
  max-width: 88%; white-space: pre-wrap; overflow-wrap: anywhere;
}
.ask-bot { background: var(--surface); color: var(--text); align-self: flex-start; border-top-left-radius: 4px; }
.ask-you { background: var(--accent); color: var(--on-accent, #10100c); align-self: flex-end; border-bottom-right-radius: 4px; }

/* The "two ways to pay" heading, between the price ladder and the payment
   cards. Smaller than the section h2 above it because it is a sub-step of the
   same question, not a new one. */
.pay-head {
  margin: 46px 0 14px; font: 500 1.05rem/1.3 var(--sans); color: var(--text);
}

/* Three things worth asking, shown until the first question is asked. A
   visitor cannot know what a helper knows, and guessing wrong is how they get
   told it could not find that. */
.ask-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 14px 10px; }
.ask-chip {
  background: transparent; color: var(--dim);
  border: 1px solid var(--border); border-radius: 14px;
  padding: 6px 11px; font: 12px/1.2 var(--sans); cursor: pointer;
  transition: color .14s var(--ease), border-color .14s var(--ease);
}
.ask-chip:hover { color: var(--text); border-color: var(--text); }
.ask-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.ask-form { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border); }
.ask-in {
  flex: 1; min-width: 0; background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px; padding: 9px 11px;
  font: 13px/1.4 var(--sans);
}
.ask-in:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ask-send {
  background: var(--accent); color: var(--on-accent, #10100c); border: 0;
  border-radius: 8px; padding: 0 15px; font: 500 13px/1 var(--sans); cursor: pointer;
}
.ask-note { margin: 0; padding: 0 14px 12px; font-size: 11px; line-height: 1.5; color: var(--dim); }
.ask-note a { color: var(--accent); }

@media (max-width: 560px) {
  /* Nearly full width, and above the thumb rather than under it. */
  .ask-panel { right: 12px; left: 12px; bottom: 68px; width: auto; }
  .ask-fab { right: 12px; bottom: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .ask-fab, .ask-chip { transition: none; }
}


/* ── The one pill that is not a capability ────────────────────────────────
   Dashed rather than solid, so it reads as an aside next to a row of things
   this business actually sells. It is still a real button with a real focus
   ring, because it does something. */
.cap-egg {
  border-style: dashed;
  opacity: 0.72;
}
.cap-egg:hover,
.cap-egg:focus-visible {
  opacity: 1;
  border-style: solid;
  border-color: var(--accent);
  color: var(--accent);
}

/* ── Supported platforms ─────────────────────────────────────────────
   A slow horizontal drift of the services we integrate for clients.

   ── Why wordmarks and not logos ──
   Because almost none of them are ours to show. Google's guidance is
   explicit — do not use the Google logo in marketing materials for a
   business or to imply endorsement — and a strip like this is exactly
   that. Stripe's Marks Usage Terms grant the *word mark* "without
   alteration in text" to any user, and stop short of granting the logo to
   anyone who is not a Verified Partner. Cloudflare permits word marks and
   its own press-kit badges, but not the logo without written permission.
   So the whole row is typeset. Setting them in our own face is the
   compliant option and the better-looking one: a row of foreign brand
   colours would be the loudest thing on the page and none of it ours.

   The wording carries the same weight. We are the integrator — we add
   these to clients' sites — so the label is SUPPORTED PLATFORMS and never
   "powered by", which would claim the reverse relationship and imply an
   endorsement nobody has given us.

   ── Why the track is duplicated ──
   Two identical runs of the same list, translated -50%. At the moment the
   first run leaves the viewport the second is exactly where the first
   began, so the reset is invisible and the loop needs no measurement, no
   resize listener and no JS. The list is marked aria-hidden on the copy so
   a screen reader hears each name once.

   ── Why the animation is on a wrapper ──
   `transform` on a composited layer only. No layout, no paint, no main
   thread — it keeps running at full rate while the 3D hero is drawing. */

/* No rules and no tinted band: the row reads as part of the page rather than
   a boxed-off widget, and the marks carry it on their own. The mask below is
   what gives it edges. */
.platforms {
  padding: 30px 0 34px;
  overflow: hidden;
}
.platforms-viewport {
  position: relative;
  overflow: hidden;
  /* The row runs full-bleed but must not appear to start and stop at a hard
     edge, so it fades out into the page background at both ends. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.platforms-track {
  display: flex;
  width: max-content;
  animation: platforms-drift var(--platforms-speed, 42s) linear infinite;
}
.platforms-track:hover { animation-play-state: paused; }

.platforms-run {
  display: flex;
  align-items: center;
  flex: none;
  gap: clamp(2.2rem, 5vw, 4.5rem);
  padding-right: clamp(2.2rem, 5vw, 4.5rem);
}
.platforms-run li {
  list-style: none;
  font-family: var(--sans, inherit);
  font-size: clamp(1.05rem, 1.9vw, 1.5rem);
  font-weight: 500;
  letter-spacing: -0.015em;
  color: var(--muted);
  white-space: nowrap;
  transition: color .2s ease;
}
.platforms-run li:hover { color: var(--text); }

@keyframes platforms-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* Motion that scrolls sideways on its own is the clearest case there is for
   honouring this: the row stops and every name stays readable. */
@media (prefers-reduced-motion: reduce) {
  .platforms-track { animation: none; }
  .platforms-viewport { overflow-x: auto; -webkit-mask-image: none; mask-image: none; }
}

/* ── Platform marks ──────────────────────────────────────────────────
   Each logo is the brand's own acceptance mark, unmodified. Provenance and
   the rule attached to each file is in images/platforms/PROVENANCE.md.

   ── Why every mark has its own height ──
   Apple requires its mark is never shown smaller than the other payment
   identities; Google requires size parity too. Setting every <img> to one
   height does NOT achieve that, because the files disagree about what is
   inside the box:

     apple-pay.svg    165 x 106, and the mark fills it
     google-pay.svg   1094 x 742, but the visible pill is only 752 x 400
                      -- Google bakes its required 0.5x clear space into the
                      viewBox, so 46% of the file's height is deliberate
                      emptiness

   At a shared CSS height Google Pay would render about half the optical size
   of Apple Pay, breaching both rules at once. So the heights below are
   compensated: Google Pay is set ~1.85x Apple Pay's so the two *visible*
   marks match. The clear space is preserved rather than cropped, which is
   the other half of what both brands ask for.

   Stripe and PayPal are bare wordmarks with no container, so they are set
   optically rather than mathematically -- a wordmark next to a pill needs to
   be shorter to read as the same weight. */
.platforms-run li.pf { display: flex; align-items: center; }
.platforms-run li.pf img {
  display: block;
  width: auto;
  /* Never let a slow-loading SVG collapse the row height. */
  min-height: 1px;
  /* The marks must not be recoloured -- both Apple and Google prohibit it --
     so no filter, no opacity shift, no currentColor trick. They sit at full
     fidelity and the row's muted tone comes from the text items only. */
}
.pf-applepay  img { height: 38px; }
.pf-googlepay img { height: 70px; }   /* ~1.85x -- see the note above */
.pf-stripe    img { height: 21px; }  /* long wordmark: set on width, not height */
.pf-paypal    img { height: 30px; }

/* The sizes above are not arbitrary and are not free to tune down. The names
   sharing this row -- Amazon Pay, Klarna, Cash App Pay, Affirm -- are payment
   identities too, so Apple's "never smaller than the other payment
   identities" and Google's size-parity rule are measured against the *text*
   here, not just against each other. Shrinking the logos to make the row
   quieter would breach both. If the row needs to be calmer, make the type
   smaller, never the marks. */

/* The typeset half of the row. These carry the muted colour; the logos do
   not, so the row reads as one line rather than two competing brightnesses. */
.platforms-run li.pf-text {
  font-size: clamp(1rem, 1.7vw, 1.35rem);
  font-weight: 500;
  letter-spacing: -0.015em;
  color: var(--muted);
  white-space: nowrap;
  transition: color .2s ease;
}
.platforms-run li.pf-text:hover { color: var(--text); }

/* Stripe ships white and slate variants. White is correct on the dark theme;
   the light theme needs slate or the wordmark vanishes into the paper. */
:root[data-theme="light"] .pf-stripe img,
:root[data-mode="light"] .pf-stripe img { content: url("/images/platforms/stripe-slate.svg"); }
.pf-klarna    img { height: 26px; }
.pf-affirm    img { height: 24px; }
/* Cloudflare's canonical mark from cloudflare.com/logo.svg is the cloud
   alone -- two orange paths, no wordmark. That is why it can sit in this row
   at all: the press-kit logo is a black wordmark that would vanish on the
   dark theme, and recolouring it is precisely what their policy forbids. The
   orange cloud needs no alteration on either theme. Written permission for
   logo use is still outstanding -- see images/platforms/PROVENANCE.md. */
.pf-cloudflare img { height: 30px; }

/* ── Amazon Pay: the real mark, on the ground it was drawn for ──
   Amazon publishes only the reversed logo at a public URL — the asset is pure
   white with their orange accent, and nothing else is offered without going
   through their brand-kit request form.

   The light page was measured rather than assumed: its ground is
   rgb(246,245,242) plain and rgb(229,233,219) with the avocado background on,
   which puts a white mark at 1.09:1 and 1.23:1. Invisible, both of them. So
   the light theme used to drop the logo and set the words "Amazon Pay"
   instead, which was safe and was not the logo.

   A reversed mark does not need recolouring to work on a light page — it
   needs a dark ground, which is the thing it was made for and the one use
   every brand guideline explicitly permits. So the light theme gives it one:
   a small chip in the site's own ink, sized off the mark rather than a
   round number, so it reads as a payment badge and not as a patch over a
   mistake. The artwork is untouched in both themes. */
.pf-amazonpay img { height: 26px; display: block; }
.pf-amazonpay .pf-fallback { display: none; }

:root[data-theme="light"] .pf-amazonpay,
:root[data-mode="light"] .pf-amazonpay {
  background: #232a2e;
  border-radius: 7px;
  padding: 7px 11px;
  /* The other marks in this row sit on the page. This one sits on a chip, so
     its optical weight is trimmed back to match them rather than shouting. */
  line-height: 0;
}
:root[data-theme="light"] .pf-amazonpay img,
:root[data-mode="light"] .pf-amazonpay img { height: 22px; }

/* Cloudflare is not in this row. It is not a payment method, so there is no
   acceptance mark to lean on, and their policy is explicit: "Use of the
   Cloudflare logos (other than the use of Web Badges described below) requires
   our written permission." The badge that IS granted is a full-bleed orange
   banner designed as a site trust badge, and dropping it into a line of quiet
   payment marks looked exactly like what it was -- a banner in the wrong room.

   So it went to the footer, which is the context that badge was drawn for, and
   this row is now one coherent statement: the ways a customer can pay. See
   .footer-cf below, and docs/brand-permissions.md for the email that would
   allow the actual logo. */

/* Cloudflare's web badge, in the footer where trust badges belong. Held a
   little back at rest because it is far more saturated than anything else down
   there, and full strength on hover for anyone who looks at it deliberately. */
.footer-cf { display: inline-flex; align-items: center; line-height: 0;
             opacity: .72; transition: opacity .18s var(--ease); }
.footer-cf:hover { opacity: 1; }
.footer-cf img { height: 26px; width: auto; border-radius: 4px; display: block; }

/* ── The avocado background switch ───────────────────────────────────────
   Matches .mode-btn exactly so the pair reads as one control, with an avocado
   green when it is on rather than the site accent -- the accent means "do
   this" elsewhere, and this is a state, not an action. */
.avo-btn {
  background: transparent; border: 1px solid var(--border); border-radius: 999px;
  color: var(--muted); cursor: pointer; width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; margin-left: 6px;
  transition: color .15s var(--ease), border-color .15s var(--ease);
}
.avo-btn:hover { color: var(--text); border-color: var(--border-hi); }
.avo-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.avo-btn svg { display: block; }
.avo-btn.on { color: #8cc63f; border-color: rgba(140, 198, 63, .55); }
@media (prefers-reduced-motion: reduce) { .avo-btn { transition: none; } }

/* The field itself. Fixed and behind everything the page draws, including the
   hero photograph -- which is hidden while this is on, because two competing
   backgrounds is just noise.

   `pointer-events: none` is essential and easy to get wrong: without it this
   layer would swallow every click on the page. Clicks reach the canvas by
   being forwarded from a document listener instead. See assets/avocado-bg.js. */
#avocadoBg {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  /* Fades in, because the models arrive a beat after the switch is pressed and
     a hard cut to a green field reads as a glitch. */
  opacity: 0; animation: avocado-in .5s var(--ease) forwards;
}
#avocadoBg .avocado-canvas { display: block; width: 100%; height: 100%; }
@keyframes avocado-in { to { opacity: 1; } }

/* A scrim in the page's own background colour, between the field and the
   content. Without it the avocados are a busy, high-contrast pattern directly
   behind body copy and the paragraphs become genuinely hard to read -- worst
   on the light theme, where the pale field and dark ink have the least room
   between them.

   The field is decoration. It is allowed to be seen and not allowed to
   compete, and this is the one line that enforces that. Heavier on light for
   the reason above. */
#avocadoBg::after {
  content: ''; position: absolute; inset: 0;
  background: var(--bg); opacity: .42; pointer-events: none;
}
:root[data-mode="light"] #avocadoBg::after { opacity: .58; }

/* Everything the page owns has to sit above it. The nav and footer already
   carry their own stacking; these are the two that did not. */
.avocado-on #page-bg,
.avocado-on .hero-media { display: none !important; }
.avocado-on main,
.avocado-on footer { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  #avocadoBg { animation: none; opacity: 1; }
}

/* ── The avocado palette ─────────────────────────────────────────────────
   The field is green and the site accent is a hot orange, which against it
   reads as a warning rather than a highlight -- worst on the 3D hero object,
   whose rim light is the accent and which turned into a solid orange cross
   over a pale green field.

   So the accent moves for as long as the field is on. Not the whole site
   design: three tokens, which is all "orange" actually is here, and every
   button, link, kicker and rim that derives from them follows without knowing
   anything about avocados.

   Two greens rather than one, for the same reason the base theme has two: a
   colour with enough contrast on near-black is washed out on cream, and one
   that reads on cream is invisible on black. Both are pulled well down in
   saturation from the field itself, so the accent stays a highlight instead
   of disappearing into its own background. */
:root.avocado-on {
  --accent: #a8cc63;
  --accent-soft: rgba(168, 204, 99, 0.14);
  --on-accent: #12180a;
}
:root.avocado-on[data-mode="light"] {
  --accent: #55702a;
  --accent-soft: rgba(85, 112, 42, 0.12);
  --on-accent: #ffffff;
}

/* ── The consent dialog ────────────────────────────────────────────────────
 *
 * Built by assets/track.js rather than sitting in 24 pages of markup, so the
 * next page added to the site cannot ship without it.
 *
 * A bar at the bottom, not a modal: it does not cover the page, does not trap
 * focus, and can be ignored — ignoring and declining have the same effect,
 * which is the test for whether a consent request is actually free. The two
 * buttons are deliberately the same size and weight. Making "accept" the
 * obvious one is the pattern regulators name when they say consent was not
 * freely given, and it would also be a lie about how much this site cares.
 */
.jc-consent {
  position: fixed; z-index: 120;
  left: 16px; right: 16px; bottom: 16px;
  margin-inline: auto; max-width: 620px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px 18px;
  padding: 14px 18px;
  background: var(--bg-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 14px;
  box-shadow: 0 2px 8px rgba(0,0,0,.18), 0 18px 48px rgba(0,0,0,.28);
  /* Off-screen until the class lands, so it slides in after first paint
     instead of being part of it — a dialog that is already there when the
     page appears reads as a page element rather than as a question. */
  transform: translateY(calc(100% + 24px));
  opacity: 0;
  transition: transform .32s var(--ease), opacity .32s var(--ease);
}
.jc-consent.is-in { transform: none; opacity: 1; }
.jc-consent.is-gone { transform: translateY(calc(100% + 24px)); opacity: 0; }

.jc-consent-text {
  flex: 1 1 280px; min-width: 0; margin: 0;
  font-size: .86rem; line-height: 1.55; color: var(--muted);
}
.jc-consent-text a { color: var(--text); text-underline-offset: 2px; }
.jc-consent-text a:hover { color: var(--accent); }

.jc-consent-actions { display: flex; gap: 8px; flex: 0 0 auto; }
/* ── Both buttons are buttons ──
   Same padding, same type size, same weight, and both carry a visible border
   so neither reads as an afterthought. The first version left the decline as
   bare text beside a filled orange accept, which is the precise pattern the
   EDPB and the CNIL name when they say consent was not freely given — and it
   would also have been a lie about how much this site cares which one you
   pick. Accept keeps the fill because it is the affirmative action, not
   because it is the preferred one. */
.jc-consent-actions .btn {
  padding: 8px 15px; font-size: .82rem; font-weight: 500;
  border: 1px solid var(--border-hi);
}
.jc-consent-actions .btn-ghost { color: var(--text); background: transparent; }
.jc-consent-actions .btn-ghost:hover {
  border-color: var(--text); background: var(--surface-2);
}
.jc-consent-actions .btn-primary { border-color: var(--accent); }

@media (max-width: 520px) {
  .jc-consent { left: 10px; right: 10px; bottom: 10px; padding: 13px 15px; }
  .jc-consent-actions { width: 100%; }
  .jc-consent-actions .btn { flex: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .jc-consent { transition: none; }
}

/* The control on /legal, which is the same choice offered again. */
.optout { margin: 18px 0 0; }
.optout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.optout-actions .btn.is-on {
  border-color: var(--accent); color: var(--accent);
  background: var(--accent-soft);
}
.optout-actions .btn[disabled] { opacity: .5; cursor: not-allowed; }
.optout-note { margin: 10px 0 0; font-size: .84rem; color: var(--muted); max-width: 62ch; }
