/* Fonts, served from this site rather than Fontshare and Google: two fewer
   connections in front of the first paint. It also fixed Gambetta, which
   never loaded from Fontshare (asking for both families in one request only
   ever returned Switzer, so every heading was quietly set in Georgia). */
@font-face { font-family: 'Switzer'; src: url('../fonts/switzer-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Switzer'; src: url('../fonts/switzer-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: 'Switzer'; src: url('../fonts/switzer-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: 'Switzer'; src: url('../fonts/switzer-700.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Gambetta'; src: url('../fonts/gambetta-300.woff2') format('woff2'); font-weight: 300; font-style: normal; font-display: swap; }
@font-face { font-family: 'Gambetta'; src: url('../fonts/gambetta-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Gambetta'; src: url('../fonts/gambetta-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: 'Gambetta'; src: url('../fonts/gambetta-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: 'Gambetta'; src: url('../fonts/gambetta-500-italic.woff2') format('woff2'); font-weight: 500; font-style: italic; font-display: swap; }
@font-face { font-family: 'Gambetta'; src: url('../fonts/gambetta-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: 'Archivo Narrow'; src: url('../fonts/archivo-narrow-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Mono'; src: url('../fonts/ibm-plex-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }

/* ==========================================================================
   Dactyl Websites — site styles

   Mobile first, and deliberately so: the site's own copy puts 70–80% of
   Sydney traffic on a phone, so every rule here is written for the narrow
   portrait case and only widened at a breakpoint once there is room for it.

   Four ideas hold it together:

   1. THE PAGE IS WARM. Linen, not white, and every neutral on it is rotated
      towards yellow. The previous version was a cool grey family sitting
      underneath a photograph of oak, brass and old leather, and a page that
      disagrees with its own picture is what "bland" actually looks like.

   2. ONE ACCENT, WITH A JOB LIST. Terracotta marks the price, the flagship
      plan, the card numerals, the ticks, the section labels and every link.
      It never sets body copy, a heading, or a background. A colour with
      duties reads as a system; the same colour used wherever a page looks
      empty reads as decoration.

   3. NAVY IS WHAT "DARK" MEANS. Not a sixth accent — the material every dark
      surface on the site is made of, lit by one shared vignette. That
      vignette is the only gradient here.

   4. TWO FACES, TWO JOBS. Gambetta sets every heading, Switzer sets
      everything a person has to read or press. One family separated only by
      size is what a word processor does.
   ========================================================================== */

/* ------------------------------------------------------------------ Tokens */
:root {
  /* Every neutral on this page is warm. The previous set was a cool grey
     family (#0f0f0f ink on #ededed) sitting underneath a photograph of oak,
     brass and old leather — the site and its own image were in two different
     colour worlds, and that mismatch is most of what read as "bland". These
     are the same values rotated towards yellow and dropped a few points in
     saturation, so the page agrees with the picture on it. */
  --ink:        #14110d;   /* warm near-black */
  --ink-2:      #453f36;
  --ink-3:      #665e52;   /* secondary text — 5.7 linen, 4.9 sand */
  /* The floor, and it is set by the footer rather than the page: --ink-4
     labels the footer columns, and the footer is sand. Measured against
     linen a lighter value looks fine and fails where it is actually used. */
  --ink-4:      #6a6253;   /* tertiary text — 5.44 linen, 4.72 sand */
  --paper:      #ffffff;

  /* Two warm neutrals, one job each. Linen is the page; sand is every box,
     card and panel cut out of it. A single flat tone doing all the
     containing is what stops the page looking like a stack of widgets, and
     keeping them only ~4% apart is what stops the cards looking stuck on. */
  --linen:      #f6f3ee;   /* the page */
  --sand:       #e9e3d8;   /* cards, panels, footer */
  --sand-2:     #ded6c8;   /* pressed / hover on a sand surface */
  --sand-3:     #d3c9b8;   /* two tones down: the middle plan */

  /* Kept as aliases. --grey is referenced from the smart-form theme block
     and a dozen component rules; renaming every call site would have been a
     larger diff than the redesign itself, for no benefit at all. */
  --grey:       var(--sand);
  --grey-2:     var(--sand-2);

  --hair:       rgba(20, 17, 13, .10);
  --hair-2:     rgba(20, 17, 13, .16);

  --black:      #14110d;

  /* ------------------------------------------------------------- Terracotta
     The one accent, and the first colour on this site that exists to be seen
     rather than to hold something. It is a hot tangerine taken down about
     15% in lightness — at full strength it measured 3.3:1 on the page and
     could only ever have lived on the dark panels, leaving the light
     two-thirds of the site exactly as flat as it was.

     Four values, because one cannot pass everywhere. The split is not
     light/dark, it is MARK versus SMALL TEXT, crossed with the surface it
     lands on:

       on linen and sand          on navy
       ------------------------   ------------------------
       --accent        marks      --accent-lit       marks
       --accent-ink    text       --accent-lit-ink   text

     A mark — a rule, a tick, a full stop at the end of a 56px heading, the
     $ on the price — is a graphic or large text and needs 3:1. Small text —
     a 13px section label, a numeral, a link in a sentence — needs 4.5:1, and
     the mark values do not reach it: #c2521f measures 4.20 on linen and 3.64
     on sand, and #e2793f measures 3.76 against the brightest point of the
     navy vignette. Every number below is measured against the worst case the
     value can actually land on, which for anything on navy is #143a71 at the
     centre of the lift rather than the navy at its edges.

     The accent's jobs are fixed: the currency symbol and rule on the price,
     the flagship plan's marker, the card numerals, the ticks, the section
     labels, inline links and the full stop that ends a heading. It never
     sets body copy, never sets a heading, and never fills a background —
     pointed at a surface it produced a solid terracotta slab across the top
     of the enquiry form, which was the loudest object the page has ever had.
     One colour with a list of duties reads as a system; the same colour
     sprayed wherever a page looks empty is what made the old blue read as
     cheap. */
  --accent:         #c2521f;   /* marks on light — 4.20 linen, 3.64 sand */
  --accent-ink:     #a7471b;   /* small text on light — 5.33 linen, 4.62 sand */
  --accent-lit:     #e2793f;   /* marks on navy — 3.76 at the vignette's peak */
  --accent-lit-ink: #e79169;   /* small text on navy — 4.62 at that peak */
  --accent-haze:    rgba(194, 82, 31, .12);

  /* ------------------------------------------------------------------ Navy
     The one colour here, and it is not an accent. It is what "dark" means on
     this site: the price panel, the closing statement, the two Pro cards,
     the flagship plan, the hero object and every primary button are all this
     same field. Used that way the blue reads as a material the page is made
     of rather than as six unrelated splashes, which is the difference
     between a colour scheme and a colour.

     The field is never flat. A radial lift sits a little above centre and
     falls away to near-black in the corners — that fall-off is the entire
     reason a dark box on a white page looks lit instead of printed, and it
     is the only place on the site where a gradient is allowed. */
  --navy:      #0c2348;
  --navy-lit:  #143a71;   /* the lift, upper centre */
  --navy-deep: #06122c;   /* the corners */
  --navy-rise: #1b477f;   /* the fill that rises inside a button */
  --navy-hov:  #102c58;   /* a dark card, pointed at */

  /* Umber: the site's neutral near-black (no brown lift: the lamp is a soft grey-black), the dark
     feature tile and the hero's film, for the one full screen of dark on
     the page (the price) and the photograph's walls either side of it. A
     whole screen of navy was the one cool field in a warm page, and put
     the terracotta straight against its opposite. Navy keeps one job:
     what you press. Lit the way navy is, from a lamp rather than a screen.
     Measured against --umber-lamp, the brightest point: white 75% 8.64:1,
     white 62% 6.41:1, --accent-lit 4.75:1, --accent-lit-ink 5.84:1,
     --ivory 12.28:1. */
  --umber:      #121212;
  --umber-lamp: #1e1e1e;
  --umber-deep: #090909;
  --ivory:      #f3eee6;   /* white, warmed, for large type on umber */

  /* Two strengths of white for text on navy. Measured against the lightest
     point of the vignette, which is the worst case: 5.3:1 and 6.6:1. */
  --on-navy:   rgba(255, 255, 255, .75);
  --on-navy-2: rgba(255, 255, 255, .62);

  --vignette: radial-gradient(118% 88% at 50% 32%,
              var(--navy-lit) 0%, var(--navy) 46%, var(--navy-deep) 100%);

  /* Two families, and the split is deliberate. One grotesque carrying the
     whole page separated only by size is exactly what a word processor does,
     and it is why the old version read as a document: a heading was the same
     letters as a paragraph, set larger. Gambetta on the headings makes the
     difference a change of voice instead of a change of size, and a contrast
     serif is also the only thing here that answers the photograph — oak,
     leather and brass are not a grotesque. */
  --sans: "Switzer", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          Arial, sans-serif;
  --display: "Gambetta", "Iowan Old Style", Georgia, "Times New Roman", serif;
  /* One component only: the embossed tape in the hero. */
  --label: "Archivo Narrow", "Haettenschweiler", "Arial Narrow", sans-serif;
  /* Section labels are set in mono, the way the reference does it. Mono at
     13px beside a 33px grotesque is a change of voice, not just of size, and
     it does the job the little dash used to do without drawing a line. */
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --gutter: clamp(1.15rem, 5.5vw, 3.5rem);
  --maxw: 74rem;

  --r-sm: 10px;
  --r:    16px;
  --r-lg: 20px;
  --r-xl: 32px;   /* cards and panels */

  --nav-h: 56px;

  /* One glide for the whole site. A long, heavily-decelerated curve is what
     makes a transition read as weighted rather than springy, and using the
     same one everywhere is most of what makes separate components feel like
     one object. */
  --glide: cubic-bezier(.16, 1, .3, 1);
  --ease:  cubic-bezier(.4, 0, .2, 1);
}

/* ------------------------------------------------------------------- Base */

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

/* Text size on narrower phones. The page was set on a Fold's cover screen,
   about 490px across; a phone narrower than that showed the same text
   sizes, so they took up more of the screen and everything wrapped sooner
   (the Design box, the plans, the forms). Below 490px the root size, and
   with it all the type and spacing set in rem, now comes down with the
   width, part of the way: 94% at 412px, 91% at 375px, where fully in
   proportion would be 84% and 77% and would take body text under 14px.
   Written as a share of the browser's own default, so a visitor who has
   turned their text size up still gets it. */
@media (max-width: 489px) {
  html { font-size: calc(.6rem + 1.3061vw); }
}

html {
  /* clip, not hidden: overflow:hidden on the root creates a scroll container
     and quietly breaks position:sticky on everything inside it. */
  overflow-x: clip;
  /* The no-JavaScript fallback. ui.js switches this off the moment Lenis
     takes over, because two smooth-scroll implementations fighting over the
     same gesture is worse than either one alone. */
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 1.5rem);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  overflow-x: clip;
  background: var(--linen);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1.125rem;     /* 18px */
  line-height: 1.5;        /* 27px, matching the reference */
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Lenis's own required rules, inlined rather than pulled as a sixth
   request. lenis-stopped is what holds the page still while the menu is
   open; [data-lenis-prevent] is on both decks so a wheel over one scrolls
   the deck rather than the page behind it. */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
/* The same hold for the phone's price stage when Lenis is not running. */
html.is-held { overflow: hidden; }
.lenis.lenis-scrolling iframe { pointer-events: none; }

img, svg { max-width: 100%; }
/* A link inside a sentence is terracotta. A link that is a whole component —
   a nav item, a footer entry, a contact detail, a button — is not: those are
   found by their position on the page, and colouring them turns the footer
   into a wall of orange, which is the accent decorating rather than doing a
   job.

   Scoped to <main> rather than written as a global rule with exceptions
   after it. The exceptions were the first attempt and they lost: `p >
   a:not(.btn)` scores (0,2,1) because :not() contributes its argument's
   weight, which outranks `.foot__big a` at (0,1,1) no matter what order
   they appear in. The header, the menu, the dock and the footer all live
   outside <main>, so the boundary that already exists in the document is a
   better fence than a list of class names that has to be kept in step. */
a { color: var(--ink); text-decoration-thickness: 1.5px; text-underline-offset: 3px; }
main p > a:not(.btn),
main li > a:not(.btn) { color: var(--accent-ink); }
/* The home page's "Building for the locals" card (orange on a phone, sand
   wide): its links keep the copy's own colour, underlined in a softer tone
   of it, since terracotta on the orange card would all but vanish. */
main .slab__lead a:not(.btn) { color: inherit; text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 55%, transparent); text-decoration-thickness: 1.5px; text-underline-offset: .2em; }
main .slab__lead a:not(.btn):hover { text-decoration-color: currentColor; }

a, button, label, select, summary, [role="button"] {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

:focus-visible {
  outline: 2.5px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Every panel on the site. One flat grey, one radius, no border and no
   shadow — a card is a shape cut out of the page, not an object floating
   above it. */
.panel {
  background: var(--grey);
  border-radius: var(--r-xl);
  padding: 2.25rem 1.5rem 2.5rem;
}
.panel__body { margin-top: 1.5rem; display: grid; gap: 1rem; }
.panel__body p { margin: 0; color: var(--ink-2); text-wrap: pretty; }
.panel__body p:first-child {
  font-size: 1.25rem;
  line-height: 1.4;
  letter-spacing: -.015em;
  color: var(--ink);
}

/* ------------------------------------------------------------ Dark surfaces
   One rule set for every dark thing on the page — the price panel, the
   closing statement, the two Pro cards and the flagship plan. They are
   written together rather than separately on purpose: the moment a dark card
   and a dark panel are allowed to drift apart in fill, in text weight or in
   radius, they stop being the same material and start being two designs.

   Nothing here gets a border or a shadow. The vignette is the edge. */
/* Doubled up on purpose. .card and .plan set their own grey further down
   this stylesheet, and a single class here loses to them on source order —
   the modifier has to outrank the component, not merely follow it. */
.panel.panel--dark,
.card.card--dark,
.plan.plan--dark {
  background-color: var(--navy);
  background-image: var(--vignette);
  color: var(--paper);
}

/* Every selector below carries its component class as well as the modifier,
   for the same reason as above. `.card p` and `.card--dark p` score exactly
   the same, so without the extra class the dark card's body copy quietly
   reverts to the grey-card ink and sits at 1.03:1 on navy — invisible, and
   invisible in a way no amount of looking at the page will reliably show
   you, because the text is still there and still the right size. */
.panel.panel--dark h2,
.panel.panel--dark h3,
.panel.panel--dark .price__fig,
.card.card--dark h3,
.plan.plan--dark h4,
.plan.plan--dark .plan__price b { color: var(--paper); }

.panel.panel--dark p,
.panel.panel--dark .panel__body p,
.card.card--dark p,
.plan.plan--dark p,
.plan.plan--dark .ticks li { color: var(--on-navy); }

.plan.plan--dark .plan__price span { color: var(--on-navy-2); }

/* Terracotta on navy is not the same terracotta. The vignette lifts to
   #143a71 under the middle of a panel, and the page value measures 3.4:1
   against that — so every accent that lands on a dark surface uses the
   lifted tone instead, which clears 4.9:1 at the panel's brightest point.
   One accent, two values, chosen by what is behind it. */
/* Marks keep --accent-lit; anything set small enough to be read as text
   takes --accent-lit-ink, which is the same colour lifted far enough to
   clear 4.5:1 against the brightest point of the vignette. */

.panel.panel--dark .eyebrow,
.panel.panel--dark .price__label,
.card.card--dark .card__n,
.card.card--dark .card__kicker { color: var(--accent-lit-ink); }

/* The tick keeps its one line weight and simply changes colour — the same
   mark, not a second design for dark. */
.plan.plan--dark .ticks li::before,
.panel.panel--dark .ticks li::before { border-color: var(--accent-lit); }
.panel.panel--dark .ticks li { color: var(--on-navy); }

/* The icon ring is a hole punched in the card rather than a white disc
   sitting on it: a solid white circle on navy is the brightest thing on the
   page and pulls the eye off the heading it is meant to introduce. */
.card.card--dark .card__icon {
  background: rgba(255, 255, 255, .10);
  stroke: var(--paper);
}

/* A navy focus ring is invisible on a navy card. Anything focusable inside
   a dark surface gets the ring in white instead — same weight, same offset. */
.panel--dark :focus-visible,
.card--dark :focus-visible,
.plan--dark :focus-visible { outline-color: var(--paper); }

/* Label and heading centred over the block beneath them. */
.sec--centre { text-align: center; }
.sec--centre .lead { margin-inline: auto; max-width: 28rem; }

/* Hugs the left gutter and stops short of the right, so the ragged edge
   becomes deliberate space instead of a short line. */
.sec--hug { padding-right: calc(var(--gutter) + 14%); }

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

.skip {
  position: fixed; top: -100px; left: 1rem; z-index: 200;
  padding: .75rem 1.25rem; border-radius: 999px;
  background: var(--ink); color: #fff; font-weight: 600; text-decoration: none;
}
.skip:focus { top: 1rem; }

/* -------------------------------------------------------------- Typography */

/* Gambetta, not Switzer. The tracking comes back up as it does: a serif
   already has the horizontal rhythm that negative tracking was faking on a
   grotesque, and pulling a contrast serif tight closes the counters and
   turns it to mud at display size. -.02em on a sans becomes -.015em here,
   and at body size it goes to zero. */
h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 500;
  letter-spacing: -.015em;
  line-height: 1.06;
  margin: 0;
  color: var(--ink);
  text-wrap: balance;
}

.display {
  font-size: clamp(3.1rem, 14vw, 7rem);
  line-height: 1.02;
  letter-spacing: -.022em;
}

/* One word per heading, set in the italic and in terracotta. The italic is
   doing the work — a serif's italic is a genuinely different set of
   letterforms rather than a slanted copy, so the accent word changes shape
   as well as colour and would still read if the colour were removed. One
   per heading, never two. */
.display em,
.display--2 em.em,
.hero__h1 em {
  font-style: italic;
  font-weight: 400;
  color: var(--accent);
}
/* The hero is sized by its longest line, "Dactyl Websites" — fifteen
   characters that have to sit on one line, because wrapping them turns a
   deliberate three-line statement into four ragged ones. Dropping to weight
   400 narrowed that line enough to go up a size without needing the heavy
   negative tracking the bold version depended on. */
.hero__h1 {
  font-size: clamp(2.9rem, 13vw, 6.25rem);
  letter-spacing: -.025em;
  line-height: 1.02;
  font-weight: 500;
}
/* Section headings sit well below the hero rather than beside it — the
   reference runs 56 against 32, a drop of nearly half, and that gap is what
   makes a hero read as the top of a page instead of the first of six equal
   shouts. */
.display--2 {
  font-size: clamp(2.2rem, 9.2vw, 3.5rem);
  line-height: 1.08;
  letter-spacing: -.018em;
}

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

/* One clipping window per line of a heading, so the line can rise into place
   from underneath. Written here rather than beside the hero because ui.js
   builds the same wrappers inside every .split heading on the page. */
.line { display: block; overflow: hidden; padding-bottom: .08em; }
.line > span { display: block; }

/* Punctuation is set in the heading's own ink. A terracotta full stop or
   question mark on every heading read as a styling trick rather than as
   type, so the accent word carries the colour and the sentence simply ends.
   Spelled out against the em rules above because the marks are <em>s, and
   those would otherwise hand them the italic and the accent. */
.dot, .qm,
.display em.dot, .display em.qm,
.hero__h1 em.dot {
  font-style: normal;
  font-weight: inherit;
  color: inherit;
}

.lead {
  font-size: clamp(1.15rem, 4.8vw, 1.4rem);
  line-height: 1.55;
  color: var(--ink-2);
  text-wrap: pretty;
}

/* The section label takes the accent. It is two or three words of monospace
   at 13px, which is both the least text on the page and the thing that
   announces every section — colour here marks structure instead of
   decorating a surface. 4.8:1 on linen, so it clears AA as ordinary text
   rather than relying on being "large". */
.eyebrow {
  margin: 0 0 1.1rem;
  font-family: var(--mono);
  font-size: max(12px, .8125rem);
  font-weight: 400;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--accent-ink);
}

/* ------------------------------------------------------------ Plan labels
   Which package something comes with, the same everywhere on the site:
   the plan's name in the mono labels' voice, on its own pricing card's
   colour. Starter the sand card, Essential the deeper sand of the middle
   card, Pro the site's orange (the accent, as on the CTA). Two labels side
   by side for "Essential and Pro" (.pkgs).
   Contrast, measured: ink-2 on sand 8.15:1, ink on sand-3 11.49:1,
   white on accent 4.65:1. */
.pkgs { display: inline-flex; flex-wrap: wrap; gap: .3rem; vertical-align: middle; }
.pkg {
  display: inline-block;
  padding: .42em .62em .36em;
  border-radius: 5px;
  font-family: var(--mono);
  font-size: 10.5px;
  font-style: normal;
  font-weight: 500;
  line-height: 1;
  letter-spacing: .14em;
  text-transform: uppercase;
  white-space: nowrap;
  vertical-align: .12em;
  box-shadow: inset 0 0 0 1px rgba(20, 17, 13, .07);
}
.pkg--starter   { background: var(--sand);   color: var(--ink-2); }
.pkg--essential { background: var(--sand-3); color: var(--ink); }
.pkg--pro       { background: var(--accent); color: #fff; box-shadow: inset 0 1px 0 rgba(255, 214, 186, .35); }

/* ---------------------------------------------------------------- Buttons
   The mechanism: the label sits in a clipping window with a second copy of
   itself parked directly underneath. On press — and on hover where there is
   a real pointer — both slide up together, so the label appears to be
   replaced by itself. It costs one extra span and reads as something that
   was built rather than styled.

   Underneath, a fill rises from the bottom edge. Between the two, the button
   doesn't just light up, it leans. */

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .7rem;
  min-height: 46px;
  padding: .6rem 1.2rem;
  border: 0;
  border-radius: 999px;
  /* Navy, not ink. The button is the one element a visitor is meant to
     act on, so it is the one element that carries the brand's colour on
     every screen — and it means the CTA matches the panel it sits on
     rather than sitting on it like a sticker. */
  background: var(--navy);
  color: #fff;
  font: inherit;
  font-weight: 500;
  font-size: .9375rem;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  overflow: hidden;
  isolation: isolate;
  transition: transform .4s var(--glide), background-color .3s var(--ease),
              color .35s var(--ease);
}

/* Scaled from the bottom rather than a background-position trick, so it
   composites on the GPU instead of repainting the button every frame. */
.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--navy-rise);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform .5s var(--glide);
}

.btn__label {
  display: grid;
  overflow: hidden;
  /* Clips exactly one line, so the parked copy is genuinely out of sight
     rather than relying on the button's own overflow. */
  line-height: 1.25;
  height: 1.25em;
}
.btn__label > span {
  grid-area: 1 / 1;
  transition: transform .55s var(--glide);
}
.btn__label > span:last-child { transform: translateY(100%); }

.btn__ico {
  display: grid;
  place-items: center;
  width: 17px; height: 17px;
  flex: none;
  margin-right: -.05rem;
  transition: transform .55s var(--glide);
}
.btn__ico svg {
  width: 16px; height: 16px;
  fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}

/* is-lit is set by ui.js on real-pointer hover rather than by :hover here,
   so a phone that reports a phantom hover after a tap can't leave a button
   stuck in its pressed state. */
.btn:active {
  transform: scale(.955);
  transition-duration: .1s;           /* the press lands, the release glides */
}
.btn:active::before,
.btn.is-lit::before { transform: scaleY(1); }
.btn:active .btn__label > span:first-child,
.btn.is-lit .btn__label > span:first-child { transform: translateY(-100%); }
.btn:active .btn__label > span:last-child,
.btn.is-lit .btn__label > span:last-child { transform: translateY(0); }
.btn:active .btn__ico,
.btn.is-lit .btn__ico { transform: translateX(4px); }

.btn--lg { min-height: 50px; padding: .75rem 1.5rem; font-size: 1rem; }
.btn--sm { min-height: 44px; padding: .5rem 1rem; font-size: .875rem; gap: .45rem; }
.btn--sm .btn__ico { width: 15px; height: 15px; }
.btn--sm .btn__ico svg { width: 14px; height: 14px; }
.btn--block { width: 100%; }

.btn--ghost {
  background: var(--grey);
  color: var(--ink);
}
.btn--ghost::before { background: var(--navy); }
.btn--ghost:active,
.btn--ghost.is-lit { color: #fff; }

/* On a black panel the primary action inverts: white pill, black label. */
/* The inverted button only ever appears on a dark surface, so its label
   takes the navy back — white button, navy word, on a navy field. */
.btn--invert { background: var(--paper); color: var(--navy); }
.btn--invert::before { background: var(--grey); }
.btn--invert:active,
.btn--invert.is-lit { color: var(--navy); }

/* ===================================================================== NAV
   Slim, transparent, and gone until it is needed. The old version was a
   floating white pill with a shadow — a permanent object sitting on top of
   the page, announcing itself. This one is a line of text at the top of a
   white page that acquires a frosted backing only once there is content
   sliding under it, and steps out of the way entirely while you are reading
   downward. */

/* A floating grey panel, inset from all three edges — the same flat grey as
   every card on the page, so the chrome is made of the same material as the
   content rather than being a separate transparent strip laid over it.

   Squared-off radius, not a full 999px pill: a capsule reads as a button and
   a 20px rounded rectangle reads as a panel, and this is a panel. */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  padding: .6rem var(--gutter) 0;
  padding-top: max(.6rem, env(safe-area-inset-top));
  pointer-events: none;
  transition: transform .55s var(--glide);
}
.nav.is-away { transform: translateY(-140%); }
/* 20px more air above the header on a phone, where 10px left the logo
   pressed against the browser's own toolbar. Logo and menu button move
   together so they stay on one centre line. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  :root { --nav-h: 76px; }
  .nav { padding-top: calc(max(.6rem, env(safe-area-inset-top)) + 20px); }
}

/* No bar. The header is a line of text at the top of the page, not an
   object sitting on top of it — no panel, no fill, no blur, no rounded
   capsule. A floating pill is the single loudest piece of chrome a page can
   have: it is always there, it always announces itself, and it makes the
   nav the first thing you see instead of the last thing you need.

   What replaces the panel is a shadow on the type itself, so the brand and
   the links stay legible where they cross the photograph without anything
   being drawn behind them. */
.nav__in {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: .6rem;
  max-width: var(--maxw);
  margin-inline: auto;
  min-height: 52px;
  padding: .35rem .25rem .35rem 0;
  background: none;
}

/* Only where it needs it: the brand and the nav links can pass over the
   photograph, the buttons carry their own fill. Two stacked shadows rather
   than one — a tight dark one for edge definition and a wide soft one to
   lift the whole word off whatever is behind it. */
.brand,
.nav__links a,
.nav__toggle-bars i {
  text-shadow: 0 1px 2px rgba(20, 17, 13, .22),
               0 2px 14px rgba(20, 17, 13, .16);
}
.nav__toggle-bars i {
  box-shadow: 0 1px 2px rgba(20, 17, 13, .22),
              0 2px 12px rgba(20, 17, 13, .18);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  min-height: 44px;
  margin-right: auto;
  padding-right: .5rem;
  color: var(--ink);
  text-decoration: none;
  font-family: var(--display);
  font-size: .95rem;
  font-weight: 500;
  letter-spacing: -.01em;
  white-space: nowrap;
}
.brand__text b { font-weight: 600; letter-spacing: .02em; }
.brand__text span { font-weight: 500; color: var(--ink-3); }

/* ------------------------------------------------------ The docked tape
   On a phone the header's logo is the tape from the hero. It is laid out at
   the size it has on the photograph and scaled down into the corner, rather
   than set small and scaled up for the flight: a scaled-up raster is soft,
   and the first thing anyone sees is this tape at full size.

   Absolutely placed so its full-size box does not push the header open.
   The link keeps a box of the docked size, which is what takes the tap —
   the tape itself ignores the pointer, so the part of it that is still out
   over the photograph mid-flight cannot swallow a tap meant for the page.

   The resting transform here is the docked position with no script at all.
   hero.js measures the real one and animates between the two.

   From 820px the header carries the tape too, but set directly at its
   docked size rather than scaled: there is no flight on a wide screen, so
   nothing is gained by drawing it large and shrinking it. */
.brand__tape + .brand__text { display: none; }

/* 70px tall: a fifth larger than the 58px docked phone tape, because a
   desktop header has the width to carry it and the letters come out at a
   comfortable 14px. Every proportion of the full-size tape is kept: gap,
   indent, padding and tracking are all in em, so one font-size sets the
   lot. The shadow is the one thing that does not scale by itself, so it is
   redrawn at this size. */
.brand__tape {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .375em;
  font-size: .912rem;
}
.brand__tape .tape__strip {
  font-size: 1em;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .15),
    inset 0 -1px 0 rgba(0, 0, 0, .55),
    0 5px 12px rgba(20, 17, 13, .3),
    0 1px 3px rgba(20, 17, 13, .3);
}
.brand__tape .tape__strip--2 { margin-left: 1.2em; }

/* Goes with the header when it slides away. The header only moves by its
   own height, which is enough to take a docked tape off screen but not one
   still on its way up from the photograph, so it fades as well. */
.nav .brand__tape { transition: opacity .35s var(--ease); }
.nav.is-away .brand__tape { opacity: 0; }

@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .brand { position: relative; width: 7.25rem; }
  /* On the photograph from the very first paint: fixed at 31% of the
     screen, on the gutter, full size. No script decides this, so there is
     no moment where it is missing or somewhere else.

     Fixed inside the header, which is itself fixed at the screen's origin.
     While the header is still, the tape is placed against the screen; when
     the header slides away, the header's transform makes it the tape's
     reference instead, and the tape goes with it. Both put it in the same
     place, because the header's origin is the screen's. */
  .brand__tape {
    gap: .42rem;
    position: fixed;
    left: var(--gutter);
    top: 31vh;
    top: 31svh;
    transform-origin: 0 0;
    pointer-events: none;
  }
  /* Docked, with no flight: no script, reduced motion, the film switched
     off, the other pages, or hero.js deciding it cannot animate. */
  html:not(.js) .brand__tape,
  html:not([data-hero-film="on"]) .brand__tape,
  html[data-tape="docked"] .brand__tape {
    top: calc(max(.6rem, env(safe-area-inset-top)) + 18.6px);
    transform: translateX(10px) scale(.68);
  }
  /* DACTYL leans a little further on a phone: -3deg rather than -1.1. It
     reads as stuck on by hand at the docked size, where the smaller tilt
     disappears, and it still clears the strip below. */
  .brand__tape .tape__strip:first-child { transform: rotate(-3deg); }
  /* Full size here, because on a phone it starts out on the photograph at
     the size the hero tape always had; the flight does the shrinking. */
  .brand__tape .tape__strip {
    font-size: clamp(1.05rem, 4.6vw, 1.5rem);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, .15),
      inset 0 -1px 0 rgba(0, 0, 0, .55),
      0 10px 24px rgba(20, 17, 13, .34),
      0 2px 5px rgba(20, 17, 13, .3);
  }
  .brand__tape .tape__strip--2 { margin-left: 1.35rem; }
}
@media (max-width: 819px) and (prefers-reduced-motion: reduce), (max-device-width: 599px) and (prefers-reduced-motion: reduce), (max-device-height: 599px) and (prefers-reduced-motion: reduce) {
  .brand__tape {
    top: calc(max(.6rem, env(safe-area-inset-top)) + 18.6px);
    transform: translateX(10px) scale(.68);
  }
}

