/* ==========================================================================
   WEBZER — DARK ACADEMIA
   Black walls, crisp white type, one accent: a cool vibrant violet.

   The accent is load-bearing and rationed. It appears ONLY where the visitor
   can act or is currently standing:
     - primary calls to action
     - the nav item for the section you are in
     - hover and pressed states
   It never touches a heading, a paragraph, a figure, or a rule. If violet
   appears somewhere you cannot click, that is a bug.

   The device screens keep their own palette. The sites Webzer builds are
   still the only full-colour thing on the page.
   ========================================================================== */

/* The webfonts are NOT imported here. They are <link>ed from the head of each
   page instead, and that is a performance decision rather than a style one.

   An @import inside a stylesheet cannot be discovered until the stylesheet
   itself has been downloaded and parsed, so it serialises: main.css, then
   Google's CSS, then the font files, one after another, with nothing able to
   start early. Fonts are render-blocking, so that chain is dead time in front
   of the hero on every single load. As <link> tags beside this file they are
   discovered in the first pass of the HTML and fetch in parallel, which is
   what the two preconnect hints up there were always for.

   If a third page is added, copy the two <link> tags from index.html. */

/* --------------------------------------------------------------------------
   1. Reset
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-top: 5rem; }
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow-x: hidden; min-height: 100svh; }
img, svg, canvas { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }
a { color: inherit; }
ul, ol, dl { list-style: none; padding: 0; }

/* --------------------------------------------------------------------------
   2. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* The walls. Black, with a few points of violet in the blue channel so the
     dark reads cool and lit rather than flatly neutral. Still black: the
     largest gap between any two channels here is 9/255. */
  --wall-950: #07060a;   /* deepest — hero ground, closing section */
  --wall-900: #0b0a0f;   /* the page */
  --wall-850: #100e16;   /* raised panels: trades strip, lead column */
  --wall-800: #16141d;
  --wall-700: #201d29;

  /* The type. Crisp white at the top, stepping down to a cool grey. Every
     step below clears 4.5:1 on --wall-900; --ink-soft clears 9:1. */
  --ink:      #ffffff;   /* headings, emphasis, figures */
  --ink-body: #e8e8ee;   /* running text */
  --ink-soft: #a8a8b4;   /* secondary text */
  --ink-mute: #7e7e8c;   /* labels, captions, step numbers */

  /* The one accent. --accent is a surface colour and carries white text at
     5.6:1; --accent-text is the same hue lifted until it is legible AS text
     on black, at 7.4:1. Do not swap them — the dark one is unreadable as
     type and the light one is washed out as a button. */
  --accent:      #7a3def;
  --accent-lift: #8a4bf7;   /* hover fill — still 4.7:1 under white text */
  --accent-text: #a98bff;   /* accent used as type or icon on a black wall */
  /* The accent sunk 45% into the wall. For a filled area large enough that the
     full accent glares — the selected tab. Only 1.5:1 against the band on its
     own, so anything using it needs an --accent edge to be findable. */
  --accent-deep: #3b1f71;
  --accent-line: rgba(169, 139, 255, .55);
  --accent-wash: rgba(122, 61, 239, .12);
  --accent-cast: rgba(122, 61, 239, .85);  /* the lamp-glow under a CTA */

  --rule:      rgba(255, 255, 255, 0.13);
  --rule-soft: rgba(255, 255, 255, 0.065);

  /* Bodoni Moda, from 2026-08-14. The user asked for the hero to read as
     luxury and Newsreader could not get there — it is a NEWS face, drawn for
     reading articles on screen, so its whole brief is low stroke contrast and
     sturdiness at text sizes. That is the opposite of the brief here.

     Bodoni Moda is a Didone: vertical stress, flat hairline serifs, violent
     thick-to-thin contrast. It is the fashion-magazine genre, and at hero size
     that contrast is what reads as expensive.

     It keeps both properties that got Newsreader the job over Instrument
     Serif. Real weights 400..900, so no bold is ever synthesised. And an
     optical size axis, 6..96 — the browser applies it from the font-size, so
     the 89px hero gets full display contrast while smaller settings get
     sturdier, lower-contrast letterforms automatically. On a Didone that axis
     is not a nicety, it is the thing that stops the hairlines vanishing.

     Even so it is kept OFF buttons and tabs — see the note at .btn. Fallbacks
     are Didones where the OS has one, so a failed webfont degrades in kind. */
  --face-display: 'Bodoni Moda', Didot, 'Bodoni MT', 'Times New Roman', serif;
  --face-body: 'Archivo', system-ui, -apple-system, sans-serif;

  /* Ceiling dropped from 5.6rem on 2026-08-14, when the hero went to spaced
     capitals. Opening the tracking to +.055em adds roughly 270px to this line,
     which took it from four wrapped lines to five and pushed the hero past the
     fold on a 720px screen. Size and spacing are trading against each other
     here, and for a Didone the spacing is worth more: capitals with air read
     expensive, capitals at maximum size read like a newspaper front page. */
  --t-hero: clamp(2.15rem, 1.0rem + 4.5vw, 4.3rem);
  --t-h2:   clamp(1.95rem, 1.3rem + 2.7vw, 3.4rem);
  --t-h3:   clamp(1.15rem, 1.05rem + 0.5vw, 1.42rem);
  --t-lead: clamp(1.06rem, 1rem + 0.34vw, 1.3rem);
  --t-body: clamp(1rem, 0.97rem + 0.16vw, 1.08rem);
  --t-fig:  clamp(2.8rem, 1.8rem + 4vw, 4.4rem);
  --t-label: 0.72rem;

  --sp-1: .5rem; --sp-2: .875rem; --sp-3: 1.25rem;
  --sp-4: 2rem;  --sp-5: 3rem;    --sp-6: 4.5rem; --sp-7: 7rem;

  --gutter: clamp(1.15rem, 0.6rem + 2.6vw, 3.4rem);
  --measure: 68ch;
  --shell: 1280px;

  /* Tells the browser this page is dark, so the parts it draws itself come out
     dark too: the scrollbar, the select dropdown list, the caret, and the
     native "please fill this in" validation bubbles on the contact form. Those
     are all rendered by the OS and cannot be reached with CSS — without this
     the enquiry form opens a white dropdown out of a black field. */
  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   3. Base
   -------------------------------------------------------------------------- */

body { background: var(--wall-900); color: var(--ink-body); font-family: var(--face-body); font-size: var(--t-body); line-height: 1.62; }
::selection { background: var(--accent); color: #fff; }

/* The focus ring stays white on purpose. It has to be findable on the walls
   AND on top of a violet button, and white is the only thing that clears
   both. Colour is not the signal here; the ring is. */
:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }

.shell { width: min(100% - (var(--gutter) * 2), var(--shell)); margin-inline: auto; }

/* 500, not 600. A Didone's whole character is the gap between its thick strokes
   and its hairlines; adding weight closes that gap and it stops looking like a
   Bodoni. Tracking is near-neutral rather than the -.02em a grotesque wanted —
   high-contrast serifs collide at negative tracking, and the hairline is the
   first thing to go when two letters touch. */
h1, h2, h3 { font-family: var(--face-display); font-weight: 500; line-height: 1.06; letter-spacing: -.005em; color: var(--ink); }
h1 { text-transform: uppercase; }
b, strong { font-weight: 600; }

.label { font-size: var(--t-label); font-weight: 600; letter-spacing: .2em; text-transform: uppercase; color: var(--ink-mute); }

.skip { position: absolute; left: -9999px; background: var(--accent); color: #fff; padding: var(--sp-2) var(--sp-3); font-weight: 600; z-index: 300; }
.skip:focus { left: var(--sp-3); top: var(--sp-3); }

/* --------------------------------------------------------------------------
   4. Buttons — one metal, one ghost, nothing else
   -------------------------------------------------------------------------- */

/* The label is Archivo, not the display face, from 2026-08-14.

   The display face is now a Didone, and a Didone at 17px on a control is the
   one place its hairlines genuinely can fail. The optical size axis exists to
   handle that and probably would, but "probably" is the wrong standard for the
   thing a visitor has to read before they can act, and PRODUCT.md's reader is
   older, outdoors and one-handed. Buttons and the trade tabs are the two places
   the serif is deliberately not used.

   This is also a cleaner split than it replaced: the serif carries the
   editorial voice, and everything you operate is set in the sans. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--face-body); font-weight: 600; font-size: 1.04rem;
  padding: .9rem 1.65rem; border: 1px solid transparent; border-radius: 999px;
  text-decoration: none; cursor: pointer; min-height: 52px;
  /* --ease, like everything else that moves here. The source component used an
     overshoot curve (.34, 1.56, .64, 1) and copying it was a reflex: it breaks
     the one-easing rule this stylesheet keeps, and a bounce reads cheap on a
     page whose whole argument is that it was built properly. Decelerating hard
     and settling is the same swell without the wobble. */
  transition: background-color .18s ease, color .18s ease, border-color .18s ease,
              box-shadow .22s ease, transform .34s var(--ease);
}
.btn > span { white-space: nowrap; }
.btn--block { width: 100%; }

/* Hover lifts and swells slightly; press drops it back through its own
   resting size, so the click has somewhere to land. */
