/* ===========================================================================
   The immersive pass.

   Andrei, 22 October 2026, after seeing direction E: keep the words and the
   balance of the live site, change what it feels like. The brand purple
   stops being an accent on a near black page and becomes the ground the
   whole thing stands on.

   This file is layered over css/styles.css rather than folded into it, so
   the live design is still there underneath, every change is in one place
   and reversible, and a page without this stylesheet is exactly the site as
   it is today.

   Three rules hold the whole thing together:

     1. Nothing is drawn over a colour edge or over content. The field is
        behind everything, always, and every panel sits on it squarely.
     2. Every surface a reader reads from is dark enough to carry text at
        4.5:1 against the brightest the field can ever go. The shader is
        clamped for that, and the panels are measured against the clamp, not
        against how it usually looks.
     3. The glass is translucency, not blur. backdrop-filter on a page that
        also runs a full screen shader is paying twice for one effect, and
        on a phone it is the difference between smooth and not.
   =========================================================================== */

/* --------------------------------------------------------------------------
   The ground
   -------------------------------------------------------------------------- */

/* The canvas is fixed, so the field stays put while the page travels over
   it. z-index below everything; nothing has to be lifted above it. */
#field {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -2;
  display: block;
  border: 0;
  /* It never takes a click, a drag or a scroll, so a phone cannot get stuck
     on it instead of the page. */
  pointer-events: none;
}

/* What the page stands on when the field is not there: no WebGL2, reduced
   motion, a context lost, or a device the watchdog decided it was not fair
   to ask. This is not a fallback in the apologetic sense. It is the design,
   and the field is the moving version of it. */
/* On the root, not on body, and that is the whole reason the field was
   invisible for a fortnight.

   Andrei said three times that he could not see the moving purple
   background. He was right, and it was not the shader: measured on 24
   October, hiding the canvas altogether changed not one pixel of the page,
   while lifting it above the ground took the same screen from 16 levels of
   luminance to 43 and set it moving. The field has been running the whole
   time, behind an opaque sheet.

   What put the sheet there was a good fix. On 23 October an opaque colour
   went on to html, because on a phone at device pixel ratio 2 anything the
   browser declined to paint had nothing behind it and composited to white.
   But a background on the root is also what stops body's background being
   propagated up to it. Before that line, body's gradient became the root's
   and was painted first, underneath everything. After it, body kept its own
   background and painted it as an ordinary in flow descendant, which in the
   painting order comes after child stacking contexts with a negative
   z-index. The canvas sits at -2. So the ground went on top of the field.

   Moving the gradients to the root fixes it without giving up either thing:
   html keeps its opaque colour underneath, so nothing can ever composite to
   white, and the gradients paint as part of the root background, which is
   the first thing painted and therefore underneath the canvas rather than
   over it. The field shows, and the ground is still there the moment the
   field is not. */
/* And body gets out of the way. The base sheet paints body in --bg, which
   is opaque, and body is an in flow descendant, so it covered the canvas
   just as surely as the gradients did. There is no risk in taking it off:
   the root underneath is opaque in the base sheet and carries the gradients
   here, so there is always something behind everything. */
html.imm body {
  background: none;
}