.nav__links { display: none; }
.nav__cta { display: none; }

/* A bare glyph, not a white disc. The disc made the menu the loudest object
   in the header; the reference simply sets an icon inside the panel. */
.nav__toggle {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  flex: none;
  border: 0;
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: transform .4s var(--glide), background-color .3s var(--ease);
}
.nav__toggle:active { transform: scale(.9); background: rgba(20, 17, 13, .06); }
.nav__toggle-bars { display: grid; gap: 4px; }
.nav__toggle-bars i {
  display: block; width: 15px; height: 1.5px; border-radius: 2px;
  background: var(--ink);
  transition: transform .45s var(--glide);
}
.nav__toggle[aria-expanded="true"] .nav__toggle-bars i:first-child { transform: translateY(2.75px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] .nav__toggle-bars i:last-child  { transform: translateY(-2.75px) rotate(-45deg); }

/* While the menu is open the header stops being a grey pill sitting on a
   navy screen and becomes part of it: the panel drops away and everything
   in it turns white. Hooked off body.is-locked, which the menu already sets,
   rather than a new class or :has() — one state, one source of truth.

   The extra .nav in the selector is not decoration: .nav.is-stuck .nav__in
   sets its own background at (0,3,0) and would otherwise re-paint the grey
   pill over the top of this whenever the menu is opened part-way down. */
body.is-locked .nav .nav__in {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
body.is-locked .brand,
body.is-locked .brand__text span { color: var(--paper); }
body.is-locked .brand__text span { opacity: .62; }
body.is-locked .nav__toggle-bars i { background: var(--paper); }
body.is-locked .nav__toggle:active { background: rgba(255, 255, 255, .12); }

/* ------------------------------------------------ Over dark ground, phones
   The logo and the menu button each turn over when what is behind them is
   dark: the photograph, the navy features sheet, a navy panel. ui.js looks
   at what is actually under each one as the page scrolls and sets the two
   classes separately, because a navy panel can sit behind one and not the
   other. Reversed, the tape is the light kind with dark letters, and the
   menu's bars go white. The open menu is navy too, so it counts. */
.nav .tape__strip {
  transition: color .35s var(--ease), background-color .35s var(--ease),
              text-shadow .35s var(--ease);
}
.nav .nav__toggle-bars i { transition: transform .45s var(--glide), background-color .35s var(--ease); }
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  /* Only once the tape is the header's: docked, or on its flight. Before
     hero.js has started the flight (the first moments on a phone) the tape
     is still on the photograph, and turning it light there flashed it. */
  html:not([data-hero-film="on"]) .nav.is-dark-logo .brand__tape:not(.is-flying) .tape__strip,
  html[data-tape] .nav.is-dark-logo .brand__tape:not(.is-flying) .tape__strip {
    color: #1a1714;
    background-color: #eee8dd;
    background-image: linear-gradient(168deg,
      rgba(255, 255, 255, .75) 0%,
      rgba(255, 255, 255, 0) 40%,
      rgba(60, 45, 30, .1) 100%);
    text-shadow:
      1px 1px 0 rgba(255, 255, 255, .8),
      -1px -1px 1px rgba(0, 0, 0, .28);
  }
  .nav.is-dark-menu .nav__toggle-bars i { background: var(--paper); }
}

/* ------------------------------------------------------------- Menu sheet */

.sheet {
  position: fixed;
  inset: 0;
  z-index: 99;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 2rem;
  padding: calc(var(--nav-h) + 3rem) var(--gutter) 2rem;
  padding-bottom: max(2rem, calc(env(safe-area-inset-bottom) + 1.25rem));
  /* The menu is the one place the whole screen changes state, so it is the
     one place the whole screen is allowed to change colour: to umber, the
     dark of the price screen, lit by the same lamp. */
  --sheet-bg: radial-gradient(118% 88% at 50% 32%, #1e1e1e 0%, #121212 46%, #090909 100%);
  --sheet-mark: var(--accent-lit);
  --sheet-ink: var(--ivory);
  --sheet-shadow: rgba(9, 9, 9, .42);
  background: var(--sheet-bg);
  color: var(--paper);
  opacity: 0;
  transition: opacity .35s var(--ease);
}
.sheet[hidden] { display: none; }
.sheet.is-open { opacity: 1; }
/* With motion the navy is a box of its own (ui.js, menu), grown out of the
   menu button to fill the screen; the sheet itself is then only the layer
   the links sit on. Negative z-index inside the sheet's own stacking
   context: over the page, under the links. */
.sheet.sheet--box { background: none; }
.sheet__box {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  border-radius: 28px;
  background: var(--sheet-bg);
  box-shadow:
    0 28px 70px var(--sheet-shadow),
    0 8px 22px var(--sheet-shadow);
  pointer-events: none;
}

.sheet__links { display: grid; }
.sheet__links a {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: .8rem 0;
  font-family: var(--display);
  font-size: clamp(2.25rem, 11vw, 3rem);
  /* The weight every other heading in the serif is set in. */
  font-weight: 400;
  letter-spacing: -.028em;
  color: var(--sheet-ink);
  text-decoration: none;
}
/* Each link is a clipping window for its own text, which is what lets ui.js
   slide them up from below one after another instead of fading a block. */
.sheet__w { display: block; overflow: hidden; padding-bottom: .06em; }
.sheet__w > span { display: block; }
/* You are here: the headings' terracotta full stop. */
.sheet__links a.is-here .sheet__w > span::after { content: "."; color: var(--sheet-mark); }
/* Email and phone: full-width rows, each its own tap target. */
.sheet__contact { margin: 0; padding: 0; list-style: none; }
.sheet__contact a {
  display: flex;
  align-items: center;
  gap: .9rem;
  min-height: 58px;
  padding: .4rem 0;
  /* Transparent, not removed: keeps the same 1px box so the row heights
     (and anything measuring them) don't shift, it just no longer draws. */
  border-top: 1px solid transparent;
  color: var(--sheet-ink);
  font-size: 1.125rem;
  font-weight: 500;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  transition: background-color .2s ease;
}
.sheet__contact li:last-child a { border-bottom: 1px solid transparent; }
.sheet__contact a:active { background: rgba(255, 255, 255, .06); }
.sheet__contact svg {
  flex: none;
  width: 21px;
  height: 21px;
  fill: none;
  stroke: var(--sheet-mark);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sheet__foot { display: grid; gap: 1.1rem; }
.sheet__meta { margin: 0; font-size: 1rem; line-height: 1.1; color: var(--on-navy-2); }
.sheet__meta a {
  display: inline-block;
  padding: .9rem 0;
  color: var(--paper);
  text-decoration: none;
  font-weight: 500;
}
.sheet__meta a:hover { color: var(--paper); }

/* Same rule as the dark panels: a navy focus ring on navy is no ring. */
.sheet :focus-visible { outline-color: var(--paper); }

body.is-locked { overflow: hidden; }

/* ==================================================================== HERO
   Mobile first, and the shape is: one screen of photograph, then the words
   arriving over the bottom of it, then the page carrying on. The photo is
   sticky rather than fixed, which is the entire difference between an image
   that releases and one that is welded to the viewport for the whole
   document.

   100svh, not 100vh: on iOS Safari vh is the *largest* viewport height, so a
   full-height hero sits partly behind the address bar until the visitor
   scrolls. */

/* No padding of its own. Anything between the bottom of the copy sheet and
   the end of the section is a band where the sticky photograph is still
   showing, which reads as a strip of the image having been left behind. The
   sheet carries that space instead, so it ends where the section ends. */
.hero {
  position: relative;
  padding: 0;
}

/* -------------------------------------------------------------- The photo
   position: sticky, not fixed. Sticky is scoped to this element's containing
   block — the .hero — so the browser pins the photo for exactly as long as
   the hero is on screen and then lets it scroll away on its own. The old
   fixed version had no such boundary: it stayed nailed to the viewport for
   the entire page and only stopped being visible because a later section
   happened to have an opaque fill painted over it. That is why it felt
   stuck. Nothing was releasing it; something was covering it up. */
.hero__media {
  position: sticky;
  top: 0;
  z-index: 0;
  height: 100svh;
}

/* The clipping happens here, not on .hero__media. The tape is a sibling of
   this element and is positioned to hang off the photograph's edge, so an
   overflow:hidden one level up would slice it in half. */
.hero__media-in {
  position: relative;
  height: 100%;
  overflow: hidden;
}

.hero__photo-canvas,
.hero__photo-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The crop is already 0.458:1, so on a portrait phone the photo very nearly
     fills the frame as-is and this only nudges which part survives. */
  object-position: 50% 24%;
}
/* The canvas is the pixel-reveal surface; the <img> underneath it is what
   shows when script never runs. Stacking them rather than swapping avoids
   the flash of a decoded photo being replaced by an empty canvas — and the
   script, not this stylesheet, is what hides the <img>, so a canvas context
   that fails to come back leaves the photograph on screen instead of a
   blank hero. */
.hero__photo-canvas { position: absolute; inset: 0; z-index: 1; }

/* The sharp first and last frames (hero.js shows one with .is-on). Centred,
   as the canvas crops the film centred, so the still lands exactly on the
   frame under it. Hidden without the film running: the markup's own <img>
   is the photograph then. */
.hero__sharp {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Gone quickly when the camera starts moving (a still over a moving
     picture is a ghost), in more slowly as it settles. */
  transition: opacity .16s var(--ease), visibility 0s linear .16s;
}
.hero__sharp.is-on {
  opacity: 1;
  visibility: visible;
  transition: opacity .35s var(--ease), visibility 0s;
}

.hero__tagline, .hero__shade { display: none; }
/* The headline's second copy is the wide layout's alone (desktop.css,
   Hero). */
.hero__h1--over { display: none; }
/* The titles' label is the phone's alone (site.css, "The titles"). */
@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) { .hero__eyebrow { display: none; } }
/* The tagline on the opening photograph (phones): the same embossed tape
   as the logo, a size down and tracked a little tighter so the whole line
   fits across a phone. hero.js puts it under the nib and slides it off to
   the right with the first swipe. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  [data-hero-film="on"] .hero__tagline {
    position: absolute;
    left: var(--gutter);
    /* Where hero.js puts it (phoneGeometry: 24px under the nib, which is
       55.2% down the opening frame), so it does not jump when that script
       arrives after the page has loaded. Exact on any upright phone, where
       the frame fills the screen's height. */
    top: calc(55.2% + 24px);
    z-index: 3;
    display: block;
    pointer-events: none;
  }
  .hero__tagline .tape__strip {
    font-size: max(12px, 3.9vw);
    letter-spacing: .15em;
    text-indent: .15em;
    white-space: nowrap;
    /* The base .tape__strip rule tilts every strip -1.1deg for the "stuck on
       by hand" look; this one line is meant to read as dead level, so it's
       cancelled here rather than just left unset (unset falls through to
       that base rotation, not to zero). */
    transform: none;
  }
}
/* A short softening at the foot of the photograph. On a phone the sheet
   below does most of this work, so the gradient only has to take the hard
   edge off the last couple of centimetres; on a wide screen it is what lets
   the photograph hand over to the page. In linen rather than white, because
   a white fade on a linen page is a pale rectangle — which is the exact bug
   a fade is here to avoid. */
.hero__media-in::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 16%;
  background: linear-gradient(to bottom,
              rgba(246, 243, 238, 0) 0%,
              rgba(246, 243, 238, .55) 55%,
              rgba(246, 243, 238, .92) 100%);
  pointer-events: none;
}

/* ---------------------------------------------------------- Embossed tape
   Two strips of Dymo tape, stuck on slightly out of true. This is the
   brand: the header's logo at every width (.brand__tape), and on a phone
   it starts out full size on the photograph and flies up into the header.

   The emboss is four separate effects and it needs all four. The tape body
   is a near-black with a faint sheen raked down it, because flat #000 is a
   rectangle rather than plastic. The glyph face is a warm off-white, never
   pure white, because what makes a letter show on real tape is the plastic
   whitening as the die stretches it. A hard dark edge sits down the lower
   right of each letter and a soft highlight on the upper left, matching the
   one light source the rest of the site is lit from. And the corners are
   barely rounded — 3px, not a pill — because the strip is cut, not moulded.

   Two strips, not one, and neither is level: real tape goes on by hand. */
.tape__strip {
  display: inline-block;
  font-family: var(--label);
  font-weight: 600;
  font-size: clamp(1.05rem, 4.6vw, 1.5rem);
  line-height: 1;
  letter-spacing: .26em;
  text-indent: .26em;          /* balances the trailing letter-space */
  text-transform: uppercase;
  padding: .62em .78em .58em;
  border-radius: 3px;
  color: #e9e2d6;
  background-color: #15130f;
  background-image:
    linear-gradient(168deg,
      rgba(255, 255, 255, .13) 0%,
      rgba(255, 255, 255, .03) 18%,
      rgba(255, 255, 255, 0) 38%,
      rgba(0, 0, 0, .28) 100%);
  text-shadow:
    -1px -1px 0 rgba(255, 255, 255, .30),
     1px  1px 1.5px rgba(0, 0, 0, .92);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .15),
    inset 0 -1px 0 rgba(0, 0, 0, .55),
    0 10px 24px rgba(20, 17, 13, .34),
    0 2px 5px rgba(20, 17, 13, .3);
  transform: rotate(-1.1deg);
}
/* The offset. Indented and tilted the other way, the way a second strip
   actually lands when someone sticks it on by eye. */
.tape__strip--2 {
  margin-left: 1.35rem;
  transform: rotate(.7deg);
}

/* ------------------------------------------------------------- The words
   A sheet of linen that rises over the foot of the photograph and carries
   the copy on it.

   This is what the frosted glass card used to be, and it is a sheet rather
   than glass for a reason that only shows up once the photo is sticky: the
   words scroll while the photograph stays, so anything translucent means the
   copy drifts across a moving, high-contrast image and becomes unreadable
   about a third of the way through the hero. An opaque sheet has one more
   advantage — you can watch it cover the photograph as you scroll, which
   makes the release legible instead of merely correct. */
.hero__in {
  position: relative;
  z-index: 2;
  margin-top: -3.25rem;
  padding-top: 2.5rem;
  padding-bottom: 4rem;
  background: var(--linen);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
}

.hero__h1 { margin: 0 0 1.5rem; }
.hero__h1 em.dot { margin-left: -.06em; }

.hero__lead {
  max-width: 33rem;
  margin: 0 0 2.25rem;
  font-size: clamp(1.15rem, 4.6vw, 1.35rem);
  line-height: 1.5;
}
.hero__cta { display: flex; flex-wrap: wrap; gap: .6rem; }

/* ------------------------------------------------------------- The facts
   A rule above, three items below, each a claim over a qualifier. Set in the
   interface face rather than the display one: these are data, and putting
   them in the serif would make them compete with the headline they sit
   under. */