.btn--metal:hover { transform: translateY(-2px) scale(1.035); }
.btn--metal:active { transform: translateY(0) scale(.985); transition-duration: .1s; }

/* --- The liquid metal outline --------------------------------------------

   Both buttons are a dark pill with a band of liquid metal travelling around
   their edge. Ported from the 21st.dev React component; the shader lives in
   assets/js/liquid-metal.js.

   The ring is a real ring, cut with mask-composite, NOT a full-face canvas
   with an opaque core laid over the middle. That was the first attempt and it
   has one bad failure mode: anything that stops the core painting — a stale
   stylesheet, a missed rule — leaves the metal covering the label. A masked
   ring cannot swallow the label no matter what the rest of the CSS does. The
   geometry is also set inline at mount for the same reason.

   Two deliberate departures from the source component:
   - the label is white, not #666. On the source's own dark body #666 lands at
     2.8:1, and PRODUCT.md names low-contrast text as an outright accessibility
     failure for this audience, not a style choice.
   - the pill sizes to its content instead of a fixed 142x46. Two of these
     carry long labels, one runs full width in the pricing table, and 46px is
     under the tap-target floor.

   Without WebGL, or without mask-composite, the inset ring below stands in as
   a still metal edge and nothing else changes. -------------------------------- */

.btn--metal {
  position: relative; border: 0; overflow: hidden; isolation: isolate;
  --rim-w: 1.8px;
}
/* :not() because the ring is a span too, and would otherwise be caught by
   this rule and pulled up to the label's layer. */
.btn > span:not(.btn__rim) { position: relative; z-index: 2; }

/* Both buttons take the accent on hover — rule 3 allows exactly this. The
   ghost can do it with a background-color, which transitions. The primary's
   body is a gradient and gradients snap rather than fade, so its wash gets a
   layer of its own that can be faded. It sits under the ring and under the
   label, so neither is tinted. */
.btn--metal::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  border-radius: inherit; pointer-events: none;
  background: var(--accent-wash);
  opacity: 0; transition: opacity .2s ease;
}
.btn--metal:hover::before { opacity: 1; }

/* The primary reads as a raised, lit object: filled body, cast shadow,
   thicker rim. */
.btn--metal {
  --rim-w: 2px;
  color: var(--ink);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .7);
  background: linear-gradient(180deg, #202027 0%, #000 100%);
  box-shadow:
    inset 0 0 0 var(--rim-w) #5f5f6b,          /* the still rim */
    0 26px 14px -14px rgba(0, 0, 0, .55),
    0 10px 10px -8px rgba(0, 0, 0, .45),
    0 2px 5px rgba(0, 0, 0, .4);
}
.btn--metal:hover {
  color: var(--accent-text);
  box-shadow:
    inset 0 0 0 var(--rim-w) #7a7a88,
    0 14px 8px -8px rgba(0, 0, 0, .45),
    0 6px 6px -4px rgba(0, 0, 0, .4),
    0 1px 3px rgba(0, 0, 0, .35);
}

/* The secondary is the same edge around nothing: flat, unfilled, thinner rim.
   It still picks up the accent on hover — that remains the accent's job. */
/* THERE IS ONE BUTTON. `.btn--ghost` was removed on 2026-08-14 at the user's
   direction, after they twice reported the secondary buttons looking faded
   beside the primary ones.

   It was a real hierarchy device, not a mistake: unfilled body, 1.3px ring
   instead of 2px, no drop shadow. But every one of those three reads as "less
   present" before it reads as "less important", and stood next to a raised,
   lit, metal-edged pill, the honest description of the result is the one the
   user gave — it looked switched off.

   The cost is real and is now paid deliberately: the hero offers two equally
   loud choices, and the two plan buttons in the pricing table carry the same
   weight. MASTER.md already flagged the version of this problem that existed
   when both buttons shared the moving edge; this is the end of that thread.
   Distinction between actions now has to come from the label, position and
   surrounding copy, which is where it was already doing most of the work.

   Do not reintroduce a quieter button variant without raising it first. */

/* The ring. padding is the band width; the two masks subtract the inside of
   the pill from the whole of it, leaving only that band. */
.btn__rim {
  position: absolute; inset: 0; z-index: 1;
  border-radius: inherit; pointer-events: none;
  padding: var(--rim-w);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
}
.btn__metal {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border-radius: inherit; pointer-events: none;
}

.btn__ripple {
  position: absolute;
  width: 20px; height: 20px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,.45) 0%, rgba(255,255,255,0) 70%);
  pointer-events: none;
  animation: btn-ripple .6s ease-out forwards;
}
@keyframes btn-ripple {
  from { transform: translate(-50%, -50%) scale(0); opacity: .55; }
  to   { transform: translate(-50%, -50%) scale(4); opacity: 0; }
}

/* --------------------------------------------------------------------------
   5. Masthead
   -------------------------------------------------------------------------- */

.masthead {
  position: sticky; top: 0; z-index: 100;
  background: color-mix(in srgb, var(--wall-950) 86%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--rule-soft);
}
.masthead__in { display: flex; align-items: center; gap: var(--sp-3); padding-block: .8rem; }
.wordmark { display: flex; align-items: center; gap: .5rem; text-decoration: none; margin-right: auto; min-height: 44px; color: var(--ink); }
/* The mark, from the supplied 1024x1024 JPEG.

   Two things are being worked around, both properties of that file rather than
   choices. It is a JPEG, so it cannot carry transparency and arrives as a black
   square; and the artwork sits in the middle of a lot of empty black, occupying
   under half the frame, so shown whole it would render about 13px tall in a
   24px box.

   The crop is done here rather than to the file because there is no image
   library on this machine: background-size scales the 1024px square up until
   the mark alone fills the box, and background-position slides the rest off.
   The numbers are the mark's bounds in the source — x 270..755, y 295..695 —
   times the 0.06 scale that puts 400 source pixels into 24.

   screen blend drops the black. White on black through screen leaves white and
   lets black fall through to whatever is behind, so the square disappears and
   the glow baked into the file becomes a soft halo instead of a grey box. Same
   trick the hero field uses over the slate.

   All of this goes away if a real SVG ever turns up: replace the whole rule
   with url(logo.svg) center / contain. */
.wordmark__mark {
  width: 29px; height: 24px; flex: none;
  background: url("../img/logo.jpg") no-repeat;
  background-size: 61.44px 61.44px;
  background-position: -16.2px -17.7px;
  mix-blend-mode: screen;
}
/* Without blend support the black square would sit on the bar, so it hides and
   the "Webzer" wordmark carries the masthead on its own. */
@supports not (mix-blend-mode: screen) { .wordmark__mark { display: none; } }
.wordmark svg { width: 27px; height: 24px; flex: none; transition: color .2s ease; }
.wordmark:hover svg { color: var(--accent-text); }
.wordmark span { font-family: var(--face-display); font-weight: 600; font-size: 1.42rem; letter-spacing: -.02em; }

/* Nav: grey at rest, violet when you point at it, violet with a solid rule
   under it when you are standing in that section. aria-current carries the
   same fact for anyone who cannot see the colour. */
.nav { display: none; gap: var(--sp-4); }
.nav a {
  display: inline-flex; align-items: center; min-height: 44px;
  font-weight: 500; color: var(--ink-soft); text-decoration: none;
  border-bottom: 2px solid transparent;
  transition: color .18s ease, border-color .18s ease;
}
.nav a:hover { color: var(--accent-text); border-bottom-color: var(--accent-line); }
.nav a[aria-current] { color: var(--accent-text); border-bottom-color: var(--accent); }
@media (min-width: 66rem) { .nav { display: flex; } }

/* 1rem, not .93rem. This is the smallest display text on the page, and a
   serif at 15px on a dark bar is where hairlines go first — opsz helps but
   does not make 15px into 16px. PRODUCT.md's reader is older and outdoors. */
.masthead .btn { min-height: 44px; padding: .55rem 1.15rem; font-size: 1rem; }

/* --------------------------------------------------------------------------
   6. HERO — cut stone
   -------------------------------------------------------------------------- */

.hero {
  position: relative; overflow: hidden;
  background: var(--wall-950);
  /* Tied to the viewport rather than a fixed height, so the trades strip
     below always peeks in by roughly the same margin on any screen. */
  min-height: min(82svh, 900px);
  display: flex; flex-direction: column; justify-content: center;
}
@media (max-width: 61.99rem) { .hero { min-height: 0; } }

/* Short laptops (1366x768 and friends): the hero's content, not its
   min-height, is what pushes the trades below the fold. Tighten the padding
   rather than shrinking the type. */
@media (min-width: 62rem) and (max-height: 860px) {
  .hero { min-height: 0; }
  .hero .hero__in { padding-block: var(--sp-4) var(--sp-2); }
  .hero .hero__grid { gap: var(--sp-3); }
}
/* The stone is the ground. The field is screened over it, so it reads as
   light moving across the slab rather than a dark layer buried underneath.
   Screening onto a dark backdrop only ever lightens, which is the whole
   reason the first attempt came out black. */
.hero__stone {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: url("../img/slate.jpg") center / cover no-repeat;
}

/* No fade-in: an undrawn canvas is opaque black, and black in a screen blend
   changes nothing, so it is already invisible until the shader draws. Making
   this depend on a transition only created a way for it to stay hidden. */
