/* =====================================================================
   Savvy Websites — v5 "Plain Numbers"
   Tonal-band architecture, specimen-sheet typography.
   Warm paper, warm ink, one saturated accent used only as a solid fill.
   Every decorative number on this page is a true number. See DESIGN.md.
   ===================================================================== */

/* ---- 1. Fonts (self-hosted, no third-party request) ----------------- */
/* Instrument Sans over the usual Inter/Geist default: tighter set (more
   headline impact at the same size), real character in the letterforms, and
   roughly half the weight on the wire. Spline Sans Mono for the micro-labels. */
@font-face {
  font-family: 'Instrument Sans';
  src: url('assets/fonts/instrument-sans-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Instrument Sans';
  src: url('assets/fonts/instrument-sans-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Instrument Sans';
  src: url('assets/fonts/instrument-sans-700.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Spline Sans Mono';
  src: url('assets/fonts/spline-sans-mono-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Spline Sans Mono';
  src: url('assets/fonts/spline-sans-mono-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}

/* ---- 2. Tokens ------------------------------------------------------ */
:root {
  /* surfaces */
  --paper:       #F7F5F1;
  --surface:     #FFFFFF;
  --ink:         #14120F;
  --ink-2:       #1D1A16;   /* the footer, one step off the outro band */

  /* text */
  --text:        #14120F;
  --text-soft:   #4A453D;
  --mute:        #6E6759;   /* 5.1:1 on paper — the 11px micro-labels need the headroom */
  --on-ink:      #F2EFE9;
  --on-ink-mute: #9C9488;

  /* rules */
  --line:        rgba(20, 18, 15, .13);
  --line-2:      rgba(20, 18, 15, .06);
  --line-ink:    rgba(242, 239, 233, .16);
  --line-ink-2:  rgba(242, 239, 233, .08);

  /* the one accent — solid fills only */
  --accent:      #0B4CF0;
  --accent-deep: #0A3FCB;
  --accent-lift: #8FA9FF;   /* the accent when it must sit ON the ink band */
  /* And a third step for type over a PHOTOGRAPH. --accent-lift is built for
     flat --ink (L 0.006) where it gives 7.5:1; over the scrimmed hero art the
     brightest pixel behind the headline is L 0.081, which needs a foreground of
     L 0.574 and --accent-lift only reaches 0.414. This is L 0.636. */
  --accent-photo: #C4CFFF;
  --accent-wash: #E7ECFE;

  --whatsapp:    #25D366;

  /* type */
  --sans: 'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --mono: 'Spline Sans Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Sized so the longest line ("Right now, someone's") always keeps headroom in
     its column — the line breaks are authored, so a wrap would break the typed
     reveal. This value is for the two-column hero; the stacked layout gets a
     bigger one at the 900px breakpoint, where the column is the full width. */
  --fs-display: clamp(1.75rem, 3.15vw, 2.875rem);
  --fs-h2:      clamp(2rem, 4.6vw, 3.5rem);
  --fs-h3:      clamp(1.1875rem, 1.9vw, 1.5rem);
  --fs-lead:    clamp(1.0625rem, 1.5vw, 1.3125rem);
  --fs-body:    1.0625rem;
  --fs-small:   .9375rem;
  --fs-micro:   .6875rem;

  /* geometry */
  --wrap:    1180px;
  --gutter:  clamp(20px, 5vw, 56px);
  --band-y:  clamp(64px, 9vw, 132px);
  --r-panel: 20px;
  --r-pill:  999px;

  --ease: cubic-bezier(.16, 1, .3, 1);
}

/* ---- 3. Reset ------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img, svg, video { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; }
button, input, textarea, select { font: inherit; color: inherit; }
h1, h2, h3, h4 { margin: 0; font-weight: 500; letter-spacing: -.028em; line-height: 1.06; }
p  { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}
.band--ink :focus-visible,
.quote-form :focus-visible,
footer.site :focus-visible { outline-color: var(--accent-lift); }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--accent); color: #fff; padding: 14px 20px;
  font: 500 var(--fs-small)/1 var(--sans);
}
.skip:focus { left: 0; }

/* ---- 4. Layout primitives ------------------------------------------ */
.wrap {
  width: 100%; max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.band {
  padding-block: var(--band-y);
  /* Land the section's first line ~56px under the sticky header rather than its
     top edge. Subtracting the band's own top padding keeps that gap identical at
     every width, since --band-y is a clamp that grows with the viewport. */
  scroll-margin-top: calc(124px - var(--band-y));
}
/* Sections that open with a drawn hairline sit ~47px lower again, so discount
   the rule and its margin to make every anchor land in the same place. */
.band:has(> .wrap > .rule:first-child) {
  scroll-margin-top: calc(124px - var(--band-y) - 1px - clamp(28px, 3.5vw, 46px));
}
.band--paper   { background: var(--paper); }
.band--surface { background: var(--surface); }
.band--ink     { background: var(--ink); color: var(--on-ink); }
.band--ink h1, .band--ink h2, .band--ink h3 { color: var(--on-ink); }
.band--ink .tag { color: var(--on-ink-mute); }
.band--ink .lead,
.band--ink .section-head p { color: var(--on-ink-mute); }
.band--ink .small-print { color: var(--on-ink-mute); }
.band--ink a:not(.btn) { color: var(--accent-lift); }
.band--tight { padding-block: 0; }

/* ---- 5. Type components -------------------------------------------- */
.tag {
  display: block;
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--mute);
  margin-bottom: 1.1em;
}
.tag b { font-weight: 500; color: var(--accent); }
.band--ink .tag b { color: var(--accent-lift); }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.45;
  color: var(--text-soft);
  max-width: 36ch;
}
.small-print {
  font-size: var(--fs-small);
  line-height: 1.55;
  color: var(--mute);
  max-width: 76ch;
}
.small-print a { color: var(--accent); }

.section-head { margin-bottom: clamp(36px, 5vw, 64px); }
.section-head h2 { font-size: var(--fs-h2); max-width: 24ch; }
.section-head > p:not(.tag) {
  margin-top: 1em;
  font-size: var(--fs-lead);
  line-height: 1.45;
  color: var(--text-soft);
  max-width: 46ch;
}
/* Heads that carry an intro paragraph set it beside the heading, so the
   right half of the row is doing work instead of sitting empty. */
@media (min-width: 901px) {
  .section-head.wide {
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: clamp(32px, 5vw, 72px);
    align-items: end;
  }
  .section-head.wide h2 { max-width: 20ch; }
  .section-head.wide > p:not(.tag) { margin-top: 0; padding-bottom: .4em; }
}

/* Proof band head (markups rev1, 30 Aug 2026: "adjust layout"). The shared
   .wide head already bottom-aligns the intro, but this heading is the longest
   on the page and 20ch broke it over three lines, so the right column opened a
   void two lines deep above a two-line intro. Giving this one head a wider
   measure drops the heading to two lines and closes the gap. Scoped to
   #examples on purpose: #compare and #included use .wide too and their
   headings are short enough that 20ch already balances. */
@media (min-width: 901px) {
  #examples .section-head.wide { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); }
  #examples .section-head.wide h2 { max-width: 27ch; }
}

/* An h2 that sits outside a .section-head block */
.section-head-h { font-size: var(--fs-h2); }

.rule {
  display: block; height: 1px; width: 100%;
  background: var(--line);
  border: 0; margin: 0;
  transform-origin: left center;
}
.band--ink .rule { background: var(--line-ink); }
.rule + .section-head { margin-top: clamp(28px, 3.5vw, 46px); }
.mt { margin-top: clamp(24px, 3vw, 36px); }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}

/* ---- 6. Buttons ----------------------------------------------------- */
.btn {
  --btn-bg: var(--accent);
  --btn-fg: #fff;
  display: inline-flex; align-items: center; justify-content: center; gap: .55em;
  background: var(--btn-bg); color: var(--btn-fg);
  border: 0; border-radius: var(--r-pill);
  padding: 14px 26px;
  font-size: var(--fs-small); font-weight: 500; letter-spacing: -.01em;
  text-decoration: none; cursor: pointer;
  transition: background .18s ease, transform .18s var(--ease);
}
.btn:hover { --btn-bg: var(--accent-deep); transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
.btn.big { padding: 18px 34px; font-size: 1rem; }
.btn.full { width: 100%; }
.btn svg { width: 18px; height: 18px; flex: none; }
.btn[disabled] { opacity: .55; cursor: default; transform: none; }

.btn.secondary { --btn-bg: var(--ink); --btn-fg: var(--on-ink); }
.btn.secondary:hover { --btn-bg: #2C2822; }
/* The outro band went dark when the Finnieston art went behind it, so it needs
   the same inversion the ink bands already use. */
.band--ink .btn.secondary,
.cta-band .btn.secondary { --btn-bg: var(--paper); --btn-fg: var(--ink); }
.band--ink .btn.secondary:hover,
.cta-band .btn.secondary:hover { --btn-bg: #fff; }

.actions { display: flex; flex-wrap: wrap; gap: 12px; }

.go {
  display: inline-flex; align-items: center; gap: .4em;
  font-weight: 500; font-size: var(--fs-small);
  color: var(--accent); text-decoration: none;
  border-bottom: 1px solid currentColor; padding-bottom: 2px;
}
.go::after { content: '↗'; transition: transform .22s var(--ease); }
.go:hover::after { transform: translate(2px, -2px); }
.band--ink .go { color: var(--accent-lift); }

.big-link {
  display: inline-block;
  font-size: var(--fs-h3); font-weight: 500; letter-spacing: -.025em;
  color: var(--accent); text-decoration: none;
  border-bottom: 2px solid currentColor; padding-bottom: 3px;
}

/* ---- 7. Header ------------------------------------------------------ */
header.site {
  position: sticky; top: 0; z-index: 90;
  background: rgba(247, 245, 241, .9);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line-2);
}
.navbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px; min-height: 68px;
}
.logo {
  font-size: 1.0625rem; font-weight: 700; letter-spacing: -.03em;
  text-decoration: none; white-space: nowrap;
  color: var(--text);
}
.logo span { color: var(--accent); }

/* The phone number in the header. Four of the six agencies ranking above us
   for "web design glasgow" put one here, and more than half of enquiries to a
   professional service arrive as a call rather than a form — so it is a
   channel, not decoration.
   It lives INSIDE nav.main rather than as a third .navbar child: that keeps
   space-between distributing two items as before, and it drops into the
   mobile drawer for free. */
.nav-tel {
  font-family: var(--mono); font-size: var(--fs-small); font-weight: 500;
  letter-spacing: .02em; white-space: nowrap; text-decoration: none;
  color: var(--text); padding: 9px 13px; border-radius: var(--r-pill);
}
.nav-tel:hover { color: var(--accent); }

nav.main { display: flex; align-items: center; gap: 2px; }
nav.main a:not(.btn) {
  padding: 9px 13px; border-radius: var(--r-pill);
  font-size: var(--fs-small); text-decoration: none; color: var(--text-soft);
  transition: opacity .22s ease, color .22s ease;
}
/* Bobbin's move: hovering one item dims its siblings. */
nav.main:hover a:not(.btn) { opacity: .3; }
nav.main a:not(.btn):hover { opacity: 1; color: var(--text); }
nav.main .btn { margin-left: 8px; }
/* Two CTAs now sit here — the free preview and the paid start — so the group
   is given a little clearance from the right edge rather than hugging it. */
nav.main { margin-right: 4px; }
@media (min-width: 881px) { nav.main { margin-right: 10px; } }
/* The paid route is the ink button: equal weight to the accent one, clearly a
   different action. Nowrap so the price never breaks onto a second line. */
nav.main .btn.secondary { white-space: nowrap; }

#nav-toggle, .nav-burger { display: none; }

@media (max-width: 880px) {
  #nav-toggle { position: absolute; width: 1px; height: 1px; opacity: 0; }
  .nav-burger {
    display: flex; flex-direction: column; justify-content: center; gap: 5px;
    width: 44px; height: 44px; padding: 0 10px; cursor: pointer;
    margin-right: -10px;
  }
  .nav-burger span { display: block; height: 1.5px; background: var(--text); border-radius: 2px; }
  #nav-toggle:focus-visible + .nav-burger { outline: 3px solid var(--accent); outline-offset: 2px; }
  nav.main {
    position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    padding: 6px var(--gutter) 22px;
    display: none;
  }
  #nav-toggle:checked ~ nav.main { display: flex; }
  nav.main a:not(.btn) { padding: 13px 0; font-size: 1.0625rem; border-bottom: 1px solid var(--line-2); }
  nav.main:hover a:not(.btn) { opacity: 1; }
  nav.main .btn { margin: 16px 0 0; }
  /* In the drawer the number is a row like the others, but it is the one
     row that is an action, so it keeps its own weight. */
  .nav-tel { font-size: 1.0625rem; padding: 13px 0; border-bottom: 1px solid var(--line-2); }
}

/* ---- 8. Hero -------------------------------------------------------- */
/* ONE image, full bleed. Michael's call, 2026-08-28 — this replaces the banded
   version (cone left, tower right, paper corridor between) and the swirl before
   that. Do not reintroduce a second image or a paper gap.

   The consequence is not optional. The copy was ink, and ink over this art
   measures 1.81:1 on the darkest sky, so a full-bleed image forces the hero to
   light-on-dark — the same treatment the outro band already uses. Every colour
   below is a token the system already had for that case: --on-ink for body,
   --on-ink-mute for the quiet lines, --accent-lift for the accent (its own
   comment reads "the accent when it must sit ON the ink band"). No new colours.

   Unlike the banded version this works at every width, so there is no longer a
   breakpoint that switches the art off — a scrim carries contrast on a phone
   just as well as on a desktop. */
.hero {
  --hero-art: url('assets/glasgow-px-flip.webp');
  --hero-art-pos: 38%;
  color: var(--on-ink);
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* Tightened so the whole quote form clears the fold on a laptop. */
  padding-top: clamp(22px, 2.8vw, 44px);
  padding-bottom: clamp(52px, 7vw, 104px);
}
.hero::before {
  content: '';
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  /* The Duke of Wellington, mirrored: the horse faces right, into the page,
     rather than off the edge, and the cone leads.

     Reworked 30 Aug 2026 (markups rev1: "lighter + more punchy — like Ref A",
     the leader pointing at the SKY). The art was never dark; the scrim was.
     A full-width scrim is the wrong instrument for that, because the darkness
     it needs under the headline gets applied to the entire picture, and the
     ceiling it imposes is brutal: measured against the real text boxes, a
     flat scrim cannot go below .66 before --accent-photo breaches 4.75, which
     is a 2% lightening — invisible.

     So the darkening is localised instead. A plate holds under the copy column
     and feathers out at 78%; everything past it carries only a .04 veil, which
     is there to stop the sky blowing out and to keep the ink form card
     separating from the art behind it.

     Measured at 1505x700 against the real text boxes, worst-case (lightest)
     pixel under each:  on-ink 7.36:1 · accent-photo 5.52:1.
     Both are BETTER than the .68/.76/.84 full-width scrim this replaces
     (7.09 / 5.31), while mean luminance over the whole hero goes
     0.023 -> 0.163, about seven times lighter.

     ⚠ --accent-photo is the binding token, not --on-ink; it fails roughly a
     full point before on-ink does. And the plate's HOLD must stay at or past
     the last text pixel (52.2% at this width) — feathering it earlier is what
     breaks first. Re-measure before moving a stop. */
  background-image:
    linear-gradient(to right,
      rgba(9, 11, 24, .72) 0%,
      rgba(9, 11, 24, .72) 54%,
      rgba(9, 11, 24, 0)   78%),
    linear-gradient(rgba(9, 11, 24, .04), rgba(9, 11, 24, .04)),
    var(--hero-art);
  background-size: cover, cover, cover;
  background-position: center, center, var(--hero-art-pos) center;
  background-repeat: no-repeat, no-repeat, no-repeat;
}
.hero > .wrap { position: relative; z-index: 1; }
/* The Edinburgh page cannot open on the Duke of Wellington. Same scrim, same
   type treatment, different landmark. */
/* Edinburgh takes the same plate. Re-measured against ITS text boxes and ITS
   artwork rather than assuming Glasgow's figures carried over (1425x725, text
   ends at 52.6%): on-ink 7.36:1 · accent-photo 5.52:1, mean luminance
   0.017 -> 0.113, about six and a half times lighter. */
.hero--edinburgh {
  --hero-art: url('assets/edinburgh-hero.webp');
  --hero-art-pos: 52%;
}

/* Both hero forms are glass, so the artwork reads through them (30 Aug 2026).
   Edinburgh first, for the castle; Glasgow measured the same and followed.

   The panel cannot simply be given an alpha. Measured over the lightened art
   the binding token is --on-ink-mute, a mid grey built for a SOLID ink panel:
   it fails at 4.28:1 by the time the panel reaches .86, a transparency you
   cannot even see. Worse, the fields carry a LIGHT wash
   (rgba(242,239,233,.06)) that lightens the ground further under light text,
   so the placeholder collapses to 1.59:1 long before anything else complains.

   So three things move together, and they are a set:
     · panel to .66, transparent enough to read as glass
     · the mute greys go to --on-ink, since the hierarchy they bought is not
       worth the contrast they cost once the ground is variable
     · the field wash INVERTS, light to dark (.55 ink), so each field carries
       its own ground and the art shows between the fields, not through them

   Measured against the real boxes at 1425 wide, worst-case (lightest) pixel.
   The two pages land within 0.01 of each other because the peak behind both
   forms is blown-out sky, not the landmark:
     Edinburgh (725 tall): labels 5.53 · h3 5.53 · link 7.61
                           placeholder 5.57 · typed text 10.70
     Glasgow   (692 tall): labels 5.53 · h3 5.53 · link 8.20
                           placeholder 5.57 · typed text 10.70
   Dropping the panel to .58 fails labels and h3 at 4.22:1 on both, so .66 is
   near the floor already. Re-measure before lowering it.

   No backdrop blur: it measured no better (the bright region behind the form
   is broad sky, so averaging does not lower the peak) and it would soften the
   landmark, which is the whole point of doing this. */
.hero .quote-form {
  background: rgba(20, 18, 15, .66);
}
.hero .quote-form .quote-form__intro,
.hero .quote-form label,
.hero .quote-form .form-note { color: var(--on-ink); }
.hero .quote-form input,
.hero .quote-form textarea { background: rgba(20, 18, 15, .55); }
.hero .quote-form input::placeholder,
.hero .quote-form textarea::placeholder { color: #B5AEA1; }
.hero .quote-form label .opt,
.hero .quote-form .form-note a { color: #B9C8FF; }

/* What the dark hero forces, and nothing more. */
/* The Edinburgh hero opens on a breadcrumb. --mute and --accent are both
   dark-on-light tokens and sit at roughly 1.5:1 over the scrimmed art.
   Scoped to .hero so the breadcrumb on /how-much-does-a-website-cost/, which
   is on a plain prose page, is untouched. */
.hero .crumbs { color: var(--on-ink); }
.hero .crumbs a { color: var(--accent-photo); }
.hero .hero__eyebrow { color: var(--on-ink); }
.hero .hero__eyebrow b { color: var(--accent-photo); }
/* The card is glass on a photo — it needs an edge to read as a panel rather
   than a hole punched in the picture, and a stronger one than --line-ink
   (.16) now that the art shows through the fill rather than being hidden by
   it. NOTE this rule is declared after the glass block above and at the same
   specificity, so the border belongs here; setting border-color up there is
   silently overridden. */
.hero .quote-form { border: 1px solid rgba(242, 239, 233, .26); }
/* WhatsApp secondary was ink-on-paper and would vanish here. */
.hero .btn.secondary { --btn-bg: var(--paper); --btn-fg: var(--ink); }
.hero .btn.secondary:hover { --btn-bg: #fff; }

/* Two columns: all copy left, the form right. Sized so the headline never
   crosses into the form's column.
   The right column used to hold the laptop and was a straight 1fr, which is
   too wide for a form — inputs stop looking like inputs past about 440px, and
   the copy is the thing that should take the extra room. */
.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 440px);
  gap: clamp(32px, 4vw, 60px);
  /* Top, not centre. Centring the form inside the taller copy column pushed it
     down by half the difference — about 93px — which is the margin between the
     form clearing the fold on a 1366x768 laptop and not. The space it leaves
     below is artwork, not emptiness. */
  align-items: start;
}

.crumbs {
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; color: var(--mute);
  margin-bottom: 1.5em;
}
.crumbs a { color: var(--accent); text-decoration: none; }
.crumbs a:hover { text-decoration: underline; text-underline-offset: 3px; }
.crumbs span { margin: 0 .45em; opacity: .55; }

.hero__title {
  font-size: var(--fs-display);
  /* The one place the site goes bold. At 500 it read as a paragraph rather
     than a statement, which is the opposite of what this hero is for. */
  font-weight: 700;
  /* -.042em was over-tightened. Display type does want negative tracking, but
     -.042em at 700 weight jammed the letterforms — bold Instrument Sans is
     already wide and close-fitting, so the usual "bigger = tighter" rule had
     been pushed a step too far. -.03em sits in the normal display range and
     stays near the site's own h1 default of -.028em. The font's kern pairs are
     intact (verified: font-kerning normal vs none measurably differs), so this
     is tracking only and leaves kerning to the font. */
  letter-spacing: -.03em;
  line-height: 1.02;
  margin: 0 0 clamp(24px, 2.8vw, 34px);
}
/* fit-content so the typing reveal stops at the end of the words, not at the
   end of the column. clip-path is paint-only, so nothing here shifts layout. */
.hero__title .l { display: block; width: fit-content; max-width: 100%; }
.hero__title .l > span { display: block; }

.tw { display: block; }
.tw--hook  { color: var(--on-ink); }
.tw--offer { color: var(--accent-photo); margin-top: .46em; }

/* Wider measure than the default .lead so this sits on two lines and the
   buttons come up to meet the headline. */
.hero__lead { max-width: 48ch; margin-bottom: 26px; color: var(--on-ink); }
/* The free preview used to be the ask. It is now the promise made alongside
   it — same offer, but it stops being a hurdle in front of the button and
   becomes a reason to press it. Sits directly under the buttons so it reads
   as a condition of the click, not as a separate paragraph. */
.hero__promise {
  margin-top: 14px; font-size: var(--fs-small); line-height: 1.5;
  color: var(--on-ink); max-width: 44ch;
}
.hero__promise strong { font-weight: 500; color: var(--on-ink); }

/* The offer, stated at full size. Every competitor leads on either a low
   monthly or a low headline fee, so the three facts that actually differ have
   to land before the visitor starts comparing — this replaces the single quiet
   line that used to sit under the buttons.
   No opacity anywhere: this sits on a photograph, and dimming text is the one
   thing that reliably breaks contrast here. Hierarchy comes from size and from
   the mono/sans pairing instead. */
.hero__offer {
  display: flex; flex-wrap: wrap;
  gap: clamp(18px, 2.6vw, 40px);
  margin: 26px 0 32px;
  padding-top: 22px;
  border-top: 1px solid var(--line-ink);
  list-style: none;
}
.hero__offer li { display: flex; flex-direction: column; gap: 6px; }
.hero__offer b {
  font-size: clamp(1.1875rem, 1.9vw, 1.5rem);
  font-weight: 700; letter-spacing: -.025em; line-height: 1;
  color: var(--on-ink);
}
/* All three at one size. The price used to be larger, which pushed its label
   below the other two and made the row read as misaligned rather than
   emphasised — the three facts carry more weight as a set than as a hierarchy.
   The accent stays on the price; only the size is levelled. */
.hero__offer li:first-child b { color: var(--accent-photo); }
.hero__offer span {
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .13em; text-transform: uppercase; color: var(--on-ink);
}
/* The three cells measure 88 + 137 + 120px, which cannot fit 350px of usable
   width — it wrapped 2 + 1 and read as a mistake. Stacked, each promise gets
   its own line, which is worth the ~60px on a hero that is already tall. */
@media (max-width: 560px) {
  .hero__offer { flex-direction: column; gap: 15px; }
}

/* Kept as a safe landing for #quote-hero. Nothing on the site jumps here any
   more — the hero button was repointed at #quote (the full form section) on
   30 Aug, because on desktop the hero form is already on screen beside the
   button and the jump appeared to do nothing. The id is still a valid target,
   so this keeps the sticky header off the first field if anything links to it.
   The .band scroll-margin cannot cover it: this is a div inside a band. */
.hero__form { scroll-margin-top: 88px; }

/* Hero form runs to the foot of the copy column (30 Aug 2026).
   Michael: "stand this form lower ... so that we can get an alignment with the
   buttons on the left." At 1440 the form's bottom sat 29px above the buttons,
   but that number is not fixed — the left column's height moves with how the
   headline and lead wrap, so nudging the form down by a constant would line up
   at one width and be wrong at every other.
   Stretching the row instead makes the alignment hold at any width, and the
   "about your business" box takes the slack, which is where Michael wanted the
   extra height to go. Desktop only: below 900px the grid is a single column
   and there is no second column to align to. */
@media (min-width: 901px) {
  .hero__grid { align-items: stretch; }

  /* Whichever column is taller sets the row height, and BOTH have to be able
     to give, or the alignment only holds at the widths where the copy happens
     to be the taller one. Measured before this was added: at 1440 the copy ran
     29px past the form, but at 1200 and 1024 the FORM ran 42px and 46px past
     the copy — stretch alone cannot fix that, because it never shrinks an item
     below its content height. So the form grows through the textarea, and the
     copy column pushes its buttons down to meet it. */
  .hero__form { display: flex; }
  .hero__form .quote-form { display: flex; flex-direction: column; width: 100%; }
  .hero__form .quote-form .field--grow { display: flex; flex-direction: column; flex: 1 1 auto; }
  /* min-height would otherwise win over the flex basis and stop it growing. */
  .hero__form .quote-form .field--grow textarea { flex: 1 1 auto; min-height: 62px; }

  .hero__copy { display: flex; flex-direction: column; }
  .hero__copy .actions { margin-top: auto; }
}

/* Edinburgh's hero was 33px taller than Glasgow's, which put its buttons 33px
   lower and dropped them under the fold on shorter screens. The cause is the
   breadcrumb, which only that page carries: 17px of type plus its 1.5em
   margin. Nothing to do with the form or the stretch.
   Absorbed into the hero's own top padding so the two heroes finish the same
   height. Desktop only — below 900px the grid is one column, the fold
   argument does not apply, and the padding has nothing spare to give. */
@media (min-width: 901px) {
  .page-edinburgh .hero { padding-top: max(4px, calc(clamp(22px, 2.8vw, 44px) - 22px)); }
  .page-edinburgh .hero .crumbs { margin-bottom: .5em; }
}

@media (max-width: 900px) {
  .hero__grid { grid-template-columns: 1fr; align-items: start; gap: clamp(34px, 6vw, 52px); }
  /* ⚠ The copy column goes full width here, so the text runs to ~95% of the
     hero and the desktop plate (which feathers out at 78%) leaves the end of
     every line sitting on bright, unscrimmed art. Measured at 375x1283 the
     plate has to go flat and cover the lot. Flat .72 holds on-ink at 7.05:1
     and accent-photo at 5.29:1; .68 already fails accent-photo at 4.54:1. */
  .hero::before {
    background-image:
      linear-gradient(rgba(9, 11, 24, .72), rgba(9, 11, 24, .72)),
      var(--hero-art);
    background-size: cover, cover;
    background-position: center, var(--hero-art-pos) center;
  }
  /* Full-width column here, so the headline can go considerably larger. */
  :root { --fs-display: clamp(1.625rem, 7.5vw, 3.125rem); }
}

/* ---- 8b. The laptop ------------------------------------------------- */
/* ⚠ UNUSED as of 2026-08-28. The hero's right column is the quote form now,
   and no page in the repo carries .hero__figure / .device / .shot-cap any
   more (the proof section already led with the same LCS screenshot, so the
   hero copy of it was a duplicate). Kept rather than deleted in case the
   stage is wanted again — about 95 lines and ~2KB. Safe to remove. */
/* Drawn, not photographed — stays sharp at any density and keeps the
   screenshot as a separate, swappable asset. */
.device { position: relative; width: 100%; }
/* Contact shadow on the ground beneath it. */
.device::after {
  content: '';
  display: block;
  height: clamp(20px, 2.6vw, 34px);
  margin: 0 7%;
  background: radial-gradient(52% 100% at 50% 0%,
              rgba(11, 28, 84, .34) 0%, rgba(11, 28, 84, .16) 42%, rgba(11, 28, 84, 0) 74%);
}
.device__lid {
  position: relative;
  border-radius: 12px;
  padding: 11px 11px 15px;
  background: linear-gradient(158deg, #3C3D42 0%, #1C1D20 52%, #26272B 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .22),
    0 0 0 1px rgba(20, 18, 15, .3),
    0 8px 16px -8px rgba(11, 28, 84, .3),
    0 34px 58px -22px rgba(11, 28, 84, .48);
}
.device__cam {
  position: absolute; top: 4px; left: 50%; transform: translateX(-50%);
  width: 5px; height: 5px; border-radius: 50%;
  background: rgba(255, 255, 255, .3);
}
.device__screen {
  border-radius: 4px; overflow: hidden; background: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .3);
}
.device__screen img { width: 100%; }

.device__base {
  position: relative;
  width: 111%; margin-left: -5.5%;
  height: clamp(9px, .95vw, 13px);
  border-radius: 0 0 9px 9px;
  background: linear-gradient(#D7DAE0 0%, #B6BBC3 40%, #8F949D 76%, #6D727B 100%);
  box-shadow: 0 16px 26px -12px rgba(11, 28, 84, .5);
}
.device__base span {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 13%; height: 40%;
  border-radius: 0 0 7px 7px;
  background: rgba(20, 18, 15, .17);
}

/* Browser frame — the proof screenshots live in one */
.browser-frame {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
}
.browser-frame .bar {
  display: flex; align-items: center; gap: 6px;
  padding: 10px 14px;
  background: #EFECE6;
  border-bottom: 1px solid var(--line-2);
}
.browser-frame .bar i {
  width: 9px; height: 9px; border-radius: 50%;
  background: rgba(20, 18, 15, .16);
}
.browser-frame .bar .url {
  margin-left: 10px;
  font-family: var(--mono); font-size: 10px; letter-spacing: .04em;
  color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.browser-frame img { width: 100%; }

figure.hero__figure { margin: 0; }
.shot-cap {
  /* clear of the laptop's contact shadow — the 11px mono was landing in the
     falloff and dropping to ~3:1 */
  margin-top: 30px;
  font-family: var(--mono); font-size: var(--fs-micro);
  letter-spacing: .12em; text-transform: uppercase; color: var(--mute);
}
.shot-cap a { color: var(--accent); text-underline-offset: 3px; }
/* This caption alone sits over the swirl. Measured worst-case ground there is
   rgb(152,181,231): --mute gives 2.7:1 and --accent 3.1:1, both failing, so the
   hero caption drops to ink (8.9:1 desktop, 13:1 mobile) and the link carries an
   underline instead of colour. */
.hero__figure .shot-cap { color: var(--text); }
.hero__figure .shot-cap a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- 9. Fact strip (ink) -------------------------------------------- */
/* The hairlines between the cells are BORDERS ON THE CELLS, not a light grid
   background showing through 1px gaps. The cells carry data-reveal, so until
   they are revealed their opacity is 0 — and with the old technique that left
   the grid's own --line-ink background as a pale block sitting inside a dark
   band, which read as a broken, half-loaded strip on first paint. Borders
   fade out with the cell they belong to, so an unrevealed strip is simply
   uniform --ink. See app.js §1 for the other half of this: anything on the
   first screen is revealed at load rather than waiting for a scroll. */
.facts {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0;
  background: var(--ink);
}
.facts .fact {
  background: var(--ink);
  border-left: 1px solid var(--line-ink);
  padding: clamp(24px, 3.2vw, 38px) clamp(16px, 2.2vw, 28px);
}
.facts .fact:first-child { border-left: 0; }
.facts .fact .n {
  display: block;
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .16em; text-transform: uppercase; color: var(--accent-lift);
  margin-bottom: 1em;
}
.facts .fact .v {
  display: block;
  font-size: clamp(1rem, 1.5vw, 1.1875rem);
  font-weight: 500; letter-spacing: -.024em; line-height: 1.25;
  color: var(--on-ink);
}
/* The two-column block is a RANGE, not a max-width. The single-column rules
   below cannot simply say `.fact { border-left: 0 }` to undo it — that is
   (0,2,0) against the 2-col `:nth-child(even)` rule's (0,3,0) and loses, which
   left a stray hairline down the left edge of cells 2 and 4 at 390px. */
@media (min-width: 421px) and (max-width: 780px) {
  .facts { grid-template-columns: repeat(2, 1fr); }
  .facts .fact:nth-child(odd)  { border-left: 0; }
  .facts .fact:nth-child(even) { border-left: 1px solid var(--line-ink); }
  .facts .fact:nth-child(n+3)  { border-top: 1px solid var(--line-ink); }
}
@media (max-width: 420px) {
  .facts { grid-template-columns: 1fr; }
  .facts .fact { border-left: 0; }
  .facts .fact:nth-child(n+2) { border-top: 1px solid var(--line-ink); }
}

/* ---- 10. Two-column section shell ----------------------------------- */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
}
@media (max-width: 900px) { .split { grid-template-columns: 1fr; } }

/* "Ready to get started?" — the pay-now route, closing the steps band rather
   than opening a band of its own. A new band here would put two paper tones
   back to back and break the alternation the whole page is built on. It is
   deliberately quieter than the hero CTA: the free preview stays the main
   road, this is the short way round for people who have already decided. */
.start-now {
  margin-top: clamp(40px, 5vw, 64px);
  padding-top: clamp(28px, 3.5vw, 40px);
  border-top: 1px solid var(--line-ink);
}
.start-now h3 { font-size: var(--fs-h3); margin-top: .25em; }
.start-now > div > p:not(.tag):not(.small-print) {
  margin-top: .7em; font-size: var(--fs-body); line-height: 1.55;
  color: var(--on-ink-mute); max-width: 46ch;
}

/* Honeypot. Off-screen rather than display:none — some bots skip hidden
   inputs, and a real user must never be able to tab into it. */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px; height: 1px;
  overflow: hidden;
}

/* ---- 11. Video facade ----------------------------------------------- */
.video-facade {
  position: relative; display: block; width: 100%;
  padding: 0; border: 1px solid var(--line-ink); border-radius: var(--r-panel);
  background: var(--ink); overflow: hidden; cursor: pointer;
  transition: transform .3s var(--ease);
}
.video-facade:hover { transform: translateY(-3px); }
/* Fixed box so swapping the poster for the player never shifts the layout,
   whatever aspect the finished film turns out to be. */
.video-facade { aspect-ratio: 16 / 9; }
.video-facade img { width: 100%; height: 100%; object-fit: cover; filter: saturate(.9); }
/* Sits low-left, clear of the poster's face — the face is the trust signal. */
.video-facade .play {
  position: absolute; left: 18px; bottom: 62px;
  width: 64px; height: 64px; border-radius: 50%;
  background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center;
  transition: transform .3s var(--ease);
}
.video-facade .play svg { width: 28px; height: 28px; margin-left: 3px; }
.video-facade:hover .play { transform: scale(1.09); }
.video-facade .video-label {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 16px 18px;
  background: linear-gradient(to top, rgba(20, 18, 15, .94), rgba(20, 18, 15, 0));
  font-family: var(--mono); font-size: var(--fs-micro);
  letter-spacing: .1em; text-transform: uppercase; color: var(--on-ink);
  text-align: left;
}
.video-el {
  width: 100%; aspect-ratio: 16 / 9; object-fit: contain;
  border-radius: var(--r-panel); background: #000;
}
/* Same box when the player is a YouTube iframe rather than a <video>. */
iframe.video-el { display: block; border: 0; }
.video-fallback {
  padding: 26px; border: 1px solid var(--line); border-radius: var(--r-panel);
  background: var(--surface); font-size: var(--fs-small); line-height: 1.6;
}
.video-fallback strong { font-weight: 500; }
.video-fallback a { color: var(--accent); }

.transcript { margin-top: 16px; }
.transcript summary {
  cursor: pointer; font-size: var(--fs-small); color: var(--text-soft);
  text-underline-offset: 3px; text-decoration: underline;
}
.transcript p { margin-top: 10px; font-size: var(--fs-small); color: var(--mute); }

/* ---- 12. Checks ----------------------------------------------------- */
.checks { display: grid; margin-top: 30px; }
.checks li {
  position: relative;
  padding: 15px 0 15px 34px;
  border-top: 1px solid var(--line);
  font-size: var(--fs-small); line-height: 1.5; color: var(--text-soft);
}
.checks li:last-child { border-bottom: 1px solid var(--line); }
.checks li strong { display: block; font-weight: 500; color: var(--text); }
.checks li a { color: var(--accent); }
.checks li::before {
  content: ''; position: absolute; left: 1px; top: 21px;
  width: 15px; height: 8px;
  border-left: 1.5px solid var(--accent);
  border-bottom: 1.5px solid var(--accent);
  transform: rotate(-45deg);
}
.band--ink .checks li { border-color: var(--line-ink); color: var(--on-ink-mute); }
.band--ink .checks li:last-child { border-bottom-color: var(--line-ink); }
.band--ink .checks li strong { color: var(--on-ink); }
.band--ink .checks li::before { border-color: var(--accent-lift); }

/* ---- 12b. The deal panel — the arithmetic, stated plainly ----------- */
/* Stretches to the height of the copy column and spreads its own rows to fill,
   so the two halves of this section read as a matched pair. */
#guarantee .split { align-items: stretch; }

/* Video band proportions (markups rev1, 30 Aug 2026: "adjust proportions in
   this one"). Two things were off. The columns were near enough 1:1, so the
   poster — the only picture in the band, and the thing the section exists to
   get pressed — came out no bigger than the paragraph beside it. And .split
   top-aligns, which left the shorter video column hanging off the top edge
   with the imbalance pooling underneath it. Widening the media column and
   centring the pair fixes both without touching .split, which #guarantee and
   the offer pages also use. */
#video .split {
  grid-template-columns: minmax(0, .92fr) minmax(0, 1.38fr);
  align-items: center;
}
@media (max-width: 900px) { #video .split { grid-template-columns: 1fr; } }
.deal {
  display: flex;
  flex-direction: column;
  /* Rows stack from the top so the payment schedule sits directly under the
     £500 rather than being pushed down to fill the column height. */
  justify-content: flex-start;
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
  padding: clamp(24px, 3vw, 34px);
  background: var(--paper);
}
.deal__row { padding-block: clamp(18px, 2.2vw, 24px); border-top: 1px solid var(--line); }
.deal__row:first-of-type { border-top: 0; padding-top: 0; }
.deal__when {
  display: block;
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--mute);
}
.deal__amt {
  display: block; margin: .18em 0 .3em;
  font-size: clamp(2.25rem, 5vw, 3.25rem); font-weight: 500;
  letter-spacing: -.045em; line-height: 1;
}
.deal__row:first-of-type .deal__amt { color: var(--accent); }
.deal__note { font-size: var(--fs-small); line-height: 1.5; color: var(--text-soft); max-width: 36ch; }

.deal__totals {
  margin: 0; padding-top: clamp(18px, 2.2vw, 24px);
  border-top: 1px solid var(--line);
  display: grid; gap: 10px;
}
.deal__totals > div { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
.deal__totals dt { font-size: var(--fs-small); color: var(--text-soft); }
.deal__totals dd {
  margin: 0;
  font-family: var(--mono); font-size: 1rem; font-weight: 500;
  letter-spacing: -.01em; color: var(--text);
}
.deal__totals dd.is-none { color: var(--accent); }
/* The schedule now sits inside a row that already draws its own hairline,
   so it must not draw a second one. */
.deal__row .deal__totals { border-top: 0; padding-top: 12px; }
/* A row with no big number: the label would otherwise butt into the prose. */
.deal__when + .deal__note { margin-top: .6em; }
/* The pay-now row. This is the page's second call to action and it sits inside
   the panel where the arithmetic is, because that is where someone who has
   decided is actually looking. */
.deal__cta .btn { margin-top: 14px; }
.deal__cta .deal__note { margin-top: 12px; }

/* ---- 13. Itemised list (what's included) ---------------------------- */
.items { border-top: 1px solid var(--line); }
.items li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px clamp(16px, 3vw, 48px);
  align-items: baseline;
  padding: clamp(22px, 2.6vw, 30px) 0;
  border-bottom: 1px solid var(--line);
}
.items li h3 { font-size: var(--fs-h3); letter-spacing: -.028em; }
.items li p {
  grid-column: 1 / 2;
  font-size: var(--fs-small); line-height: 1.55; color: var(--text-soft);
  max-width: 58ch;
}
.items .amount {
  grid-row: 1; grid-column: 2;
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--mute);
  white-space: nowrap;
}
.items .addon .amount { color: var(--accent); }
.items .total { border-bottom: 0; padding-top: clamp(28px, 3vw, 40px); }
.items .total h3 { font-size: clamp(1.5rem, 3vw, 2.25rem); }
.items .total .amount {
  font-family: var(--sans); font-size: clamp(1.75rem, 4vw, 3rem);
  font-weight: 500; letter-spacing: -.04em; text-transform: none;
  color: #fff; background: var(--accent);
  padding: .02em .16em .09em; border-radius: .06em;
}

.mid-cta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 20px;
  margin-top: clamp(34px, 4vw, 52px);
}
.mid-cta span { font-size: var(--fs-small); color: var(--mute); }
.band--ink .mid-cta span { color: var(--on-ink-mute); }

/* ---- 14. How it works (ink) ----------------------------------------- */
.steps {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--line-ink);
  border-top: 1px solid var(--line-ink);
}
.steps .step {
  background: var(--ink);
  padding: clamp(26px, 3vw, 40px) clamp(18px, 2vw, 28px) clamp(30px, 4vw, 48px);
}
.steps .step .n {
  display: block;
  font-size: clamp(2.5rem, 5vw, 4rem); font-weight: 400;
  letter-spacing: -.055em; line-height: 1;
  color: var(--accent-lift);
  margin-bottom: .34em;
}
.steps .step .when {
  display: inline-block;
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink); background: var(--on-ink);
  padding: 5px 10px; border-radius: var(--r-pill);
  margin-bottom: 1.2em;
}
.steps .step h3 { font-size: var(--fs-h3); margin-bottom: .65em; }
.steps .step p { font-size: var(--fs-small); line-height: 1.55; color: var(--on-ink-mute); }
@media (max-width: 900px) { .steps { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .steps { grid-template-columns: 1fr; } }

/* ---- 15. Proof ------------------------------------------------------ */
.badge-live, .badge-demo {
  display: inline-block;
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase;
  padding: 6px 11px; border-radius: var(--r-pill);
}
.badge-live { background: var(--accent); color: #fff; }
.badge-demo { background: rgba(20, 18, 15, .07); color: var(--text-soft); }

.demos {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(20px, 2.2vw, 30px);
}
@media (max-width: 1040px) { .demos { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px)  { .demos { grid-template-columns: 1fr; max-width: 460px; } }
/* Cards stretch to the row height and the link is pushed to the bottom, so the
   links stay on one line across all four regardless of how long each blurb is. */
.demos figure { margin: 0; display: flex; flex-direction: column; }
.demos figcaption { display: flex; flex-direction: column; flex: 1; }
.demos figcaption .go { margin-top: auto; align-self: flex-start; }
.demos figure > .badge-demo,
.demos figure > .badge-live { display: inline-block; margin-bottom: 12px; }
/* The live screenshot and the example shots have different native aspects —
   normalise so the row of cards lines up. */
.demos .thumb {
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  background: var(--surface);
  aspect-ratio: 16 / 10;
}
.demos .thumb img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
  transition: transform .5s var(--ease);
}
.demos a:hover .thumb img { transform: scale(1.03); }
.demos figcaption { margin-top: 16px; }
.demos figcaption b { display: block; font-weight: 500; font-size: 1.0625rem; letter-spacing: -.022em; }
.demos figcaption span {
  display: block; margin: 7px 0 13px;
  font-size: var(--fs-small); line-height: 1.5; color: var(--text-soft);
}

/* ---- 16. Comparison table ------------------------------------------- */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-top: 1px solid var(--line);
}
table.compare {
  width: 100%; min-width: 680px;
  border-collapse: collapse;
  font-size: var(--fs-small);
}
table.compare th, table.compare td {
  text-align: left; vertical-align: top;
  padding: 16px 18px;
  border-bottom: 1px solid var(--line);
}
table.compare thead th {
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; color: var(--mute);
  padding-block: 15px;
}
table.compare tbody th { font-weight: 400; color: var(--text-soft); width: 24%; }
table.compare td { color: var(--text-soft); }
table.compare .us { background: var(--accent-wash); color: var(--text); }
table.compare thead th.us { color: var(--accent); }
table.compare td.us strong { font-weight: 500; color: var(--accent); }

/* ---- 17. Areas ------------------------------------------------------ */
.areas-copy p { color: var(--text-soft); max-width: 52ch; }
.areas-copy p + p { margin-top: 1em; }
.areas-copy a { color: var(--accent); }

.ticker {
  margin-top: clamp(48px, 6vw, 80px);
  overflow: hidden;
  border-block: 1px solid var(--line);
  padding-block: 18px;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.ticker-track {
  display: flex; gap: 34px; width: max-content;
  animation: drift 52s linear infinite;
}
.ticker-track span {
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .16em; text-transform: uppercase; color: var(--mute);
  white-space: nowrap;
}
@keyframes drift { to { transform: translateX(-50%); } }

/* ---- 17b. Care plan -------------------------------------------------- */
/* Deliberately late on the page, and deliberately quiet. It sits AFTER the
   comparison table so nobody meets a monthly figure before they have seen
   that £500 once beats "from £299" plus a mandatory £25.99 a month.
   The whole point of the card is the last line — cancel any time, site still
   yours. Every local competitor's monthly either ties you in (18 months) or
   takes the site down when you stop paying, so restraint here is the
   argument: it must not look like a plan we are pushing. */
/* The card has to read as a card on either band, because the two offer pages
   land it on different ones. (Until the 30 Aug markups rev1 re-order that was
   Glasgow on surface, Edinburgh on paper; Glasgow's #care now sits on paper
   too, but the rule stays keyed on the band so either can move again.) So it takes the opposite fill to whatever it sits on,
   the same trick .band--ink already plays with .checks. A white card on a
   white band is just a border. */
.care {
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
  background: var(--surface);
  padding: clamp(24px, 3vw, 34px);
}
.band--surface .care { background: var(--paper); }
.care__tag {
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .13em; text-transform: uppercase; color: var(--mute);
}
.care__price {
  display: flex; align-items: baseline; gap: 8px;
  margin: 10px 0 4px;
}
.care__price b {
  font-size: clamp(2.25rem, 4.6vw, 3rem); font-weight: 700;
  letter-spacing: -.03em; line-height: 1; color: var(--accent);
}
.care__price span { font-size: var(--fs-small); color: var(--text-soft); }
.care__note { font-size: var(--fs-small); line-height: 1.5; color: var(--text-soft); }
/* The one-line "who it's for", sitting between the price and the small print.
   Carries the positioning that separates the two plans: one is a queue you
   submit to, the other is a person you talk to. */
.care__who {
  margin: 2px 0 10px;
  font-size: var(--fs-small); line-height: 1.5;
  color: var(--text); font-weight: 500;
}
.care .checks { margin-top: 20px; }
.care__out {
  margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line);
  font-size: var(--fs-small); line-height: 1.55; color: var(--text);
}
.care__out strong { font-weight: 500; }

/* Two tiers (markups rev1, 30 Aug 2026: "this needs tiers — use pricing from
   the competitor"). Super Simple Websites runs four (£25.99 Core / £35.99
   Basic / £39.99 Standard / £65.99 Pro) and the rung between their first two
   is exactly "do you want content changes included". Michael asked for two, so
   that is the rung we cut on: £25 keeps the site alive, £35 also keeps it
   current. The £35 tier is the plan that was already on the page, unchanged,
   so nothing that quotes "£35" elsewhere goes stale.
   NOTE the £25 is deliberately close to the £25.99 the comparison table calls
   out as a competitor's COMPULSORY monthly. The distinction the page rests on
   is optional-and-cancellable, not cheaper, and the copy says so — but if that
   reads as muddying the argument, this is the one number to move. */
/* The intro under the care head. Paragraphs had no gap between them, so the
   two ran together as one slab while the .mt one below sat apart, which is
   what made the block read as badly proportioned. */
/* The care head carries ALL the section's copy in its right column now, not a
   headline over a separate block, so the text column wants to be the wider of
   the two rather than the narrower. Without this override .section-head.wide
   puts the bigger share on the heading, which leaves three paragraphs stacked
   in a narrow gutter. */
.care-intro { max-width: none; }
.care-intro p + p { margin-top: .9em; }
@media (min-width: 901px) {
  /* Must out-specify .section-head.wide (0,2,0) or the column split and the
     end-alignment it sets both win, which bottom-aligns the heading against a
     top-aligned three-paragraph column. */
  .section-head.wide.care-head {
    grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr);
    align-items: start;
  }
  .section-head.wide.care-head h2 { max-width: 16ch; }
  .section-head.wide.care-head > .care-intro { padding-bottom: 0; }
}
.care-tiers {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(20px, 2.4vw, 30px);
  margin-top: clamp(30px, 3.6vw, 44px);
  align-items: stretch;   /* two plan cards read as a pair, so square them off */
}
@media (max-width: 760px) { .care-tiers { grid-template-columns: 1fr; } }
/* The picked tier carries the accent edge rather than a loud fill: this
   section's whole argument is that it is not being pushed. */
.care--picked { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.care--picked .care__tag b { color: var(--accent-deep); font-weight: 500; }
/* Centred under the two cards. It applies to BOTH plans, so hanging it off the
   left edge made it read as a footnote to the first card rather than to the
   pair. auto margins centre the 78ch measure; text-align centres the lines
   inside it. */
.care-tiers + .care__out {
  margin: clamp(26px, 3vw, 36px) auto 0;
  max-width: 78ch;
  border-top: 1px solid var(--line);
  padding-top: 18px;
  text-align: center;
}

/* ---- 17c. City cards ------------------------------------------------- */
/* The first warmth on the page. Everything else here is plain type on paper,
   which is right for talking about money and wrong for the one section that is
   about *place* — a Glasgow tradesman should recognise where he is.
   Deliberately kept to the areas section rather than the hero: the art is loud
   and saturated, and it earns its place next to "where we work" in a way it
   would not behind a headline about red flags.
   Same frame as .demos .thumb (1px line, 12px radius, clipped) so it reads as
   the same system rather than a decoration bolted on. */
.cities {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(20px, 2.4vw, 32px);
  margin-top: clamp(34px, 4vw, 52px);
}
@media (max-width: 720px) { .cities { grid-template-columns: 1fr; max-width: 480px; } }
.city { margin: 0; }
.city__art {
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--surface);
}
/* The source is 900x502, so it is a downscale at every width — leave the
   browser to smooth it. `pixelated` is for upscaling true low-res art and
   would only alias these. */
.city__art img { display: block; width: 100%; height: 100%; object-fit: cover; }
.city figcaption { margin-top: 14px; }
.city figcaption b {
  display: block; font-weight: 500; font-size: 1.0625rem; letter-spacing: -.022em;
}
.city figcaption span {
  display: block; margin: 7px 0 0;
  font-size: var(--fs-small); line-height: 1.5; color: var(--text-soft);
}
.city figcaption .go { margin-top: 12px; }

/* ---- 18. FAQ -------------------------------------------------------- */
.faq { border-top: 1px solid var(--line); }
.faq details { border-bottom: 1px solid var(--line); }
.faq summary {
  position: relative;
  list-style: none; cursor: pointer;
  padding: 23px 52px 23px 0;
  font-size: clamp(1.0625rem, 1.7vw, 1.25rem); font-weight: 500; letter-spacing: -.024em;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::before,
.faq summary::after {
  content: ''; position: absolute; right: 8px; top: 50%;
  background: var(--text);
  transition: transform .28s var(--ease), opacity .28s ease;
}
.faq summary::before { width: 15px; height: 1.5px; margin-top: -.75px; }
.faq summary::after  { width: 1.5px; height: 15px; margin-top: -7.5px; right: 14.75px; }
.faq details[open] summary::after { transform: rotate(90deg); opacity: 0; }
.faq .a { padding: 0 clamp(20px, 8vw, 120px) 27px 0; }
.faq .a p { font-size: var(--fs-small); line-height: 1.65; color: var(--text-soft); max-width: 68ch; }
.faq .a p + p { margin-top: .85em; }
.faq .a a { color: var(--accent); }

/* ---- 19. Quote section ---------------------------------------------- */
.quote-form {
  background: var(--ink); color: var(--on-ink);
  border-radius: var(--r-panel);
  padding: clamp(26px, 3.4vw, 40px);
}
/* The panel is ALWAYS ink, so links inside it always need the on-ink blue.
   savvywebsite scopes this fix to `#quote` (see also the rules near the end of
   this file). This site renamed that section to `#book`, which silently
   dropped the fix and left the privacy link at --accent-lift on ink: measured
   2.30:1 against the 4.75 floor. Scoping to `.quote-form` instead of an id
   means renaming a section can never break it again. */
.quote-form .form-note a { color: #B9C8FF; }
.quote-form h3 { font-size: clamp(1.375rem, 2.4vw, 1.75rem); color: var(--on-ink); margin-bottom: 24px; }
/* The "+ Free plan" half of the form heading. It is emphasised by WEIGHT, not
   colour, and that is a measured decision rather than a stylistic one. The hero
   form is 66% glass over the pixel-art, and the pixel-art has white clouds in
   it, so the worst-case backdrop under this heading is near-white: composited,
   L 0.124. Against that the accent used for the privacy link (#B9C8FF) gives
   3.67:1 and --accent-photo (#C4CFFF) 3.94:1 — both under the 4.75:1 floor.
   Clearing it needs a foreground of L 0.777, which is lighter than any blue
   still recognisable as the accent. --on-ink is L 0.865 and gives 5.26:1 here
   and 6.55:1 on the #quote form, so the span keeps the heading's own colour and
   takes 700 to stand apart. Nowrap so the phrase breaks as a unit rather than
   orphaning "plan". */
.quote-form h3 .h3-plus { font-weight: 700; white-space: nowrap; }
.quote-form .field { margin-bottom: 16px; }
.quote-form label {
  display: block; margin-bottom: 8px;
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .13em; text-transform: uppercase; color: var(--on-ink-mute);
}
.quote-form label .opt { color: var(--accent-lift); }
.quote-form input, .quote-form textarea {
  width: 100%;
  background: rgba(242, 239, 233, .06);
  border: 1px solid var(--line-ink);
  border-radius: 10px;
  padding: 14px 16px;
  color: var(--on-ink);
  font-size: 1rem;
  transition: border-color .18s ease, background .18s ease;
}
.quote-form input::placeholder, .quote-form textarea::placeholder { color: #8F887C; }
.quote-form input:focus, .quote-form textarea:focus {
  outline: none;
  border-color: var(--accent-lift);
  background: rgba(242, 239, 233, .1);
}
.quote-form textarea { resize: vertical; min-height: 92px; }

/* The hero copy of the form. Same panel, same validation, same payload — it
   is simply shorter, because a hero form competes with the headline for
   height and loses the moment it needs scrolling to see the button. Three
   fields, no upload: .file-drop alone is 92px and is what makes the full
   form tall. Anyone with a logo to send uses the one at #quote. */
.quote-form--compact { padding: clamp(22px, 2.4vw, 28px); }
.quote-form--compact h3 { font-size: clamp(1.125rem, 1.7vw, 1.3125rem); margin-bottom: 6px; }
.quote-form--compact .quote-form__intro {
  font-size: var(--fs-small); color: var(--on-ink-mute); margin-bottom: 18px;
}
.quote-form--compact .field { margin-bottom: 11px; }
.quote-form--compact input, .quote-form--compact textarea { padding: 12px 14px; }
.quote-form--compact textarea { min-height: 62px; }
.quote-form--compact .form-note { margin-top: 12px; }

/* Field-level validation. Same warm red as the file-list error so there is
   one "something's wrong" colour on the form, not two. */
.field__err { display: none; }
.field__err.is-on {
  display: block; margin-top: 8px;
  font-size: var(--fs-small); line-height: 1.45; color: #FFB4A2;
}
.quote-form input[aria-invalid="true"] { border-color: #FFB4A2; }

/* File field: the native input is stretched invisibly over the box so the whole
   panel is clickable and accepts a drop, while staying keyboard-focusable. */
.file-drop {
  position: relative;
  display: flex; align-items: center;
  min-height: 92px;
  padding: 16px;
  border: 1px dashed rgba(242, 239, 233, .28);
  border-radius: 10px;
  background: rgba(242, 239, 233, .04);
  cursor: pointer;
  transition: border-color .18s ease, background .18s ease;
}
.file-drop input[type="file"] {
  position: absolute; inset: 0;
  opacity: 0; cursor: pointer;
}
.file-drop__text { display: block; font-size: var(--fs-small); line-height: 1.45; }
.file-drop__text b {
  display: block; font-weight: 500; color: var(--on-ink); margin-bottom: 3px;
}
.file-drop__text span { color: var(--on-ink-mute); }
.file-drop:hover,
.file-drop:focus-within { border-color: var(--accent-lift); background: rgba(242, 239, 233, .08); }
.file-drop:focus-within { outline: 3px solid var(--accent-lift); outline-offset: 3px; }

.file-list:not(:empty) {
  margin-top: 10px;
  font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: .06em;
  line-height: 1.7; color: var(--accent-lift);
}
.file-list.err { color: #FFB4A2; }
.quote-form .btn { margin-top: 6px; }
.form-note { margin-top: 14px; font-size: var(--fs-small); line-height: 1.5; color: var(--on-ink-mute); }
.form-note a { color: var(--accent-lift); }
/* Upload progress. Sits on the ink form panel, so the fill is --accent-lift
   (the accent that clears AA on ink) rather than --accent. */
.progress { display: none; margin-top: 14px; }
.progress.is-on { display: block; }
.progress__track {
  height: 6px; border-radius: var(--r-pill);
  background: var(--line-ink); overflow: hidden;
}
/* Scaled, not widened: animating width forces layout on every frame, whereas
   a transform is composited. transform-origin pins it to the left so it grows
   rightwards like a fill rather than from the centre. */
.progress__bar {
  display: block; width: 100%; height: 100%;
  transform: scaleX(0); transform-origin: left center;
  border-radius: inherit; background: var(--accent-lift);
  transition: transform .25s linear;
}
/* The upload itself cannot be measured (see app.js — xhr.upload listeners
   force a CORS preflight Apps Script can't answer), so it sweeps rather than
   showing an invented percentage. */
.progress__bar.is-sweeping {
  width: 42%;
  transform: none;
  transition: none;
  animation: progress-sweep 1.15s var(--ease) infinite;
}
@keyframes progress-sweep {
  0%   { transform: translateX(-105%); }
  100% { transform: translateX(345%); }
}
@media (prefers-reduced-motion: reduce) {
  .progress__bar { transition: none; }
  /* No sweep: a steady half-filled bar plus the status text still says
     "working" without moving anything. */
  .progress__bar.is-sweeping { width: 50%; animation: none; transform: none; }
}


.form-status:not(:empty) { margin-top: 12px; font-size: var(--fs-small); color: var(--accent-lift); }
.form-status.err { color: #FFB4A2; }
.quote-form.sent p { margin-top: 12px; font-size: var(--fs-small); line-height: 1.6; color: var(--on-ink-mute); }
.quote-form.sent a { color: var(--accent-lift); }

/* The success message sits INSIDE a dark form panel, but the panel sits inside
   a light band — so the band's link colour applies to it and wins.
   `.band--bleed a:not(.btn)` (0,2,1) ties with `.quote-form.sent a` (0,2,1)
   and is declared later, which turned the phone number --accent-deep: a dark
   blue on near-black, effectively invisible. Same trap for --on-ink-mute on
   the body copy, which is built for a solid panel, not glass.
   Both are pinned to --on-ink here, at a specificity the band rules cannot
   reach. Do not "simplify" these selectors back to .quote-form.sent. */
.hero .quote-form.sent p,
#quote .quote-form.sent p { color: var(--on-ink); }
.hero .quote-form.sent a,
#quote .quote-form.sent a {
  color: var(--on-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- 20. Outro + footer (ink) --------------------------------------- */
/* The Finnieston Crane and the Clyde Arc at dusk. This band used to be paper
   with the hero swirl on multiply; the art is a night scene, so the whole band
   inverted to light-on-dark and the secondary button had to invert with it
   (see §6).
   The scrim is not decoration and must not be lightened without re-measuring.
   Unscrimmed, the lit tenement windows on the right bank hit rgb(250,235,231)
   — effectively white — which puts --on-ink text at 1.01:1.
   Measured by hiding .cta-band > .wrap, capturing the band and sampling the
   real text boxes — NOT by eye and not over the whole band, which reads far
   darker than it needs to be because the worst pixels sit outside the type.
   At 1440px: h2 6.36:1, lead 9.96:1, buttons 10.1 and 10.8. The h2 is the
   binding one and .52 drops it to 4.87, which is under the site's 4.75 floor
   once you allow for the art's dithering — hence .60 and not lower.
   It also darkens as it falls so the band dissolves into the --ink-2 footer
   instead of butting a navy edge against a warm near-black one. */
.cta-band {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--ink);
  color: var(--on-ink);
  padding-block: clamp(64px, 8vw, 112px);
  text-align: center;
}
.cta-band::before {
  content: '';
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to bottom,
      rgba(12, 14, 32, .60) 0%,
      rgba(12, 14, 32, .70) 46%,
      rgba(16, 14, 22, .86) 84%,
      var(--ink-2) 100%),
    url('assets/outro-px.webp');
  background-size: cover, cover;
  background-position: center, center bottom;
  background-repeat: no-repeat, no-repeat;
}
.cta-band > .wrap { position: relative; z-index: 1; }
.cta-band h2 { font-size: var(--fs-h2); max-width: 17ch; margin-inline: auto; color: var(--on-ink); }
.cta-band p {
  margin: 22px auto 32px; max-width: 52ch;
  font-size: var(--fs-lead); line-height: 1.45; color: var(--on-ink);
}

/* Two CTAs, deliberately the same width. inline-grid shrink-wraps to content
   while grid-auto-columns:1fr forces both tracks equal, so the longer label
   ("Get my free preview") sets the width and "Start now" matches it. No magic
   numbers to go stale when the copy changes. */
.cta-pair {
  display: inline-grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 14px;
  justify-content: center;
}
/* Below this the pair is wider than a phone, so stack rather than shrink text */
@media (max-width: 560px) {
  .cta-pair { grid-auto-flow: row; width: 100%; max-width: 360px; }
}

footer.site {
  background: var(--ink-2); color: var(--on-ink-mute);
  padding-block: clamp(48px, 6vw, 76px) 32px;
  font-size: var(--fs-small);
}
footer.site .cols {
  display: grid; grid-template-columns: 1.6fr 1fr 1fr;
  gap: clamp(28px, 4vw, 56px);
}
@media (max-width: 760px) { footer.site .cols { grid-template-columns: 1fr; } }
footer.site .brand {
  font-size: 1.0625rem; font-weight: 700; letter-spacing: -.03em;
  color: var(--on-ink); margin-bottom: 12px;
}
/* accent-lift, not accent — #0B4CF0 on the near-black footer is unreadable */
footer.site .brand span { color: var(--accent-lift); }
footer.site p { max-width: 40ch; line-height: 1.6; }
footer.site h3 {
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--on-ink);
  margin-bottom: 14px;
}
footer.site ul { display: grid; gap: 10px; }
footer.site .mt { margin-top: 24px; }
footer.site a { color: var(--on-ink-mute); text-decoration: none; }
footer.site a:hover { color: var(--on-ink); text-decoration: underline; text-underline-offset: 3px; }
/* footer.site a (0,1,2) outranks .btn (0,1,0), so buttons in the footer would
   inherit the muted link colour and pick up the hover underline. Restate both. */
footer.site a.btn { color: var(--btn-fg); }
footer.site a.btn:hover { color: var(--btn-fg); text-decoration: none; }
/* Black-on-near-black is invisible down here — same swap the ink bands make */
footer.site .btn.secondary { --btn-bg: var(--paper); --btn-fg: var(--ink); }
footer.site .btn.secondary:hover { --btn-bg: #fff; }
/* The footer column is ~337px and the pair needs ~392px side by side, so the
   labels wrapped to two lines. Stack instead — still equal width, still both
   routes, and it holds at every breakpoint rather than flipping at one. */
.footer-cta {
  margin-top: 26px;
  grid-auto-flow: row;
  width: 100%;
  max-width: 232px;
  gap: 12px;
}
.footer-bottom {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px 24px;
  margin-top: clamp(36px, 5vw, 60px); padding-top: 24px;
  border-top: 1px solid var(--line-ink-2);
  font-family: var(--mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
}

/* ---- 21. Sticky mobile CTA ------------------------------------------ */
.sticky-cta { display: none; }
@media (max-width: 760px) {
  .sticky-cta {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 95;
    display: flex; gap: 8px;
    padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
    background: rgba(247, 245, 241, .95);
    backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
    border-top: 1px solid var(--line);
    transition: transform .3s var(--ease), opacity .3s ease;
  }
  .sticky-cta .btn { flex: 1; padding: 15px 16px; }
  .sticky-cta .btn.call { flex: 0 0 auto; padding-inline: 22px; }
  .sticky-cta.hide { transform: translateY(115%); opacity: 0; pointer-events: none; }
  body { padding-bottom: 78px; }
}

/* ---- 20b. Prose pages (terms, privacy, 404) -------------------------- */
.page-hero {
  background: var(--paper);
  padding-block: clamp(48px, 6vw, 88px) clamp(28px, 3.5vw, 44px);
  border-bottom: 1px solid var(--line);
}
.page-hero .eyebrow {
  display: block;
  font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--mute);
  margin-bottom: 1.1em;
}
.page-hero h1 {
  font-size: clamp(2rem, 4.2vw, 3.25rem);
  font-weight: 700; letter-spacing: -.038em; line-height: 1.04;
  max-width: 18ch;
}
.page-hero > .wrap > p {
  margin-top: .9em;
  font-size: var(--fs-lead); line-height: 1.45; color: var(--text-soft);
  max-width: 52ch;
}

.prose-band { background: var(--surface); }
.prose {
  max-width: 68ch;
  padding-block: clamp(40px, 5vw, 72px);
}
.prose h2 {
  font-size: clamp(1.25rem, 2.1vw, 1.625rem);
  letter-spacing: -.028em;
  margin: clamp(30px, 3.4vw, 46px) 0 .6em;
}
.prose h2:first-of-type { margin-top: 0; }
.prose p { font-size: var(--fs-body); line-height: 1.65; color: var(--text-soft); }
.prose p + p, .prose ul + p { margin-top: 1em; }
.prose a { color: var(--accent); }
/* .prose a beats .btn on specificity, which would paint accent text on an
   accent fill. Restore the button's own foreground. */
.prose a.btn { color: var(--btn-fg); }
.prose ul {
  display: grid; gap: 10px;
  margin: 1em 0 0;
  font-size: var(--fs-body); line-height: 1.6; color: var(--text-soft);
}
.prose ul li { position: relative; padding-left: 22px; }
.prose ul li::before {
  content: ''; position: absolute; left: 2px; top: .62em;
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
}
.prose ul li strong, .prose p strong { font-weight: 500; color: var(--text); }
.prose .updated {
  font-family: var(--mono); font-size: var(--fs-micro);
  letter-spacing: .12em; text-transform: uppercase; color: var(--mute);
  margin-bottom: clamp(24px, 3vw, 34px);
}
.prose .note {
  background: var(--accent-wash);
  border-radius: 12px;
  padding: clamp(18px, 2.2vw, 24px);
  font-size: var(--fs-small); line-height: 1.6; color: var(--text);
  margin-bottom: clamp(30px, 3.4vw, 44px);
}
.prose .note strong { font-weight: 500; }

/* ---- 21b. Floating WhatsApp ----------------------------------------- */
.wa-float {
  position: fixed; right: 20px; bottom: 20px; z-index: 96;
  min-height: 60px; padding: 0 22px 0 18px; border-radius: 999px;
  display: inline-flex; align-items: center; gap: 10px;
  background: var(--whatsapp); color: #fff;
  font-weight: 600; font-size: 1.0rem; letter-spacing: .1px; line-height: 1;
  text-decoration: none; white-space: nowrap;
  box-shadow: 0 12px 26px -8px rgba(11, 28, 84, .45), 0 2px 6px rgba(20, 18, 15, .18);
  transition: transform .22s var(--ease), background .18s ease;
}
.wa-float svg { width: 30px; height: 30px; flex: none; }
.wa-float__label { display: inline; }
.wa-float:hover { background: #1FBF5B; transform: translateY(-2px) scale(1.04); }
.wa-float:active { transform: translateY(0) scale(1); }

@media (max-width: 760px) {
  /* Lifted clear of the sticky CTA bar. */
  .wa-float {
    right: 14px;
    bottom: calc(86px + env(safe-area-inset-bottom));
    min-height: 54px; padding: 0 18px 0 15px;
    font-size: .95rem;
  }
  .wa-float svg { width: 27px; height: 27px; }
}

/* ---- 22. Motion ------------------------------------------------------
   Elements are visible by default. The `js` class is only added when
   IntersectionObserver exists AND reduced motion is off, so a JS failure,
   a blocked script or a reduced-motion preference all leave a fully
   readable static document.                                             */
.js [data-reveal="rise"]:not(.in) { opacity: 0; transform: translateY(20px); }
/* The wipe clip goes on the CHILD, never on the observed element itself.
   Chrome's IntersectionObserver honours an element's own clip-path, so an
   element clipped to zero area never reports as intersecting — it hides
   itself from the very observer that would reveal it and deadlocks forever.
   Measured: with the clip on the element, 0 of 1 wipe targets ever received
   `.in`; with it on the child, 1 of 1. (`rule`'s scaleX(0) does NOT have this
   problem — verified 3 of 3 either way — so it is left alone.) */
.js [data-reveal="wipe"]:not(.in) > * { clip-path: inset(0 0 101% 0); }
.js [data-reveal="rule"]:not(.in) { transform: scaleX(0); }

.js [data-reveal="rise"] {
  transition: opacity .8s var(--ease), transform .8s var(--ease);
  transition-delay: calc(var(--i, 0) * 65ms);
}
.js [data-reveal="wipe"] > * {
  transition: clip-path .95s var(--ease);
  transition-delay: calc(var(--i, 0) * 65ms);
}
.js [data-reveal="rule"] {
  transition: transform .9s var(--ease);
  transition-delay: calc(var(--i, 0) * 65ms);
}

/* The headline types itself in, line by line, like a message being written.
   clip-path with steps() gives the character-by-character advance; the text is
   fully present in the DOM the whole time, so screen readers and no-JS both
   get the complete sentence. */
.js .hero__title .l {
  clip-path: inset(0 100% 0 0);
  animation: typeIn calc(var(--ch) * 26ms) steps(var(--ch), end) var(--d, 0s) forwards;
}
@keyframes typeIn { to { clip-path: inset(0 0 0 0); } }

/* On the inner span, not on .l — as a sibling of a block child it formed its
   own line box and added a phantom 45px under the headline. */
.js .hero__title .l.is-last > span::after {
  content: '';
  display: inline-block;
  width: .07em; height: .74em;
  margin-left: .09em;
  vertical-align: -.04em;
  background: currentColor;
  opacity: 0;
  animation: caretBlink .9s steps(1) 1.18s infinite;
}
@keyframes caretBlink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

.js .hero__eyebrow,
.js .hero__lead,
.js .hero .actions,
.js .hero__promise,
.js .hero__offer,
.js .hero__form,
.js .hero__figure { animation: fadeRise .9s var(--ease) both; }
.js .hero__eyebrow { animation-delay: .05s; }
.js .hero__lead    { animation-delay: .34s; }
.js .hero .actions { animation-delay: .42s; }
.js .hero__offer   { animation-delay: .46s; }
.js .hero__promise { animation-delay: .56s; }
/* The form arrives with the headline rather than after the copy — it is the
   point of the page, not a footnote to it. */
.js .hero__form    { animation-duration: 1.1s; animation-delay: .3s; }
.js .hero__figure  { animation-duration: 1.1s; animation-delay: .3s; }

@keyframes fadeRise  { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .ticker-track { animation: none; }
}

/* ---- 22b. Art bands -------------------------------------------------- */
/* Two treatments, because the page has two kinds of band.

   .band--art  — for bands that are ALREADY dark. Image plus scrim, type stays
                 light. Same pattern as the hero and the outro, and the scrim
                 is sized by measuring the real text boxes, never by eye.

   .band--wash — for bands that are LIGHT and must stay light, because their
                 content cannot survive inversion: the itemised list and the FAQ
                 accordion both depend on ink on paper. Here the art is a faint
                 texture, not a picture. Ink over the bare art is 1.81:1, but
                 composited onto paper at low opacity the background stays light
                 enough for ink to clear the floor comfortably. The ceiling is
                 about .45 — past that the darkest sky drags ink under 4.75:1 —
                 so .2 leaves a wide margin and still kills the flatness. */
.band--art,
.band--wash { position: relative; isolation: isolate; overflow: hidden; }
.band--art > .wrap,
.band--wash > .wrap { position: relative; z-index: 1; }
.band--art::before,
.band--wash::before {
  content: '';
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
}
/* A strip along the bottom, not a full-section background, and the height is
   declared rather than inherited. Two reasons:
   1. These sections are 1700-1900px tall against art 744px tall, so `cover`
      scales the image to ~4000px wide and shows a hugely magnified patch of
      sky — no landmark, just clouds.
   2. With the pseudo spanning the whole section, the image's own top edge lands
      at whatever percentage the section height happens to make it, so there is
      no reliable place to put a fade and it cuts off as a hard horizontal line.
   Giving the strip a known height puts that edge at a known fraction, so the
   mask can dissolve it into the paper every time. */
.band--wash::before {
  top: auto; bottom: 0;
  height: clamp(300px, 44vw, 700px);
  opacity: .16;
  background-size: cover;
  background-position: center bottom;
  -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 42%, #000 100%);
          mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 42%, #000 100%);
}

/* Two tokens cannot survive a wash, and the fix is to go DARKER, not to weaken
   the art. --mute is the site's lightest ink and only clears 5.15:1 on bare
   paper — it needs a background of L>=0.840, which is barely darker than paper
   at all, so it drops to 3.70:1 over the wash. --accent at #0B4CF0 is a dark
   blue that reads against paper and falls to 3.44:1 here.
   Swapping them for --text-soft and --accent-deep restores the margin without
   touching the artwork; both are existing tokens.
   (The pills in "how it works" measured 1.00:1 in testing — that was an
   artefact of hiding the wrapper to photograph the background. .steps .step
   .when carries its own opaque --on-ink pill, so it is unaffected by anything
   behind it.) */
.band--wash .tag,
.band--wash .items .amount { color: var(--text-soft); }
.band--wash a:not(.btn) { color: var(--accent-deep); }

/* Kelvingrove behind "how it works". Already an ink band, so this only needs
   the scrim to hold the existing light type. */
#how.band--art::before {
  background-image:
    linear-gradient(to bottom, rgba(9, 11, 24, .88) 0%, rgba(9, 11, 24, .91) 100%),
    url('assets/kelvingrove.webp');
  background-size: cover, cover;
  background-position: center, center;
}
/* The University tower behind "what's included" — the tallest, emptiest section
   on the page. Wash only; the type here stays ink. */
#included.band--wash::before { background-image: url('assets/hero-px.webp'); }
/* George Square behind the FAQ. */
#faq.band--wash::before { background-image: url('assets/george-square.webp'); }

/* ---- 22b-i. Edinburgh's own landmarks -------------------------------
   The three rules above are keyed on id alone, so until 30 Aug 2026 the
   Edinburgh page was serving Kelvingrove behind "how it works", the Glasgow
   University tower behind "what's included" and George Square behind the FAQ.
   Three Glasgow landmarks on a page selling Edinburgh web design, and nothing
   in the markup said which city a band belonged to.

   Fixed by giving <body> a page class rather than duplicating the bands.
   Glasgow keeps the id-only rules; Edinburgh overrides the image only.

   Measured, since the artwork changed under type that was signed off against
   different pictures:
     #included, edinburgh-px wash .16 on white  — text 12.89:1, soft 6.55:1,
       links 5.60:1
     #faq, edinburgh-px wash .16 on paper       — text 11.87:1, soft 6.03:1,
       links 5.16:1
     #how, edinburgh-hero under the existing .88-.91 ink scrim —
       on-ink 12.70:1, on-ink-mute 4.86:1, accent-lift 6.45:1
   The tightest of those is on-ink-mute at 4.86:1 in #how, which clears 4.75
   but has little room: lighten that scrim and it is the first thing to break.

   Only two Edinburgh assets exist, so edinburgh-px does both washes. They sit
   at positions 6 and 12 and are faint bottom strips, so the repeat is quiet —
   but a third Edinburgh image would be the honest fix. */
.page-edinburgh #how.band--art::before {
  background-image:
    linear-gradient(to bottom, rgba(9, 11, 24, .88) 0%, rgba(9, 11, 24, .91) 100%),
    url('assets/edinburgh-hero.webp');
}
.page-edinburgh #included.band--wash::before { background-image: url('assets/edinburgh-px.webp'); }
.page-edinburgh #faq.band--wash::before      { background-image: url('assets/edinburgh-px.webp'); }

/* ---- 22c. Full-bleed art bands (markups rev1, 30 Aug 2026) -----------
   Michael asked for two sections to carry the art edge to edge rather than as
   the bottom strip .band--wash uses. Full bleed reintroduces the two problems
   the strip was invented to dodge, so both are handled explicitly:

   1. Aspect. The art is 900x502 against bands 900-1600px tall, so `cover`
      would crop to a magnified patch of sky with no landmark in it. Pinning
      background-position to `center bottom` keeps the skyline in frame and
      lets the empty sky take the overflow at the top, where the heading sits.
   2. Contrast. The strip could sit at .16 because it only ever met body copy.
      Edge to edge it also sits under headings, tables and the accordion, so
      the art is composited UNDER a paper veil rather than simply faded: the
      veil holds a known floor colour, and the opacity below was set by
      measuring, not by eye. Re-measure if you change either number. */
.band--bleed { position: relative; isolation: isolate; overflow: hidden; }
.band--bleed > .wrap { position: relative; z-index: 1; }
.band--bleed::before {
  content: '';
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center bottom;
  opacity: .18;
}
/* The wash fixes two named selectors. The veil sits under the WHOLE band, so
   anything using --mute can land on the darkest part of the art, not just the
   two the wash knew about. Measured over both images at .18 the darkest
   composite is rgb(203,201,198): --mute falls to 3.38:1, under the 4.75 floor,
   while --text-soft holds 5.74:1 and --text 11.29:1. --mute is only ever a
   text colour (14 uses, all `color:`), so redefining the token on the band is
   safe and catches every descendant rather than a hand-listed few. */
.band--bleed { --mute: var(--text-soft); }
.band--bleed a:not(.btn) { color: var(--accent-deep); }   /* 4.90:1 */

/* Ref B on the markups: Edinburgh Castle, full bleed behind the comparison. */
#compare.band--bleed::before { background-image: url('assets/edinburgh-px.webp'); }
/* The FAQ's George Square art, reused full bleed behind the care plan. */
#care.band--bleed::before { background-image: url('assets/george-square.webp'); }
/* Kelvingrove behind the quote section (Michael's pick, 30 Aug). Note it is
   ALSO the art in #how, which is the section immediately before this one — so
   the same building runs back to back, dark and heavily scrimmed there, light
   here. That is deliberate, not an oversight.
   Measured over the white surface band at .18: --text 12.26:1,
   --text-soft 6.23:1, --accent-deep 5.32:1. Note this band leans on the
   .band--bleed token swaps rather than clearing on its own — raw --mute is
   3.67:1 here and raw --accent 4.16:1, both under the floor. */
#quote.band--bleed::before { background-image: url('assets/kelvingrove.webp'); }

/* The quote form is glass too, so the Kelvingrove behind it reads (30 Aug).
   Same three-part treatment as the hero forms, but it CANNOT go as far. The
   hero panels sit on a darkened photo; this one sits on a WHITE band under a
   .18 wash, so thinning the panel drags it toward paper rather than toward
   ink, and the light type loses ground much faster. Measured against the real
   boxes at 1425x1029, worst-case (lightest) pixel:
     h3 6.58:1 · labels 6.62:1 · placeholder 6.16:1 · typed text 11.82:1
     file-drop 12.02:1 · privacy link 5.15:1
   The privacy link is the binding token here, not the labels: it is 4.82:1 at
   panel .70 and fails just below that, which is why this stops at .72 while
   the hero forms go to .66. Do not copy the hero numbers onto this one. */
#quote .quote-form {
  background: rgba(20, 18, 15, .72);
  border: 1px solid rgba(242, 239, 233, .26);
}
#quote .quote-form .quote-form__intro,
#quote .quote-form label,
#quote .quote-form .form-note,
#book .quote-form .form-note { color: var(--on-ink); }
#quote .quote-form input,
#quote .quote-form textarea { background: rgba(20, 18, 15, .60); }
#quote .quote-form input::placeholder,
#quote .quote-form textarea::placeholder { color: #B5AEA1; }
#quote .quote-form label .opt,
#quote .quote-form .form-note a,
#book .quote-form .form-note a { color: #B9C8FF; }
#quote .quote-form .file-drop { background: rgba(20, 18, 15, .60); }

/* ---- 23. Print ------------------------------------------------------- */
@media print {
  header.site, .sticky-cta, .ticker, .video-facade, .wa-float { display: none; }
  /* Both backdrops go. The outro's is a dark night scene with a scrim over it,
     so leaving it would print a black slab across the black text. */
  .hero::before, .cta-band::before { display: none; }
  body { background: #fff; padding-bottom: 0; }
  .band--ink, .cta-band, footer.site, .quote-form { background: #fff !important; color: #000 !important; }
  /* These two carry an explicit --on-ink, which beats the inherited colour above. */
  .band--ink h2, .band--ink h3, .quote-form h3,
  .cta-band h2, .cta-band p { color: #000 !important; }
  /* nothing may stay hidden in a printout */
  .js [data-reveal], .js [data-reveal] > * { opacity: 1 !important; transform: none !important; clip-path: none !important; }
}