.hero__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 1.1rem 1.5rem;
  margin: 2.75rem 0 0;
  padding: 1.5rem 0 0;
  border-top: 1px solid var(--hair);
  list-style: none;
  max-width: 34rem;
}
.hero__facts li { display: grid; gap: .15rem; }
/* Service areas: the label in the site's mono, the regions, the suburbs. */
.hero__areas { display: block; }
.hero__areas p { margin: 0; }
.hero__areas-h {
  margin-bottom: .55rem !important;
  font-family: var(--mono);
  font-size: max(12px, .75rem);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.hero__areas-r { font-size: 1rem; font-weight: 600; color: var(--ink); }
.hero__areas-s { margin-top: .25rem !important; font-size: .95rem; line-height: 1.5; color: var(--ink-3); }
.hero__facts b {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -.005em;
  font-variant-numeric: tabular-nums;
}
.hero__facts span { font-size: .875rem; color: var(--ink-3); }

/* ------------------------------------------------------ The film, phone
   The film gets the whole screen to itself, then the copy rises straight
   up over the finished shot, from the foot of the screen, with the scroll.

   .hero__stage carries all of the scroll. Its top margin (--film-run) is
   the run the film plays over, with the photograph sticky behind it; the
   sheet inside the stage comes up from the foot of the screen the moment
   the film ends and carries on up the page normally. (It used to be pinned
   with its heading peeking while it slid in from the right, then rise:
   --peek and --slide-run are now 0, and the sticky rule below has nothing
   left to hold.)

   Sticky, not a script chasing the scroll: the compositor holds the sheet
   still, so it does not shiver a frame behind the finger the way a
   transform written on every scroll event does on a phone.

   hero.js reads all of this back out of the layout, so these three numbers
   are the whole of the timing. Everything is only switched on while the
   film is actually running; without it the sheet sits directly under the
   photograph, just out of sight.

   The photograph is 100lvh, the tallest the viewport gets. Once the address
   bar slides away the screen grows to that height, and a picture sized to
   the smaller viewport left a strip of bare page along the bottom for the
   whole of the film. With the bar showing, the extra sits under it.

   overflow: clip rather than hidden, so the sticky photograph still sticks
   while nothing inside the hero can paint below its bottom edge — and the
   sheet, parked off to the right before it slides in, cannot widen the
   page. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .hero {
    /* None: the film plays on its own clock from the first swipe (hero.js,
       autoplay), and the sheet comes up with that same swipe. */
    --film-run: 0px;
    --slide-run: 0px;
    /* Sideways only: the photograph is allowed to run on below the hero's
       foot (see --media-over below), under the studio. */
    overflow-x: clip;
    overflow-y: visible;
  }
  .hero__media { height: 100vh; height: 100lvh; }
  .hero__in { margin-top: 0; padding-top: 3.5rem; }
  /* The finished shot stays pinned, zooming and going to bokeh, until the
     sheet has covered the whole screen. The hero ends before that (the
     sheet is shorter than the screen), and a sticky photograph lets go
     when its section ends: it used to slide up the screen for the last
     stretch of the defocus. --media-over (hero.js) is that stretch. The
     photograph's margin box is shortened by it, so it sticks that much
     longer, running on past the hero's foot, under the studio's linen;
     the stage's top margin takes the same amount back, so nothing else on
     the page moves. Held by the compositor, like the rest of the hero, not
     by a script chasing the scroll. */
  [data-hero-frames="on"] .hero__media { margin-bottom: calc(-1 * var(--media-over, 0px)); }
  [data-hero-frames="on"] .hero__stage { margin-top: calc(var(--film-run) + var(--media-over, 0px)); }
  [data-hero-frames="on"] .hero__in {
    position: sticky;
    top: calc(100lvh - var(--peek, 16rem));
  }
  /* The slide, then the rest after it (--rest, set by hero.js): a short
     stretch where the sheet has landed and nothing moves. */
  [data-hero-frames="on"] .hero__stage::after {
    content: "";
    display: block;
    height: calc(var(--slide-run) + var(--rest, 0px));
  }
}

/* ------------------------------------------------------ The titles, phone
   With the film running, the copy is not on a sheet any more: it comes up
   over the playing film like a film's opening titles, and the film is in
   view the whole way through, behind the words, round them and below them.

   What keeps the words readable over a moving picture (the reason there
   used to be an opaque sheet) is the picture itself going quiet behind
   them, not a box:
     - the band (.hero__copy::before): a soft dark field that travels with
       the words, in black, the same kind of band as the window's caption
       further down.
     - the shade (.hero__shade): a black fall-off from the foot of the
       frame that deepens as the titles come up from there, and eases back
       off as they rise away from it (ui.js, intro). The top of the frame
       stays clear.
     - the defocus: once the film has ended the camera racks focus off the
       scene and pushes in (hero.js), so by the time the words are high on
       the screen they sit over soft light rather than detail.
   The page's linen then rises over the film with a rounded edge, like the
   next sheet of the page, carrying the studio (.studio below).

   The lines of the heading rise in their windows as it comes up, the label
   draws its rule, and the paragraph and buttons follow (ui.js, intro); on
   the way out the three drift apart a little, the heading fastest (ui.js,
   heroExit), which is what gives the words depth over the still picture. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  [data-hero-frames="on"] .hero__in {
    background: none;
    border-radius: 0;
    /* The titles set to the right-hand side. */
    text-align: right;
    padding-top: 2.5rem;
    /* Film in view under the buttons before the linen comes up. */
    padding-bottom: 30svh;
    color: var(--paper);
  }
  [data-hero-frames="on"] .hero__media-in::after { display: none; }
  /* Off from the first paint when the film is on: hero.js (which sets
     data-hero-frames) only arrives after the page has loaded on a phone,
     and until then this linen fade sat across the foot of the photograph
     as a pale band. data-hero-film is set in the page's head. */
  [data-hero-film="on"] .hero__media-in::after { display: none; }
  [data-hero-frames="on"] .hero__shade {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 2;
    opacity: 0;
    pointer-events: none;
    background: linear-gradient(to top,
      rgba(0, 0, 0, .9) 0%,
      rgba(0, 0, 0, .74) 30%,
      rgba(0, 0, 0, .5) 56%,
      rgba(0, 0, 0, .16) 80%,
      rgba(0, 0, 0, 0) 100%);
  }
  /* The band: a soft dark field that travels with the words, full width
     and fading out above and below them, as behind the window's caption.
     Wherever the titles are on the screen, and whatever frame is playing
     behind them, the words sit on it and the picture round it stays clear.
     --band is drawn in as the titles arrive (ui.js, intro). */
  [data-hero-frames="on"] .hero__copy { position: relative; }
  [data-hero-frames="on"] .hero__copy::before {
    content: "";
    position: absolute;
    top: -5.5rem;
    bottom: -5.5rem;
    left: calc(50% - 50vw);
    width: 100vw;
    z-index: -1;
    opacity: var(--band, 1);
    background: linear-gradient(to bottom,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, .55) 24%,
      rgba(0, 0, 0, .55) 76%,
      rgba(0, 0, 0, 0) 100%);
    pointer-events: none;
  }
  [data-hero-frames="on"] .hero__eyebrow {
    margin: 0 0 1.2rem;
    color: rgba(255, 255, 255, .78);
    letter-spacing: .22em;
    font-size: max(12px, .72rem);
  }
  [data-hero-frames="on"] .hero__eyebrow::before {
    content: "";
    display: inline-block;
    vertical-align: middle;
    width: 1.5rem;
    height: 1px;
    margin: -.15em .75rem 0 0;
    background: var(--accent-lit);
    transform: scaleX(var(--rule, 1));
    transform-origin: left center;
  }
  [data-hero-frames="on"] .hero__h1 {
    margin-bottom: 1.4rem;
    font-size: clamp(3rem, 14vw, 4rem);
    line-height: .98;
    color: var(--paper);
    text-shadow: 0 2px 28px rgba(6, 14, 32, .35);
  }
  [data-hero-frames="on"] .hero__h1 em { color: var(--accent-lit); }
  [data-hero-frames="on"] .hero__h1 em.dot { color: inherit; }
  [data-hero-frames="on"] .hero__cta { justify-content: flex-end; }
  [data-hero-frames="on"] .hero__lead {
    max-width: 30ch;
    margin-left: auto;
    margin-bottom: 2rem;
    font-size: 1.06rem;
    line-height: 1.55;
    color: rgba(255, 255, 255, .82);
  }
  /* On the dark: the primary action is the white pill, the second a clear
     one drawn in a hairline, filling white as it is pressed. */
  [data-hero-frames="on"] .hero__cta .btn { background: var(--paper); color: var(--navy); }
  [data-hero-frames="on"] .hero__cta .btn::before { background: var(--grey); }
  [data-hero-frames="on"] .hero__cta .btn--ghost {
    background: rgba(255, 255, 255, .06);
    color: var(--paper);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .38);
  }
  [data-hero-frames="on"] .hero__cta .btn--ghost::before { background: var(--paper); }
  [data-hero-frames="on"] .hero__cta .btn--ghost:active,
  [data-hero-frames="on"] .hero__cta .btn--ghost.is-lit { color: var(--navy); }

  /* The next sheet of the page, rising over the film. One sheet of linen
     behind everything in the stack (the cards and the showcase), not a
     fill on each section: the sections between the cards
     have none of their own (each card overlaps the one before), and with
     the fill on the studio alone the film showed through behind the orange
     card for a stretch, then vanished as it switched off, which read as
     that card's background flickering.

     Its top edge (--sheet-top, from the top of the stack) sits part-way
     down the studio card, across the first line of its heading, so the
     card is lying half on the film and half on the page: it bridges the
     two rather than waiting on the far side of the edge. A square edge,
     softened by the fade just above it (.stack::after, below). hero.js
     keeps the film in place until this edge reaches the top of the
     screen. */
  [data-hero-frames="on"] .stack {
    --sheet-top: 13.25rem;
    position: relative;
    z-index: 0;
  }
  [data-hero-frames="on"] .stack::before {
    content: "";
    position: absolute;
    top: var(--sheet-top);
    right: 0;
    bottom: 0;
    left: 0;
    z-index: -1;
    background: var(--linen);
    pointer-events: none;
  }
  [data-hero-frames="on"] .studio { background: none; }

  /* The film itself (hero.js, phoneVideo): over the canvas, under the
     sharp stills, shown once its first frame is in. */
  .hero__video {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 50% 50%;
    opacity: 0;
    pointer-events: none;
  }
  .hero__video.is-ready { opacity: 1; }
}

/* ================================================================== STUDIO */

.studio { padding-block: 6.5rem; }


/* ================================================================ FEATURES */

.features { padding-block: 6.5rem; }

/* ------------------------------------------------------------- Swipe decks
   One shared component for the features and the plans. The scrolling is
   entirely the browser's — CSS scroll-snap, native momentum, native
   rubber-band — and ui.js only reads the position back out to drive the
   rail and to soften the cards that aren't the one being looked at.

   Reading rather than driving is the point. A scripted carousel has to
   reimplement flick velocity, snapping and overscroll, and on a phone it
   never quite matches what every other scrolling surface on the device
   does. Here there is nothing to match: it IS the native behaviour. */

.deck-wrap { margin-top: 2.25rem; }
/* The features' 3D stage is the desktop's (desktop.css). */
.feat-stage { display: none; }
/* The wide collage in "Growing together" is the desktop's too. */
.window__wide { display: none; }

.deck {
  display: flex;
  gap: 1rem;
  margin: 0;
  padding: .75rem var(--gutter) 1.75rem;
  list-style: none;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.deck::-webkit-scrollbar { display: none; }
.deck > * { scroll-snap-align: start; flex: 0 0 min(84vw, 22rem); }
.deck:focus-visible { outline-offset: -3px; }

.deck-ui {
  display: flex;
  align-items: center;
  gap: 1rem;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.deck-btn {
  display: none;
  place-items: center;
  width: 44px; height: 44px;
  flex: none;
  border: 0;
  border-radius: 999px;
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--hair-2);
  cursor: pointer;
  transition: box-shadow .3s var(--ease), background-color .3s var(--ease), opacity .3s var(--ease);
}
.deck-btn svg { width: 17px; height: 17px; fill: none; stroke: var(--ink); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
/* The ring was a leftover royal blue from a palette that no longer exists.
   It takes the accent now, which is also the only place on the page a deck
   control says anything at all. */
.deck-btn:hover { background: var(--grey); box-shadow: inset 0 0 0 1px var(--accent); }
.deck-btn:hover svg { stroke: var(--accent); }
.deck-btn[disabled] { opacity: .25; cursor: default; }
.deck-btn[disabled]:hover { background: transparent; box-shadow: inset 0 0 0 1px var(--hair-2); }

/* The rail doubles as the swipe affordance: a visibly part-filled track says
   "there is more of this sideways" before anything has been touched. */
.deck-rail {
  flex: 1;
  height: 2px;
  border-radius: 999px;
  background: var(--hair);
  overflow: hidden;
}
.deck-rail i {
  display: block;
  width: 30%;
  height: 100%;
  border-radius: 999px;
  background: var(--navy);
  transform-origin: left;
}
/* The rail, arrows and counter arrive with the deck rather than being there
   before it. They sit directly beside content that types itself in and draws
   itself, and were the one static thing left in the section. */
.js .deck-ui { opacity: 0; }
.js.no-motion .deck-ui { opacity: 1; }

.deck-count {
  margin: 0;
  flex: none;
  font-family: var(--display);
  font-size: max(12px, .8rem);
  font-weight: 600;
  letter-spacing: .05em;
  color: var(--ink-4);
  font-variant-numeric: tabular-nums;
}
.deck-count b { color: var(--ink); font-weight: 600; }

/* --------------------------------------------------------------- Cards */

.card {
  display: flex;
  flex-direction: column;
  gap: .55rem;
  padding: 1.85rem 1.6rem 2rem;
  border-radius: var(--r-xl);
  background: var(--grey);
  transition: transform .5s var(--glide), background-color .35s var(--ease);
}

/* The numeral takes the accent, and it is set in the mono rather than the
   display face: 01 in a serif at 12px is an ornament, 01 in a monospace is
   an index. */
.card__n {
  font-family: var(--mono);
  font-size: max(12px, .75rem);
  font-weight: 400;
  letter-spacing: .1em;
  color: var(--accent-ink);
}

/* A hairline ring matching the card's own edge, rather than a filled chip.
   The whole card is built from one line weight, and that is what stops six
   of them side by side looking like six separate widgets. */
.card__icon {
  width: 46px; height: 46px;
  padding: 12px;
  margin: .55rem 0 .7rem;
  border-radius: 999px;
  background: var(--paper);
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.card h3 { font-size: 1.35rem; line-height: 1.14; }
.card__kicker {
  margin: -.15rem 0 0;
  font-family: var(--mono);
  font-size: max(12px, .78rem);
  letter-spacing: .01em;
  text-transform: none;
  color: var(--ink-3);
}
.card p {
  margin: 0;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink-2);
  text-wrap: pretty;
}

/* ============================================= FEATURES, phone: the stack
   The three cards (the studio, the goal, the process).

   The features section is navy on a phone: the one change of material big
   enough to read as a new sheet arriving rather than as the page carrying
   on. Its top edge is one gentle arc, low at the left and rising to the
   right: the top of a very large circle (about four screens across) whose
   crown sits at the right-hand edge, so it climbs the whole way and never
   turns back the way the earlier S-curve did. That edge is a separate shape
   sitting on top of the section (an SVG stretched to the width) rather than
   a clip-path on it, because a clip-path would also clip the shadow the
   edge casts back onto the card under it, and that shadow is most of what
   makes it look like a thick sheet rather than a coloured stripe. A lighter
   line along the curve is the lit bevel of its edge. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  /* ------------------------------------------------------------ The cards
     The studio, staying local and the process: three big cards, each
     wider than the phone, tilted, and overlapping the one before like
     sheets dealt onto a table. Each one cartwheels in from the side in its
     section's data-roll (ui.js, slabs) and lands where these put it.

     THE KNOBS. Set per card below; every one is plain CSS, so change a
     number and reload.
       --slab-angle  its resting tilt. Negative leans anticlockwise (the
                     right-hand end up), positive clockwise.
       --slab-w      its width. A little under the screen's, so with the
                     tilt the rounded top corner on the leading side (top
                     right for the studio and the process, top left for
                     staying local) sits on screen while the corner below it
                     runs off the edge.
       --slab-h      its height (the least it will be: longer copy grows it).
       --slab-x      sideways. + moves it right, - moves it left.
       --slab-y      up and down. + moves it down, - moves it up. Only the
                     card moves, not the page around it.
       --slab-lap    how far the section is pulled up over the one before:
                     the overlap.
       --slab-pad    the padding inside the card, the same on all four
                     sides.
     The width, height and padding are shared (all three the same size);
     the angle, the offsets and the overlap are each card's own. */
  .slab {
    --slab-w: 86vw;
    --slab-h: 25rem;
    --slab-pad: 2rem;
    --slab-x: 10.5vw;
    --slab-y: 0px;
    --slab-angle: -10deg;
    --slab-lap: 0px;
  }
  .slab--studio  { --slab-angle: -6deg; --slab-x: 15.5vw; --slab-y: 0px; }
  .slab--local   { --slab-angle: 6deg;  --slab-x: 0vw;    --slab-y: 0px; --slab-lap: 5.5rem; }
  .slab--process { --slab-angle: -6deg; --slab-x: 15.5vw; --slab-y: 0px; --slab-lap: 6rem; }

  /* The cards run off the sides of the screen; this keeps the page from
     scrolling sideways to follow them (clip, not hidden: nothing in here
     sticks, and clip makes no scroll box). Up and down is left alone, so
     a tilted corner can rise over the section above. */
  .stack { overflow-x: clip; }

  .slab {
    position: relative;
    margin-top: calc(-1 * var(--slab-lap));
    padding-block: 3.25rem;
  }
  .slab > .wrap { max-width: none; padding: 0; }
  .slab__card {
    position: relative;
    left: var(--slab-x);
    top: var(--slab-y);
    box-sizing: border-box;
    width: var(--slab-w);
    min-height: var(--slab-h);
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: var(--slab-pad);
    border-radius: var(--r-xl);
    transform: rotate(var(--slab-angle));
    /* Its own GPU layer, kept for good: see ui.js, slabs. */
    will-change: transform;
    /* Lying on the page, not floating over it: a soft shadow close under
       it, and a long faint one, both warm. */
    box-shadow:
      0 2px 6px rgba(40, 24, 8, .08),
      0 34px 60px -34px rgba(40, 24, 8, .42);
  }

  /* The studio. It is the section straight after the hero, so it keeps
     the linen fill that covers the finished shot as the page leaves it,
     and it is tucked up under the empty foot of the hero's sheet. The top
     padding keeps the card's raised top-right corner clear of the foot of
     the hero's sheet, which is a layer above and would cut it off. */
  .studio {
    background: var(--linen);
    margin-top: -4rem;
    padding-top: 5.5rem; /* 7.5rem */
  }

  /* Sand (the studio's own panel colour needs nothing added). */
  .slab--sand .slab__card { background: var(--grey); color: var(--ink); }
  .slab--sand .slab__lead { color: var(--ink-2); }
  .slab--sand .slab__steps li { color: var(--ink-2); }
  .slab--sand .slab__steps li::before { color: var(--accent-ink); }
  .slab__lead + .slab__lead { margin-top: .8rem; }
  /* The cards' shows (ui.js, cardShows): a canvas over the card's face,
     under its words. */
  .card__fx {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border-radius: inherit;
    pointer-events: none;
  }
  .slab__card.has-fx > :not(.card__fx) { position: relative; }

  /* The studio's two paragraphs are one thought, so on a phone they are
     one size, the same as the copy on the cards after it (.slab__lead):
     no larger first paragraph here. */
  .studio .panel__body { margin-top: 1.2rem; gap: .8rem; }
  .studio .panel__body p,
  .studio .panel__body p:first-child {
    font-size: 1.02rem;
    line-height: 1.5;
    letter-spacing: normal;
    color: var(--ink-2);
  }
  /* Terracotta, lit from the top left like the orange tab further down
     the page. */
  .slab--ember .slab__card {
    color: var(--paper);
    background: linear-gradient(150deg, #d8672e 0%, #c2521f 46%, #a8441a 100%);
    box-shadow:
      inset 0 1px 0 rgba(255, 214, 186, .45),
      0 2px 6px rgba(110, 36, 8, .14),
      0 34px 60px -34px rgba(110, 36, 8, .5);
  }
  .slab--ember .eyebrow { color: rgba(255, 244, 236, .82); }
  .slab--ember .slab__steps li { color: rgba(255, 244, 236, .88); }
  .slab--ember .slab__steps li::before { color: rgba(255, 236, 222, .7); }

  .slab__card .eyebrow { margin: 0 0 1rem; }
  .slab__h {
    margin: 0;
    font-family: var(--display);
    font-size: clamp(2.1rem, 9.4vw, 2.8rem);
    font-weight: 400;
    line-height: 1.02;
    letter-spacing: -.028em;
    color: inherit;
  }
  .slab--ember .slab__h .dot { color: var(--paper); }

  .slab__steps {
    display: grid;
    gap: .7rem;
    margin: 1.4rem 0 0;
    padding: 0;
    list-style: none;
    counter-reset: step;
  }
  .slab__steps li {
    position: relative;
    padding-left: 1.75rem;
    font-size: .98rem;
    line-height: 1.45;
    color: var(--on-navy);
    counter-increment: step;
  }
  .slab__steps li::before {
    content: "0" counter(step);
    position: absolute;
    left: 0;
    top: .3em;
    font-family: var(--mono);
    font-size: .72rem;
    letter-spacing: .08em;
    color: var(--accent-lit-ink);
  }
  .slab__steps b { font-weight: 600; color: var(--paper); }

  .slab__lead {
    margin: 1.2rem 0 0;
    font-size: 1.02rem;
    line-height: 1.5;
    color: rgba(255, 248, 242, .92);
  }
  /* Not on a phone: the footer carries the areas. */
  .hero__areas { display: none; }
}
/* The goal, the process and the showcase are the phone layout's; a wide
   screen keeps the hero's service areas and goes straight on to the
   features. */
@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) {
  .slab--process, .slab--local, .showcase { display: none; }
}

/* ------------------------------------------------------------ The showcase
   Phones. A heading, a phone and a card, set like the rest of the page:
   on the linen, the heading in the display serif with its second line in
   the accent italic (the way the hero sets "business"), and the copy on a
   sand card with the cards' own corners and shadows.

   The phone stands at the right. "Mobile" sits at the left, its end just
   behind the phone's edge; "Responsiveness", too long to sit beside it,
   runs across the phone's lower half, in front of it: the phone's shader
   paints the part over the phone white (ui.js, phoneDrawer). The card
   comes last and its top edge is tucked under both, so the heading and
   the phone lie a little over it.

   This is where everything ends up; ui.js (showcase) brings the three in
   as the section comes up the screen, scrubbed by the scroll. Without it
   (no script, or reduced motion) this is simply the section.

   The footage is a phone on white; the video carries its own cut-out
   (tools/build-phone-video.py) and ui.js draws the phone through it onto
   a canvas, so the phone stands on the page with nothing round it.

   THE KNOBS
     --phone-w      the phone's width (its height is 1.348 of it).
     --word-back    "Mobile"'s size.
     --word-front   "Responsiveness"'s size.
     --back-y / --front-y   where each word's top sits, as a share of the
                    phone's height from its top. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .showcase {
    --phone-w: 36vw;
    --phone-h: calc(var(--phone-w) * 1.348);
    --word-back: 17.5vw;
    --word-front: 11.6vw;
    --back-y: .12;
    --front-y: .55;
    position: relative;
    /* Its own layer under the features that follow (z-index 1). */
    z-index: 0;
    /* A screen of its own: at least the full height of the screen (with
       the address bar gone, the tallest it gets) plus 2rem at each end,
       with the heading, phone and card centred in it. Centred on the
       screen, neither the section above nor the one below shows, and not
       a lot more space than that. The padding is the floor on a short
       screen, and leaves room at the foot for the card's long shadow,
       which the features after it (a layer above) would cut off along a
       line. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: calc(100lvh + 4rem);
    padding: 4rem 0 4.5rem;
  }
  .showcase__stage {
    position: relative;
    /* The words come in from far off the right edge. */
    overflow-x: clip;
  }
  /* Held (ui.js, showcase, which measures --stage-h and --pin): the stage
     is sticky, dead centre on the screen, for --pin of scroll while its
     entrance plays on the scroll; the ::after is that length of page
     under it. --gap either side is the same room as unheld: centred, the
     sections above and below are just out of sight. */
  .showcase.is-pinned {
    --gap: max(4rem, calc((100lvh - var(--stage-h, 600px)) / 2 + 2rem));
    display: block;
    min-height: 0;
    padding: var(--gap) 0;
  }
  .showcase.is-pinned .showcase__stage {
    position: sticky;
    top: calc(50svh - var(--stage-h, 600px) / 2);
  }
  .showcase.is-pinned::after {
    content: "";
    display: block;
    height: var(--pin, 80svh);
  }
  .showcase__phone {
    position: relative;
    z-index: 2;
    width: var(--phone-w);
    aspect-ratio: 368 / 496;
    margin-left: auto;
    margin-right: calc(var(--gutter) * .45);
  }
  .showcase__float { position: absolute; inset: 0; }
  /* Standing on the card, not floating over the page: a soft contact
     shadow under its foot, still. */
  .showcase__phone::after {
    content: "";
    position: absolute;
    left: 16%;
    right: 12%;
    top: 97%;
    height: 7%;
    border-radius: 50%;
    background: radial-gradient(closest-side, rgba(40, 24, 8, .34), rgba(40, 24, 8, 0));
    filter: blur(3px);
    z-index: -1;
  }
  .showcase__canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }
  /* The video is only the source the canvas is drawn from: kept in the
     page (a phone will not play a video that is not) but out of sight. */
  .showcase__video {
    position: absolute;
    left: 0;
    top: 0;
    width: 2px;
    height: 2px;
    opacity: 0;
    pointer-events: none;
  }
  /* No WebGL: the video itself, its top half (the picture), multiplied
     onto the linen, and "Responsiveness" simply in front. */
  .showcase.is-plain .showcase__canvas { display: none; }
  .showcase.is-plain .showcase__word--front { z-index: 3; }
  .showcase.is-plain .showcase__float { overflow: hidden; }
  .showcase.is-plain .showcase__video {
    width: 100%;
    height: 200%;
    opacity: 1;
    mix-blend-mode: multiply;
  }

  /* The heading. Both words are under the phone (z-index 1 to its 2), and
     "Responsiveness", which runs across it, is redrawn over it in white by
     the phone itself. "Mobile" carries the accent and the italic together,
     as every heading's key word on the page does; "Responsiveness" is ink,
     so where it crosses the phone it turns cleanly from black to white. */
  .showcase__word {
    position: absolute;
    left: var(--gutter);
    z-index: 1;
    margin: 0;
    font-family: var(--display);
    font-weight: 400;
    line-height: 1;
    letter-spacing: -.028em;
    white-space: nowrap;
    color: var(--ink);
    pointer-events: none;
    will-change: transform;
  }
  .showcase__word--back {
    top: calc(var(--phone-h) * var(--back-y));
    font-size: var(--word-back);
    font-style: italic;
    color: var(--accent);
  }
  .showcase__word--front {
    top: calc(var(--phone-h) * var(--front-y));
    font-size: var(--word-front);
  }
  .showcase__word--front em { font-style: normal; }

  /* The card. Under the heading and the phone, its top tucked beneath
     them; the top padding keeps the first line clear of what lies over
     it. */
  .showcase__copy {
    position: relative;
    z-index: 0;
    margin: -1.6rem var(--gutter) 0;
    padding: 2.6rem 1.45rem 0;
    overflow: hidden;
    border-radius: var(--r-xl);
    background: var(--sand);
    box-shadow:
      0 2px 6px rgba(40, 24, 8, .08),
      0 34px 60px -34px rgba(40, 24, 8, .42);
  }
  .showcase__q { margin: 0; color: var(--ink-2); }
  /* Each say opens with a line in the serif: the question the larger, as
     the card's lead. */
  .showcase__say .showcase__q:first-child {
    font-family: var(--display);
    font-size: 1.28rem;
    line-height: 1.25;
    letter-spacing: -.01em;
    color: var(--ink);
    text-wrap: balance;
  }
  .showcase__say:first-child .showcase__q:first-child { font-size: 1.5rem; line-height: 1.2; }
  /* The figure, as a heading's key word: the accent italic, and a size up. */
  .showcase__q em {
    font-style: italic;
    font-size: 1.28em;
    line-height: 1;
    color: var(--accent);
  }
  /* "(That is 8 in every 10 customers.)": the figure, set as a note in
     the section labels' mono. */
  .showcase__say:first-child .showcase__q + .showcase__q {
    margin-top: .75rem;
    font-family: var(--mono);
    font-size: max(12px, .8rem);
    letter-spacing: .04em;
    color: var(--ink-3);
  }
  /* The tally (ui.js, TALLY): eight people and two laptops, drawn as
     outlines, standing on one line. "8 in every 10", drawn, under the words
     that say it. Each person comes on as --lit goes from 0 to 1 (ui.js
     sets it, once its light has landed): filled in terracotta, its edge
     warming with it, and a glow of the lit accent behind it. The laptops
     stay dark. */
  .showcase__tally {
    display: flex;
    align-items: flex-end;
    gap: 7px;
    margin: 1rem 0 0;
  }
  /* --s, --ty and --glow: the lighting (ui.js), each person growing
     towards the camera with a glow round it, then slamming back down. */
  .showcase__tally i {
    --lit: 0;
    --glow: 0;
    position: relative;
    display: block;
    transform: translateY(var(--ty, 0)) scale(var(--s, 1));
    transform-origin: 50% 100%;
  }
  .showcase__tally svg { display: block; width: 100%; height: 100%; overflow: visible; }
  .showcase__tally i.is-body { width: 13px; height: 22px; }
  .showcase__tally i.is-bust { width: 18px; height: 19px; }
  .showcase__tally i.is-laptop { width: 26px; height: 17px; margin-left: 2px; }
  .showcase__tally .t-fill {
    fill: rgb(194 82 31 / var(--lit));
    stroke: color-mix(in srgb, rgba(20, 17, 13, .34), rgba(120, 44, 12, .55) calc(var(--lit) * 100%));
    stroke-width: 1.3;
    stroke-linejoin: round;
  }
  .showcase__tally i.is-one svg {
    filter: drop-shadow(0 0 calc(var(--glow) * 7px) rgb(255 170 110 / calc(var(--glow) * .9)));
  }
  .showcase__tally .t-lid { fill: none; stroke: rgba(20, 17, 13, .34); stroke-width: 1.3; }
  .showcase__tally .t-screen { fill: rgba(20, 17, 13, .08); }
  .showcase__tally .t-deck { fill: rgba(20, 17, 13, .34); }
  .showcase__tally .t-notch { stroke: var(--linen); stroke-width: .9; stroke-linecap: round; }
  /* The answer: the card's lower half, a tone deeper, run to its edges,
     so the card is a question and its answer rather than one block of
     type. Its line in the serif, then the copy in the body face like
     every other card's. */
  .showcase__say--next {
    margin: 1.6rem -1.45rem 0;
    padding: 1.45rem 1.45rem 1.6rem;
    background: var(--sand-2);
  }
  .showcase__say--next .showcase__q + .showcase__q {
    margin-top: .7rem;
    text-wrap: pretty;
    font-size: 1.02rem;
    line-height: 1.6;
  }
}