.hero__field {
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%; display: block;
  mix-blend-mode: screen;
  opacity: .62;
}

/* Without blend support the field would sit as an opaque box over the stone,
   so it stays hidden and the photograph carries the hero on its own. */
@supports not (mix-blend-mode: screen) { .hero__field { display: none; } }

/* Scrim: the stone stays visible, the type stays readable. */
.hero::before {
  content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  /* Heavy where the type sits, thin on the right so the field can be seen
     moving. The 4.5:1 floor is what sets the left-hand values. */
  background:
    linear-gradient(102deg, rgba(7,6,10,.93) 0%, rgba(7,6,10,.8) 32%, rgba(7,6,10,.36) 60%, rgba(7,6,10,.12) 100%),
    linear-gradient(to bottom, rgba(7,6,10,.45) 0%, transparent 26%, transparent 64%, rgba(7,6,10,.64) 100%);
}
@media (max-width: 61.99rem) {
  .hero::before { background: linear-gradient(to bottom, rgba(7,6,10,.64) 0%, rgba(7,6,10,.82) 44%, rgba(7,6,10,.91) 100%); }
}

.hero__in { position: relative; z-index: 3; padding-block: var(--sp-6) var(--sp-4); }
.hero__grid { display: grid; gap: var(--sp-5); align-items: center; }
@media (min-width: 62rem) { .hero__grid { grid-template-columns: minmax(0, 1.06fr) minmax(0, 1fr); gap: var(--sp-4); } }

/* The hero, set as capitals should be set.

   Weight drops to 400: at 89px the size carries the presence on its own, and
   the lightest weight is where the thick/thin contrast is widest, which is the
   entire reason for choosing a Didone.

   Tracking goes POSITIVE, and this is the correction that matters most. It was
   -.02em, inherited from a setting tuned for a grotesque. Capitals are drawn
   with sidebearings that assume lowercase alongside them, so set solid they
   crowd, and set tight they collide — which is what made this read dense and
   tabloid rather than expensive. Luxury typography does the reverse almost
   without exception: caps, light, and generously spaced. */
.hero__title {
  font-size: var(--t-hero);
  font-weight: 400;
  letter-spacing: .055em;
  line-height: 1.12;
}
.hero__lead { margin-top: var(--sp-4); font-size: var(--t-lead); max-width: 42ch; color: var(--ink-soft); }
.hero__lead b { color: var(--ink); font-weight: 600; }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-4); }

.hero__strip {
  position: relative; z-index: 3;
  padding-block: var(--sp-3); border-top: 1px solid var(--rule);
}
.hero__aside { color: var(--ink-soft); font-size: .92rem; }

/* --- The fine print, drifting -------------------------------------------

   Gives the hero motion even while the shop demos are mid-hold. Two identical
   runs, and the track travels exactly one run's width — at which point run two
   sits precisely where run one started, so the reset is invisible.

   The mask fades both ends so the text enters and leaves rather than being
   guillotined at the shell edge. */

.ticker {
  overflow: hidden;
  -webkit-mask: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
          mask: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
}
.ticker__track {
  display: flex; width: max-content;
  animation: ticker-run 42s linear infinite;
  /* This strip is ~2700px wide and lives inside a masked parent. Without a
     layer of its own the browser re-rasterises that whole width against the
     mask on every frame instead of just moving a finished texture, and any
     main-thread work — a pointer crossing the hero, a hover recalculating —
     shows up as a stutter. will-change is right here rather than lazy: the
     element animates for the entire life of the page. */
  will-change: transform;
}
.ticker__run {
  flex: none; display: flex; align-items: center;
  gap: var(--sp-4); padding-right: var(--sp-4);
}
.ticker__run > * { white-space: nowrap; }
.ticker__sep { flex: none; width: 4px; height: 4px; border-radius: 50%; background: var(--ink-mute); }

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

/* It does NOT pause on hover. That was here for WCAG 2.2.2 — content that starts
   moving by itself and runs over five seconds needs a way to stop it — and was
   removed on 2026-08-12 at the user's request: the hero should never look
   frozen, and stopping every time a pointer crossed the strip made it look
   broken rather than considerate.

   What still satisfies 2.2.2 is the reduced-motion path below, which does not
   pause this thing but dismantles it: the animation is unset, the duplicate run
   dropped, and the line left wrapped and still. That is a deliberate user
   setting rather than an accident of where the pointer happens to be, which is
   the better mechanism of the two anyway.

   If a link is ever put inside this strip, restore a :focus-within pause with
   it — a keyboard user cannot chase a moving target. Nothing in here is
   focusable today, so the rule would never have fired. */
.hero__price { font-family: var(--face-display); font-weight: 600; font-size: clamp(1.2rem, .95rem + 1.2vw, 1.85rem); letter-spacing: -.04em; }

/* --------------------------------------------------------------------------
   7. The devices — real shop sites, rotating

   Everything below the .screen keeps its own colours. These are the sites
   Webzer builds, not Webzer's site, and the accent rules do not apply inside
   them — a client's brand is not violet.
   -------------------------------------------------------------------------- */

.rig { position: relative; padding-top: var(--sp-4); }
.rig__label { position: absolute; top: 0; right: 0; z-index: 5; }