html.imm {
  background-image:
    radial-gradient(1200px 700px at 18% -10%, rgba(82, 31, 136, 0.55), transparent 62%),
    radial-gradient(900px 600px at 88% 8%, rgba(124, 63, 191, 0.32), transparent 60%),
    linear-gradient(180deg, #1A0F28 0%, #120B1B 45%, #0C0A0C 100%);
  background-attachment: fixed;
}

/* The painted ground is never given up, even when the field is running on
   top of it. Taking it away bought nothing (the canvas is opaque and covers
   it) and risked everything: any moment the canvas is not painting, a
   stretched resize, a slow first frame, a context handed back, left a white
   page under light text, which is the page unreadable rather than merely
   plain. Seen in a full page capture on 22 October and fixed by keeping the
   ground underneath for good. */

/* The old halos were there to lift a flat near black page. The field does
   that now, and two soft purple glows on top of a soft purple ground is
   just mud. */
html.imm .glow,
html.imm .glow-a,
html.imm .glow-c {
  display: none;
}

/* --------------------------------------------------------------------------
   Glass

   One surface token, used everywhere, so a panel cannot drift away from the
   contrast the text on it was measured against.
   -------------------------------------------------------------------------- */

html.imm {
  /* Measured against the shader's own ceiling, vec3(0.42, 0.21, 0.63),
     which is the brightest pixel the field is allowed to produce. At this
     weight the muted ink still measures 7.8 on a panel, so the glass is as
     light as it looks rather than as dark as it needs to be. The first
     version of this was 0.74 and read as a black rectangle cut out of the
     page, which is the opposite of glass. */
  --pane: rgba(14, 10, 20, 0.42);
  --pane-solid: rgba(12, 9, 17, 0.62);
  --pane-line: rgba(243, 239, 249, 0.16);
  --pane-line-strong: rgba(243, 239, 249, 0.26);
  /* The lit top edge and the shadow underneath. A pane of glass catches the
     light along its top edge and casts; without those two it is a coloured
     rectangle, however transparent you make it. */
  --pane-lift:
    inset 0 1px 0 rgba(243, 239, 249, 0.15),
    0 24px 60px -34px rgba(0, 0, 0, 0.75);
}

/* The header is the one piece of glass that has to hide what is behind it
   rather than show it. At 0.6 with saturate(140%) a green button scrolling
   underneath came through as a bright green smear across the bar, and the
   saturation boost was making it worse rather than better: it was amplifying
   exactly the colour that needed damping. Heavier, blurrier, and no boost. */
html.imm .header {
  background: rgba(12, 9, 17, 0.86);
  backdrop-filter: blur(20px) saturate(104%);
  border-bottom: 1px solid var(--pane-line);
}

/* --------------------------------------------------------------------------
   What is a panel and what is not

   The first pass put a pane on everything with a border, including two
   things that were never cards in the base design: the three facts under
   the hero and the rows of the trust list. Both are text with a rule above
   them, and giving them a dark background turned them into black boxes
   stamped on a soft purple ground. They are back to being text on the
   field, which is what the field is for.

   What is left is the things that are genuinely objects: a product card, a
   price, a finding, a panel you read something out of.
   -------------------------------------------------------------------------- */

html.imm .product,
html.imm .card,
html.imm .price-card,
html.imm .plan,
html.imm .head-card,
html.imm .tile,
html.imm .finding,
html.imm .passed,
html.imm .ring,
html.imm .chat,
html.imm .one-rule,
html.imm .person-photo-wait,
html.imm .conf,
html.imm .vx {
  background: var(--pane);
  border-color: var(--pane-line);
  border-radius: var(--r-card);
  box-shadow: var(--pane-lift);
  /* No backdrop blur here, and that is a decision rather than an omission.
     Blurring a dozen panels on top of a full screen shader was too much
     together: the watchdog stepped the field down twice, ran out of room
     and switched it off, so the page bought frosted glass by paying for it
     with the thing the whole design is. The panels are translucent, lit
     along the top edge and casting, which is already glass; the blur only
     softened what showed through. The field matters more.

     The header keeps its blur, because that is one small bar which has to
     hide what passes under it rather than show it. */
}

/* On a phone the page is already running a full screen shader, and a
   backdrop blur on top of it is paying twice for one effect. The glass gets
   its weight from opacity instead, which costs nothing. */
@media (max-width: 820px) {
  html.imm .product,
  html.imm .card,
  html.imm .price-card,
  html.imm .plan,
  html.imm .head-card,
  html.imm .tile,
  html.imm .finding,
  html.imm .passed,
  html.imm .ring,
  html.imm .chat,
  html.imm .one-rule,
  html.imm .person-photo-wait,
  html.imm .conf,
  html.imm .vx {
    background: var(--pane-solid);
  }
}

/* The three facts under the hero, and the trust rows. Text, with a rule. */
html.imm .band-item,
html.imm .trust-row {
  background: none;
  box-shadow: none;
  backdrop-filter: none;
}

/* --------------------------------------------------------------------------
   The three promises

   Third time. Boxes were wrong, bare text under hairlines was worse, and
   the clause numbers and corner brackets were decoration dressed up as
   structure: the numbering implied an order that does not exist and the
   brackets were the logo doing a job the logo should not have to do.

   What was actually working in that version was the hierarchy. The promise
   had been set at the same size as its own explanation, which is why the
   whole block read as grey paragraphs with nothing to catch. So the
   hierarchy stays and everything stuck on top of it goes.

   One rule across the whole group rather than three fragments above three
   columns, because it is one list and the rule should say so. Then air,
   and the promises carrying themselves.
   -------------------------------------------------------------------------- */

/* No rule here either. It was one line for the whole group rather than a
   fragment over each column, which was the better of the two bad options,
   but it is still a hairline doing a job that space does better and it is
   still the thing Andrei keeps pointing at. The air it is replaced with is
   worth more than the line was. */
html.imm .trust {
  padding-top: clamp(1rem, 2vw, 1.5rem);
  margin-top: clamp(2.5rem, 5vw, 4rem);
}

html.imm .trust-row {
  border-top: 0;
  padding: 0;
}

/* Rows of two (On every website we build, What to expect): with the row
   padding gone, the next heading sat on the last line above it. */
html.imm .trust {
  row-gap: clamp(2rem, 3.5vw, 2.75rem);
}

/* The base closes the stacked list with a rule under the last row. Side by
   side that is a line under one column and nothing under the other two. */
html.imm .trust-row:last-child {
  border-bottom: 0;
}

/* The headings and the explanations line up across a row because the rows
   share the grid, not because every heading reserves two lines.

   min-height: 2.3em did make the paragraphs start level, and it did it by
   leaving a hole under every heading short enough to fit on one line, which
   is most of them. Measured on 24 October: 46 pixels of nothing under
   "Reviews made easy", and the same under nine others on that page and
   eight more on /websites/. Andrei saw it before I measured it.

   Subgrid does the same job with no hole: the row owns two tracks, the
   heading track is as tall as the tallest heading in that row, and a short
   heading simply sits in it. */
html.imm .trust-row h3 {
  font-family: var(--display);
  font-size: clamp(1.25rem, 1.9vw, 1.5rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-wrap: balance;
  margin: 0;
}

html.imm .trust-row {
  grid-template-rows: subgrid;
  grid-row: span 2;
  row-gap: 0.75rem;
}

html.imm .trust-row p {
  align-self: start;
}

html.imm .trust-row p {
  margin-top: 0;
  max-width: 34ch;
}

/* One line under the three, tying them to the page they are written on.
   Hidden from a page that has no trust list. */
.trust-note {
  margin: clamp(1.75rem, 3vw, 2.5rem) 0 0;
  color: var(--ink-muted);
}

html.imm .band {
  background: linear-gradient(180deg, rgba(10, 8, 14, 0.42), rgba(10, 8, 14, 0.18));
  border-block: 1px solid var(--pane-line);
}

html.imm .strip,
html.imm .section-tight.strip {
  background: rgba(10, 8, 14, 0.3);
}

/* The footer is the ground, not another pane of glass. At 82 per cent
   the moving field showed through it, so on a phone, where the footer is
   the last thing on the page and the field is fixed behind everything,
   the company numbers were read against something drifting. */
html.imm .footer {
  background: #08060B;
  border-top: 1px solid var(--pane-line);
  /* The page keeps 88 pixels of room at the foot for the sticky button,
     and that button hides itself once the footer is on screen, so at the
     very bottom of a phone there was a band under the footer with the
     moving field showing through it. The shadow paints that band in the
     footer's own colour without taking up any room, so the page ends on
     ground rather than on a gap. */
  box-shadow: 0 50vh 0 #08060B;
}

/* A field edge must never cut through a panel's own edge, so inputs and
   buttons carry the same ground as the panels they sit in. */
html.imm input,
html.imm textarea,
html.imm select {
  background: rgba(8, 6, 11, 0.62);
  border-color: var(--pane-line-strong);
}

/* Except the one inside the search pill, where the pill is the panel. A
   second ground inside it draws a black box in the middle of the pill,
   which is what this looked like until it was measured. */
html.imm .pick-row input {
  background: none;
}

html.imm .btn-quiet {
  background: rgba(10, 8, 14, 0.46);
  border-color: var(--pane-line-strong);
}

/* --------------------------------------------------------------------------
   Type

   Over a live ground, quiet grey text stops being quiet and starts being
   unreadable. The muted ink is lifted until it clears the bar against the
   panels above, and the faint one is retired on this page: there is nothing
   on a page like this that deserves to be nearly invisible.
   -------------------------------------------------------------------------- */

html.imm {
  /* Measured, not picked. The shader's ceiling is vec3(0.42, 0.21, 0.63),
     which is #6B36A1, and the hero's lede sits straight on it with no panel
     under it at all. Against that ceiling the base page's #A79BB8 measures
     3.01 and a first lift to #C3B8D2 only reached 4.16, both under the bar
     for body text. #D0C7DB measures 4.83 on the worst pixel the field can
     produce and 9.10 on a panel, so it clears everywhere either can go.
     Same walk for the faint ink, which is why there is barely a step
     between them now: on a live ground there is not room for one. */
  --ink-muted: #D0C7DB;
  --ink-faint: #CAC1D7;
}

/* The one thing the eye should land on first. Slightly tighter and heavier
   than the flat page, because it now has a ground to hold its own against. */
html.imm .hero h1 {
  letter-spacing: -0.035em;
  text-wrap: balance;
}

/* --------------------------------------------------------------------------
   Motion

   One signature, and it is the field itself: it moves away from the cursor,
   it opens when the assistant speaks, it tightens while a phone is ringing.
   Everything else on the page stays as still as it already was. A page that
   moves in six places moves in none.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  #field { display: none; }
}

/* --------------------------------------------------------------------------
   The trust list

   It was four rows in two columns, which divided. It is three now, because
   the company's own registration moved to the footer where it belongs, and
   three in two columns leaves one stranded on a row of its own with half
   the width empty beside it. Three across, so the row is a row.
   -------------------------------------------------------------------------- */

@media (min-width: 720px) {
  /* Exactly three, and only exactly three. The homepage has three since the
     company's own registration moved to the footer, and three in the base
     two columns strands one with half a row empty beside it.

     :last-child:nth-child(3) is true only when there are three children in
     total, which is the point: the first version of this rule said "every
     trust list is three across" and broke /websites/, which has eight. Eight
     and four both divide by the two columns the base already uses, so they
     are left alone. */
  html.imm .trust:has(> :last-child:nth-child(3)) {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: clamp(1.5rem, 3vw, 3rem);
  }

  /* No per-cell rule. There is one rule across the whole group now, above
     all three, and this left a stray line over the third column only. It
     was written when each row had its own rule and outlived that by two
     rewrites. */
}

/* --------------------------------------------------------------------------
   A grid track that can actually shrink

   /websites/ scrolls sideways by 10px between about 700 and 860 wide, and it
   does so on the live site too, so this is not something the immersive layer
   introduced. `1fr` is shorthand for minmax(auto, 1fr), and that `auto` floor
   is the widest the content refuses to go below, so three price cards that
   will not shrink push the grid wider than the page and take the whole
   document with them.

   minmax(0, 1fr) lets the track shrink and the words inside wrap, which is
   what should have happened in the first place. Worth carrying back to the
   live site whether or not the immersive direction ships.
   -------------------------------------------------------------------------- */

@media (min-width: 700px) {
  html.imm .products,
  html.imm .plans {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  html.imm .products.products-4,
  html.imm .plans.plans-4,
  html.imm .steps.steps-4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --------------------------------------------------------------------------
   The voice chooser

   Kept as it is on the live site: accent chips, then a row per voice with a
   play button, the name and what it sounds like. That is already the thing
   Andrei wanted, a list you pick with your ears rather than a dropdown
   wearing a costume. What the prototype did instead, twenty names as pills
   in a strip with a scrollbar under it, is not coming across.

   All that changes here is the ground it stands on, and one thing it does
   that it could not before: while a voice is playing, the field behind the
   page is driven by that clip's own waveform.
   -------------------------------------------------------------------------- */

html.imm .vx {
  background: var(--pane);
  border-color: var(--pane-line);
}

html.imm .vxrow {
  border-color: var(--pane-line);
}

html.imm .vxchip {
  background: rgba(8, 6, 11, 0.5);
  border-color: var(--pane-line-strong);
}

html.imm .vxchip.is-on {
  background: var(--ink);
  border-color: var(--ink);
  color: #0C0A0C;
}

html.imm .vxplay {
  background: rgba(243, 239, 249, 0.12);
}

html.imm .vxplay:hover {
  background: rgba(243, 239, 249, 0.22);
}

/* While a clip plays, its button is the only green thing on the page, and
   the ground behind the whole page is moving to the same voice. */
html.imm .vxplay.is-on {
  background: var(--green);
  color: var(--green-ink);
}

/* --------------------------------------------------------------------------
   Links, and the one card that was tinted instead of panelled

   Both came out of the audit on 22 October, measured against the shader's
   ceiling rather than against how the page usually looks.

   Links were --purple-tint #C4A9EC, which is 3.84 on the brightest pixel the
   field can produce. On the legal pages a link is most of the page, and
   those pages have no panel under them at all. #D7C5F2 measures 4.93 there.

   The demo card was a seven percent green tint, which over near black was a
   whisper and over the field is a pale purple: its own green heading landed
   at 3.77 on it. It gets the same dark pane as every other panel, where the
   green measures 8.67, and keeps its green edge so it is still the loudest
   thing on the page.
   -------------------------------------------------------------------------- */

html.imm {
  --purple-tint: #D7C5F2;
}

html.imm a:not(.btn) {
  color: var(--purple-tint);
}

html.imm .demo {
  background: var(--pane);
  border-color: rgba(37, 211, 102, 0.5);
}

/* --------------------------------------------------------------------------
   The browser window

   A white card on a purple ground reads as a hole punched in the page. The
   white is not the problem, it is the right colour for a search result and
   the point is that it is the outside world rather than ours. What was
   missing is the thing that says so. In a window it is obviously a screen,
   and the white stops being a mistake.

   Nothing in here is decoration for its own sake: the dots and the search
   bar are the two marks that make a rectangle read as a browser, and the
   query in the bar is our own name.
   -------------------------------------------------------------------------- */

.screen {
  border-radius: 14px;
  overflow: hidden;
  background: #1B1522;
  border: 1px solid rgba(243, 239, 249, 0.2);
  box-shadow:
    inset 0 1px 0 rgba(243, 239, 249, 0.12),
    0 40px 80px -38px rgba(0, 0, 0, 0.85);
}

.screen-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.625rem 0.75rem;
  background: rgba(243, 239, 249, 0.06);
  border-bottom: 1px solid rgba(243, 239, 249, 0.1);
}

.screen-dots {
  display: flex;
  gap: 0.3125rem;
  flex: 0 0 auto;
}

.screen-dots i {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: rgba(243, 239, 249, 0.26);
}

.screen-url {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  flex: 1;
  min-width: 0;
  padding: 0.3125rem 0.75rem;
  border-radius: 999px;
  background: rgba(10, 8, 14, 0.5);
  color: var(--ink-faint);
  font-size: 0.8125rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.screen-url svg {
  width: 0.875rem;
  height: 0.875rem;
  flex: 0 0 auto;
  opacity: 0.8;
}

.screen-page {
  background: #fff;
  padding: 1.125rem 1.25rem 1.375rem;
}

/* The search result, on our ground rather than on Google's.

   It was white, twice over: the window on the home page and every mirror
   in the site check. Andrei does not want white on this site and has said
   so twice, and he is right that a white slab on a moving purple field
   reads as a hole punched in the page rather than as a screen. The shapes
   stay exactly as Google draws them, the typeface stays Arial because that
   is the thing that says "this is a search result and not us", and only
   the ground and the inks move into our own range.

   Contrast measured against this ground, not assumed: the title at 10.2,
   the address at 9.9, the line under it at 8.7. Well clear of the 4.5
   bar. */
html.imm .screen-page,
html.imm .serp {
  background: #140F1E;
}

html.imm .serp-url { color: #C2B8D2; }
html.imm .serp-title { color: #C9B4F5; }
html.imm .serp-desc { color: #B6ACC6; }
html.imm .serp .is-missing { color: #9A90AB; }

/* Inside the window the result has no card of its own: the window is the
   card. A border here would be a box inside a box. */
.screen-page .serp {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  box-shadow: none;
  backdrop-filter: none;
}

/* --------------------------------------------------------------------------
   Who it is for

   Forty now rather than nine, because the job of this list is for somebody
   to find themselves in it, and nine names is a sample, not a mirror. The
   first weight is the trades where a missed call is a lost job that day;
   the quieter weight runs on bookings. Nothing in it claims to be a client.

   It is allowed to be tall. The chips wrap, there is no strip to drag and
   no scrollbar anywhere, which is the thing Andrei rejected in the
   prototype and is not coming back in another costume.
   -------------------------------------------------------------------------- */

html.imm .who {
  gap: 0.5rem;
  max-width: 62rem;
}

html.imm .who li {
  border-color: var(--pane-line-strong);
  background: rgba(10, 8, 14, 0.26);
  color: var(--ink-muted);
}

html.imm .who li.lead {
  border-color: rgba(199, 170, 240, 0.55);
  background: rgba(10, 8, 14, 0.42);
  color: var(--ink);
}

.who-note {
  margin: 1.25rem 0 0;
  max-width: 54ch;
  color: var(--ink-muted);
  text-wrap: pretty;
}

/* --------------------------------------------------------------------------
   Proof, in the footer

   Andrei: put the things that prove we are a real company in the footer.
   It was already there, as one unbroken grey sentence that nobody has ever
   read, which is not proof, it is small print.

   Four facts, each labelled, each checkable by the reader without asking
   us: the public register, the address on it, what we do about VAT, and the
   documents that bind us before anyone pays. Nothing here is a claim we
   cannot evidence, which is why there is no ICO number and no insurance
   line: we have not got those to hand, and an unverifiable badge in a block
   headed "you can check every word of this" would poison the three that
   are true.
   -------------------------------------------------------------------------- */

.proof {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: clamp(1.75rem, 3vw, 2.5rem);
  border-top: 1px solid var(--pane-line, rgba(243, 239, 249, 0.14));
}

.proof h2 {
  margin: 0 0 1.5rem;
  font-size: clamp(1.0625rem, 1.6vw, 1.25rem);
  color: var(--ink);
}

.proof ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 1.25rem 2rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 1000px) {
  .proof ul { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (max-width: 600px) {
  .proof ul { grid-template-columns: 1fr; }
}

.proof li {
  font-size: 0.9375rem;
  color: var(--ink-muted);
  text-wrap: pretty;
}

.proof-k {
  display: block;
  margin-bottom: 0.3125rem;
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.proof a {
  color: var(--purple-tint);
}

.footer-legal {
  margin-top: clamp(1.75rem, 3vw, 2.5rem);
  padding-top: 1.25rem;
  border-top: 1px solid var(--pane-line, rgba(243, 239, 249, 0.1));
}

/* The documents, under the four facts rather than beside them. They are a
   list of links, not a fact with a value, and giving them a column of their
   own made five items in a grid that divides by four. */
.proof-docs {
  margin: clamp(1.25rem, 2.5vw, 1.75rem) 0 0;
  font-size: 0.9375rem;
  color: var(--ink-muted);
}

/* --------------------------------------------------------------------------
   A footer that fits what is in it

   The client preview pages lost their "Who we are" column, because the proof
   block directly underneath now says the same thing better. That left a
   four column grid holding two columns, so the content sat on the left half
   with two empty tracks beside it: a blank band, which is the one thing the
   footer should never have.

   The grid now follows its contents. Two children, two columns.
   -------------------------------------------------------------------------- */

.footer-grid:has(> :last-child:nth-child(2)) {
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
}

@media (max-width: 760px) {
  .footer-grid:has(> :last-child:nth-child(2)) {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   Tell us what you do. Watch it get built.

   One section where there used to be two. The line and the chips find the
   trade, the canvas builds a website for it, the card names it and the
   button takes them out with the trade already attached.
   -------------------------------------------------------------------------- */

.pick {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  max-width: 62rem;
}

.pick-row {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  max-width: 34rem;
  padding: 0.45rem 0.6rem 0.45rem 1.05rem;
  border: 1px solid var(--pane-line-strong);
  border-radius: 999px;
  background: rgba(10, 8, 14, 0.44);
  transition: border-color 0.3s var(--ease);
}

.pick-row:focus-within {
  border-color: var(--purple-tint);
}

.pick-row > svg {
  width: 1.05rem;
  height: 1.05rem;
  flex: 0 0 auto;
  color: var(--ink-faint);
}

.pick-row input {
  flex: 1;
  min-width: 0;
  padding: 0.4rem 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
}

.pick-row input::placeholder { color: var(--ink-faint); }
.pick-row input:focus { outline: none; }

.pick-clear {
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  border: 0;
  border-radius: 50%;
  background: rgba(243, 239, 249, 0.08);
  color: var(--ink-muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.pick-note {
  margin: 0.8rem 0 0;
  max-width: 52ch;
  font-size: 0.95rem;
  color: var(--ink-muted);
}

/* The list narrows rather than being rebuilt, so nothing moves that does
   not have to and every word stays on the page. */
.who-pick {
  margin-top: 1.1rem;
  min-height: 2.6rem;
}

.who-pick li[hidden] { display: none; }

/* The order comes from flex order, so a search reorders the row without
   moving a single element in the document. */
.pick-more {
  margin: 0.7rem 0 0;
  font-size: 0.9rem;
  color: var(--ink-muted);
}

.who-pick.is-empty + .pick-more { display: none; }

html.imm .who-pick li {
  transition:
    border-color 0.3s var(--ease),
    background-color 0.3s var(--ease),
    transform 0.3s var(--ease);
}

@media (hover: hover) and (pointer: fine) {
  html.imm .who-pick li:hover {
    border-color: rgba(199, 170, 240, 0.7);
    transform: translate3d(0, -1px, 0);
  }
}

html.imm .who-pick li.is-on {
  border-color: var(--purple-bright);
  background-color: rgba(123, 61, 191, 0.28);
  color: var(--ink);
}

/* --------------------------------------------------------------------------
   The two columns
   -------------------------------------------------------------------------- */

.howgrid {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
  margin-top: clamp(2rem, 4vw, 3rem);
  grid-template-columns: minmax(0, 1fr) minmax(0, 30rem);
  align-items: start;
}

@media (max-width: 900px) {
  .howgrid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "stage" "steps";
  }
  .stagecol { grid-area: stage; }
  .howsteps { grid-area: steps; }
}

/* ---- the rail the steps travel down ---- */

/* Each step has a screenful to itself, which is what lets the scroll drive
   the build beside it. The cost of that is distance between the steps, and
   the rail is what turns that distance from a void into ground covered. */
.howsteps {
  position: relative;
  margin-top: 0;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  padding-left: 2.1rem;
  /* The sticky column stops travelling when this one runs out, and
     measured without this the stage was 22px above the top of the screen
     by the third step, with its own top cut off. The tail is what the
     stage parks against while the last step is being read. */
  padding-bottom: clamp(2rem, 18vh, 14rem);
}

.howsteps::before,
.howsteps::after {
  content: "";
  position: absolute;
  left: 0.34rem;
  top: 0.2rem;
  bottom: 0.2rem;
  width: 1px;
}

.howsteps::before { background: rgba(243, 239, 249, 0.13); }

/* scaleY, not height: a height transition is a layout change every frame,
   a scale is handed to the compositor. */
.howsteps::after {
  background: linear-gradient(180deg, var(--purple-bright), var(--purple-tint));
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.7s var(--ease);
}

/* To the end on the last step, not nearly to it. A line that stops short
   of the thing it is pointing at reads as unfinished (Andrei, 23 October
   2026). */
.howsteps[data-live="1"]::after { transform: scaleY(0.34); }
.howsteps[data-live="2"]::after { transform: scaleY(0.67); }
.howsteps[data-live="3"]::after { transform: scaleY(1); }

.howsteps li {
  min-height: 42vh;
  display: grid;
  grid-template-columns: 0.69rem minmax(0, 1fr);
  grid-template-rows: auto auto;
  column-gap: 1.4rem;
  align-content: center;
  border-top: 0;
  padding-top: 0;
  z-index: 1;
  margin-left: -2.1rem;
  padding-left: 2.1rem;
}

.howsteps li h3 {
  grid-column: 2;
  grid-row: 1;
  font-family: var(--display);
  font-size: clamp(1.375rem, 2.4vw, 1.875rem);
  line-height: 1.12;
  letter-spacing: -0.025em;
  margin-bottom: 0.625rem;
  text-wrap: balance;
}

.howsteps li p {
  grid-column: 2;
  grid-row: 2;
  max-width: 38ch;
  font-size: clamp(1rem, 0.4vw + 0.9rem, 1.0625rem);
}

.howsteps .marker {
  position: static;
  grid-column: 1;
  grid-row: 1;
  align-self: center;
  width: 0.69rem;
  height: 0.69rem;
  border-radius: 50%;
  background: transparent;
  border: 1px solid rgba(243, 239, 249, 0.22);
  box-shadow: 0 0 0 0 rgba(123, 61, 191, 0);
  transition: background 0.45s var(--ease), border-color 0.45s var(--ease), box-shadow 0.45s var(--ease);
}

/* Reached, so it is filled. Nothing is dimmed to say the opposite: three
   paragraphs held at 40 per cent on a purple ground is a contrast failure
   dressed up as focus. */
.howsteps[data-live="1"] li[data-step="1"] .marker,
.howsteps[data-live="2"] li[data-step="1"] .marker,
.howsteps[data-live="2"] li[data-step="2"] .marker,
.howsteps[data-live="3"] li[data-step="1"] .marker,
.howsteps[data-live="3"] li[data-step="2"] .marker,
.howsteps[data-live="3"] li[data-step="3"] .marker {
  background: var(--purple-bright);
  border-color: var(--purple-tint);
  box-shadow: 0 0 0 5px rgba(123, 61, 191, 0.16);
}

/* On one column the beats go away. They exist to give the scroll something
   to drive, and here it drives nothing: the canvas above plays its own
   build. All the height would buy is three screens of mostly nothing. */
@media (max-width: 900px) {
  .howsteps {
    padding-left: 1.8rem;
    margin-top: 2rem;
    padding-bottom: 0;
  }
  .howsteps li {
    min-height: 0;
    padding-block: 1.45rem;
    column-gap: 1.1rem;
    margin-left: -1.8rem;
    padding-left: 1.8rem;
  }
  .howsteps li:first-child { padding-top: 0.2rem; }
  .howsteps li:last-child { padding-bottom: 0.2rem; }
}

@media (prefers-reduced-motion: reduce) {
  .howsteps li { min-height: 0; padding-block: 1.45rem; }
}

/* ---- the stage ---- */

.stagecol {
  position: sticky;
  /* Held high enough that the stage, the card under it and the button
     under that all sit on one screen, which is what the sticky column is
     for. Lower than this and the button fell off a laptop. */
  top: clamp(3.5rem, calc(50vh - 20rem), 12vh);
}

@media (max-width: 900px) {
  .stagecol { position: static; }
}

/* Square, and the page inside it is 0.56 by 0.76 of the short side, so
   there is a margin but not a field of nothing.

   The width is what every word on the stage is measured against, so it
   decides whether this reads as a website or as a diagram of one. At the
   22rem it started at, the smallest line came out at four pixels, which
   is a smudge. The cap is as large as still leaves the stage, the card
   under it and the button under that on a 768 pixel laptop screen, which
   is the shortest screen a sticky column has to survive. */
/* The stage is the container the words measure themselves against, so the
   type scales with it and nothing has to be recalculated in script when
   the window changes. */
/* The light the page is built in.

   The stage sat on whatever the field happened to be doing behind it,
   which at that depth on the page is the quiet end of it, so the thing
   the whole section exists to show stood on black. This puts a light
   behind it that comes up as the page builds: dim while the pieces are
   still flying, full by the time it is finished. It is behind the canvas,
   not over it, so nothing is laid on top of anything. */
.stagecol {
  --lit: 0;
}

/* The light spreads by blurring, not by being bigger than the stage.
   Drawn a third outside its box it reached past the edge of a phone
   screen and gave the whole page thirteen pixels of sideways scroll. A
   blur paints outside the box without taking any room. */
.stage-box::before {
  content: "";
  position: absolute;
  inset: 8%;
  z-index: -1;
  pointer-events: none;
  border-radius: 50%;
  background:
    radial-gradient(closest-side, rgba(156, 94, 232, 0.52), rgba(112, 58, 184, 0.22) 60%, transparent 78%);
  filter: blur(42px);
  opacity: calc(0.2 + var(--lit, 0) * 0.8);
  transition: opacity 0.4s linear;
}

@media (prefers-reduced-motion: reduce) {
  .stage-box::before { transition: opacity 0.3s linear; }
}

.stage-box {
  position: relative;
  width: 100%;
  /* As wide as the screen can hold with the card and the button still
     under it, and no wider. Fixed at 22rem the smallest line came out at
     four pixels on every screen, including the ones with room to spare. */
  max-width: min(29rem, calc(100svh - 24.5rem));
  margin-inline: auto;
  aspect-ratio: 1 / 1;
  container-type: inline-size;
}

.stage canvas {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 16px;
}

/* The line art that fills the photo slots. One drawing per slot, placed
   from the same piece list the shader draws, so it cannot drift from the
   rectangle it belongs in. It takes the page's colour and arrives when
   its slot lands. */
.stage-art {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Opaque. The transparency is applied once, to the whole drawing, in
     the rule below. Carried on the stroke colour it was applied to every
     path separately, so a crossing composited twice and came out a third
     brighter than the line either side of it. */
  color: rgb(203, 176, 243);
}

.stage-art svg > g {
  opacity: 0.62;
}

.stage-art svg {
  position: absolute;
  overflow: visible;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
  transform: scale(0.94);
  transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
}

.stage-art svg.is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .stage-art svg {
    transform: none;
    transition: opacity 0.25s var(--ease);
  }
}

/* --------------------------------------------------------------------------
   What happens after it is built

   The page assembled and then sat there, which told half the story. The
   whole pitch is that the website turns a stranger into a phone call, so
   once the page lands a pointer crosses it, presses the one button that
   page was built around, and the thing that button does arrives. It is
   the argument acted out, in two seconds, without a word of copy.
   -------------------------------------------------------------------------- */

.stage-hand {
  position: absolute;
  width: 7%;
  pointer-events: none;
  color: var(--ink);
  opacity: 0;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.55));
  transition: opacity 0.3s var(--ease);
}

.stage-hand.is-live { opacity: 1; }

/* With motion turned down the pointer does not travel. The button still
   changes, because that is the point being made. */
@media (prefers-reduced-motion: reduce) {
  .stage-hand { display: none; }
}

.stage-words {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.stage-words span {
  position: absolute;
  display: block;
  white-space: nowrap;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--ink);
  opacity: 0;
  transform: translate3d(0, 6px, 0);
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}

.stage-words span.is-in {
  opacity: 1;
  transform: none;
}

/* The words a page is built around. */
/* Clash Display ships a narrow space glyph, and the negative tracking above
   takes more off it. styles.css says so at the top of its type block and
   gives every display element word-spacing back. This one was written later
   and did not get it, so measured on 24 October the space here was 0.144em
   against 0.26 to 0.38em for the same words in Satoshi: at bold fifteen
   pixels that is 2.2px between two words, and "Your Business Name" reads as
   "Your BusinessName". Andrei spotted it on the salon. Keep the two
   properties together, the same as every other display element. */
.stage-words .w-head {
  font-family: var(--display);
  font-weight: 600;
  color: var(--ink);
  word-spacing: 0.12em;
}
.stage-words .w-act { font-weight: 700; color: var(--green-ink); }
/* After the button has been pressed it says what it did, in the same
   place, so nothing is ever laid over the page to report it.

   This rule used to set a transition on a class that changes nothing
   animatable, so the one frame the whole section builds towards was a
   hard cut from one word to another. The word is faded out, swapped and
   faded back with a small rise, which is the only moment on the page
   that deserves it. */
.stage-words .w-act {
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}

.stage-words .w-act.is-going {
  opacity: 0;
  transition-duration: 0.12s;
}

/* Opacity only. The word carries its own inline transform, which is what
   centres it on its button, so an animation touching transform would pull
   it off the button for the length of the animation. */
.stage-words .w-act.is-done {
  opacity: 1;
  transition-duration: 0.26s;
}
.stage-words .w-quiet { color: var(--ink-muted); }
.stage-words .w-price { color: var(--ink); font-weight: 700; }
.stage-words .w-tiny { color: var(--ink-faint); letter-spacing: 0.06em; text-transform: uppercase; }
/* Dark, because it sits on a light pill. */
.stage-words .w-chip { color: #1A1024; font-weight: 700; letter-spacing: 0.04em; }

@media (prefers-reduced-motion: reduce) {
  .stage-words span {
    transform: none;
    transition: opacity 0.25s var(--ease);
  }
}

.stage-fallback {
  margin: 0;
  padding: 2.5rem 1.25rem;
  text-align: center;
  color: var(--ink-muted);
  border: 1px solid var(--pane-line);
  border-radius: var(--r-card);
  background: var(--pane-solid);
}

.builtcard {
  margin-top: 0.6rem;
  padding: clamp(0.9rem, 2vw, 1.15rem);
  border: 1px solid var(--pane-line);
  border-radius: var(--r-card);
  background: var(--pane-solid);
}

.builtcard-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-bottom: 0.65rem;
  border-bottom: 1px solid var(--pane-line);
  font-size: 0.82rem;
  color: var(--ink-muted);
}

.builtcard-dot {
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: var(--green);
  flex: 0 0 auto;
}

.builtcard-t {
  display: block;
  margin-top: 0.8rem;
  font-family: var(--display);
  font-size: clamp(1.05rem, 1.7vw, 1.3rem);
  line-height: 1.25;
  color: var(--ink);
}

.builtcard-d {
  display: block;
  margin-top: 0.4rem;
  font-size: 0.95rem;
  color: var(--ink-muted);
}

/* The claim, in words, under the page that makes it. */
.builtcard-act {
  display: block;
  margin-top: 0.75rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--pane-line);
  font-size: 0.9rem;
  color: var(--purple-tint);
}

/* The way out, where they are looking, the moment it finishes. */
.stage-go {
  margin-top: 1.1rem;
}

.stage-go .btn {
  width: 100%;
  justify-content: center;
}

.stage-note {
  margin: 0.7rem 0 0;
  font-size: 0.9rem;
  color: var(--ink-muted);
  text-align: center;
}


/* The hairline above the closing block. Gone (Andrei, 23 October 2026).
   On a dark page with a shader moving behind it a full width rule is not a
   section break, it is a scratch across the screen, and the block below it
   already reads as its own thing. */
html.imm .close-cta {
  border-top: 0;
}


/* --------------------------------------------------------------------------
   What the shader ground does to the edges of controls

   Every ink on this site was measured against the shader's ceiling when
   the field went in. These were not, because they are not ink: they are
   the lines that say a control is there at all, and WCAG 1.4.11 asks the
   same 3 to 1 of them. Measured on 23 October 2026 against that ceiling
   and against each control's own interior.
   -------------------------------------------------------------------------- */

/* 3.99 to 1 on the field, which is under the 4.5 body text needs. This is
   the only inline error colour on the site and it was the one token the
   immersive layer never re-measured. 5.6 at this value. */
html.imm {
  --error: #FFC2C2;
}

/* The resting pill on /start/ and /receptionist/hear/ had a 1.43 edge and
   no fill, so there was nothing to say a control was there. */
html.imm .pill span {
  border-color: rgba(243, 239, 249, 0.55);
  background: rgba(8, 6, 11, 0.5);
}

/* Selects and textareas were missed by the input rule because .field input
   is more specific than html.imm input, so they kept a 2.18 edge while
   every text field got a passing 3.29. */
html.imm .field select,
html.imm .field textarea,
html.imm select,
html.imm textarea {
  border-color: rgba(243, 239, 249, 0.55);
}

/* The accent chips on the receptionist page: a 2.19 edge against their own
   fill when off. */
html.imm .vxchip {
  border-color: rgba(243, 239, 249, 0.55);
}

/* And when one is on it was pure --ink, a near white pill that became the
   brightest thing on a screen whose subject is twenty voices underneath
   it. The accent belongs on the control that is on, not a floodlight. */
html.imm .vxchip.is-on {
  background: var(--purple-bright);
  border-color: var(--purple-tint);
  color: var(--ink);
}

/* The 3px ring on a focused field composited to 1.08 against the page it
   was drawn on, so the visible part of the indicator was invisible. Every
   other control on the site uses this outline and measures 4.66 to 12.4. */
html.imm .field input:focus-visible,
html.imm .field select:focus-visible,
html.imm .field textarea:focus-visible,
html.imm .check-form input:focus-visible {
  outline: 2px solid var(--purple-tint);
  outline-offset: 2px;
  box-shadow: none;
}


/* --------------------------------------------------------------------------
   Anything a thumb has to hit

   Measured at 360 wide. The floor is 40px, and these were under it: the
   trade field at 38.4, its clear button at 32, and the section links that
   carry people between pages at 27.2.
   -------------------------------------------------------------------------- */

.pick-row input {
  padding-block: 0.6rem;
}

.pick-clear {
  width: 2.5rem;
  height: 2.5rem;
}

html.imm .link {
  display: inline-block;
  padding-block: 0.55rem;
}

/* --------------------------------------------------------------------------
   The call, on the field

   The two frames are glass like every other panel, so the field runs behind
   the call rather than the call sitting in a box on top of it. The motion
   lives here rather than in the base sheet for the same reason the rest of
   the immersive layer does: take this one file away and the call is still a
   readable transcript with the texts next to it, just without the theatre.
   -------------------------------------------------------------------------- */

html.imm .cx-frame {
  background: var(--pane);
  border-color: var(--pane-line);
  box-shadow: var(--pane-lift);
}

html.imm .cx-o {
  border-color: var(--pane-line);
  background: rgba(243, 239, 249, 0.04);
}

html.imm .cx-o-you.is-in {
  border-color: rgba(196, 169, 236, 0.5);
  background: rgba(82, 31, 136, 0.3);
}

/* A line lights rather than arrives. It is already in place, so there is
   nothing to move, and the only thing that changes is how lit it is. Two
   hundred and forty milliseconds, which is about the gap between two
   sentences of speech: slower and the line is still brightening when the
   next one starts, faster and it snaps.

   The left mark is a border colour, which costs a repaint of two pixels by
   the height of one line, once every couple of seconds. */
html.imm .cx-l .cx-said,
html.imm .cx-l .cx-who {
  transition: color 0.24s linear;
}

html.imm .cx-l {
  transition: border-color 0.24s linear;
}

/* The message itself does move, because it is arriving from a phone that is
   somewhere else. It comes in from the side the panel is on, and the slot it
   lands in stays exactly where it was. */
html.imm .cx-o {
  transition:
    border-color 0.38s var(--ease),
    background-color 0.38s var(--ease);
}

html.imm .cx-o p {
  transform: translate3d(12px, 0, 0);
  transition:
    opacity 0.38s var(--ease),
    transform 0.38s var(--ease);
}

html.imm .cx-o.is-in p {
  transform: none;
}

@media (max-width: 899px) {
  html.imm .cx-o p {
    transform: translate3d(0, 10px, 0);
  }
}

/* Ringing. The dot is the only thing on the page allowed to loop, it loops
   for one second before the call is answered, and it stops. */
@keyframes cx-ring {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.75); opacity: 0.35; }
}

html.imm .cx.is-ringing .cx-dot {
  animation: cx-ring 0.55s var(--ease) infinite;
  background: var(--purple-tint);
}

html.imm .cx.is-live .cx-dot {
  background: var(--green);
}

html.imm .cx.is-done .cx-dot {
  background: var(--ink-faint);
}

/* While the greeting is playing. The dot is already the call's state, so
   this is a ring around it rather than another thing to look at, and it
   stops when the clip does. */
@keyframes cx-loud {
  0% { box-shadow: 0 0 0 0 rgba(196, 169, 236, 0.55); }
  100% { box-shadow: 0 0 0 10px rgba(196, 169, 236, 0); }
}

html.imm .cx.is-loud .cx-dot {
  animation: cx-loud 1.3s var(--ease) infinite;
}

@media (prefers-reduced-motion: reduce) {
  html.imm .cx.is-loud .cx-dot {
    animation: none;
  }
}

/* The clock is the claim. While it is running it is lit, and when it stops
   it stays lit, because the number it stopped on is the point. */
html.imm .cx.is-live .cx-clock,
html.imm .cx.is-done .cx-clock {
  color: var(--purple-tint);
}

@media (prefers-reduced-motion: reduce) {
  html.imm .cx-l,
  html.imm .cx-l .cx-said,
  html.imm .cx-l .cx-who,
  html.imm .cx-o,
  html.imm .cx-o p {
    transform: none;
    transition: none;
  }

  html.imm .cx.is-ringing .cx-dot {
    animation: none;
  }
}

/* The built in panel is glass like every other panel, so the field runs
   behind it rather than being blocked by a black slab. Same recipe as the
   call frame and the plans. */
html.imm .builtin {
  background: var(--pane);
  border-color: var(--pane-line);
  box-shadow: var(--pane-lift);
}

/* The rungs are glass, like every other object on the field. */
html.imm .rungs a {
  background: var(--pane);
  border-color: var(--pane-line);
  box-shadow: var(--pane-lift);
}

/* The receipts are glass, like every other object on the field. */
html.imm .rc {
  background: var(--pane);
  border-color: var(--pane-line);
  box-shadow: var(--pane-lift);
}

/* The price tags are glass like every other object on the field. */
html.imm .tags li {
  background: var(--pane);
  border-color: var(--pane-line);
  box-shadow: var(--pane-lift);
}