/* ------------------------------------------------ Foldables and tablets
   The phone layout on a screen from 600px wide (a foldable open, a tablet
   upright). The cards are three quarters of their phone width and stand
   in from the walls rather than running off them; the showcase's phone
   and words are smaller, and its card no wider than it reads well. */
@media (min-width: 600px) and (max-width: 819px) {
  .slab { --slab-w: 64.5vw; }
  .slab--studio, .slab--process { --slab-x: 22vw; }
  .slab--local { --slab-x: 13.5vw; }
  .showcase { --phone-w: 27vw; --word-back: 13vw; --word-front: 8.6vw; }
}

/* ---------------------------------------------------------------- The egg
   Phones. Ten taps on the showcase phone (ui.js): a black dot opens from
   the middle of the phone and takes the whole screen, the golden egg drops
   in, and the message comes up above it, all centred. The close button, a
   tap anywhere or Escape puts it all away again. */
.egg[hidden] { display: none; }
.egg {
  position: fixed;
  inset: 0;
  z-index: 200;
}
/* The black: a full-screen layer cut to a circle whose radius grows
   (ui.js animates the clip), so its edge is drawn sharp at every size. A
   small dot blown up with a transform was upscaled, and went jagged. */
.egg__dot {
  position: fixed;
  inset: 0;
  background: #000;
  clip-path: circle(0px at 50% 50%);
}
.egg__body {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2rem var(--gutter);
  text-align: center;
  color: var(--paper);
  pointer-events: none;
}
.egg__h {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(2.4rem, 11vw, 3.4rem);
  line-height: 1.05;
  letter-spacing: -.02em;
}
.egg__p {
  margin: .6rem 0 2rem;
  font-size: 1.05rem;
  color: rgba(255, 255, 255, .78);
}
.egg__img {
  display: block;
  width: min(52vw, 15rem);
  height: auto;
  filter: drop-shadow(0 24px 40px rgba(226, 160, 60, .28));
}
.egg__close {
  position: absolute;
  top: max(1rem, env(safe-area-inset-top));
  right: 1rem;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, .1);
  color: var(--paper);
  cursor: pointer;
}
.egg__close svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) { .egg { display: none; } }

/* -------------------------------------------------------------- The window
   Phones. A tall gap in the page between the features and the price: the
   photograph (.window__img) is fixed to the screen, and the gap is the
   only place it shows, so the page reads as scrolling past a picture that
   stays put behind it.

   Not background-attachment: fixed, which iPhones ignore. The picture is
   position: fixed inside the gap, and the gap's clip-path (which, unlike
   overflow, does clip fixed children) cuts it down to the gap's own box.
   Nothing above it in the page may have a transform or a filter, or the
   picture would be fixed to that instead of to the screen.

   ui.js (windowNear) only puts the picture in the page while the gap is
   within a screen of being seen, so it is not a full-screen layer sitting
   under everything else, and it is not downloaded until then. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .window {
    /* The gap: a whole screen. The picture is fixed to the screen, so on
       the way through it is found as the gap travels, framed by the linen
       above and below, and for one moment, the gap exactly filling the
       screen, it is the whole screen: the three businesses as the one
       composition they were made as, and the page's one full-bleed pause
       between the cards before it and the features after. */
    --window-h: 100svh;
    position: relative;
    height: 100vh;
    height: var(--window-h);
    /* Tucked a pixel under the linen either side, so no seam between them
       can open at a fractional position. */
    margin-block: -1px;
    background: var(--sand-2);           /* until the picture has arrived */
  }
  /* The cut-out is its own box inside the navy one, not the window
     itself. A clip-path's edge is smoothed against whatever is behind it:
     with the window clipping itself, that was the page's linen, and on a
     phone's screen it showed as a hard light line along each edge. Now it
     is the window's own umber. */
  .window__clip {
    position: absolute;
    inset: 0;
    clip-path: inset(0);
  }
  /* The depth: the linen above and below is the thickness of a wall, and
     the picture is behind it. Each edge casts a shadow into the gap, a
     tight dark one at the lip and a long soft one. No lit line along the
     lips: on the phone it read as a hard white rule. Over the picture (a
     layer above it), not on the gap's own background, which the picture
     covers. */
  .window::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    box-shadow:
      inset 0 10px 10px -6px rgba(40, 24, 8, .45),
      inset 0 34px 44px -26px rgba(40, 24, 8, .32),
      inset 0 -10px 10px -6px rgba(40, 24, 8, .4),
      inset 0 -30px 40px -26px rgba(40, 24, 8, .26);
  }
  .window__img {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100vh;
    height: 100lvh;
    object-fit: cover;
    object-position: 50% 50%;
    pointer-events: none;
  }
  .js .window__img { display: none; }
  .js .window.is-near .window__img { display: block; }
  /* The film (ui.js, windowFilm): drawn at its frames' own size and
     fitted like the still. Hidden until its first frame is in, and then
     the still under it is left out. */
  .window__film { visibility: hidden; }
  .window.has-film .window__film { visibility: visible; }
  .window.has-film img.window__img { visibility: hidden; }

  /* Room on the linen after the features before the gap opens: about
     100px under the icons' labels. The section's full-screen minimum height is
     let go here, or it, not this padding, decides the gap. */
  .features { padding-bottom: 14rem; }
  .features.features--wheel { min-height: 0; }

  /* The caption. One quiet line in the gap, set like a magazine's photo
     caption rather than a banner: the mono label, and a sentence in the
     display serif at a modest size, left-aligned in the page's gutter, its
     top two thirds of the way down the gap. It is part of the gap and travels
     with it, so the picture (which stays put) slides past behind the words
     and gives them depth; the lines come in from the right, one after
     another (ui.js, windowCap). Behind the words, a soft band of dark that fades out
     above and below them (CAP_PAD of it either side), so they hold on the
     pale florist's flowers as well as the dark wall while the rest of the
     picture stays clear. */
  .window__cap {
    --cap-pad: 4.5rem;
    position: absolute;
    left: 0;
    right: 0;
    top: calc(66% - var(--cap-pad));
    z-index: 2;
    padding: var(--cap-pad) var(--gutter);
    background: linear-gradient(to bottom,
      rgba(9, 9, 9, 0) 0%,
      rgba(9, 9, 9, .55) 32%,
      rgba(9, 9, 9, .55) 68%,
      rgba(9, 9, 9, 0) 100%);
    pointer-events: none;
  }
  /* Its lines come in from off the right-hand edge (ui.js, windowCap):
     cut at the screen's sides so they never widen the page. */
  .window__cap { overflow-x: clip; }
  .window__h .cap-line { display: block; white-space: nowrap; will-change: transform; }
  .window__cap .eyebrow {
    margin-bottom: .85rem;
    color: rgba(255, 255, 255, .78);
    letter-spacing: .22em;
    font-size: max(12px, .72rem);
  }
  /* A short lit rule ahead of the label, drawn in as it arrives (ui.js
     sets --rule on every eyebrow). Inline, not a flex gap: the label's
     letters are each their own span (monoLabels), and a flex gap came
     between every one of them. */
  .window__cap .eyebrow::before {
    content: "";
    display: inline-block;
    vertical-align: middle;
    width: 1.5rem;
    height: 1px;
    margin: -.15em .75rem 0 0;
    background: var(--accent-lit);
    transform: scaleX(var(--rule, 1));
    transform-origin: left center;
  }
  .window__h {
    max-width: 20ch;
    margin: 0;
    font-family: var(--display);
    font-size: clamp(1.9rem, 8.4vw, 2.4rem);
    font-weight: 400;
    line-height: 1.06;
    letter-spacing: -.024em;
    color: var(--paper);
    text-wrap: balance;
    text-shadow: 0 1px 18px rgba(6, 14, 32, .35);
  }
  .window__h em { color: var(--accent-lit); }
}
@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) {
  .window { display: none; }
}
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {

  /* The features on the page's own linen, after the photograph, a
     section's space below it. Out of the stack (index.html), so they clip
     their own sides: a card swiped away flies off the edge. */
  .features {
    position: relative;
    z-index: 1;
    padding-top: 9rem;
    overflow-x: clip;
    color: var(--ink);
    background: var(--linen);
  }
  .features--wheel .deck-wrap { display: none; }

  /* ---------------------------------------------------- Why us
     What made this read as cheap was that nothing in it was set with any
     care: a stock orange label, a centred heading at the same size as every
     other, and no relationship between them. Now it is set like the hero:
     the heading larger and tighter, with one word in the accent italic the
     way the hero sets "business", and the label quiet and widely spaced
     between two hairlines, as a caption rather than a shout. */
  .features .eyebrow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .85rem;
    margin-bottom: 1.25rem;
    font-size: max(12px, .72rem);
    letter-spacing: .28em;
    text-indent: .28em;
    color: var(--ink-3);
  }
  .features .eyebrow::before,
  .features .eyebrow::after {
    content: "";
    width: 1.75rem;
    height: 1px;
    background: var(--accent);
    opacity: .8;
    /* Drawn outward from the label as it arrives (ui.js, --rule), not
       already there waiting under the photograph. */
    transform: scaleX(var(--rule, 1));
  }
  .features .eyebrow::before { transform-origin: right; }
  .features .eyebrow::after { transform-origin: left; }
  .features .display {
    font-size: clamp(2.6rem, 11.4vw, 3.4rem);
    font-weight: 400;
    line-height: 1.02;
    letter-spacing: -.028em;
    color: var(--ink);
  }
  .features .display em:not(.qm) { color: var(--accent); }

  /* The wheel, on linen: dark glass tiles (tools/build-feature-icons.py)
     on a soft warm shadow, ink labels, and the text box a pale sand cut
     out of the page with a hairline. Prefixed with .features so they win
     over the wheel's own rules further down.

     The shadow is cast by a box the shape of the drawing (which sits a
     few pixels inside its square at top and bottom), under the picture:
     on the tile itself a sliver of linen showed between the two. A plain
     box-shadow, not a filter: the top icon is repainted every frame while
     the gold trace runs round it, and a filter there made the wheel
     stutter. */
  .features .wheel__tile { overflow: visible; box-shadow: none; }
  .features .wheel__tile::before {
    content: "";
    position: absolute;
    top: 2.83px;
    right: 1.13px;
    bottom: 2.83px;
    left: 1.13px;
    border-radius: 11.6px;
    box-shadow:
      0 12px 20px -6px rgba(38, 28, 18, .34),
      0 2px 5px rgba(38, 28, 18, .2);
  }
  .features .wheel__tile img { position: relative; }
  /* Only the icon at the top is the dark tile; the rest are the light set
     (tools/build-feature-icons.py) on a lighter shadow, so the one being
     read is the single strong shape here rather than one of six. The
     light picture sits over the dark one and fades as its icon reaches the
     top, so a drag darkens each in turn as it passes the middle. */
  .features .wheel__tile .wheel__lite {
    position: absolute;
    inset: 0;
    transition: opacity .32s var(--ease);
  }
  .features .wheel__item.is-top .wheel__lite { opacity: 0; }
  /* The lit icon's show is drawn over both pictures, and a show that is
     stopped leaves its last frame on its canvas: off with the light. */
  .features .wheel__alive { transition: opacity .25s var(--ease); }
  .features .wheel__item:not(.is-lit) .wheel__alive { opacity: 0; }
  .features .wheel__tile::before {
    transition: box-shadow .32s var(--ease);
  }
  .features .wheel__item:not(.is-top) .wheel__tile::before {
    box-shadow:
      0 8px 14px -6px rgba(38, 28, 18, .16),
      0 1px 3px rgba(38, 28, 18, .1);
  }
  .features .wheel__label { color: var(--ink-3); }
  .features .wheel__item.is-top .wheel__label { color: var(--ink); }
  .features .wheel__item:focus-visible .wheel__tile { outline-color: var(--ink); }

  /* The card. The same object as the three cards up the page (.slab__card):
     the same corners, lying on the linen on the same two warm shadows, and
     it tilts the way it is pulled when swiped (wheel.js, the card).
     Straight while it is being read. In two of the page's own materials,
     each with a job (wheel.js, tone): umber for the two Pro-package
     features, as the Pro plan is; sand for the rest. */
  .features .wheel__panel { touch-action: pan-y; }
  .features .wheel__page {
    padding: 1.4rem 1.45rem 1.25rem;
    background: var(--sand);
    box-shadow:
      0 2px 6px rgba(40, 24, 8, .08),
      0 34px 60px -34px rgba(40, 24, 8, .42);
    -webkit-mask-image: none;
    mask-image: none;
    z-index: 1;
    cursor: grab;
    -webkit-user-select: none;
    user-select: none;
    will-change: transform;
  }
  /* The next card, under this one: its foot shows below, a tone down.
     Not on the last card, where there is nothing left. */
  .features .wheel__panel::before {
    content: "";
    position: absolute;
    left: calc(var(--gutter) + 1rem);
    right: calc(var(--gutter) + 1rem);
    bottom: -.8rem;
    height: 4rem;
    border-radius: var(--r-xl);
    /* The colour of the card to come (wheel.js sets --peek). */
    background: var(--peek, var(--sand-2));
    box-shadow:
      0 1px 3px rgba(40, 24, 8, .08),
      0 14px 22px -14px rgba(40, 24, 8, .34);
    transition: opacity .3s var(--ease), transform .3s var(--ease), background-color .3s var(--ease);
  }
  .features .wheel__panel.is-end::before { opacity: 0; transform: translateY(-.5rem); }
  /* While a card is dragged the real card it would go to is underneath
     (wheel.js, the card), so the edge drawn for it at rest steps aside. */
  .features .wheel__panel.is-dragging::before { opacity: 0; transition: none; }
  /* Where the card is in the six: the section labels' mono. */
  .features .wheel__n {
    margin: 0 0 .8rem;
    font-family: var(--mono);
    font-size: max(12px, .78rem);
    letter-spacing: .12em;
    color: var(--ink-3);
  }
  .features .wheel__n b { font-weight: 400; color: var(--accent-ink); }
  /* The edge of the next card is part of the card: nothing shows until
     the card itself has faded up at the end of the intro (wheel.js,
     wheel--bare). Showing on its own, it was an empty rounded box sitting
     there before the icons had even arrived. */
  .features .wheel--bare .wheel__panel::before { opacity: 0; transform: translateY(-.5rem); }
  /* The icon being read stands a size up, from its foot. */
  .features .wheel__item.is-top .wheel__tile { scale: 1.3; }
  .features .wheel__title { color: var(--ink); }
  .features .wheel__kicker { color: var(--ink-3); }
  .features .wheel__body { color: var(--ink-2); }
  .features .wheel__more { color: var(--accent-ink); }
  .features .wheel__sash { box-shadow: 0 3px 8px rgba(60, 30, 10, .3); }

  /* Umber, the Pro plan's material (site.css, "The top plan"). Measured
     at #1d1d1d, its lightest point: white 75% 9.20:1, 62% 6.76:1,
     --accent-lit-ink 6.31:1. */
  .features .wheel__page--umber {
    background: radial-gradient(118% 88% at 50% 26%, #1d1d1d 0%, #121212 50%, #090909 100%);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, .08),
      0 2px 6px rgba(40, 24, 8, .12),
      0 34px 60px -34px rgba(40, 24, 8, .55);
  }
  .features .wheel__page--umber .wheel__title { color: var(--ivory); }
  .features .wheel__page--umber .wheel__body { color: var(--on-navy); }
  .features .wheel__page--umber :is(.wheel__kicker, .wheel__n) { color: var(--on-navy-2); }
  .features .wheel__page--umber :is(.wheel__n b, .wheel__more) { color: var(--accent-lit-ink); }
}