.laptop { width: min(100%, 600px); margin-left: auto; }
.laptop__lid {
  background: linear-gradient(#26232e, #17151d); border-radius: 12px 12px 3px 3px;
  box-shadow: 0 30px 60px -20px rgba(0,0,0,.9), 0 0 0 1px rgba(255,255,255,.08);
  padding: 9px 9px 11px;
}
.laptop__base { height: 11px; margin: 0 -4%; border-radius: 0 0 9px 9px; background: linear-gradient(#2f2b38, #1a1820); clip-path: polygon(2% 0, 98% 0, 100% 100%, 0 100%); }
.laptop__base::after { content: ""; display: block; width: 13%; height: 3px; margin: 0 auto; border-radius: 0 0 3px 3px; background: rgba(0,0,0,.6); }

.phone {
  position: absolute; left: 0; bottom: -4%; z-index: 3;
  width: clamp(118px, 19vw, 166px);
  background: linear-gradient(#26232e, #17151d); border-radius: 24px; padding: 6px;
  box-shadow: 0 28px 48px -18px rgba(0,0,0,.9), 0 0 0 1px rgba(255,255,255,.08);
}
.phone__notch { position: absolute; top: 6px; left: 50%; transform: translateX(-50%); width: 42%; height: 14px; background: #17151d; border-radius: 0 0 9px 9px; z-index: 4; }

.screen { position: relative; overflow: hidden; background: #fff; container-type: inline-size; }
.laptop .screen { aspect-ratio: 16 / 10; border-radius: 3px; }
.phone .screen { aspect-ratio: 9 / 19.5; border-radius: 19px; }

/* One shop's site: nav, hero photo, headline, action. Real proportions. */
.shop { position: absolute; inset: 0; display: flex; flex-direction: column; opacity: 0; transition: opacity .55s ease; }
.shop.is-on { opacity: 1; }

.shop__nav {
  flex: 0 0 auto; display: flex; align-items: center; gap: 3cqw;
  padding: 2.4cqw 3.4cqw; background: #fbf9f6; border-bottom: 1px solid rgba(0,0,0,.09);
}
.shop__mark { font-family: var(--face-body); font-weight: 700; font-size: 2.5cqw; letter-spacing: -.02em; color: #16150f; white-space: nowrap; }
.shop__links { display: flex; gap: 2.6cqw; margin-left: auto; }
.shop__links span { font-size: 1.9cqw; color: #5f5b53; white-space: nowrap; }
.shop__navcta { font-size: 1.9cqw; font-weight: 600; padding: .9cqw 2cqw; border-radius: 99px; background: #16150f; color: #fbf9f6; white-space: nowrap; }

.shop__hero { position: relative; flex: 1; overflow: hidden; }
.shop__hero img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.shop__hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, rgba(6,5,4,.86) 8%, rgba(6,5,4,.34) 52%, rgba(6,5,4,.12) 100%); }

.shop__copy { position: absolute; left: 3.6cqw; right: 3.6cqw; bottom: 3.4cqw; z-index: 2; }
.shop__h { font-family: var(--face-body); font-weight: 700; font-size: 5.4cqw; line-height: 1.02; letter-spacing: -.03em; color: #fff; }
.shop__p { font-size: 2.1cqw; color: rgba(255,255,255,.86); margin-top: 1.2cqw; }
.shop__cta { display: inline-block; margin-top: 2.4cqw; font-size: 2cqw; font-weight: 600; padding: 1.4cqw 3.2cqw; border-radius: 99px; background: #fff; color: #16150f; }

/* The phone runs the same site, sized for its own width */
.phone .shop__nav { padding: 3.2cqw 4cqw; gap: 2cqw; }
.phone .shop__mark { font-size: 5cqw; }
.phone .shop__links { display: none; }
.phone .shop__navcta { font-size: 3.6cqw; padding: 1.6cqw 3.4cqw; }
.phone .shop__copy { left: 5cqw; right: 5cqw; bottom: 5cqw; }
.phone .shop__h { font-size: 9.4cqw; }
.phone .shop__p { font-size: 4cqw; margin-top: 2cqw; }
.phone .shop__cta { font-size: 3.8cqw; padding: 2.6cqw 5.6cqw; margin-top: 4cqw; }

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

/* --------------------------------------------------------------------------
   8. Sections
   -------------------------------------------------------------------------- */

.section { padding-block: var(--sp-7); }
.section + .section { border-top: 1px solid var(--rule-soft); }
.section__title { font-size: var(--t-h2); max-width: 18ch; }

/* The contact page's h1 wears .section__title, and h1 is uppercase site-wide.
   Capitals are drawn with sidebearings that assume lowercase beside them, so
   set solid they crowd and at this size in a Didone the hairlines start
   touching. +.055em is the same figure the homepage hero uses and it is not
   decoration — it is what stops the letters colliding. The measure widens with
   it because capitals are wider than the lowercase the 18ch was counted for. */
.choose .section__title { letter-spacing: .055em; max-width: 24ch; }
.section__intro { margin-top: var(--sp-3); color: var(--ink-soft); font-size: var(--t-lead); max-width: 52ch; }

/* Tighter on top than the bottom: the trade names have to clear the fold on a
   900px screen, and the hero owns the space above them. This is why the label
   stays beside the strip rather than above it — stacking pushed the pills under
   the fold. */
.trades { background: var(--wall-850); padding-block: var(--sp-3) var(--sp-5); }
@media (min-width: 62rem) { .trades { padding-block: var(--sp-2) var(--sp-6); } }

/* --- The trades tablist -------------------------------------------------

   One border around all five. A highlight slides to whatever the pointer is
   over and settles back on whatever was last clicked. Ported from the 21st.dev
   slide-tabs component.

   The source keeps its label legible over the moving pill with
   mix-blend-difference, which only resolves cleanly for a black or white
   highlight — over violet it turns the type yellow-green. Not needed here:
   white clears 19:1 on the band and 5.6:1 on the accent, so the label is
   simply white in both states and nothing has to be timed to the slide.

   No :has() growth and no padding animation: with a tablist one panel is
   always open, so the band has a fixed height and never moves. */

/* Side by side, with the strip to the right — the user's layout, kept.

   flex-start, not center: .tabs is the strip AND the panel under it, so
   centring would float the label halfway down the pair instead of setting it
   beside the strip. The small padding-top lines its first line up with the tab
   labels rather than with the top of the pill.

   A stacked version was tried on 2026-08-14 and reverted at the user's
   direction. It closed the vertical gap but moved the strip off the right,
   which was not what was wanted. The gap is the honest cost of this
   arrangement: the strip carries a tab row AND a description panel, so it will
   always be taller than one line beside it. Size is what closes most of it. */
.trades__in { position: relative; display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--sp-3) var(--sp-4); }

/* It was a <p> at .92rem grey — 14.7px, under the 16px floor this system sets
   for body text, and squarely what PRODUCT.md calls an accessibility failure
   for an older buyer reading outdoors. Reported as simply "too small", which it
   was, by rule and not only by eye.

   Now an <h2>, so it inherits Bodoni, weight 500 and --ink from the h1/h2/h3
   rule; only size and measure are set here. White rather than grey is half the
   gain on its own — it stops reading as a caption on the strip and starts
   reading as the statement of who the band is for, which is what it is. */
/* 15rem, and the number is doing real work — see the note on .trades__in about
   the row that was wrapping. Label 240 + 32 gap + the strip's natural 881 comes
   to 1153 inside a 1179 shell, which leaves 26px of slack. Widen this and the
   row wraps again, the strip drops onto its own line, and the empty space comes
   straight back. If the strip ever gets a sixth trade, take width off here
   first. */
.trades__label {
  max-width: 15rem;
  padding-top: .35rem;
  font-size: clamp(1.3rem, 1.05rem + .85vw, 1.75rem);
}

.tabs__list {
  position: relative; display: flex; flex-wrap: wrap; gap: 4px;
  padding: 4px; border: 1px solid rgba(255, 255, 255, .18); border-radius: 999px;
}

/* Sit against the right edge of the shell rather than trailing the label. Costs
   no width, so it cannot push the strip onto a second line. Only while the label
   and the strip are actually side by side — once the row wraps, the strip is on
   its own line and pushing it right just strands it. */
@media (min-width: 62rem) { .tabs { margin-left: auto; } }

/* Wide screens have genuinely spare width to the right, so the strip takes it
   and the tabs spread into it. Spreading rather than padding each tab means the
   stretch follows the screen instead of being a fixed number that is wrong on
   most of them.

   82rem, not 78rem, and the margin is the whole point. space-between only
   behaves while the tabs are on ONE row; the moment the strip wraps it strands
   the last row's tabs 150px apart. At 78rem the strip cleared its container by
   5px, close enough that a different font rendering would wrap it and the
   layout would fall apart. Below 82rem the strip stays compact, which is what
   it should do anyway — there is no spare width to spread into down there. */
@media (min-width: 82rem) {
  .tabs { flex: 1; }
  .tabs__list { justify-content: space-between; }
}

.tabs__tab {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .6rem 1.1rem; border: 0; border-radius: 999px; background: none;
  /* Archivo, for the same reason as .btn — these are controls, and a Didone at
     16px is where hairlines get fragile. */
  font-family: var(--face-body); font-weight: 600;
  font-size: clamp(1rem, .94rem + .3vw, 1.22rem);
  letter-spacing: -.01em; color: var(--ink);
  cursor: pointer; white-space: nowrap;
  min-height: 44px;                       /* thumb-sized, per PRODUCT.md */
}
.tabs__tab svg { width: 1.1em; height: 1.1em; flex: none; color: var(--ink-mute); transition: color .25s ease; }
.tabs__tab[aria-selected="true"] svg { color: var(--ink); }

/* The highlight marks the selection and nothing else, so hover needs its own
   answer or the tabs stop feeling clickable. Violet type is the same hover
   language as the nav and the buttons. Excluded on the selected tab, which
   already has white type on the violet pill and would only get muddier. */
.tabs__tab:not([aria-selected="true"]):hover { color: var(--accent-text); }
.tabs__tab:not([aria-selected="true"]):hover svg { color: var(--accent-text); }

/* The travelling highlight. Position is a transform, so the slide itself costs
   the compositor nothing.

   Width IS animated, deliberately, and it is the one layout property left in
   this stylesheet. Every tab is a different width, so the pill has to change
   shape as it travels or it snaps. The usual replacement — scaleX on a fixed
   box — is wrong here: this is a pill, and scaling it horizontally turns its
   semicircular caps into ellipses, not only during the slide but at rest,
   since almost no tab lands at scale 1.

   What makes it affordable is that the element is out of flow, empty, and has
   no in-flow siblings depending on it, so a width change lays out exactly one
   box and nothing else on the page moves. That is a different thing from the
   padding animation this file used to have on .trades, which reflowed every
   section below the band on every frame.

   Height is set per tab but deliberately NOT transitioned: every tab is the
   same height (nowrap labels, shared padding, one font size), so it never
   actually changes and animating it was dead weight. */
.tabs__cursor {
  position: absolute; top: 0; left: 0; z-index: 0;
  height: 0; width: 0; opacity: 0;
  border-radius: 999px; pointer-events: none;
  /* Dark body, bright edge — the same construction as the buttons. The full
     accent as a fill this size glared; sunk into the wall it stops shouting,
     but at 1.5:1 it would then be invisible against the band, so the --accent
     border is what makes the selection findable. Together they read as one
     lit object rather than a swatch. */
  background: var(--accent-deep);
  border: 1px solid var(--accent);
  box-shadow: 0 8px 20px -12px var(--accent-cast);
  transition: transform .38s var(--ease), width .38s var(--ease), opacity .2s ease;
}

/* The panel drops out of the strip and inherits its left edge and width, because
   it is a child of .tabs rather than its own .shell row. Nothing measures or
   aligns it — being nested IS the alignment.

   Until the script says otherwise all five are just visible, one under another,
   which is the no-script state and is readable. */
/* inline-size containment, and it is load-bearing. Without it the panel's
   unwrapped sentence (~1390px) becomes .tabs' natural width, .tabs demands more
   room than the row has, and the label gets bumped onto its own line. Contained,
   the panel's width no longer feeds back into its parent, so .tabs is sized by
   the strip and the panel simply wraps inside whatever that turns out to be. */
.tabs__panels { margin-top: .6rem; contain: inline-size; }
.tabs__panel {
  margin: 0;
  padding: var(--sp-3) var(--sp-3);
  border: 1px solid rgba(255, 255, 255, .14); border-radius: 16px;
  background: var(--wall-900);
  color: var(--ink-soft); font-size: 1rem; line-height: 1.55;
}
.tabs__panel + .tabs__panel { margin-top: .5rem; }

/* The prompt that holds the cell before anything is picked. Hidden entirely
   without script, where it would be inviting a click that does nothing and the
   five real panels are all on show anyway. */
.tabs__panel--empty { display: none; color: var(--ink-mute); }
.tabs__panels.is-enhanced .tabs__panel--empty { display: block; }

/* Enhanced: the panels stack into one cell so the band cannot change height
   between trades, and only the selected one is shown. */
.tabs__panels.is-enhanced { display: grid; }
.tabs__panels.is-enhanced .tabs__panel {
  grid-area: 1 / 1; margin: 0;
  opacity: 0; visibility: hidden;
  transition: opacity .28s ease;
}
.tabs__panels.is-enhanced .tabs__panel.is-on { opacity: 1; visibility: visible; }

@media (max-width: 47.99rem) {
  .tabs__list { border-radius: 22px; }
  .tabs__tab { padding: .55rem .85rem; }
}

/* --- The launch film ------------------------------------------------------

   Sits between the trades band and the first content section: the first thing
   met on the way down.

   aspect-ratio holds the 16:9 box before a single frame has loaded, so nothing
   below it jumps when the poster arrives — which matters more than usual here,
   because preload="none" means the video itself may not arrive for a while. */

/* The darkest ground on the page, the hero's own, so the film reads as a room
   you have walked into rather than a video dropped on the page. The band above
   it is --wall-850 and the section below is --wall-900, which puts this at the
   bottom of that rhythm. It carries .section, so its spacing is the same as
   every other section rather than a one-off. */
.reel { background: var(--wall-950); }

/* Centred, and released from the 18ch measure the other section titles use —
   that cap exists to force a two-line break on a left-aligned heading, and
   here it would wrap a twenty-character line for no reason. */
.reel .section__title { max-width: 28ch; margin-inline: auto; text-align: center; }

.reel__frame {
  position: relative; overflow: hidden;
  margin-top: var(--sp-5);
  aspect-ratio: 16 / 9;
  border-radius: 18px; border: 1px solid var(--rule);
  background: #000;
  box-shadow: 0 40px 80px -44px rgba(0, 0, 0, .95);
}
.reel__video { display: block; width: 100%; height: 100%; object-fit: cover; }

@media (max-width: 47.99rem) { .reel__frame { border-radius: 12px; margin-top: var(--sp-4); } }

.ledger { margin-top: var(--sp-5); border-top: 1px solid var(--rule); }
.ledger__row { display: grid; gap: var(--sp-1) var(--sp-5); padding-block: var(--sp-4); border-bottom: 1px solid var(--rule); }
@media (min-width: 48rem) { .ledger__row { grid-template-columns: minmax(0, 16rem) minmax(0, 1fr); } }
.ledger dt { font-family: var(--face-display); font-weight: 600; font-size: var(--t-h3); letter-spacing: -.015em; color: var(--ink); }
.ledger dd { color: var(--ink-soft); max-width: var(--measure); }

/* Pricing — the two ways to pay, and the call where the figure is said.

   ⚠ THE .tariff RULES BELOW ARE NOW UNUSED, and they are kept on purpose. The
   two-column pricing table they styled was removed from index.html on
   2026-09-03 because the site became the landing page for cold outreach and
   the owner did not want a stranger meeting the number before the call. The
   pricing itself did not change. Leaving the rules here means restoring the
   table is a revert of the markup and nothing else. Delete them only if the
   decision is made permanently, and say so here when you do. */

/* The .choice panels are borrowed from contact.html's fork for their layout.
   There they are two doors; here they are description, so the lift and the
   accent border have to be scoped to the ones that actually go somewhere —
   a panel that rises under the cursor and does nothing reads as a dead link. */
div.choice:hover { border-color: var(--rule); transform: none; }
div.choice:hover .choice__title { color: var(--ink); }

/* The button and its caveat sit on one line where there is room, and the
   caveat wraps under it where there is not. align-items keeps the sentence
   optically level with the pill rather than with its shadow. */
.rates__act {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-5);
}
.rates__note { color: var(--ink-mute); font-size: .95rem; max-width: 30ch; }

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

.tariff {
  width: 100%; margin-top: var(--sp-5);
  border-collapse: collapse;
  text-align: left;
  table-layout: fixed;
}

.tariff th, .tariff td { vertical-align: top; padding: var(--sp-3) var(--sp-3); }
.tariff thead th, .tariff thead td { padding-top: 0; }
.tariff th:first-child, .tariff td:first-child { padding-left: 0; width: 24%; }
.tariff td:last-child, .tariff th:last-child { padding-right: 0; }

/* Row labels: quiet, so the values carry the weight */
.tariff tbody th {
  font-family: var(--face-body); font-weight: 600;
  font-size: var(--t-label); letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-mute); padding-top: calc(var(--sp-3) + .35rem);
}

.tariff tbody tr { border-top: 1px solid var(--rule); }
.tariff tbody td { color: var(--ink-soft); }
.tariff tbody td b { color: var(--ink); }

/* The column head */
.tariff thead th { border-bottom: 2px solid var(--ink); padding-bottom: var(--sp-3); }
.tariff__name { display: block; font-family: var(--face-display); font-weight: 600; font-size: var(--t-h3); letter-spacing: -.015em; color: var(--ink); }
.tariff__for { display: block; margin-top: .3rem; font-weight: 400; font-size: .93rem; color: var(--ink-mute); }

/* The numbers that decide it. White, not violet: a price is not a button. */
.tariff__fig {
  display: block; font-family: var(--face-display); font-weight: 700;
  font-size: clamp(1.9rem, 1.2rem + 2.2vw, 3rem); line-height: 1;
  letter-spacing: -.05em; color: var(--ink);
}
.tariff__note { display: block; margin-top: .5rem; font-size: .9rem; color: var(--ink-mute); }

/* The recommended column reads as a panel running the height of the table.
   The panel itself is the whole marker.

   There was a 3px white cap across the top of this header too. It sat flush
   against the words with a 0px gap, directly above a header that already has a
   2px white rule under it — white bar, text, white bar — so it read as a
   rendering fault rather than as emphasis, which is exactly how the user
   reported it. It was also unlabelled, and labelling it is not available: the
   honest labels for a lead column ("most popular", "what most people pick")
   are all customer-count claims, and hard rule 1 says there are none to make.
   An emphasis marker nobody can explain is just a line. */
.tariff .is-lead { background: var(--wall-850); }

.tariff__corner { border: 0 !important; background: none !important; }

/* Actions live outside the table — they are not tabular data, and a fixed
   table layout will not give them a usable width. The grid mirrors the
   table's columns so each one still sits under its own plan. */
.tariff__actions { display: grid; gap: var(--sp-3); }

@media (min-width: 48rem) {
  .tariff__actions { grid-template-columns: 24% 1fr 1fr; align-items: start; gap: 0; }
  .tariff__actions > div { padding: var(--sp-4) var(--sp-3) var(--sp-4); }
  .tariff__actions > div:last-child { padding-right: 0; }
  .tariff__actions .is-lead { background: var(--wall-850); }
}
@media (max-width: 47.99rem) {
  .tariff__actions { margin-top: var(--sp-4); }
  .tariff__actions .tariff__corner { display: none; }
}

@media (max-width: 47.99rem) {
  .tariff th, .tariff td { padding: var(--sp-2) var(--sp-2); font-size: .94rem; }
  .tariff th:first-child, .tariff td:first-child { width: 26%; }
  .tariff tbody th { font-size: .64rem; letter-spacing: .1em; }
  .tariff__for { display: none; }

  /* A third of a phone screen is not a button. Break the actions out of the
     columns and let them run full width, named so they still say which plan
     they belong to. */
  .tariff tfoot tr { display: block; }
  .tariff tfoot td { display: block; width: 100%; padding: var(--sp-3) 0 0; background: none; }
  .tariff tfoot .tariff__corner { display: none; }
  .tariff tfoot .btn { padding-inline: 1rem; }
  .tariff tfoot .btn > span { white-space: normal; text-align: center; }
}

.run { margin-top: var(--sp-5); counter-reset: step; display: grid; }
@media (min-width: 52rem) { .run { grid-template-columns: repeat(4, 1fr); gap: var(--sp-4); } }
.run__step { counter-increment: step; padding-block: var(--sp-4); border-top: 1px solid var(--rule); }
/* The step number. Was --ink-mute at 1.15rem, and the owner reported the 4 as
   hard to see — correctly. --ink-mute is the faintest token on the site and
   sits ON the AA floor at 4.93:1, which the ambient layer then trimmed to
   4.77:1. A single glyph in a Didone at 1.15rem is the worst possible case for
   that: high stroke contrast means the hairlines are the first thing to go,
   and there is no word shape around it to help the eye guess.

   --ink-soft instead, measured 8.11:1 over the ambient ground — roughly 1.7x
   the separation — and a small size bump. It is still quieter than the heading
   it labels, which is the whole job of a step number. */
.run__step::before { content: counter(step); display: block; margin-bottom: var(--sp-3); font-family: var(--face-display); font-weight: 600; font-size: 1.34rem; color: var(--ink-soft); }
.run__step h3 { font-size: var(--t-h3); letter-spacing: -.03em; }
.run__step p { color: var(--ink-soft); margin-top: var(--sp-2); }
.run__when { color: var(--ink-mute); font-size: .86rem; }

/* FAQ — native disclosure: no script, keyboard-operable, and it prints.
   Heading holds the left column so the questions get a readable measure. */

.faq-grid { display: grid; gap: var(--sp-4); }
@media (min-width: 62rem) {
  .faq-grid { grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); gap: var(--sp-6); align-items: start; }
  .faq-head { position: sticky; top: 6rem; }
}
.faq-head__note { margin-top: var(--sp-3); color: var(--ink-mute); }

.faq { border-top: 1px solid var(--rule); }
.faq__item { border-bottom: 1px solid var(--rule); }

.faq__item summary {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) 0;
  font-family: var(--face-body); font-weight: 500;
  font-size: clamp(1.02rem, .98rem + .22vw, 1.16rem);
  letter-spacing: -.005em; line-height: 1.4;
  color: var(--ink);
  cursor: pointer; list-style: none;
  transition: color .2s ease;
}
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary:hover { color: var(--accent-text); }
.faq__item[open] summary { font-weight: 600; }

/* Marker: a ringed plus that empties to a minus and fills violet when open —
   open is a state you put it in, so the accent is doing its job. */
.faq__mark {
  flex: none; margin-left: auto; position: relative;
  width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--rule);
  transition: background-color .3s var(--ease), border-color .3s var(--ease);
}
.faq__mark::before, .faq__mark::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  background: var(--ink-soft);
  transition: transform .35s var(--ease), background-color .3s var(--ease);
}
.faq__mark::before { width: 11px; height: 1.5px; }
.faq__mark::after  { width: 1.5px; height: 11px; }

.faq__item summary:hover .faq__mark { border-color: var(--accent-line); }
.faq__item summary:hover .faq__mark::before,
.faq__item summary:hover .faq__mark::after { background: var(--accent-text); }
.faq__item[open] .faq__mark { background: var(--accent); border-color: var(--accent); }
.faq__item[open] .faq__mark::before,
.faq__item[open] .faq__mark::after { background: #fff; }
.faq__item[open] .faq__mark::after { transform: rotate(90deg) scaleX(0); }

.faq__body {
  padding: 0 0 var(--sp-4) 0; max-width: 62ch; color: var(--ink-soft);
}
.faq__body p + p { margin-top: var(--sp-2); }

@media (prefers-reduced-motion: reduce) {
  .faq__mark, .faq__mark::before, .faq__mark::after { transition: none; }
}


.close { text-align: center; background: var(--wall-950); }
.close__title { font-size: var(--t-h2); margin-inline: auto; max-width: 20ch; }
.close__lead { margin: var(--sp-3) auto 0; max-width: 48ch; font-size: var(--t-lead); color: var(--ink-soft); }
.close__actions { margin-top: var(--sp-4); }

.todo { display: inline-block; border: 1px dashed var(--ink-mute); color: var(--ink-mute); font-size: .74rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; padding: .3rem .7rem; }

.foot { padding-block: var(--sp-4); border-top: 1px solid var(--rule-soft); }
.foot__in { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); justify-content: space-between; }
.foot p { color: var(--ink-mute); font-size: .88rem; }

/* The direct lines, moved here from the contact page on 2026-08-15. On that
   page they sat beside the two calls and offered an alternative to the exact
   thing being sold; a footer is where someone looks for a number when they have
   already decided not to use the form, which is a different moment entirely.

   Set above the fine print rather than in it: these are the one thing in the
   footer anyone comes looking for, and the rule underneath separates them from
   the legal-ish line without making them a section. Sized and spaced for a
   thumb — 44px minimum, because a phone number that cannot be tapped in
   daylight is the failure PRODUCT.md names. */
.foot__reach {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-4);
  padding-bottom: var(--sp-3); margin-bottom: var(--sp-3);
  border-bottom: 1px solid var(--rule-soft);
}
.foot__reach a {
  display: inline-flex; align-items: center; min-height: 44px;
  font-family: var(--face-display); font-weight: 500;
  font-size: var(--t-h3); letter-spacing: -.005em;
  color: var(--ink); text-decoration: none;
  border-bottom: 2px solid var(--rule);
  transition: color .18s ease, border-color .18s ease;
}
.foot__reach a:hover { color: var(--accent-text); border-bottom-color: var(--accent); }

/* --------------------------------------------------------------------------
   9. The contact page

   Everything below is contact.html. The accent rule holds here exactly as it
   does everywhere else, and a form is where it is easiest to break: labels,
   legends, hint text and the privacy note are all type, so they stay white or
   grey. Violet appears on the border of a field you are pointing at, on the
   dot of a radio you have chosen, and nowhere else.
   -------------------------------------------------------------------------- */

/* --- The page header ------------------------------------------------------

   A modifier on .hero rather than a second construction: same stone, same
   WebGL field, same scrim, with the min-height dropped and the two-column grid
   never introduced. A client site needs five of these, so it is worth having
   one that is a variant of something already built and tested. */

/* The .reach direct-lines block that used to live here went with its markup on
   2026-08-15. It offered a phone number, an email and a booking link under the
   heading "If you would rather just talk", which was offering as an alternative
   the exact thing both calls already are. Booking a call is now the only route
   in. Its .reach__line and .reach__note rules are gone with it rather than left
   behind as styling for markup that no longer exists. */

/* --- The enquiry form -----------------------------------------------------

   Same two-column shape as the FAQ: the heading holds a sticky left column so
   the form gets a readable width, collapsing to one column on a phone with the
   heading first. */

.enquiry-grid { display: grid; gap: var(--sp-4); }
@media (min-width: 62rem) {
  .enquiry-grid { grid-template-columns: minmax(0, 21rem) minmax(0, 1fr); gap: var(--sp-6); align-items: start; }
  .enquiry-head { position: sticky; top: 6rem; }
}

.enquiry { display: grid; gap: var(--sp-4); }

.fld { display: grid; gap: .55rem; }
/* fieldset needs its border and padding removed explicitly — the reset only
   zeroes margin — and it stays display:block rather than joining the grid,
   because a grid fieldset drags <legend> out of its normal box behaviour in
   several browsers. */
.fld--set { display: block; border: 0; padding: 0; }
.fld--set legend { padding: 0; margin-bottom: .6rem; }

/* THIS RULE IS LOAD-BEARING AND HAS BEEN MISSED ONCE ALREADY.

   `display: block` above beats the `hidden` attribute's own display: none, so
   any fieldset carrying .fld--set stays on screen even after script sets
   hidden on it. It was added for .launch-only alone, which left the route
   fieldset visible on step two: a "which call do you want?" question sitting
   under a form whose questions had already been chosen, and dead, because the
   routing moved to the URL and its change handlers went with it. Someone
   switching it saw nothing happen.

   Scoped to .fld--set so it covers every fieldset in this form rather than the
   one that happened to be reported. If another display value is ever set on a
   fieldset here, give it a matching [hidden] rule at the same time. */
.fld--set[hidden] { display: none; }

.fld > label, .fld--set legend {
  font-family: var(--face-body); font-weight: 600;
  font-size: 1rem; line-height: 1.4; color: var(--ink);
}
.fld__opt { font-weight: 400; font-size: .88rem; color: var(--ink-mute); margin-left: .35rem; }

.enquiry input[type="text"],
.enquiry input[type="tel"],
.enquiry input[type="email"],
.enquiry select,
.enquiry textarea {
  width: 100%; min-height: 52px;
  padding: .8rem 1rem;
  font-family: var(--face-body);
  /* Never below 1rem. Safari zooms the whole page in when a field under 16px
     takes focus, and then the visitor is pinched out and scrolling sideways on
     the one surface PRODUCT.md calls primary. This is a functional floor, not
     a typographic preference. */
  font-size: 1rem; line-height: 1.5;
  color: var(--ink);
  background: var(--wall-850);
  border: 1px solid var(--rule);
  border-radius: 10px;
  transition: border-color .18s ease;
}
.enquiry textarea { min-height: 7.5rem; resize: vertical; }

/* --ink-soft, not --ink-mute. The mute grey is the label colour everywhere
   else on the site and clears 4.9:1 there, but these sit on --wall-850 rather
   than --wall-900 and it drops to 4.79:1 — measured, not guessed. That still
   passes WCAG AA, and it is under the 4.9 floor this design system set for
   itself, on the two pieces of text that tell a first-time visitor what to put
   in the box. --ink-soft is 8.14:1 on the same panel and is still visibly
   quieter than the white of real input text, so the hierarchy survives. */
.enquiry ::placeholder { color: var(--ink-soft); opacity: 1; }

/* Hover is one of the two places the accent is allowed, and on a form it is
   doing real work: it tells you which of nine boxes you are about to land in.
   Focus is deliberately NOT accented — the global white 3px ring handles that,
   because the ring has to stay findable on the walls and on a violet surface
   alike, and colour is not carrying the state either way. */
.enquiry input[type="text"]:hover,
.enquiry input[type="tel"]:hover,
.enquiry input[type="email"]:hover,
.enquiry select:hover,
.enquiry textarea:hover { border-color: var(--accent-line); }

/* The native arrow is drawn by the OS in the OS's colours, which on Windows is
   a dark glyph on a light chip sitting in the middle of a black form. Replaced
   with an authored chevron matching the inline SVG set: 1.7 stroke, currentColor
   via a stroke value that matches --ink-soft. */
.enquiry select {
  appearance: none; -webkit-appearance: none;
  padding-right: 3rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a8a8b4' stroke-width='1.7' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1rem center;
  background-size: 20px 20px;
}
/* Until something is picked the placeholder option is showing, and it should
   read as a prompt rather than as an answer — quieter than a real choice, but
   not dim. Same 4.79:1 measurement as the placeholder above; see the note
   there for why this is --ink-soft. */
.enquiry select:invalid { color: var(--ink-soft); }

/* Radios as full-width rows, not a horizontal run of small circles. Every row
   is a 48px tap target with the label inside it, so the whole row is the hit
   area — the thumb does not have to find a 20px dot. */
.opts { display: grid; gap: .5rem; }
.opt {
  display: flex; align-items: center; gap: .8rem;
  min-height: 48px; padding: .55rem .9rem;
  background: var(--wall-850);
  border: 1px solid var(--rule); border-radius: 10px;
  color: var(--ink-body); cursor: pointer;
  transition: border-color .18s ease, color .18s ease;
}
.opt:hover { border-color: var(--accent-line); color: var(--accent-text); }
/* accent-color paints the native control's checked dot, which keeps the whole
   thing a real radio — keyboard, arrow keys, screen reader semantics and form
   submission all stay the browser's job. A chosen radio is a control you have
   acted on, so the accent belongs on it. */
.opt input { flex: none; width: 20px; height: 20px; margin: 0; accent-color: var(--accent); }

/* Lifts the chosen row so the selection is legible without relying on the dot
   alone. :has() is an enhancement here on purpose — without it the native dot
   still shows the state, so nothing is lost where it is unsupported. */
.opts:has(input:checked) .opt:has(input:checked) {
  border-color: var(--accent); color: var(--ink);
}

/* --- Step one: the fork ---------------------------------------------------

   Two panels, and they are the only thing on the screen worth acting on. Not
   `.btn`: those are pills sized to a single label, and each of these carries a
   title, a posture about money and two lines of explanation.

   Deliberately NOT identical, which is also what keeps them clear of craft rule
   4 — the launch panel is raised on --wall-850, the same language the pricing
   table uses to mark its lead column, so the pair reads as a recommendation
   rather than a matched set of cards. */

.choose__pair { display: grid; gap: var(--sp-3); margin-top: var(--sp-5); }
@media (min-width: 48rem) {
  .choose__pair { grid-template-columns: 1fr 1fr; gap: var(--sp-4); align-items: stretch; }
}

.choice {
  display: flex; flex-direction: column;
  padding: var(--sp-4);
  border: 1px solid var(--rule); border-radius: 18px;
  background: transparent;
  text-decoration: none; color: inherit;
  transition: border-color .2s ease, background-color .2s ease, transform .34s var(--ease);
}
.choice--lead { background: var(--wall-850); }

/* Hover and focus are the two places the accent is allowed, and this is the
   whole point of the screen — it should feel like the thing you press. */
.choice:hover { border-color: var(--accent); transform: translateY(-2px); }
.choice:hover .choice__title { color: var(--accent-text); }
.choice:hover .choice__go { color: var(--accent-text); border-bottom-color: var(--accent); }

.choice__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .6rem var(--sp-2); }
.choice__title {
  font-family: var(--face-display); font-weight: 500;
  font-size: clamp(1.45rem, 1.15rem + 1vw, 2rem);
  letter-spacing: -.005em; line-height: 1.06; color: var(--ink);
  transition: color .2s ease;
}
/* The money posture, said once and quietly. "Completely free" is the entire
   argument for the left-hand panel and should not need reading twice. */