/* ------------------------------------------------------------- The strip
   Laid out by assets/js/wheel.js: the icons sit in a row on a track that
   is moved sideways by a transform, so moving it never touches layout.
   Each icon's own transform (its pop, and its tip on arrival) pivots on
   its foot; the icon being read stands up a size from its foot too, so
   every label stays on one line and only the tiles change height.

   The icons come first and the card sits under them. The strip fades out
   at its right edge, where there is always more to come, and only a hair
   at its left, where Design sits at rest.

   touch-action: pan-y hands vertical swipes to the browser as a page
   scroll and keeps sideways ones for the strip. */
.wheel[hidden] { display: none; }
.wheel {
  display: flex;
  flex-direction: column;
  margin-top: 1.9rem;
}

.wheel__ring {
  position: relative;
  z-index: 2;
  order: 1;
  /* Room above the tiles for the one being read to stand taller. */
  padding: 26px 0 4px;
  overflow: hidden;
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 10px, #000 calc(100% - 52px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 10px, #000 calc(100% - 52px), transparent 100%);
}
.wheel__track {
  position: relative;
  display: flex;
  /* Each icon from its top: the tiles are all one size, so their feet
     line up, and a two-line label simply hangs lower than a one-line one.
     Aligned by the foot of the whole icon, the tiles over a one-line
     label sat lower than the rest. */
  align-items: flex-start;
  width: max-content;
  padding: 0 var(--gutter);
  will-change: transform;
}

/* Before the icons arrive (wheel.js): not there at all. */
.wheel--pending .wheel__item { visibility: hidden; }

.wheel__item {
  position: relative;
  flex: none;
  width: 90px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .55rem;
  transform-origin: 50% 100%;
  cursor: grab;
  -webkit-tap-highlight-color: transparent;
}
.wheel__tile {
  position: relative;
  display: block;
  width: 68px;
  height: 68px;
  border-radius: 17px;
  overflow: hidden;
  transform-origin: 50% 100%;
  transition: scale .5s cubic-bezier(.34, 1.45, .64, 1);
  box-shadow:
    0 12px 22px rgba(3, 10, 26, .42),
    0 2px 5px rgba(3, 10, 26, .32);
}
.wheel__tile img {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  -webkit-user-drag: none;
}
/* A corner ribbon on the two Pro-package tiles: a strip laid at 45deg
   across the top right, cut off by the icon's own drawn edge. The drawing
   sits a little inside its square image (2.8px in at top and bottom, 1.1px
   at the sides, corners of 11.6px), so the clip is the drawing's outline,
   not the tile's: clipped to the tile, the ribbon ran on past the drawing's
   top edge and over its shoulder. */
.wheel__corner {
  position: absolute;
  top: 2.83px;
  right: 1.13px;
  bottom: 2.83px;
  left: 1.13px;
  border-radius: 11.6px;
  overflow: hidden;
  pointer-events: none;
}
.wheel__ribbon {
  position: absolute;
  top: 6.5px;
  right: -20px;
  width: 72px;
  padding: 2px 0 1px;
  transform: rotate(45deg);
  background: linear-gradient(to bottom, #d8652c, #b04a1c);
  box-shadow: 0 1px 3px rgba(60, 20, 5, .45);
  color: #fff;
  font-family: var(--sans);
  font-size: .5625rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: .14em;
  text-indent: .14em;
  text-transform: uppercase;
  text-align: center;
}

/* The trace (wheel.js, which runs it): a thread of gold light running round
   the edge of the icon at the top, clockwise from its top-left corner.
   Laid exactly over the tile, not in it, so its glow is not clipped. */
.wheel__trace {
  position: absolute;
  top: 0;
  left: 50%;
  width: 68px;
  height: 68px;
  margin-left: -34px;
  overflow: visible;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s var(--ease);
  /* White-gold, as the light round the gold plan. */
  filter: drop-shadow(0 0 1.5px rgba(255, 244, 205, .85)) drop-shadow(0 0 3.5px rgba(255, 226, 150, .45));
}
.wheel__trace rect {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
}
.wheel__item.is-lit .wheel__trace { opacity: 1; }
/* The lit icon's show (wheel.js, LIVE): inside the tile, exactly over its
   picture and under the Pro ribbon. */
.wheel__alive {
  position: absolute;
  top: 0;
  left: 50%;
  width: 68px;
  height: 68px;
  margin-left: -34px;
  pointer-events: none;
}

.wheel__label {
  display: block;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: .01em;
  text-align: center;
  color: var(--on-navy-2);
  transition: color .18s var(--ease);
}
.wheel__item.is-top .wheel__label { color: var(--paper); }
.wheel__item:focus-visible { outline: none; }
.wheel__item:focus-visible .wheel__tile { outline: 2px solid var(--paper); outline-offset: 4px; }

/* The text. Every page is laid over the same spot and the panel is held at
   the height of the tallest, so changing feature never moves the page. */
.wheel__panel { position: relative; order: 2; margin-top: 1.1rem; }
/* Every box fills the reserved height, so the wheel always overlaps the
   same bottom edge; the shorter intro sits centred in the space above it. */
.wheel__page {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--gutter);
  right: var(--gutter);
  overflow: hidden;
  /* The bottom padding is the gap between the last line and the icons,
     plus the part of the box the wheel sits across: 5.75rem of that is the
     overlap and 1.7rem is one line of body copy (1.0625rem x 1.6) of air. */
  padding: 1.6rem 1.45rem 7.45rem;
  border-radius: var(--r-xl);
  background: rgba(255, 255, 255, .045);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .09);
  /* The box has no foot: its fill and both side borders fade away down
     through the bottom padding, which is the part the wheel sits across.
     The fade starts below the last line of text, so no words are dimmed. */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 5.5rem), transparent calc(100% - .5rem));
  mask-image: linear-gradient(to bottom, #000 calc(100% - 5.5rem), transparent calc(100% - .5rem));
}
/* The page's own scroll anchoring must not move the page when the panel
   is re-measured. */
.features:has(.wheel), .pricing { overflow-anchor: none; }
/* The sash: the ribbon again, larger, over the box's right shoulder. The
   title stops short of it. */
.wheel__sash {
  position: absolute;
  top: 20px;
  right: -40px;
  width: 150px;
  padding: 5px 0 4px;
  transform: rotate(45deg);
  background: linear-gradient(to bottom, #de6a30, #b04a1c);
  box-shadow: 0 3px 8px rgba(3, 10, 26, .45);
  color: #fff;
  font-family: var(--sans);
  font-size: max(12px, .75rem);
  font-weight: 700;
  letter-spacing: .2em;
  text-indent: .2em;
  text-transform: uppercase;
  text-align: center;
}
.wheel__page--pro .wheel__title { padding-right: 3.25rem; }
@keyframes wheel-nudge {
  0%, 45%, 100% { transform: translateY(0); }
  15% { transform: translateY(6px); }
  30% { transform: translateY(1px); }
  38% { transform: translateY(4px); }
}
.wheel__title {
  margin: 0 0 .55rem;
  font-family: var(--display);
  font-size: 1.6rem;
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -.01em;
  color: var(--paper);
}
.wheel__kicker {
  margin: 0 0 .7rem;
  font-family: var(--mono);
  font-size: max(12px, .8rem);
  letter-spacing: .04em;
  color: var(--on-navy-2);
}
.wheel__body { margin: 0; font-size: 1.0625rem; line-height: 1.6; color: var(--on-navy); }
.wheel__more,
.card__more {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  margin-top: .35rem;
  padding: .55rem 0;
  font-size: max(12px, .92rem);
  font-weight: 600;
  text-decoration: none;
}
.wheel__more { color: var(--accent-lit-ink); }
.card__more { color: var(--accent-ink); }
.card--dark .card__more { color: var(--accent-lit-ink); }

/* ================================================================= PRICING
   The old version of this was a solid dark slab with an orange glow inside
   it — the only object on the site that looked like it came from a different
   website, which is what it was. This one is set the way the rest of the
   page is set: white, one enormous figure, a rule, and nothing else. The
   number is made unmissable by size, and by counting itself up on arrival,
   rather than by being put on a coloured card. */

.pricing { padding-block: 6.5rem; }

/* The panel is a two-column composition on anything wider than a phone: the
   figure on the left, what the money buys on the right. Stacked below that,
   because two 45%-wide columns on a 360px screen is two narrow columns
   rather than one clear statement. */
.price {
  display: grid;
  gap: 2.25rem;
  margin: 0 0 3.5rem;
  padding: 2.5rem 1.6rem 2.75rem;
}

.price__label {
  margin: 0 0 1rem;
  font-family: var(--mono);
  font-size: max(12px, .8125rem);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--accent-lit);
}

.price__fig {
  display: flex;
  align-items: flex-start;
  gap: .04em;
  margin: 0;
  font-family: var(--display);
  font-weight: 500;
  line-height: .88;
  letter-spacing: -.035em;
  font-variant-numeric: tabular-nums;
  font-size: clamp(4.5rem, 22vw, 8.5rem);
}
/* Raised to the cap line and set at a third of the size, the way a price is
   set in print. A currency symbol at the same size as the number is the
   detail that makes a figure look typed rather than composed. Same white as
   the figure: the size and position already set it apart. */
.price__cur {
  font-size: .3em;
  line-height: 1;
  margin-top: .34em;
  letter-spacing: 0;
}
.price__num { display: block; }

/* Drawn on by ui.js as the figure lands. Terracotta, and 2px rather than a
   hairline: it is the underline under the number, not a divider. */
.price__rule {
  height: 2px;
  width: 4.5rem;
  margin: 1.5rem 0 1.25rem;
  border-radius: 2px;
  background: var(--accent-lit);
  transform-origin: left;
}

.price__note {
  margin: 0;
  max-width: 26rem;
  font-size: 1rem;
  line-height: 1.55;
  text-wrap: pretty;
}

/* The right column. Separated by a rule rather than by a change of fill — a
   second surface inside a dark panel would make the panel look like two
   panels, and the vignette only works as one continuous field. */
.price__incl { padding-top: 1.75rem; border-top: 1px solid rgba(255, 255, 255, .16); }
.price__incl-h {
  margin: 0 0 1rem;
  font-family: var(--mono);
  font-size: max(12px, .8125rem);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--on-navy-2);
}

.plans__h {
  font-size: clamp(1.6rem, 7vw, 2.25rem);
  letter-spacing: -.035em;
}

.plans__foot {
  margin: 1.75rem 0 0;
  max-width: 34rem;
  font-size: 1rem;
  color: var(--ink-3);
  text-wrap: pretty;
}
/* "Ask us anything" is a call to action inside a sentence. Padding it out to
   44px would open a gap in the paragraph's leading, so the tap area is an
   invisible pseudo-element over the top instead. */
.plans__foot a { position: relative; font-weight: 600; white-space: nowrap; }
.plans__foot a::after { content: ""; position: absolute; inset: -13px -8px; }

/* The price screen's top edge (phones): the plans' arc, mirrored, so the
   dark screen comes up over the features as a curved sheet. Over the
   features (a layer above them). */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .pricing.pricing--stage { position: relative; z-index: 2; }
  .pricing--stage .price-run::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(100% - 1px);
    height: calc(13vw + 1px);
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath d='M100 10 A429.6 330.5 0 0 0 0 .9 V10 Z' fill='%2319140f'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
    transform: scaleX(-1);
    pointer-events: none;
  }
}

/* ------------------------------------------------------ The price stage
   Phones, with motion only: ui.js (priceStage) adds .pricing--stage, and
   without it the run and the stage are plain blocks.

   The run is a few screens of scroll and the stage is one umber screen
   held inside it by position:sticky, the same mechanism as the studio: no
   fixed-position pin to judder under a phone's address bar. The stage is
   deep umber, the colour of the photograph's wall just above, so the two
   run together with no seam and the screen simply stays dark.

   Inside it, the question and the price are two layers over the same
   centre: the question leaves to the left, and the price appears out of
   the empty screen where it was (ui.js). The price sits on
   .price-stage__lift, which carries the one-notch rise, inside
   .price-stage__fit, which is scaled down only if the price and the line
   under it would not fit the screen together. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  /* Gone on a phone with or without the stage: the question is label
     enough, and "one build price" said again what the figure already says. */
  .pricing .price-stage__q .eyebrow,
  .pricing .price__label { display: none; }
  .pricing.pricing--stage { padding-top: 0; }
  /* The padding: a stretch of plain umber under the stage once it has
     scrolled on, so "Then you pick a plan" and its chevron have room to
     breathe before the linen's curve comes up after them. */
  .pricing--stage .price-run { position: relative; background: var(--umber-deep); padding-bottom: 16svh; }
  .pricing--stage .price-stage {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100svh;
    overflow: hidden;
    background: var(--umber-deep);
    color: var(--paper);
  }
  /* The stage's light: umber, lit as if by a lamp a little below the
     middle of the first screen and darkening to deep umber at the foot,
     on a strip two screens wide. The swap slides the strip one
     screen left with the question (ui.js, priceStage), so the price
     arrives on its right half: the same light carrying on, falling away
     and getting darker all the way to the right-hand edge. */
  .pricing--stage .price-stage__bg {
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    width: 200%;
    height: 100%;
    background-color: var(--umber);
    background-image:
      linear-gradient(to right, rgba(9, 9, 9, 0) 45%, rgba(9, 9, 9, .55) 75%, rgba(9, 9, 9, .9) 100%),
      radial-gradient(45% 38% at 25% 58%, rgba(30, 30, 30, .95) 0%, rgba(30, 30, 30, 0) 100%),
      linear-gradient(to bottom, var(--umber) 0%, var(--umber) 36%, var(--umber-deep) 100%);
    will-change: transform;
    pointer-events: none;
  }
  .pricing--stage .price-stage__q,
  .pricing--stage .price-stage__box {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
  .pricing--stage .price-stage__q .display {
    font-size: clamp(2.6rem, 11.4vw, 3.4rem);
    font-weight: 400;
    line-height: 1.04;
    letter-spacing: -.028em;
    color: var(--ivory);
  }
  /* The figure in the same warm white as the question. */
  .pricing--stage .price__fig { color: var(--ivory); }
  .pricing--stage .price-stage__lift { width: 100%; }
  .pricing--stage .price-stage__fit {
    position: relative;
    transform: scale(var(--fit, 1));
  }
  /* "Complete build price", set as "After launch" is over the plans, in the
     dark screen's colours. */
  .pricing--stage .price-stage__eyebrow {
    display: flex;
    margin-bottom: 1.1rem;
    color: var(--on-navy);
  }
  .pricing--stage .price-stage__eyebrow::before,
  .pricing--stage .price-stage__eyebrow::after { background: var(--accent-lit); }
  /* On a dark screen a dark panel has no edge, so the price takes the
     features box's treatment instead: a faint lift of white and a hairline,
     the same material as the text boxes on the wheel just above. */
  .pricing--stage .price.panel--dark {
    margin: 0;
    background: rgba(255, 255, 255, .05);
    box-shadow:
      inset 0 0 0 1.5px rgba(255, 255, 255, .24),
      inset 0 1px 0 rgba(255, 255, 255, .2),
      0 24px 60px -28px rgba(0, 0, 0, .7);
  }
  .pricing--stage .price-stage__next {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    /* Well clear of the panel above and of the chevron below, so the
       line stands on its own rather than hanging off either. */
    padding-top: 4.6rem;
    padding-bottom: .6rem;
  }
  .pricing--stage .price-stage__next .price-stage__cue { margin-top: 1.9rem; }
  .pricing--stage .plans__h { color: var(--ivory); }
  .pricing--stage .price-stage__cue { display: block; }
  /* Empty until the stage fills it in. */
  .pricing--stage .price-stage__box { visibility: hidden; }

  /* The linen comes back over the umber on the same arc the dark arrived
     on, mirrored, rather than as a straight cut. */
  .pricing--stage .plans {
    --slant: 13vw;
    position: relative;
    z-index: 1;
    /* Enough that, with the heading and cards held in the middle of the
       screen, the dark stage above has gone off the top rather than
       leaving its last line peeking out under the header. */
    padding-top: 6rem;
    background: var(--linen);
  }
  .pricing--stage .plans::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(100% - 1px);
    height: calc(var(--slant) + 1px);
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath d='M100 10 A429.6 330.5 0 0 0 0 .9 V10 Z' fill='%23f6f3ee'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
    pointer-events: none;
  }
  .pricing--stage .plans .deck-wrap { margin-top: .25rem; }

  /* The cards are stepped through by swiping while the block is held
     (ui.js, plansHold), so the rail and counter have nothing left to say,
     and the foot line ends at its full stop. */
  .plans .deck-ui,
  .plans__foot a { display: none; }

  /* The foot line comes up under the pile once the $129 plan has landed
     (ui.js), centred under it like the heading above, and close enough to
     read as part of the same group. */
  .plans__foot {
    margin: -.5rem auto 0;
    text-align: center;
  }
}

/* Ad-hoc updates: a quiet price list under the plans, kept small. A
   hairline above, the mono label in the eyebrow's voice, a line on when
   it applies, then two rows: what it is on the left, the flat price in the
   serif on the right. */
.adhoc {
  max-width: 19rem;
  margin: 1.6rem auto 0;
  padding: 1.05rem 0 0;
  border-top: 1px solid var(--hair-2);
}
.adhoc__h {
  margin: 0 0 .4rem;
  font-family: var(--mono);
  font-size: max(12px, .72rem);
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.adhoc__note {
  margin: 0 0 .2rem;
  font-size: .84rem;
  line-height: 1.45;
  color: var(--ink-3);
  text-wrap: pretty;
}
.adhoc__list { margin: 0; }
.adhoc__row {
  display: flex;
  align-items: baseline;
  gap: .8rem;
  padding: .7rem 0;
}
.adhoc__row + .adhoc__row { border-top: 1px solid var(--hair); }
.adhoc__row dt {
  display: flex;
  flex-direction: column;
  gap: .2rem;
  margin: 0;
  font-size: .96rem;
  font-weight: 500;
  color: var(--ink);
}
.adhoc__row dt span {
  font-family: var(--mono);
  font-size: max(12px, .75rem);
  letter-spacing: .06em;
  color: var(--ink-3);
}
.adhoc__row dd {
  flex: 1;
  margin: 0;
  text-align: right;
  font-family: var(--display);
  font-size: 1.4rem;
  letter-spacing: -.03em;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: lining-nums tabular-nums;
}
.adhoc__cur {
  font-size: .55em;
  vertical-align: .55em;
  margin-right: .06em;
  color: var(--ink-3);
}
.adhoc__row dd em {
  display: block;
  margin-top: .25rem;
  font-family: var(--mono);
  font-style: normal;
  font-size: max(12px, .72rem);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
}
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .pricing--stage .adhoc { margin-top: .75rem; }
}

/* Set the way the page's other big moments are set: a quiet label between
   two short terracotta rules, then the heading large and light with one
   word in the accent italic, as "business" is in the hero and "Dactyl" is
   over the features. */
.plans__eyebrow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .85rem;
  margin: 0 0 .9rem;
  font-family: var(--mono);
  font-size: max(12px, .72rem);
  letter-spacing: .28em;
  text-indent: .28em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.plans__eyebrow::before,
.plans__eyebrow::after {
  content: "";
  width: 1.75rem;
  height: 1px;
  background: var(--accent);
}
/* The plan pills (ui.js, plansPile): phones only, over the fan from the
   start, one per plan in its card's colour. */
.plan-pills { display: none; }
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .plan-pills {
    display: flex;
    justify-content: center;
    gap: .55rem;
    /* A line of space above (under "Monthly plans") and below (over the
       cards). */
    padding: 2.1rem var(--gutter) .85rem;
    box-sizing: content-box;
  }
  .plan-pill {
    flex: 0 1 6.6rem;
    display: grid;
    gap: .05rem;
    justify-items: center;
    min-height: 48px;
    padding: calc(.45rem - 1px) calc(.8rem - 1px) calc(.5rem - 1px);
    /* A real border, clear until chosen, so the chosen pill's terracotta
       line is the pill's own edge. The fill runs under it (the default
       border-box clip), so a clear one looks exactly like none. */
    border: 1px solid transparent;
    border-radius: 10px;
    background: var(--sand);
    color: var(--ink);
    font: inherit;
    line-height: 1.1;
    cursor: pointer;
    box-shadow: 0 6px 16px -10px rgba(40, 24, 8, .45), inset 0 0 0 1px rgba(20, 17, 13, .06);
    transition: box-shadow .3s var(--ease), border-color .3s var(--ease), transform .3s var(--glide);
    -webkit-tap-highlight-color: transparent;
  }
  .plan-pill b {
    font-family: var(--display);
    font-size: 1.02rem;
    font-weight: 500;
    letter-spacing: -.01em;
  }
  .plan-pill span {
    font-size: max(12px, .7rem);
    font-weight: 500;
    letter-spacing: .02em;
    color: var(--ink-3);
  }
  .plan-pill--deep { background: var(--sand-3); }
  .plan-pill--deep span { color: var(--ink-2); }
  .plan-pill--ember {
    background: radial-gradient(118% 88% at 50% 26%, #1d1d1d 0%, #121212 50%, #090909 100%);
    color: var(--paper);
  }
  .plan-pill--ember span { color: var(--on-navy); }
  /* The plan on top: a hairline of terracotta that is the pill's edge
     (its border, flush with the outside of the fill; an inset ring sat
     just inside it), a soft breath of the same colour beyond it, blurred
     rather than a band so it never reads as a second edge, and a slightly
     deeper shadow, so it sits a touch higher than the other two. */
  .plan-pill.is-on {
    border-color: rgba(194, 82, 31, .8);
    box-shadow:
      0 10px 20px -12px rgba(40, 24, 8, .5),
      0 0 8px rgba(194, 82, 31, .16);
  }
  .plan-pill:active { transform: scale(.96); }
  .plan-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
}

/* Only the phone's price stage shows it (below). */
.price-stage__eyebrow { display: none; }
.price-stage__bg { display: none; }
.plans__title {
  margin: 0;
  font-size: clamp(2.5rem, 11vw, 3.25rem);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -.028em;
}
.plans__title em {
  font-style: italic;
  font-weight: 400;
  color: var(--accent);
}
/* A phone heading: from 820px "Then you pick a plan" sits right above the
   cards and says the same thing. */