.choice__tag {
  font-family: var(--face-body); font-weight: 600;
  font-size: .74rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-mute); border: 1px solid var(--rule); border-radius: 999px;
  padding: .28rem .7rem; white-space: nowrap;
}
.choice__tag--lead { color: var(--ink-body); border-color: var(--accent-line); }

.choice__body { margin-top: var(--sp-3); color: var(--ink-soft); font-size: 1rem; line-height: 1.55; }

/* Pushed to the bottom so both panels' calls to action line up however
   differently the two descriptions wrap. */
.choice__go {
  margin-top: auto; padding-top: var(--sp-3);
  align-self: flex-start;
  font-family: var(--face-body); font-weight: 600; color: var(--ink);
  border-bottom: 2px solid var(--rule);
  transition: color .2s ease, border-color .2s ease;
}

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

/* Step two's header: which call you are booking, and the way back to change it.
   Built by contact.js so it can never be left in the markup saying the wrong
   one. */
.enquiry__which {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  margin-bottom: var(--sp-3);
  font-family: var(--face-body); font-weight: 600;
  font-size: .74rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-mute);
}
.enquiry__change {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--ink-body); text-decoration: underline; text-underline-offset: 3px;
  letter-spacing: .1em;
  transition: color .18s ease;
}
.enquiry__change:hover { color: var(--accent-text); }