@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) {
  .plans__head { display: none; }
}

/* The fan (phones, set by ui.js, plansPile): the three plans lie in one
   spot, and ui.js sets them as a straight stack, each card behind showing
   a sliver to the left of the one in front, deals them in by cartwheeling
   them from the right, and opens them into a row on the first swipe
   (touch-action keeps vertical swipes for the page). --fan-w is a card's
   width: wide, so a plan's list runs in long lines and the card is not a
   tall narrow strip; the stack's slivers and the row's neighbours both
   fit either side of it. No light round the gold card's edge here. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .volt { display: none; }
  /* The pile from the first paint, not from when ui.js adds deck--pile
     (~2s in): the row-to-pile switch at that point counted as a 0.95
     layout shift in PageSpeed. html.js is set in the head, so without
     JavaScript the plans stay a swipeable row. */
  .js #deckPlans,
  .deck.deck--pile {
    --fan-w: min(calc(100vw - 2 * var(--gutter) - 30px), 24rem);
    display: grid;
    justify-items: center;
    overflow: visible;
    scroll-snap-type: none;
    touch-action: pan-y;
    -webkit-user-select: none;
    user-select: none;
    padding-top: 1.5rem;
    padding-bottom: 2.75rem;
  }
  .js #deckPlans > *,
  .deck--pile > * {
    grid-area: 1 / 1;
    width: var(--fan-w);
    will-change: transform;
  }
}

/* The filter's own SVG takes no space. */
.fx-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* The caret the stage's two headings are typed behind. A zero-width box so
   it never pushes a letter along, with the bar drawn by its ::after. Solid
   while letters are arriving, blinking once they stop. It is a mark, so it
   takes the lit accent (3.76:1 at the vignette's brightest point). */
.type-caret {
  position: relative;
  display: inline-block;
  width: 0;
  height: .82em;
  vertical-align: -.06em;
}
.type-caret::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: .05em;
  width: max(2px, .055em);
  border-radius: 1px;
  background: var(--accent-lit);
  animation: caret-blink 1.1s steps(1) infinite;
}
/* The chevron under the stage's last line: there (so the line's block
   is measured at its full height) but invisible until the line has been
   typed, then it fades up and bobs like the one on the features wheel. */
.price-stage__cue {
  display: none;
  width: 22px;
  height: 22px;
  margin: 1.1rem auto 0;
  color: var(--accent-lit);
  opacity: 0;
  transition: opacity .6s var(--ease);
}
.price-stage__cue svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.price-stage__cue.is-on {
  opacity: 1;
  animation: wheel-nudge 1.8s var(--ease) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .price-stage__cue.is-on { animation: none; }
}

.type-caret.is-typing::after,
.type-caret.is-held::after { animation: none; }
.type-caret.is-off { display: none; }
/* The line is finished: the caret stops and fades away (ui.js, typer). */
.type-caret.is-done::after {
  animation: none;
  opacity: 0;
  transition: opacity .5s var(--ease);
}
@keyframes caret-blink { 50% { opacity: 0; } }

/* ----------------------------------------------------------------- Plans */

.deck--plans > * { flex-basis: min(84vw, 21rem); }
/* Room above and below the cards for their shadow and the top plan's rim
   of fire, which the deck's own overflow would otherwise cut off flat. */
.deck--plans { padding-top: 1.25rem; padding-bottom: 2.75rem; }

.plan {
  --plan-pad-x: 1.6rem;
  --plan-pad-y: 1.85rem;

  position: relative;
  display: flex;
  flex-direction: column;
  gap: .3rem;
  padding: var(--plan-pad-y) var(--plan-pad-x) 2.1rem;
  border-radius: var(--r-xl);
  background: var(--grey);
  /* A lit top edge and a soft warm shadow under it: just enough to make
     the cards objects on the linen rather than patches of a slightly
     different linen. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .55),
    0 22px 40px -26px rgba(64, 42, 18, .45);
  transition: transform .5s var(--glide), background-color .35s var(--ease);
}

/* Built like the price panel above it, so the plans read as the same
   price list continued rather than three widgets: the name small and
   spaced in the mono, the figure large in the serif with its dollar sign
   raised and small, and the same short terracotta rule under it. Then the
   one-line summary, a hairline, and what is included. */
.plan__n {
  position: absolute;
  top: var(--plan-pad-y);
  right: var(--plan-pad-x);
  font-family: var(--mono);
  font-size: max(12px, .75rem);
  letter-spacing: .1em;
  line-height: 1.6;
  color: var(--accent-ink);
}
/* Not on a phone: in the pile only the top card shows, so 01 / 02 / 03
   was numbering a sequence nobody could see. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .plan__n { display: none; }
}
.plan h4 {
  margin: 0;
  font-family: var(--mono);
  font-size: max(12px, .78rem);
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.plan__price {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: .3rem;
  margin: 1.1rem 0 1.15rem;
  padding-bottom: 1.15rem;
  font-family: var(--display);
  letter-spacing: -.035em;
  font-variant-numeric: tabular-nums;
}
.plan__price::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 2.5rem;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}
.plan__price b {
  font-size: 3.1rem;
  font-weight: 500;
  line-height: .9;
  letter-spacing: -.035em;
}
/* Raised by relative position rather than by flex alignment, so the
   figure keeps its own baseline and "/month" sits on it. */
.plan__cur {
  position: relative;
  top: -1.2em;
  margin-right: .08em;
  font-size: .36em;
  letter-spacing: 0;
}
.plan__price > span { font-size: .95rem; font-weight: 500; color: var(--ink-3); }
.plan .ticks {
  padding-top: 1.15rem;
  border-top: 1px solid var(--hair);
}
.plan__lede {
  margin: 0 0 1.15rem;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink-2);
  text-wrap: pretty;
}

.ticks { margin: 0; padding: 0; list-style: none; display: grid; gap: .6rem; }
.ticks li {
  position: relative;
  padding-left: 1.6rem;
  font-size: 1rem;
  line-height: 1.45;
  color: var(--ink-2);
}
/* Drawn with a border rather than an inline <svg> in every list item: the
   same tick twelve times over is markup nobody reading the HTML benefits
   from. */
.ticks li::before {
  content: "";
  position: absolute;
  left: .15rem; top: .42rem;
  width: .58rem; height: .32rem;
  border-left: 1.75px solid var(--accent);
  border-bottom: 1.75px solid var(--accent);
  transform: rotate(-45deg);
}

/* What a plan leaves out. Not a cross (a verdict, and louder than the
   ticks it follows) but an empty ring where the tick would be: a place
   not yet filled, which reads as "not on this plan yet" rather than
   "no". The words a step quieter and a touch smaller, and the group set
   apart from the included list by a dashed hairline, so the card reads
   as what you get, then quietly what the next plan adds. */
.ticks li.no { color: var(--ink-3); font-size: .94rem; }
.ticks li.no::before {
  left: .2rem; top: .47rem;
  width: .5rem; height: .5rem;
  border: 1.25px solid currentColor;
  border-radius: 50%;
  transform: none;
  opacity: .55;
}
.ticks li:not(.no) + li.no {
  margin-top: .45rem;
  padding-top: .95rem;
  border-top: 1px dashed var(--hair-2);
}
.ticks li:not(.no) + li.no::before { top: calc(.95rem + .47rem); }
.plan.plan--deep .ticks li.no { color: var(--ink-2); }

/* The three plans darken as they climb: sand, then two tones deeper, then
   navy. On the deeper sand the muted ink and the terracotta tick both fall
   short (3.90:1 and 2.84:1), so that card takes the next value up of each:
   --ink-2 at 6.35:1 and --accent-ink at 3.60:1. */
.plan.plan--deep { background: var(--sand-3); }
.plan.plan--deep .plan__price > span { color: var(--ink-2); }
.plan.plan--deep .ticks li::before { border-color: var(--accent-ink); }

/* ------------------------------------------------------- The top plan
   Warm near-black rather than navy: after a whole screen of navy, a third
   navy object read as more of the same. Lit by the same kind of vignette
   as every navy surface, but in umber, so it is the site's dark material
   in a warmer key rather than a new idea.

   Its edge is electric: a current drawn on a canvas by ui.js (volt), which
   also throws light onto the face of the card.

   The card is isolated and paints nothing itself: the surface is the
   ::after. Done that way because the deck puts transforms on its cards,
   and a card with a transform paints any negative-z layer on top of its
   own background; with the background moved into a layer too, the order
   is fixed either way.

   Measured against #1d1d1d, the lightest point of the umber vignette:
   white 75% 9.20:1, white 62% 6.76:1, --accent-lit 5.14:1,
   --accent-lit-ink 6.31:1. */
.plan.plan--ember {
  --ember: radial-gradient(118% 88% at 50% 26%, #1d1d1d 0%, #121212 50%, #090909 100%);
  --volt: #ff8a3d;
  --volt-lit: #ffbd80;
  isolation: isolate;
  background: none;
  box-shadow: 0 22px 44px -24px rgba(40, 20, 6, .6);
  color: var(--paper);
}
.plan.plan--ember::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  border: 1px solid rgba(255, 138, 61, .4);
  background: var(--ember) padding-box;
  pointer-events: none;
}
/* The light round the edge (ui.js, volt). The canvas overhangs the card by
   16px all round so the glow can spill over the edge. Screen, so on the
   black it is light added to the card, and white text under it stays
   white; the card is isolated, so off the card it simply draws as itself.
   It fades out while the card is under the others in the pile. */
.volt {
  position: absolute;
  inset: 0;
  pointer-events: none;
  transition: opacity .4s var(--ease);
}
.plan--ember.is-under .volt { opacity: 0; }
.volt__canvas {
  position: absolute;
  top: -16px;
  left: -16px;
  display: block;
  mix-blend-mode: screen;
}
.plan.plan--ember h4,
.plan.plan--ember .plan__price > span { color: var(--on-navy-2); }
.plan.plan--ember .plan__price b { color: var(--paper); }
.plan.plan--ember .plan__n { color: var(--accent-lit-ink); }
.plan.plan--ember .plan__price::after { background: var(--accent-lit); }
.plan.plan--ember p,
.plan.plan--ember .ticks li { color: var(--on-navy); }
.plan.plan--ember .ticks { border-top-color: rgba(255, 255, 255, .12); }
.plan.plan--ember .ticks li::before { border-color: var(--accent-lit); }
.plan.plan--ember :focus-visible { outline-color: var(--paper); }
.gild { display: none; }

/* ------------------------------------------------------- The gold plan
   The top plan in gold: polished, not flat yellow. Its face
   (.gild, under everything on the card) is built up the way light lies on
   a sheet of metal:
     - the metal itself, a long diagonal run from deep bronze through
       bright gold to pale, and back down, so it is never one colour;
     - long soft streaks of reflected light across it, the brightest a
       third of the way down on the left, and a shade falling off to the
       right, as in a blurred photograph of gold;
     - a brushed grain of fine horizontal lines, barely there;
     - a lit top edge and a darker foot, the thickness of the sheet;
     - and, every nine seconds, a glint sweeping slowly across it
       (.gild::before, moved by transform only, so it costs the phone
       nothing to draw).
   The words are a deep bronze-black, at least 4.5:1 against the darkest
   gold they sit on. The light running round the edge (ui.js, volt) goes
   white-gold on this card. Every width: the desktop's fan is gold too. */