/* --- The two calls -------------------------------------------------------

   The route options carry a title and an explanation rather than a bare label,
   because the difference between them is a payment and a commitment, not a
   preference. .opt already gives the row its box, hit area and hover; this only
   changes what sits inside it. */

.opt--rich { align-items: flex-start; padding: var(--sp-3) 1rem; }
.opt--rich input { margin-top: .2rem; }        /* aligns the dot to the title's cap height, not the block */
.opt--rich > span { display: block; }
.opt--rich b {
  display: block; font-weight: 600; color: var(--ink);
  font-size: 1.05rem; line-height: 1.3;
}
.opt--rich em {
  display: block; margin-top: .3rem;
  font-style: normal; font-size: .92rem; line-height: 1.5;
  color: var(--ink-soft);
}
/* The description is explanatory, not a second heading, so it must not take the
   violet with the title on hover — that would read as two links. */
.opt--rich:hover em { color: var(--ink-soft); }

/* --- The launch-only block -----------------------------------------------

   Set in from the shared fields by a rule down its left edge, so it reads as a
   branch of the form rather than more of the same. Hidden AND disabled together
   when the intro call is chosen; see the note in contact.js for why disabled is
   the half that matters. */

.launch-only {
  margin-top: var(--sp-1);
  padding-left: var(--sp-3);
  border-left: 2px solid var(--rule);
}
/* Hiding is handled by the .fld--set[hidden] rule further up — see the note
   there, and do not re-add a one-off rule for this block alone. */
.launch-only__legend {
  font-family: var(--face-body); font-weight: 600; font-size: 1rem;
  color: var(--ink);
}
.launch-only__note { margin-top: .35rem; color: var(--ink-soft); font-size: .92rem; }
.launch-only .fld { margin-top: var(--sp-3); }

/* Groups inside the block use a span rather than a nested <legend>, since a
   fieldset may only have one. Same weight as a label, and the radiogroup is
   named by it through aria-labelledby. */
.fld__legend {
  display: block;
  font-family: var(--face-body); font-weight: 600;
  font-size: 1rem; line-height: 1.4; color: var(--ink);
  margin-bottom: .55rem;
}

/* Checkboxes sit in the same rows as the radios and take the same accent when
   ticked — both are controls the visitor has acted on. */
.opt input[type="checkbox"] { accent-color: var(--accent); }

.enquiry__send .todo { display: block; width: fit-content; margin-bottom: var(--sp-2); }
.enquiry__blocked {
  margin-top: var(--sp-3); max-width: 54ch;
  color: var(--ink-soft); font-size: .93rem;
}
/* The live calendar link inside the not-yet-connected notice. It is the only
   working way through this page until the form has a destination, so it is a
   proper 44px target rather than a word in a sentence. */
.enquiry__direct {
  display: inline-flex; align-items: center; min-height: 44px;
  margin-top: .35rem;
  color: var(--ink); font-weight: 600;
  text-decoration: underline; text-underline-offset: 4px;
  text-decoration-thickness: 2px;
  transition: color .18s ease;
}
.enquiry__direct:hover { color: var(--accent-text); }

/* A switched-off button must not look pressable. The metal ring is not mounted
   on it either — see the selector in liquid-metal.js — so it sits still as well
   as reading as unavailable. */
.btn[disabled] { opacity: .5; cursor: not-allowed; }
.btn--metal[disabled]:hover { transform: none; color: var(--ink); }
.btn--metal[disabled]:hover::before { opacity: 0; }

/* .foot__in a, not .foot a. Scoped to the fine-print row on purpose: as
   `.foot a` this matched the phone number and email in .foot__reach too, and
   since both selectors carry the same specificity and this one is declared
   later, it quietly won — the direct lines rendered at --ink-mute, the same
   grey as the copyright line, instead of the white they are set to. Found by
   swapping the real links in and measuring the computed colour, which is the
   only way that surfaces: both rules look right in isolation. */
.foot__in a { color: var(--ink-mute); text-decoration: underline; text-underline-offset: 3px; transition: color .18s ease; }
.foot__in a:hover { color: var(--accent-text); }

/* --------------------------------------------------------------------------
   10. Motion

   The rules that sweep and the timeline that draws itself stay white. They
   are decoration, not affordances, and nothing here is clickable.
   -------------------------------------------------------------------------- */

/* One easing for everything that moves. */
:root { --ease: cubic-bezier(.16, 1, .3, 1); }