@media all {
  .plan.plan--ember {
    color: #1d1405;
    box-shadow:
      0 26px 46px -22px rgba(96, 62, 8, .55),
      0 4px 10px -4px rgba(96, 62, 8, .3);
  }
  .plan.plan--ember::after {
    background: none;
    border-color: rgba(255, 238, 186, .75);
  }
  .gild {
    display: block;
    position: absolute;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    border-radius: inherit;
    pointer-events: none;
    background-color: #c9973c;
    background-image:
      repeating-linear-gradient(0deg, rgba(255, 255, 255, .035) 0 1px, rgba(255, 255, 255, 0) 1px 3px),
      radial-gradient(95% 15% at 16% 33%, rgba(255, 252, 214, .92) 0%, rgba(255, 244, 170, 0) 72%),
      radial-gradient(85% 11% at 72% 15%, rgba(255, 236, 160, .6) 0%, rgba(255, 236, 160, 0) 72%),
      radial-gradient(75% 10% at 62% 76%, rgba(255, 228, 150, .55) 0%, rgba(255, 228, 150, 0) 72%),
      radial-gradient(55% 55% at 104% 52%, rgba(110, 72, 20, .3) 0%, rgba(110, 72, 20, 0) 72%),
      linear-gradient(170deg, #bd8a2e 0%, #d9ab4a 20%, #f2d886 38%, #e8c466 50%, #cc9a3e 66%, #dcb059 82%, #b3812f 100%);
    box-shadow:
      inset 0 1.5px 0 rgba(255, 250, 222, .85),
      inset 0 -1.5px 0 rgba(110, 72, 20, .45),
      inset 0 0 0 1px rgba(255, 236, 180, .3);
  }
  /* The glint: not a band of white laid over the card but light caught
     by the metal. A wide, soft rise to a narrow bright core, with a
     fainter second reflection trailing it (as a polished sheet gives
     back two edges of the light), blended as overlay so it brightens the
     gold it crosses, warm, rather than whitening it. Slow across and
     long between passes. */
  .gild::before {
    content: "";
    position: absolute;
    top: -30%;
    bottom: -30%;
    left: -100%;
    width: 100%;
    background: linear-gradient(104deg,
      rgba(255, 248, 220, 0) 0%,
      rgba(255, 248, 220, .05) 26%,
      rgba(255, 246, 212, .16) 37%,
      rgba(255, 250, 230, .38) 43%,
      rgba(255, 253, 242, .56) 47%,
      rgba(255, 250, 230, .38) 51%,
      rgba(255, 246, 212, .14) 56%,
      rgba(255, 248, 220, .2) 60%,
      rgba(255, 248, 220, .06) 66%,
      rgba(255, 248, 220, 0) 80%);
    mix-blend-mode: overlay;
    transform: translateX(0);
    animation: gild-sheen 9s cubic-bezier(.55, 0, .35, 1) 1.5s infinite;
    will-change: transform;
  }
  .plan.plan--ember h4,
  .plan.plan--ember .plan__price > span { color: #33230a; }
  .plan.plan--ember .plan__price b { color: #1a1103; }
  .plan.plan--ember .plan__price::after { background: #5a3a0c; }
  .plan.plan--ember p,
  .plan.plan--ember .ticks li { color: #22170a; }
  .plan.plan--ember .ticks { border-top-color: rgba(70, 44, 8, .25); }
  .plan.plan--ember .ticks li::before { border-color: #5a3a0c; }
  .plan.plan--ember :focus-visible { outline-color: #1a1103; }

  /* The same gold as the card's drawn face (ui.js, goldLeaf): the rich
     yellow gold lit from the top left, a soft band of the same studio
     light across it with a hot line at its heart, a lit top edge and a
     hairline rim. Still, not animated: it is a button. */
  .plan-pill--ember {
    /* One smooth run of the card's gold, no band: it peaks at the card's
       brightest tone (#f2d886) a third of the way along and eases down to
       the deeper gold, every stop close to its neighbours so there is no
       line anywhere across the pill. */
    background: linear-gradient(135deg,
      #d9ab4a 0%, #e6c063 22%, #f2d886 40%, #ebc971 55%,
      #dcb059 75%, #cc9a3e 100%);
    color: #1a1103;
    box-shadow:
      0 6px 16px -10px rgba(120, 92, 30, .55),
      inset 0 1px 0 rgba(255, 250, 225, .85),
      inset 0 0 0 1px rgba(255, 244, 205, .45);
  }
  .plan-pill--ember span { color: #3a2808; }
}
/* Phones: the face drawn instead (ui.js, goldLeaf), brushed metal under a
   moving light, over everything above; the glint goes with it. */
.gild__leaf {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.gild.has-leaf::before { display: none; }
@keyframes gild-sheen {
  0% { transform: translateX(0); }
  36%, 100% { transform: translateX(200%); }
}
@media (prefers-reduced-motion: reduce) {
  .gild::before { animation: none; opacity: 0; }
}

/* ===================================================================== CTA
   On the page's own linen, so it runs on from the plans: a big terracotta
   tab, square against the left wall and rounded on its right, stopping
   short of the right edge. The question in white, the name in a light
   italic, and one button to the form. ui.js (ctaStage) slides the tab out,
   types the question and uncovers the button on a phone. White on the
   tab's gradient runs from 3.6:1 at its light top-left to 6:1 at its deep
   end: fine for the large question only, so nothing small goes on it. */
.cta {
  display: grid;
  align-items: center;
  padding-block: 3.5rem 4rem;
  overflow: hidden;
}
/* Given depth like a real object: lit from the top left (a gradient from a
   lighter to a deeper terracotta, and a soft sheen), a bright top edge and
   a shaded bottom one, a slot shadow where it comes out of the wall on the
   left, and a close shadow plus a long soft one under it. */
.cta__tab {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin-right: var(--gutter);
  padding: 2.75rem 1.6rem 2.9rem var(--gutter);
  border-radius: 0 var(--r-xl) var(--r-xl) 0;
  background: linear-gradient(150deg, #d8672e 0%, #c2521f 46%, #a8441a 100%);
  color: var(--paper);
  box-shadow:
    inset 0 1px 0 rgba(255, 214, 186, .5),
    inset 0 -2px 0 rgba(90, 26, 4, .28),
    inset -1px 0 0 rgba(255, 196, 160, .22),
    inset 14px 0 16px -14px rgba(60, 14, 0, .45),
    0 1px 2px rgba(110, 36, 8, .25),
    0 14px 26px -12px rgba(120, 40, 8, .5),
    0 44px 80px -34px rgba(90, 28, 6, .55);
}
.cta__tab::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(110% 80% at 18% 0%, rgba(255, 236, 220, .2) 0%, rgba(255, 236, 220, 0) 60%);
  pointer-events: none;
}
.cta__q {
  margin: 0 0 2rem;
  font-size: clamp(2.2rem, 9.6vw, 3.6rem);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -.028em;
  color: var(--paper);
}
/* The name in the page's navy: the one other colour the site uses, and a
   change of hue rather than a paler orange. The deep navy, which holds at
   3.1:1 even against the darkest corner of the tab (5.2:1 at the light
   one): enough for type this large (3:1), not for small text. */
.cta__q em:not(.qm) { color: var(--navy-deep); }
.cta__q .qm { color: var(--paper); }
.cta__btn {
  min-width: 12.5rem;
  justify-content: center;
  box-shadow: 0 1px 1px rgba(60, 16, 2, .2), 0 12px 22px -10px rgba(60, 16, 2, .55);
}
/* Phones: room under the tab for all of its long shadow. The section clips
   (overflow: hidden, for the tab sliding in off the wall), and the shadow
   reaches about 90px below the tab (44px down, 80px of blur, pulled in 34):
   at 4rem it was cut off in a hard line along the top of the contact. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  /* More room above as well, so the tab has the screen to itself. */
  .cta { padding-block: 6rem 11rem; }
  /* The question and button sit in the middle of the tab rather than hard
     against the wall: the left padding matches the room the longest line
     leaves on its right. */
  .cta__tab {
    padding-left: calc(var(--gutter) + 2rem);
    /* Slides in and out with the scroll, on its own GPU layer kept for
       good: see ui.js, ctaStage. */
    will-change: transform;
  }
}
@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) {
  .cta__tab { margin-right: max(var(--gutter), calc(100% - 64rem)); padding: 4rem 3rem 4.25rem max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter))); }
}

/* ============================================================ FEATURES PAGE
   features.html: the six features in full. The same materials as the home
   page: sand cards, the two Pro-package features in navy, a mono label with
   the number, a serif title and the full text at a comfortable measure. */
.fpage__intro { padding: calc(var(--nav-h) + 3.5rem) 0 2.25rem; }
.fpage__title {
  margin: 0 auto;
  font-size: clamp(2.6rem, 11.4vw, 4.4rem);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -.028em;
}
.fpage__lead { margin: 1.25rem auto 0; max-width: 28rem; }
.fpage__list { padding: .5rem 0 2rem; }
.fpage__list .wrap { display: grid; gap: 1.1rem; max-width: 52rem; }
.feat {
  display: grid;
  gap: 1.15rem;
  padding: 1.6rem 1.5rem 1.8rem;
  border-radius: var(--r-xl);
  background: var(--sand);
  scroll-margin-top: calc(var(--nav-h) + 1rem);
}
.feat__icon {
  width: 72px;
  height: 72px;
  filter: drop-shadow(0 10px 14px rgba(40, 24, 8, .28));
}
.feat__n {
  display: flex;
  align-items: center;
  gap: .7rem;
  margin: 0 0 .55rem;
  font-family: var(--mono);
  font-size: max(12px, .75rem);
  letter-spacing: .18em;
  color: var(--accent-ink);
}
/* White on the terracotta: 4.6:1. */
.feat__pro {
  padding: .22rem .55rem .2rem;
  border-radius: 6px;
  background: var(--accent);
  color: #fff;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.feat__h {
  margin: 0 0 .6rem;
  font-family: var(--display);
  font-size: clamp(1.7rem, 7vw, 2.3rem);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -.025em;
}
.feat__kicker {
  margin: -.2rem 0 .7rem;
  font-family: var(--mono);
  font-size: max(12px, .8rem);
  color: var(--ink-3);
}
.feat__body { margin: 0; font-size: 1.0625rem; line-height: 1.65; color: var(--ink-2); text-wrap: pretty; }
.feat--pro { background-color: var(--navy); background-image: var(--vignette); color: var(--paper); }
.feat--pro .feat__h { color: var(--paper); }
.feat--pro .feat__body { color: var(--on-navy); }
.feat--pro .feat__n { color: var(--accent-lit-ink); }
@media (min-width: 700px) {
  .feat { grid-template-columns: 96px 1fr; gap: 1.75rem; padding: 2.25rem 2.25rem 2.4rem; }
  .feat__icon { width: 96px; height: 96px; }
}

/* ================================================================= CONTACT */

/* Set like the plans: a quiet label between two rules, the heading, then the
   invitation and the promise under it, all centred, and the form below. */
.contact { padding-block: 6.5rem; }
/* Phones: more room between the end of the form and the foot of the page,
   so the form finishes before the footer starts rather than running into
   it. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .contact { padding-bottom: 9rem; }
}
.contact__head { text-align: center; }
.contact__head .plans__eyebrow { margin-bottom: 1.1rem; }
/* The same size and weight as "Monthly plans". */
.contact__h {
  margin: 0 auto;
  font-size: clamp(2.5rem, 11vw, 3.25rem);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -.028em;
}
.contact__lead {
  margin: 1.35rem auto 0;
  max-width: 31rem;
  text-wrap: pretty;
}
/* The promise: its own line, in the heading's italic, marked with a small
   terracotta tick. */
.contact__promise {
  margin: 1.35rem auto 0;
  max-width: 31rem;
  font-family: var(--display);
  font-size: 1.2rem;
  font-style: italic;
  line-height: 1.35;
  color: var(--ink);
  text-wrap: balance;
}
.contact__promise svg {
  display: inline-block;
  width: 16px;
  height: 16px;
  margin-right: .45rem;
  vertical-align: -1px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.contact__direct {
  margin: 1.35rem 0 0;
  font-size: .95rem;
  color: var(--ink-3);
}
.contact__direct a {
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--hair-2);
  text-underline-offset: .2em;
}
.contact .finder { margin: 2.75rem auto 0; max-width: 44rem; }

.noscript {
  margin-top: 1.5rem;
  padding: 1rem 1.25rem;
  border-radius: var(--r-lg);
  background: var(--grey);
  font-size: 1rem;
}

/* ================================================== DACTYL SMART FORM THEME
   The form as part of the page rather than a widget dropped onto it: a
   sheet of paper on the linen, lifted by a soft, warm, layered shadow, made
   of the site's own pieces. A mono label for where you are, a serif
   question, navy for what you press, terracotta only as a mark (the
   progress and the required stars).

   It answers every touch at once: tiles and buttons sink a touch while
   pressed (160ms, ease-out, never ease-in); a chosen tile takes a navy edge
   and a tick that grows into its dot; a field being typed in goes to paper
   with a soft navy ring. Hover only where there is a real pointer. Fields
   are never under 16px, so a phone does not zoom into them.

   Nothing here reaches into smartform.css's mechanism: the steps, the
   buttons' hidden states and the review stay its job. */

[data-smartform] {
  --sf-ink: var(--ink);
  --sf-ink-faint: var(--ink-3);
  --sf-border: var(--hair-2);
  --sf-accent: var(--navy);
  --sf-accent-ink: var(--navy);
  --sf-tint: rgba(12, 35, 72, .04);
  --sf-white: #fff;
  --sf-error: #a3231c;
  --sf-radius: var(--r-xl);
  --sf-radius-sm: 14px;
  --sf-ease: cubic-bezier(.23, 1, .32, 1);

  max-width: 38rem;
  border: 0;
  background: var(--paper);
  box-shadow:
    0 0 0 1px rgba(20, 17, 13, .06),
    0 1px 2px rgba(40, 28, 14, .05),
    0 14px 34px -14px rgba(40, 28, 14, .2),
    0 44px 80px -50px rgba(40, 28, 14, .3);
}

/* Where you are: a thin bar in four segments and a mono count, on the paper
   itself. The navy slab it used to sit in was the heaviest thing on the
   page after the price screen, for a line of metadata. */
[data-smartform] .finder__progress {
  gap: 1rem;
  padding: 1.4rem 1.45rem 0;
  background: none;
  color: var(--ink-3);
}
[data-smartform] .finder__bar {
  height: 3px;
  border-radius: 0;
  background: var(--sand-2);
  /* One segment per question; --sf-q is set by smartform.js. */
  --sf-seg: calc(100% / var(--sf-q, 5));
  -webkit-mask: repeating-linear-gradient(90deg, #000 0 calc(var(--sf-seg) - 4px), transparent calc(var(--sf-seg) - 4px) var(--sf-seg));
  mask: repeating-linear-gradient(90deg, #000 0 calc(var(--sf-seg) - 4px), transparent calc(var(--sf-seg) - 4px) var(--sf-seg));
}
[data-smartform] .finder__bar i {
  background: var(--accent);
  transition: width .5s var(--sf-ease);
}
[data-smartform] .finder__count {
  font-family: var(--mono);
  font-size: max(12px, .7rem);
  font-weight: 400;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-3);
}
[data-smartform] .finder__body { padding: 1.5rem 1.45rem 1.55rem; }

/* The stage, as an eyebrow; the question, as a heading. */
.step__tag {
  margin: 0 0 .65rem;
  font-family: var(--mono);
  font-size: max(12px, .7rem);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
[data-smartform] .step h2 {
  margin: 0 0 1.35rem;
  font-family: var(--display);
  font-size: clamp(1.55rem, 6.6vw, 1.95rem);
  font-weight: 400;
  line-height: 1.12;
  letter-spacing: -.025em;
  text-wrap: balance;
}
[data-smartform] .step > .lead { margin: -.7rem 0 1.35rem; font-size: .95rem; color: var(--ink-3); }

/* ---------------------------------------------------------------- Tiles */
[data-smartform] .fs { border: 0; padding: 0; margin: 0; }
[data-smartform] .fs__legend {
  padding: 0;
  margin-bottom: .6rem;
  font-size: .88rem;
  font-weight: 500;
  color: var(--ink-2);
}
[data-smartform] .options { gap: .6rem; }
[data-smartform] .option {
  min-height: 58px;
  gap: .9rem;
  padding: .85rem 1rem;
  border: 1px solid var(--hair-2);
  border-radius: var(--sf-radius-sm);
  background: var(--linen);
  -webkit-tap-highlight-color: transparent;
  transition:
    transform 160ms var(--sf-ease),
    border-color 200ms ease,
    background-color 200ms ease,
    box-shadow 200ms ease;
}
[data-smartform] .option--tight { min-height: 52px; }

/* Multi-pick chips (the features step): the tiles' own material and weight
   rather than smartform.css's bold pills, which read as six shouting
   buttons. Regular weight at rest; chosen is navy, the same as a chosen
   tile's edge, with the type stepping up to 500 so it still reads as on. */
[data-smartform] .chips { gap: .55rem; }
[data-smartform] .chip span {
  min-height: 46px;
  padding: .6rem 1.05rem;
  border: 1px solid var(--hair-2);
  background: var(--linen);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.3;
  color: var(--ink);
  text-align: center;
  -webkit-tap-highlight-color: transparent;
  transition: transform 160ms var(--sf-ease), border-color 200ms ease, background-color 200ms ease, color 200ms ease;
}
[data-smartform] .chip:active span { transform: scale(.97); }
@media (hover: hover) and (pointer: fine) {
  [data-smartform] .chip span:hover { border-color: rgba(20, 17, 13, .3); background: var(--paper); }
}
[data-smartform] .chip input:checked + span { border-color: var(--navy); background: var(--navy); color: #fff; font-weight: 500; }
[data-smartform] .chip input:focus-visible + span { outline: 2px solid var(--navy); outline-offset: 2px; }

/* While the shared markup (dactyl-smart-form/form.html) is on its way, hold
   roughly the form's height so the page under it doesn't jump when it lands. */
[data-smartform][data-sf-src]:not(.is-loaded) { min-height: 31rem; }
[data-smartform] .sf-fallback { margin: 0; font-size: 1rem; line-height: 1.6; color: var(--ink-2); }
[data-smartform] .sf-fallback a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--accent); text-underline-offset: .2em; }
[data-smartform] .option:active { transform: scale(.97); }
@media (hover: hover) and (pointer: fine) {
  [data-smartform] .option:hover { border-color: rgba(20, 17, 13, .3); background: var(--paper); }
}
[data-smartform] .option__title { font-size: 1rem; font-weight: 500; line-height: 1.3; }
/* The dot: a tick grows into it when chosen, rather than a smaller dot
   appearing inside a bigger one. */
[data-smartform] .option__dot {
  width: 22px;
  height: 22px;
  border: 1.5px solid rgba(20, 17, 13, .28);
  background: var(--paper);
  transition: background-color 200ms ease, border-color 200ms ease;
}
[data-smartform] .option__dot::after {
  width: 12px;
  height: 12px;
  border-radius: 0;
  background: var(--paper);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 6.4 4.8 9 10 3.2' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 6.4 4.8 9 10 3.2' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
  opacity: 0;
  transform: scale(.5);
  transition: transform 220ms var(--sf-ease), opacity 150ms ease;
}
/* Chosen: the tile fills navy, the same as a chosen chip on the features
   step, so every step answers a pick the same way. The dot turns to paper
   with a navy tick. White on navy 13+:1; the sub line at 75% white 8+:1. */
[data-smartform] .option:has(input:checked) {
  border-color: var(--navy);
  background: var(--navy);
  color: #fff;
  box-shadow: 0 10px 22px -12px rgba(12, 35, 72, .6);
}
[data-smartform] .option:has(input:checked) .option__title { color: #fff; }
[data-smartform] .option:has(input:checked) .option__sub { color: rgba(255, 255, 255, .78); }
[data-smartform] .option:has(input:checked) .option__dot {
  border-color: #fff;
  background: #fff;
}
[data-smartform] .option:has(input:checked) .option__dot::after { background: var(--navy); }
[data-smartform] .option:has(input:checked) .option__dot::after { opacity: 1; transform: scale(1); }
[data-smartform] .option:has(input:focus-visible) { outline: 2px solid var(--navy); outline-offset: 2px; }

/* --------------------------------------------------------------- Fields */
[data-smartform] .field { margin-bottom: 1.05rem; }
[data-smartform] .field label {
  margin-bottom: .45rem;
  font-size: .88rem;
  font-weight: 500;
  color: var(--ink-2);
}
/* The required mark is small text: the accent's small-text shade. */
[data-smartform] .field label span[aria-hidden] { color: var(--accent-ink); }
[data-smartform] .input,
[data-smartform] .textarea,
[data-smartform] .select {
  min-height: 52px;
  padding: .8rem .95rem;
  border: 1px solid var(--hair-2);
  border-radius: var(--sf-radius-sm);
  background-color: var(--linen);
  color: var(--ink);
  font-size: max(16px, 1rem);
  transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease;
}
[data-smartform] .textarea { min-height: 120px; }
[data-smartform] .input::placeholder,
[data-smartform] .textarea::placeholder { color: var(--ink-4); }
[data-smartform] .input:focus,
[data-smartform] .textarea:focus,
[data-smartform] .select:focus {
  outline: none;
  border-color: var(--navy);
  background-color: var(--paper);
  box-shadow: 0 0 0 4px rgba(12, 35, 72, .1);
}
[data-smartform] .input[aria-invalid="true"],
[data-smartform] .textarea[aria-invalid="true"] {
  border-color: var(--sf-error);
  box-shadow: 0 0 0 4px rgba(163, 35, 28, .08);
}
[data-smartform] .error { margin-top: .45rem; font-size: .82rem; font-weight: 500; }
[data-smartform] .field--select { margin-top: 1.3rem; }
[data-smartform] .select {
  width: 100%;
  padding-right: 2.6rem;
  /* Chevron drawn as a background image so the native select keeps its own
     picker on mobile — a scripted dropdown would lose it. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2314110d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .9rem center;
  background-size: 16px 16px;
  appearance: none;
  -webkit-appearance: none;
}

/* ---------------------------------------------------------- The nav row
   smartform.css rules it off from the questions, which is right when there
   is a button under the rule. On step 1 there isn't one (it moves on by
   itself, and there is nowhere back to go), so there the row goes
   entirely. */
[data-smartform] .finder__nav {
  margin-top: 1.5rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--hair);
}
[data-smartform] .finder__nav--solo { border-top-color: transparent; }
[data-smartform] .finder__steps:has(.step[data-step="1"].is-active) + .finder__nav {
  height: 0;
  margin-top: 0;
  padding-top: 0;
  min-height: 0;
  border-top: 0;
  overflow: hidden;
}
[data-smartform] .btn {
  -webkit-tap-highlight-color: transparent;
  transition: transform 160ms var(--sf-ease), background-color 200ms ease, color 200ms ease;
}
[data-smartform] .btn:active { transform: scale(.97); }
[data-smartform] .finder__nav .btn:not(.btn--ghost) { min-width: 8.5rem; }
/* Back is the site's ghost pill: grey at rest, and on hover the navy rises
   through it (ui.js's is-lit) with the word turning white, the same as
   every other ghost button. It used to keep its dark word over the navy,
   so the word vanished. */
[data-smartform] .btn--ghost {
  padding-inline: 1.25rem;
  box-shadow: none;
  background: var(--grey);
  color: var(--ink);
}
[data-smartform] .btn--ghost:active,
[data-smartform] .btn--ghost.is-lit { color: #fff; }
@media (hover: hover) and (pointer: fine) {
  [data-smartform] .btn:not(.btn--ghost):hover { background: var(--navy-hov); }
  [data-smartform] .btn--ghost:hover { background: var(--grey); }
}

/* --------------------------------------------------------------- Review
   A list you can read down, not a table: each answer under its label, with
   a hairline between them. */
[data-smartform] .sf-review {
  display: block;
  padding: 0;
  border-top: 1px solid var(--hair);
  background: none;
  font-size: 1rem;
}
[data-smartform] .sf-review dt {
  padding-top: .85rem;
  font-family: var(--mono);
  font-size: max(12px, .68rem);
  font-weight: 400;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
[data-smartform] .sf-review dd {
  padding: .25rem 0 .85rem;
  border-bottom: 1px solid var(--hair);
  font-weight: 500;
}
.sf-fine { margin: 1rem 0 0; font-size: .85rem; color: var(--ink-3); }

@media (prefers-reduced-motion: reduce) {
  [data-smartform] .option,
  [data-smartform] .btn { transition: border-color 200ms ease, background-color 200ms ease; }
  [data-smartform] .option:active,
  [data-smartform] .btn:active { transform: none; }
}

/* ================================================================== FOOTER */

/* Linen, not white. This is the page, and every fade in the hero — the foot
   of the photograph, its left edge on a wide screen, the sheet the copy sits
   on — resolves to linen. Painting <main> white left all three of them
   dissolving into a colour the page underneath them was not, which shows up
   as a pale seam exactly where the join was supposed to be invisible. */
main {
  display: block;
  position: relative;
  z-index: 1;
  background: var(--linen);
  border-radius: 0 0 var(--r-xl) var(--r-xl);
  /* The page is a sheet lying over the footer: a soft shadow along its
     bottom edge only (pulled in at the sides by the negative spread), which
     is what lets the linen-on-sand edge read at all, and what the footer's
     content is uncovered from (ui.js, footerIn). */
  box-shadow: 0 26px 40px -30px rgba(40, 24, 8, .45);
}

.foot { padding: 5rem 0 2.5rem; background: var(--grey); }
/* The sand runs up behind the page's rounded foot, so the corners curve
   onto sand. Without it the footer began exactly at main's bottom edge and
   the page's own ground showed in the corners: a square notch, a hard
   line beside the curve. main sits above (z-index 1). */
.foot { position: relative; }
.foot::before { content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: calc(var(--r-xl) + 2px); background: var(--grey); pointer-events: none; }
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  /* Clears the fixed dock, which would otherwise sit across the signature. */
  .foot { padding-bottom: calc(2.5rem + 84px); }
}

.foot__cols { display: grid; gap: 2.5rem; }
/* The same mono label as every other on the site. */
.foot__h {
  margin: 0 0 .9rem;
  font-family: var(--mono);
  font-size: max(12px, .75rem);
  font-weight: 400;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.foot__social { display: flex; gap: .6rem; margin: 1rem 0 0; padding: 0; list-style: none; }
.foot__social a {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--hair-2);
  color: var(--ink);
  transition: background-color .2s ease, transform .16s cubic-bezier(.23, 1, .32, 1);
}
.foot__social a:active { transform: scale(.95); }
@media (hover: hover) and (pointer: fine) { .foot__social a:hover { background: var(--sand-2); } }
.foot__social svg { width: 20px; height: 20px; fill: currentColor; }
.foot__social svg.is-line { fill: none; stroke: currentColor; stroke-width: 1.6; }
.foot__big { margin: 0 0 .1rem; }

/* The underline draws itself from the left on hover instead of sitting there
   the whole time. text-decoration can't be animated; a scaled pseudo-element
   can, and it composites rather than reflowing. */
.foot__big a {
  position: relative;
  display: inline-block;
  padding: .6rem 0;
  font-family: var(--display);
  font-size: clamp(1.1rem, 4.8vw, 1.35rem);
  font-weight: 500;
  letter-spacing: -.025em;
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.foot__big a::after,
.foot__list a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: .45rem;
  height: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .55s var(--glide);
}
.foot__big a:hover, .foot__list a:hover { color: var(--ink); }
.foot__big a:hover::after,
.foot__list a:hover::after { transform: scaleX(1); transform-origin: left; }
/* The phone and email lines: once they have arrived (ui.js, footerIn) the
   underline draws itself from the left and stays, faint, so on a phone,
   with no hover, they still read as things to tap. Hover brings it up to
   full strength. */
.foot__big a.is-drawn::after {
  transform: scaleX(1);
  transform-origin: left;
  opacity: .35;
  transition: transform .8s var(--glide), opacity .3s ease;
}
.foot__big a.is-drawn:hover::after { opacity: 1; }

.foot__list { margin: 0; padding: 0; list-style: none; display: grid; }
.foot__list a {
  position: relative;
  display: inline-block;
  justify-self: start;
  padding: .6rem 0;
  color: var(--ink);
  text-decoration: none;
  font-weight: 500;
}

.foot__where { margin: 0 0 .4rem; font-weight: 600; }
.foot__areas { margin: 0; color: var(--ink-3); font-size: 1rem; }

/* The signature. Solid ink, sized to fill the measure — a
   wordmark, not a gradient. It was a three-colour gradient clipped to text,
   which reads as decoration rather than as a name. A studio signs its work
   in its own ink. */
.foot__sig {
  /* Not a clipping window any more: the tape is pressed on from the air
     (ui.js, footerIn), a little large and turned, and a clip would shave
     its corners. The 22px on top is kept (with the margin reduced to
     match, so nothing moves) as the room the 14px drift rises into. */
  padding-top: 22px;
  margin: calc(3rem - 22px) 0 1.6rem;
}
.foot__sig > span {
  display: block;
  font-family: var(--display);
  font-size: clamp(2.9rem, 16.5vw, 9rem);
  font-weight: 500;
  line-height: .92;
  /* Gambetta carries the signature now, and a serif needs far less negative
     tracking than the grotesque this was set for — at -.055em the caps
     collide. */
  letter-spacing: -.03em;
  color: var(--ink);
  user-select: none;
  /* line-height is below 1, so the descender on the last line hangs outside
     its own line box — and the box above is the clipping window the reveal
     needs. The padding grows the window rather than the gap. */
  padding-bottom: .1em;
}
/* The full stop is ink, the same way every heading on the page ends. */
.foot__sig em { font-style: normal; }
/* The signature is the tape now: the two strips, large. Its own padding
   leaves the strips' shadows room inside the clipping window. */
.foot__sig > span {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .42em;
  font-family: inherit;
  font-size: clamp(1.6rem, 7.6vw, 3rem);
  line-height: 1;
  letter-spacing: 0;
  padding: .3em 0 .7em .1em;
}
.foot__sig .tape__strip { font-size: 1em; }
.foot__sig .tape__strip--2 { margin-left: 1.35em; }

.foot__copy {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .3rem 1.25rem;
  margin: 0;
  font-size: .875rem;
  color: var(--ink-3);
}
/* Sits on one line with the copyright, so the tap area is an invisible
   pseudo-element rather than padding that would open a gap in the row. */
/* A real tap target, and readable. */
.foot__copy a {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 1rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--hair-2);
  text-underline-offset: .22em;
}
.foot__copy a::after { content: ""; position: absolute; inset: -12px -10px; }
.foot__copy a:hover { color: var(--ink); text-decoration: underline; }

/* ============================================================== MOBILE DOCK
   A persistent way to start a project once the hero's buttons are gone.
   Hidden on the hero (nothing to remind anyone of yet) and hidden again over
   the contact section, where it would cover the form it points at. */

/* Liquid glass: a floating capsule that bends and brightens whatever passes
   under it rather than covering it. A strong blur and a lift in saturation
   (the refraction), a thin milky tint, a bright rim that catches the light
   along the top and fades round the sides (::before, a 1px ring cut out
   with a mask), and a soft shadow to float it. The call button is a bead
   of the same glass, a little more opaque so its icon reads over navy.
   The main button stays solid navy: it is the one action on the bar, and
   glass under glass would wash its label out over the dark sections. */
.dock {
  position: fixed;
  left: .6rem; right: .6rem;
  bottom: max(.6rem, env(safe-area-inset-bottom));
  z-index: 90;
  display: flex;
  gap: .5rem;
  padding: .4rem;
  border-radius: 999px;
  /* A little more tint than pure clear glass, and a fine dark hairline
     outside the bright rim: over navy the hairline disappears and the rim
     does the work; over the linen, where white glass all but vanished, the
     hairline and the tint are what draw its edge. */
  background: linear-gradient(180deg, rgba(255, 255, 255, .5) 0%, rgba(246, 243, 238, .3) 100%);
  box-shadow:
    0 0 0 1px rgba(20, 17, 13, .1),
    inset 0 1px 1px rgba(255, 255, 255, .8),
    inset 0 -1px 1px rgba(255, 255, 255, .25),
    0 12px 32px -8px rgba(8, 16, 36, .32),
    0 2px 8px rgba(8, 16, 36, .14);
  backdrop-filter: blur(22px) saturate(1.9) brightness(1.06);
  -webkit-backdrop-filter: blur(22px) saturate(1.9) brightness(1.06);
  transform: translateY(calc(100% + 1.5rem + env(safe-area-inset-bottom)));
  transition: transform .5s var(--glide);
}
.dock::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(160deg, rgba(255, 255, 255, .9) 0%, rgba(255, 255, 255, .15) 38%, rgba(255, 255, 255, .08) 62%, rgba(255, 255, 255, .55) 100%);
  -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 exclude, linear-gradient(#000 0 0);
  pointer-events: none;
}
.dock[hidden] { display: none; }
.dock.is-up { transform: none; }

.dock__call {
  position: relative;
  display: grid;
  place-items: center;
  width: 50px; height: 50px;
  flex: none;
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(255, 255, 255, .82), rgba(255, 255, 255, .6));
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, .95),
    inset 0 0 0 1px rgba(255, 255, 255, .5),
    0 3px 10px -3px rgba(8, 16, 36, .25);
  transition: transform .4s var(--glide), background-color .3s var(--ease);
}
.dock__call:active { transform: scale(.94); }
.dock__call svg { width: 21px; height: 21px; fill: none; stroke: var(--ink); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.dock__btn { flex: 1; }

/* ================================================================= REVEALS
   The resting state for anything ui.js will animate in. Applied under .js
   only, so with scripting off every one of these renders as ordinary
   visible content — and .no-motion, which ui.js sets when GSAP failed to
   load or the visitor asked for reduced motion, puts them straight back. */

.js .reveal { opacity: 0; }

.js.no-motion .reveal { opacity: 1; }
.js.no-motion .line > span,
.js.no-motion .foot__sig span,
.js.no-motion .sheet__w > span { transform: none; }
.js.no-motion .price__rule,
.js.no-motion .cue__rail i { transform: scaleX(1); }

/* ============================================================= BREAKPOINTS */

@media (min-width: 560px) {
  .hero__cta { display: flex; flex-wrap: wrap; }
  .hero__cta .btn { flex: 0 1 auto; }
  .studio__box { padding: 2.4rem 2.2rem; }
  .foot__cols { grid-template-columns: 1fr 1fr; }
}

@media (min-width: 768px) {
  /* The price panel goes two-up. 1.05fr against .95fr rather than a straight
     half: the figure is the larger object and an exactly even split makes the
     smaller column look like it is trying to match it. */
  .price {
    grid-template-columns: 1.05fr .95fr;
    gap: 3.25rem;
    align-items: start;
    padding: 3rem 2.6rem 3.25rem;
  }
  /* The rule moves from the top of the column to its left edge, which is
     what turns two stacked blocks into two columns. */
  /* No top padding: the two column labels have to sit on the same line, and
     the eye reads a 4px discrepancy between them as the panel being out of
     true even when it cannot say why. */
  .price__incl {
    padding: 0 0 .35rem 3.25rem;
    margin-left: -3.25rem;
    border-top: 0;
    border-left: 1px solid rgba(255, 255, 255, .16);
  }

  .deck-btn { display: grid; }
  .deck > * { flex-basis: min(58vw, 24rem); }
  .deck--plans > * { flex-basis: min(56vw, 22rem); }
  .studio, .features, .pricing, .contact { padding-block: 9rem; }
  .foot { padding-top: 7rem; }
}
/* A tablet upright, in the phone layout: the figure and the note under it
   sit in the middle of their half of the panel, both ways, rather than in
   its top left corner. */
@media (min-width: 768px) and (max-width: 819px) {
  .price__fig-col {
    align-self: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
  .price__rule { transform-origin: center; }
}

@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) {
  /* 90, not 66: the 70px tape is the tallest thing in the header now, and
     anchor jumps and the hero's top padding both read this. */
  :root { --nav-h: 90px; }
  /* The links set as the site's mono labels (October 2026): the same
     voice as the eyebrows, numbers and package flags, so the header reads
     as part of the design rather than a template's default menu. The page
     you are on is set in ink, the rest in ink-3 (5.7:1 on linen). */
  .nav__links { display: flex; gap: .5rem; margin-right: 1.4rem; }
  .nav__links a {
    position: relative;
    padding: .7rem .75rem;
    color: var(--ink-3);
    font-family: var(--mono);
    font-size: 12px;
    font-weight: 400;
    letter-spacing: .16em;
    text-transform: uppercase;
    text-decoration: none;
    transition: color .3s var(--ease);
  }
  /* The same draw-from-left underline as the footer links, in the accent. */
  .nav__links a::after {
    content: "";
    position: absolute;
    left: .75rem; right: .75rem; bottom: .35rem;
    height: 1px;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform .5s var(--glide);
  }
  .nav__links a:hover, .nav__links a[aria-current="page"] { color: var(--ink); }
  .nav__links a:hover::after { transform: scaleX(1); transform-origin: left; }
  .nav__cta .btn__label { font-family: var(--mono); font-size: 12px; letter-spacing: .14em; text-transform: uppercase; }
  .nav__cta { padding-inline: 1.15rem .55rem; }
  .nav__cta { display: inline-flex; }
  .nav__toggle { display: none; }
  .dock { display: none !important; }

  /* ------------------------------------------------------ Hero, wide
     The photograph stops being a full-screen backdrop and becomes the right
     half of the page — absolutely positioned inside the hero so it can run
     off the right edge of the viewport instead of stopping at the container.
     Bleeding off-canvas is what stops it reading as "a box beside some
     text", which was the arrangement that made the old hero feel like an
     advert.

     Absolute rather than sticky here on purpose. Sticky solved a scrolling
     problem that only exists on a phone, where the photo owns a whole screen
     before the words arrive; at this width the photo and the words are side
     by side and share one scroll. */
  .hero {
    display: grid;
    align-items: center;
    min-height: 100svh;
    padding: calc(var(--nav-h) + 3.5rem) 0 6rem;
  }

  /* The left edge is derived from where the copy column actually ends,
     rather than from a fixed width off the right. A fixed width anchored to
     the viewport edge holds at 1440 and comes apart above it: the copy stays
     put inside the centred 74rem container while the photograph keeps
     hugging the right edge, so on a 1920 screen the two drifted nearly 300px
     apart and left a band of empty linen down the middle of the hero.

     Reading 100% (the hero's own width) rather than 100vw on purpose —
     100vw includes the scrollbar and would push everything a few pixels
     left of where the rest of the page's container sits. */
  .hero__media {
    position: absolute;
    top: 0; right: 0; bottom: 0;
    left: calc(max(0px, (100% - var(--maxw)) / 2)
               + var(--gutter)
               + min(32rem, 42vw)
               + 3rem);
    width: auto;
    height: auto;
  }
  .hero__media-in { border-radius: 0 0 0 var(--r-xl); }

  /* Two dissolves, at right angles. The left edge fades into linen so the
     photograph hands over to the copy column rather than butting against it,
     and the bottom still fades into the page so the section flows on.

     Both are gradient overlays, and the left one is deliberately NOT a
     mask-image. A mask on this element is correct in principle and breaks in
     practice: ui.js scrubs a transform on it as the hero leaves, and a
     masked layer that is also being transformed on scroll rasterises to
     nothing in Chromium — the whole page composited blank from the first
     scroll onward. An overlay costs one pseudo-element and cannot fail that
     way. It is safe to paint over the photograph because the tape is a
     sibling of this element rather than a child, so the overlay passes
     underneath it. */
  .hero__media-in::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 24%;
    z-index: 2;
    background: linear-gradient(to right,
                var(--linen) 0%,
                rgba(246, 243, 238, .82) 34%,
                rgba(246, 243, 238, 0) 100%);
    pointer-events: none;
  }
  .hero__media-in::after { height: 22%; z-index: 2; }

  .hero__photo-canvas,
  .hero__photo-img { object-position: 50% 20%; }


  /* No sheet at this width: the photograph is beside the copy rather than
     behind it, so there is nothing to lift the words clear of. */
  .hero__in {
    margin-top: 0;
    padding-top: 0;
    padding-bottom: 0;
    background: none;
    border-radius: 0;
  }
  /* Widened deliberately, and it is the photograph's shape that decides it.
     The hero media's left edge is derived from where this column ends, so a
     narrow column pushes the picture wide — at 1920 a 32rem column left the
     photograph nearly square, which crops a portrait film to a letterbox
     band and throws away most of the frame. Letting the copy run to 52rem
     pulls the picture back to roughly 0.6:1, close to the footage's own
     0.56:1, so the crop is a trim rather than an amputation.

     The column being wide does not make the LINES long: .hero__lead keeps
     its own 33rem measure. What gains the room is the headline, which wants
     it. */
  .hero__copy { max-width: min(52rem, 56vw); }
  .hero__h1 { font-size: clamp(3rem, 5.4vw, 4.6rem); }

  .studio__box { max-width: 52rem; }
  .price__note { font-size: 1.0625rem; }
}

/* From here there is room for everything at once, so the decks become plain
   grids: same markup, same cards, no sideways scrolling and no controls. */
@media (min-width: 1024px) {
  .deck {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.25rem;
    overflow: visible;
    padding: .5rem var(--gutter) 0;
    max-width: var(--maxw);
    margin-inline: auto;
  }
  .deck > *, .deck--plans > * { flex-basis: auto; }
  .deck-ui { display: none; }
  .card { padding: 2.1rem 1.9rem 2.35rem; }
  .plan {
    --plan-pad-x: 1.9rem;
    --plan-pad-y: 2.1rem;
    padding-bottom: 2.35rem;
  }
  /* With a pointer available the card can respond to being pointed at. Kept
     to a 2px lift and a slightly deeper shadow — the change should be felt
     more than seen. */
  .card:not(.card--dark):hover,
  .plan:not(.plan--ember):not(.plan--deep):hover { background: var(--grey-2); }
  .plan.plan--deep:hover { background: #cbc0ad; }
  /* background-color, not background — the shorthand would reset the
     vignette to none and flatten the card on hover. The top plan is left
     out: its surface is a layer, and the electricity is response enough. */
  .card--dark:hover { background-color: var(--navy-hov); }

}

@media (min-width: 1200px) {
  .hero { padding-bottom: 8rem; }
  .foot__cols { grid-template-columns: 1.4fr 1fr 1.2fr; gap: 3rem; }
}

/* ========================================================== REDUCED MOTION
   ui.js also reads this and skips Lenis and every GSAP timeline entirely, so
   this block is the CSS half of the same decision rather than a duplicate of
   it. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .js .reveal { opacity: 1; }
  .deck { scroll-behavior: auto; }
}

/* --------------------------------------------------------------- Printing */
@media print {
  .nav, .sheet, .dock, .cue, .deck-ui { display: none !important; }
  .deck { display: block; overflow: visible; }
  .js .reveal { opacity: 1; }
  body { color: #000; }
}

/* The step's question takes focus when the step changes so a screen reader
   reads it out; it isn't something to act on, so no ring is drawn round it. */
[data-smartform] .step h2:focus { outline: none; }

/* ================================================================ The signed footer
   One piece on the sand, held together by space and alignment, with no
   rules and no numbers. One measure of space, --fgap, sits above the
   address, between the address and the columns, and between the columns
   and the fine print, so the three parts are evenly set.

     the address    the footer's headline, in the serif, as wide as its
                    column on one line (sized to the column, cqw), its
                    letters rising into place as it arrives (footer.js); a
                    terracotta line draws under it on hover. The phone
                    under it, quieter.
     the columns    the tape, the studio's pages, the guides and where we
                    work, on one top line: the tape as wide as it needs,
                    then three columns of one width, one gap between all. The links
                    are set in the serif, as the address and the places
                    are, so the footer speaks in one voice.
     the fine print and a round way back up. */
/* The top is deeper than the gaps inside: the footer is its own room, and
   arriving in it straight off the orange tab needs air (7rem on a phone,
   up to 10rem wide). */
.foot--sign { --fgap: clamp(4rem, 9vw, 7rem); padding: clamp(9.5rem, 28vw, 11rem) 0 2.2rem; }
.foot--sign .foot__hello { position: relative; container-type: inline-size; }
.foot--sign .foot__say-k { margin: 0 0 1.1rem; font-family: var(--mono); font-size: max(12px, .72rem); letter-spacing: .24em; text-transform: uppercase; color: var(--ink-3); }
.foot--sign .foot__mail {
  position: relative;
  display: inline-block;
  max-width: 100%;
  font-family: var(--display);
  /* Sized to the footer's own width so the whole address always sits on
     one line, phone or desktop: split over lines it read as three words. */
  font-size: min(8.1cqw, 8rem);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -.045em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
}
.foot--sign .foot__mail .foot__ch, .foot--sign .foot__mail .foot__cx { display: inline-block; will-change: transform; }
/* Clipped below, for the rise; room above (.75em, given back by the
   margin) for the wave and the pointer's crest together. */
.foot--sign .foot__mail .foot__ln { display: inline-block; overflow: hidden; padding: .75em 0 .12em; margin: -.75em 0 -.12em; vertical-align: bottom; }
.foot--sign .foot__mail::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -.08em;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .9s var(--glide);
}
.foot--sign .foot__mail:focus-visible { outline: 2px solid var(--ink); outline-offset: 6px; border-radius: 4px; }
/* The ink over the address on hover (footer.js), cut to the letters. */
.foot--sign .foot__ink { position: absolute; left: -24px; top: -24px; pointer-events: none; }
.foot--sign .foot__tel {
  display: inline-block;
  margin-top: 1.4rem;
  font-family: var(--display);
  font-size: clamp(1.4rem, 5vw, 2.1rem);
  letter-spacing: -.02em;
  color: var(--ink-2);
  text-decoration: none;
  transition: color .4s var(--ease);
}
.foot--sign .foot__tel:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; border-radius: 4px; }
@media (hover: hover) and (pointer: fine) {
  .foot--sign .foot__tel:hover { color: var(--accent-ink); }
}

.foot--sign .foot__row { display: grid; grid-template-columns: 1fr 1fr; gap: 2.6rem 1.6rem; margin-top: var(--fgap); }
/* The tape's own box carries 22px of air on top for its press (site.css,
   .foot__sig); taken back here so its top sits on the columns' top line. */
.foot--sign .foot__sig { grid-column: 1 / -1; margin: -22px 0 0; }
.foot--sign .foot__sig > span { font-size: clamp(1.3rem, 6vw, 1.75rem); padding: .3em 0 .2em .1em; }
.foot--sign .foot__col:last-child { grid-column: 1 / -1; }
.foot--sign .foot__h { margin: 0 0 1.1rem; }
.foot--sign .foot__list { gap: .15rem; }
.foot--sign .foot__list a {
  padding: .2rem 0;
  font-family: var(--display);
  font-size: 1.25rem;
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: -.012em;
  color: var(--ink);
  transition: color .3s var(--ease);
}
.foot--sign .foot__list a::after { bottom: .1rem; height: 1px; background: var(--accent); }
.foot--sign .foot__list a:hover { color: var(--accent-ink); }
.foot--sign .foot__where { margin: 0 0 .6rem; font-family: var(--display); font-size: 1.25rem; font-weight: 400; line-height: 1.3; letter-spacing: -.012em; }
.foot--sign .foot__areas { max-width: 22em; font-size: .95rem; line-height: 1.55; }

.foot--sign .foot__copy { margin-top: var(--fgap); gap: .3rem 1.6rem; }
.foot--sign .foot__up { gap: .7rem; margin-left: auto; text-decoration: none; }
.foot--sign .foot__up svg { width: 2.6rem; height: 2.6rem; padding: .7rem; border-radius: 50%; box-shadow: inset 0 0 0 1px var(--ink); fill: none; stroke: var(--ink); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; transition: background-color .45s var(--ease), stroke .45s var(--ease); }
@media (hover: hover) and (pointer: fine) {
  .foot--sign .foot__up:hover svg { background: var(--ink); stroke: var(--linen); }
}
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  /* Clears the fixed dock, as the old footer did. */
  .foot--sign { padding-bottom: calc(2.2rem + 84px); }
  /* A touch smaller than the column on a phone, so the ".au" has air
     before the screen's edge rather than finishing hard against it. */
  .foot--sign .foot__mail { font-size: 7.4cqw; }
  /* Back to top, on a phone: just the round arrow, moved by footer.js into
     the Where column and set in the middle of the empty space beside its
     words, centred on them top to bottom. The words stay for screen
     readers only. */
  .foot--sign .foot__col:last-child { display: grid; grid-template-columns: minmax(0, 12rem) 1fr; align-items: center; }
  .foot--sign .foot__col:last-child > :not(.foot__up) { grid-column: 1; }
  .foot--sign .foot__col:last-child .foot__up { position: relative; display: inline-flex; grid-column: 2; grid-row: 1 / span 3; justify-self: center; align-self: center; margin: 0; color: var(--ink); }
  .foot--sign .foot__col:last-child .foot__up::after { content: ""; position: absolute; inset: -12px; }
  .foot--sign .foot__up span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}
@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) {
  /* The tape takes what it needs; the three columns after it share the
     rest equally, so Studio, Guides and Where are the same width, with
     the same gap between every one. */
  .foot--sign .foot__row { grid-template-columns: auto repeat(3, minmax(0, 1fr)); gap: 0 clamp(3rem, 6vw, 6rem); align-items: start; }
  .foot--sign .foot__sig, .foot--sign .foot__col:last-child { grid-column: auto; }
}

/* The pricing page's plans table, on the home page: wide screens only (desktop.css). */
.plans__table { display: none; }


/* ====================================================== THE HERO LEAD (phones)
   Every inner page's hero line, the same way (the features page set it):
   large in the serif, in ink, under a short terracotta rule, its words
   coming up from faint to full as it is read (lead.js). Wide screens keep
   each page's own lead. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .hero-lead.hero-lead {
    position: relative;
    max-width: none !important;
    margin-top: 2.2rem;
    padding-top: 1.5rem;
    font-family: var(--display);
    font-size: clamp(1.6rem, 7vw, 1.95rem);
    font-weight: 400;
    line-height: 1.24;
    letter-spacing: -.015em;
    color: var(--ink);
  }
  .hero-lead.hero-lead p { font: inherit; color: inherit; }
  .hero-lead::before { content: ""; position: absolute; left: 0; top: 0; width: 3rem; height: 2px; background: var(--accent); }
  .hero-lead .w { opacity: .16; transition: opacity .25s linear; }
  .hero-lead .w.is-read { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .hero-lead .w { opacity: 1; } }

/* Phones, home page: one size per tier of heading.
   Cards (The studio, Our goal, Our process): the studio's heading is a
   .display--2 inside a panel and came out heavier (500) and a shade smaller
   than the two slab cards beside it; it takes the slab heading's values.
   Sections (Why Dactyl, Pricing, Monthly plans, Let's have a chat): two
   were 11vw and two 11.4vw; all four are the features heading now. "Then
   you pick a plan" stays the smaller subheading, at the same weight. */
@media (max-width: 819px), (max-device-width: 599px), (max-device-height: 599px) {
  .slab__card .display--2 {
    font-size: clamp(2.1rem, 9.4vw, 2.8rem);
    font-weight: 400;
    line-height: 1.02;
    letter-spacing: -.028em;
  }
  .pricing .display--2,
  .plans__title,
  .contact__h {
    font-size: clamp(2.6rem, 11.4vw, 3.4rem);
    font-weight: 400;
    line-height: 1.02;
    letter-spacing: -.028em;
  }
  .plans__h { font-weight: 400; }
}

/* ============================================================ PAGE HERO
   Every inner page opens the way Features does (October 2026): the page's
   name on the brand's Dymo tape stuck across the heading's top-left
   corner, the heading across the whole width on two lines, and the lede
   under its end. A hero picture, where a page has one, sits under the
   heading opposite the lede. The measures are Features' own (feats.css,
   desk.css .asks__h), so the pages read as one site.

   Markup: section.phero-sec > .gd-in|.px-in.phero > .phero__pin (tape, h1
   .phero__h), .phero__lede, optional .phero__media (a picture) or
   .phero__main (a block of content, e.g. Contact's ways in), optional
   .phero__wide.
   The section carries the top space, so main's own is taken off. */
main:is(.gd, .pg, .lost, .ok-page):has(.phero-sec) { padding-top: 0; }
/* Phones: 50px more than the wide screens' start, clear of the docked tape. */
.phero-sec { padding-top: calc(var(--nav-h, 64px) + 7.5rem + 50px); }
.phero__pin { position: relative; }
/* A hairline under the heading the width of the page: what sits under it
   (the lede, a picture, a deck) hangs from the line rather than floating
   in the space the heading leaves. Contact's ways in carry their own rule. */
.phero__pin { padding-bottom: 1.5rem; border-bottom: 1px solid var(--hair); }
.phero:has(.cx-ways) .phero__pin { padding-bottom: 0; border-bottom: 0; }
/* A deck: the lede's first thought set large in the serif under the line,
   opposite the practical detail (Pricing). */
.phero__deck {
  max-width: 21em;
  margin: 0;
  font-family: var(--display);
  font-size: clamp(1.45rem, 5.8vw, 1.8rem);
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: -.015em;
  color: var(--ink);
  text-wrap: balance;
}
.phero__tape {
  position: absolute;
  z-index: 2;
  top: -3.5rem;
  left: -.2rem;
  margin: 0;
  transform: rotate(-5deg);
  transform-origin: left center;
}
.phero__tape .tape__strip { font-size: clamp(1.3rem, 1.65vw, 1.75rem); }
.phero__h {
  max-width: none;
  margin: 0;
  font-family: var(--display);
  font-size: clamp(2.8rem, 13vw, 3.6rem);
  font-weight: 400;
  line-height: .96;
  letter-spacing: -.035em;
  color: var(--ink);
  text-wrap: balance;
}
.phero__h em { font-style: italic; color: var(--accent); }
.phero__h em.dot, .phero__h em.qm { font-style: normal; color: inherit; }
.phero__lede { margin-top: 1.2rem; }
.phero__lede > :first-child { margin-top: 0; }
.phero__lede p:not(.gd-stamp):not(.pv-meta) {
  max-width: 34rem;
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--ink-2);
  text-wrap: pretty;
}
/* A lede on its own (no picture or block under the heading) is the
   deck: its first paragraph large in the serif, hanging from the line on
   the left, so it never floats alone in the right column. */
.phero:not(:has(.phero__media, .phero__main)) .phero__lede p:first-child {
  max-width: 21em;
  font-family: var(--display);
  font-size: clamp(1.45rem, 5.8vw, 1.8rem);
  line-height: 1.2;
  letter-spacing: -.015em;
  color: var(--ink);
  text-wrap: balance;
}
.phero__media { margin: 2.6rem 0 0; }
.phero__main { margin-top: 2.2rem; }
.phero__wide { margin-top: 2.6rem; }
/* A hero's picture, under the heading's two columns: a plate the full
   width of the page, cropped wide on a wide screen (the small-business
   guide's proportions) and tall on a phone, drifting inside its frame as
   the page moves (guides.js, data-deep). The Western Sydney map is the
   same plate in umber, the sketch centred in it. */
.phero__band { margin: 0; aspect-ratio: 4 / 5; }
.phero__band--map { display: flex; align-items: center; justify-content: center; aspect-ratio: auto; padding: 1.6rem 1rem; }
.phero .phero__band--map svg { width: 100%; max-width: 26rem; height: auto; }

@media (min-width: 820px) and (min-device-width: 600px) and (min-device-height: 600px) {
  .phero-sec { padding-top: calc(var(--nav-h, 56px) + clamp(6.5rem, 18svh, 11.5rem)); }
  .phero {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
    column-gap: clamp(3rem, 6vw, 7rem);
    align-items: start;
  }
  .phero__pin { grid-column: 1 / -1; grid-row: 1; }
  .phero__tape { top: -4.25rem; left: -.4rem; }
  .phero__pin { padding-bottom: clamp(1.8rem, 4svh, 2.8rem); }
  .phero__deck { font-size: clamp(1.7rem, 2.3vw, 2.5rem); }
  .phero:not(:has(.phero__media, .phero__main)) .phero__lede { grid-column: 1; }
  .phero:not(:has(.phero__media, .phero__main)) .phero__lede p:first-child { font-size: clamp(1.7rem, 2.3vw, 2.5rem); }
  .phero__h { font-size: clamp(3.4rem, 6.6vw, 8rem); line-height: .94; letter-spacing: -.04em; }
  .phero__lede { grid-column: 2; grid-row: 2; margin-top: clamp(1.8rem, 4svh, 3rem); }
  .phero__lede p:not(.gd-stamp):not(.pv-meta) { font-size: clamp(1.05rem, 1.15vw, 1.2rem); }
  .phero__media { grid-column: 1; grid-row: 2; justify-self: start; width: min(100%, 30rem); margin-top: clamp(2.5rem, 5svh, 3.5rem); }
  .phero__main { grid-column: 1; grid-row: 2; margin-top: clamp(1.8rem, 4svh, 3rem); }
  .phero__wide { grid-column: 1 / -1; grid-row: 3; margin-top: clamp(3rem, 6svh, 4.5rem); }
  .phero__band { aspect-ratio: 1600 / 679; }
  .phero__band--map { aspect-ratio: 1600 / 679; padding: clamp(1.8rem, 3vw, 2.6rem); }
  .phero .phero__band--map svg { flex: none; width: auto; max-width: 100%; height: 100%; max-height: 100%; }
}
/* Pages built on .wrap (404) take the same width as the rest. */
.phero-sec > .wrap { max-width: calc(92rem + 2 * var(--gutter)); }