.js [data-reveal] { opacity: 0; transform: translateY(20px); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.js [data-reveal].is-in { opacity: 1; transform: none; }

/* --- The headline sets itself, word by word ---------------------------- */

.word {
  display: inline-block; overflow: hidden; vertical-align: bottom;
  /* room for descenders, so the mask never clips a 'y' */
  padding-bottom: .09em; margin-bottom: -.09em;
}
.word > i {
  display: inline-block; font-style: normal;
  transform: translateY(112%);
  /* The fade is what makes the arrival read as smooth rather than mechanical.
     The word is already clipped by its parent, so without it each one appears
     as a hard edge sliding past a cut line. Deliberately quicker than the
     travel (.5s against .9s) so a word is fully opaque before it settles —
     fading and moving in lockstep looks like the text is dissolving. */
  opacity: 0;
  /* Promoted for the duration only. These are 3–8 boxes moving at once over a
     WebGL canvas and a photograph, which is the most expensive place on the
     page to be repainting; the class is dropped when the last word lands. */
  will-change: transform, opacity;
  transition: transform .9s var(--ease), opacity .5s ease-out;
}
.hero__title.is-set .word > i { transform: none; opacity: 1; }
.hero__title.is-done .word > i { will-change: auto; }

/* --- The ledger writes itself: rule sweeps, then the row lands --------- */

.ledger__row { position: relative; }
.js .ledger__row::after {
  content: ""; position: absolute; left: 0; bottom: -1px; height: 1px; width: 100%;
  background: var(--ink); transform: scaleX(0); transform-origin: left;
  transition: transform 1.1s var(--ease);
}
.js .ledger__row.is-in::after { transform: scaleX(1); }

.js .ledger__row > dt,
.js .ledger__row > dd {
  opacity: 0; transform: translateY(15px);
  transition: opacity .75s var(--ease), transform .75s var(--ease);
}
.js .ledger__row.is-in > dt { opacity: 1; transform: none; transition-delay: .1s; }
.js .ledger__row.is-in > dd { opacity: 1; transform: none; transition-delay: .19s; }

/* --- The process draws its own timeline -------------------------------- */

.run { position: relative; }
@media (min-width: 52rem) {
  .js .run::before {
    content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px;
    background: var(--ink); transform: scaleX(0); transform-origin: left;
    transition: transform 1.6s var(--ease); z-index: 1;
  }
  .js .run.is-in::before { transform: scaleX(1); }
}
.js .run__step { opacity: 0; transform: translateY(18px); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.js .run__step.is-in { opacity: 1; transform: none; }

/* --- The tariff fills in, row by row ------------------------------------ */

.js .tariff tbody tr { opacity: 0; transform: translateY(12px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.js .tariff tbody tr.is-in { opacity: 1; transform: none; }

/* --- The devices drift as the hero scrolls ----------------------------- */

.laptop { transform: translate3d(0, var(--drift, 0px), 0); }
.phone  { transform: translate3d(0, var(--drift-2, 0px), 0); }

@media (prefers-reduced-motion: reduce) {
  /* opacity as well as transform. The split does not run under reduced motion
     so these boxes should not exist at all, but the start state is now a
     hidden one and a rule that can hide the headline does not get to depend on
     that staying true — hard rule 5. */
  .word > i { transform: none !important; opacity: 1 !important; }
  .js .ledger__row::after { transform: scaleX(1) !important; }
  .js .ledger__row > dt, .js .ledger__row > dd,
  .js .run__step, .js .tariff tbody tr { opacity: 1 !important; transform: none !important; }
  .js .run::before { transform: scaleX(1) !important; }
  .laptop, .phone { transform: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
  .js [data-reveal] { opacity: 1; transform: none; }

  /* Not just un-animated — removed. With transitions clamped to nothing, a
     hover swell becomes an instant jump, which is worse than no swell. */
  .btn--metal:hover,
  .btn--metal:active { transform: none; }

  /* The ticker has to be dismantled, not merely stopped. The clamp above would
     otherwise run it to its end frame instantly, parking the track a full run
     to the left with the real text off-screen. Unset the animation, drop the
     duplicate run, and let the line wrap where it stands. */
  .ticker { overflow: visible; -webkit-mask: none; mask: none; }
  .ticker__track { animation: none; transform: none; width: 100%; }
  .ticker__run { flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); padding-right: 0; }
  .ticker__run > * { white-space: normal; }
  .ticker__run + .ticker__run { display: none; }
}

/* --------------------------------------------------------------------------
   The ambient. A violet warmth on the page ground, everywhere, very faint.
   -------------------------------------------------------------------------- */

/* WHY THIS IS NOT A BREACH OF THE ONE-ACCENT RULE, and where the line is.

   Rule 3 says violet marks what you can act on, and that if violet appears
   somewhere you cannot click it is a bug. This is the same exemption the hero
   field already runs under, in the words used there: you cannot click the
   weather. It is ambient light in a room rather than the accent doing its job.

   THE LINE IS CONTRAST, NOT INTENT, so it is measured rather than argued.
   The accent as a control reads at 3.5:1 against the wall. This peaks at 6%
   alpha, which moves the ground from #0b0a0f to about #120d1c — a shift of
   about 13 points in the blue channel and 7 in red. It is at the edge of
   visible on a good screen and invisible on a bad one, which is the intent.

   Push it and the mechanism breaks: if the page is generally violet, the nav's
   violet current-section marker stops being a signal and becomes the
   background. If this ever needs to be stronger, take the strength out of the
   saturation and put it into the size of the blobs instead.

   CONTRAST, MEASURED at 6% over --wall-900:
     --ink        19.73 -> 19.09:1
     --ink-body   16.17 -> 15.64:1
     --ink-soft    8.38 ->  8.11:1
     --ink-mute    4.93 ->  4.77:1   <- the binding one, AA floor is 4.5
   --ink-mute is the constraint and it has 0.27 of headroom. Anything that
   raises the alpha, adds a third blob, or overlaps two of them harder must be
   re-measured against --ink-mute, not eyeballed. */

body::before {
  content: "";
  position: fixed;
  /* Oversized on purpose. The layer drifts a couple of percent each way, and
     at inset 0 that would swing a hard edge into view along one side. */
  inset: -12%;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(42rem 34rem at 18% 22%, rgba(122, 61, 239, .06), transparent 70%),
    radial-gradient(38rem 30rem at 82% 68%, rgba(122, 61, 239, .05), transparent 70%);
  animation: ambient-drift 90s ease-in-out infinite alternate;
}

/* Same trick as the barber hero: `alternate` means the end frame is the next
   start frame, so a 90-second cycle never snaps back. Two blobs at different
   offsets keep the movement from reading as one object sliding. */
@keyframes ambient-drift {
  from { transform: translate3d(-2.2%, -1.6%, 0) scale(1); }
  to   { transform: translate3d(2.2%, 1.6%, 0) scale(1.06); }
}

/* Explicitly none, not merely un-animated. The global reduce block further up
   clamps every animation to 0.01ms with !important, which would run this
   straight to its end frame and park the blobs off-centre — the one place the
   layer is least even. Killing the animation outright leaves it at `from`. */
@media (prefers-reduced-motion: reduce) {
  body::before { animation: none; transform: none; }
}

/* Raised panels sit ON the ground and occlude it, which is what "raised"
   should mean. The ambient shows in the open stretches between them. */

/* --------------------------------------------------------------------------
   Legal pages — privacy policy and anything like it
   -------------------------------------------------------------------------- */

/* A document, not a designed page. Nobody reads these for pleasure and the
   only job is legibility: one column, a narrow measure, and enough air between
   the numbered clauses that someone hunting for section 13 can find it by
   scanning. No reveals, no field, nothing that moves. */
.legal { padding-block: var(--sp-6); }
.legal .shell { max-width: 46rem; }

.legal__title { font-size: var(--t-h1-sm, 2.6rem); text-transform: uppercase; letter-spacing: .055em; }

/* The date line. --ink-mute is the right weight for it — this is the one thing
   on the page that genuinely is a caption. */
.legal__stamp {
  margin-top: var(--sp-2); margin-bottom: var(--sp-5);
  color: var(--ink-mute); font-size: .92rem;
}

/* More space above a heading than below it — the same rhythm as the rest of
   the site, so the sections read as a stack rather than as a wall. */
.legal h2 {
  margin-top: var(--sp-5); margin-bottom: var(--sp-3);
  font-size: 1.45rem; letter-spacing: -.005em;
}
.legal h3 {
  margin-top: var(--sp-4); margin-bottom: var(--sp-2);
  font-size: 1.12rem; color: var(--ink);
}
.legal p { margin-bottom: var(--sp-3); color: var(--ink-body); }

/* Discs, not the site-wide bare list. In running legal text an unmarked list
   reads as more paragraphs; the marker is what says "these are items". */
.legal__list {
  list-style: disc; padding-left: 1.4rem;
  margin-bottom: var(--sp-3); color: var(--ink-body);
}
.legal__list li { margin-bottom: .5rem; }

.legal__contact { color: var(--ink); }
.legal a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.legal a:hover { color: var(--accent-text); }

/* The quiet footer link to this page. Small, low, and not competing with the
   phone number and email above it — somebody looking for a privacy policy will
   look in a footer, and nobody else should notice it. */
.foot__legal { margin-top: var(--sp-2); }
.foot__legal a { color: var(--ink-mute); font-size: .88rem; text-decoration: none; }
.foot__legal a:hover { color: var(--accent-text); }
