/* ---- tokens.css ---- */
/* ============================================================================
   TOKENS: SysTekk OneConnect, prototype style.

   Components use only these variables. No two tokens share a value.

   The blue is the client's, chosen by the operator on 2026-09-22: #395A83.
   Note it is NOT quite the logo's own blue, which measures #335F81 in both
   supplied PNGs — close enough to sit beside it, and a deliberate choice
   rather than a mismatch to fix.

   The stone is still the OneConnect portal's background, observed at
   oneconnect.systekk.ca/portal/ on 2026-09-21. Every other colour here is a
   proposal. Fonts and photography are still to come (CLIENT-QUESTIONS.md 6).
   ========================================================================= */

:root {
  /* ---- colour --------------------------------------------------------- */
  --line-blue: #395a83;        /* the brand blue: links, marks, diagram hub */
  --line-blue-deep: #2c4666;   /* link hover */
  --harbour: #1d3447;          /* headings; hero, banner and footer bands */
  --harbour-deep: #152838;     /* top bar, diagram well, footer bottom */
  --stone: #dedddc;            /* portal background: rules, borders, leaders */
  --stone-light: #efeeec;      /* alternate bands, photo wells */
  --paper: #ffffff;
  --slate: #43525f;            /* body copy */
  --slate-muted: #5f6c78;      /* small print */
  --mist: #c9d6e2;             /* text and lines on the dark bands */
  --answer: #1f7a4a;           /* the one action colour: every "Request a quote" */
  --answer-deep: #186139;      /* its hover */
  --lamp: #3fae6f;             /* a lit line lamp: "included", "this rings", a form sent */
  --alert: #b3392d;            /* 911, required-field marks, a form not sent */

  /* ---- TODO chrome: goes when the last client question is answered ---- */
  --todo-ink: #6b4300;
  --todo-bg: #fff4d6;
  --todo-line: #c98a12;

  /* ---- type ----------------------------------------------------------- */
  --font-text: "Atkinson Hyperlegible Next", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: "Archivo", "Atkinson Hyperlegible Next", system-ui, sans-serif;
  --display-stretch: 112%;     /* Archivo's width axis: wide, like a rate card */

  --fs-small: 0.875rem;                                      /* 14 */
  --fs-body: 1.0625rem;                                      /* 17 */
  --fs-lead: clamp(1.125rem, 1.05rem + 0.35vw, 1.3125rem);   /* 18 -> 21 */
  --fs-h3: 1.375rem;                                         /* 22 */
  --fs-h2: clamp(1.75rem, 1.3rem + 1.9vw, 2.625rem);         /* 28 -> 42 */
  --fs-banner: clamp(2rem, 1.45rem + 2.4vw, 3.25rem);        /* 32 -> 52 */
  --fs-h1: clamp(2.375rem, 1.5rem + 3.9vw, 4.25rem);         /* 38 -> 68 */

  --lh-text: 1.6;
  --lh-heading: 1.18;
  --lh-tight: 1.06;
  --measure: 66ch;

  /* ---- spacing -------------------------------------------------------- */
  --space-section: clamp(3.5rem, 2rem + 6vw, 6.5rem);
  --space-section-tight: clamp(2.5rem, 1.6rem + 3.6vw, 4rem);
  --gap: clamp(1.25rem, 0.9rem + 1.4vw, 2rem);

  --container: 1180px;
  --gutter: clamp(16px, 4vw, 32px);

  /* auto-fit grid minimums: columns drop away on their own */
  --col-min-2: 400px;
  --col-min-3: 280px;
  --col-min-4: 230px;
  /* The hardware catalogue's cards, which share their row with a 260px filter
     rail and so need to be narrower than --col-min-3 to still make three
     columns: 3 x 240 + 2 x 32 gap = 784, inside the 888px the rail leaves at
     1512px. At 280 only two fit. */
  --col-min-card: 240px;

  /* The width of the catalogue's filter rail. A token rather than a literal
     because two rules have to agree on it: the rail column itself, and the
     "Don't see what you're looking for?" line under the results, which starts
     where the results do. */
  --rail-w: 260px;

  /* Padding inside the site's panels — the bill sheets, the form cards, the
     plan cards and the hero's call diagram. They sit beside and under each other, so their text edges
     only line up if they share one value. */
  --pad-panel: clamp(20px, 3vw, 36px);

  /* ---- chrome --------------------------------------------------------- */
  --header-h: 80px;
  --header-h-stuck: 64px;
  --logo-w: clamp(170px, 14vw, 208px);

  --radius-control: 8px;       /* buttons, inputs, FAQ rows */
  --radius-panel: 14px;        /* the bill, cards, the hero diagram */
  --radius-pill: 999px;        /* badges */

  --shadow-stuck: 0 2px 16px rgba(21, 40, 56, 0.12);
  --shadow-drawer: -8px 0 40px rgba(21, 40, 56, 0.24);

  --transition: 0.2s ease;

  /* The drawer breakpoint. Media queries cannot read custom properties, so
     header.css and 20-nav.js repeat it literally — change all three together.

     1099px, not the 991px this started at. Measured 2026-09-22 at nine widths:
     with seven nav items and the "Request a quote" button, the button breaks
     out of the container by 64px at 992px and is still 5px over at 1060px. It
     first fits at 1100px. The drawer therefore has to cover the whole band
     below that, or the site's primary action hangs off the edge of the header
     on every page. */
  --bp-nav: 1099px;
}

/* ---- base/fonts.css ---- */
/* ---- self-hosted fonts ---------------------------------------------------
   Both are variable fonts from Fontsource (latin subset, which covers French
   accents). Archivo carries a width axis as well as weight; the headings use
   it wide. No font host is named in the CSP: font-src stays 'self'. */

@font-face {
  font-family: 'Atkinson Hyperlegible Next';
  src: url('/assets/fonts/atkinson-hyperlegible-next.woff2') format('woff2-variations'),
       url('/assets/fonts/atkinson-hyperlegible-next.woff2') format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Archivo';
  src: url('/assets/fonts/archivo.woff2') format('woff2-variations'),
       url('/assets/fonts/archivo.woff2') format('woff2');
  font-weight: 100 900;
  font-stretch: 62% 125%;
  font-style: normal;
  font-display: swap;
}

/* ---- base/reset.css ---- */
/* ---- reset & document defaults ------------------------------------------ */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--font-text);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-text);
  color: var(--slate);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img,
svg {
  max-width: 100%;
  height: auto;
  display: block;
}
img { border: 0; }

a {
  color: var(--line-blue);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color var(--transition);
}
a:hover { color: var(--line-blue-deep); }

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0 0 1rem; padding-left: 1.25rem; }

a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
summary:focus-visible {
  outline: 3px solid var(--line-blue);
  outline-offset: 2px;
}

/* On the navy surfaces the blue ring all but disappears: --line-blue is
   1.81:1 on --harbour (hero, page banner, CTA band, the pricing total bar)
   and 2.13:1 on --harbour-deep (top bar, footer), under the 3:1 a focus
   indicator needs. Paper is 12.85:1 and 15.08:1. Only the colour changes,
   and only inside these surfaces, so the light ones keep the blue ring
   (7.08:1 on paper, 6.11:1 on stone-light). :is() counts as its strongest
   argument, so this is (0,2,0) and beats a:focus-visible's (0,1,1) wherever
   it sits. The drawer is paper, not navy, so it is not in the list. A light
   card placed inside one of these bands would inherit the white ring: give
   it its own rule the day one exists. */
:is(.topbar, .hero, .page-banner, .cta-band, .site-footer, .builder-total) :focus-visible {
  outline-color: var(--paper);
}

.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-link {
  position: absolute;
  top: -60px;
  left: 12px;
  z-index: 2000;
  padding: 10px 18px;
  background: var(--harbour);
  color: var(--paper);
  font-weight: 700;
  transition: top var(--transition);
}
.skip-link:focus { top: 12px; color: var(--paper); }
/* Focused, the link sits across the top bar and the white header (y 12–59
   against a 40px bar), so a ring outside it is half on navy and half on
   white, and no single colour reads on both. Drawn inside the link's own
   navy box instead, a paper ring is 12.85:1 all the way round. */
.skip-link:focus-visible { outline-color: var(--paper); outline-offset: -5px; }

/* ---- base/typography.css ---- */
/* ---- typography ----------------------------------------------------------
   Archivo, set wide and heavy, for headings; Atkinson Hyperlegible Next for
   everything a visitor reads closely: prices, phone numbers, form labels.
   All heading sizes are fluid, so there is no font-size media query.
   -------------------------------------------------------------------------- */

h1, h2, h3, h4 {
  margin: 0 0 1rem;
  color: var(--harbour);
  font-family: var(--font-display);
  font-stretch: var(--display-stretch);
  font-weight: 800;
  line-height: var(--lh-heading);
  letter-spacing: -0.01em;
  /* Evens out the lines, so a heading does not end on one stranded word
     ("Find the right phone for your / business"). It moves the breaks, not
     the number of lines: measured on every page at four widths, no heading
     gained or lost a line. It will break at a typed hyphen, so a compound
     that must stay whole goes in a .nowrap span (utilities.css). */
  text-wrap: balance;
}
h3, h4 {
  font-family: var(--font-text);
  font-stretch: normal;
  font-weight: 700;
  letter-spacing: 0;
}

.section-title {
  max-width: 24ch;
  margin-bottom: 1rem;
  font-size: var(--fs-h2);
}

/* Running text avoids a one-word last line where it can. Where `pretty` is
   unsupported the text wraps as before. To avoid that last word it may break
   a compound at its typed hyphen ("work- / life"), which is ordinary in
   running text; a spaced em dash is kept off the start of a line by build.js,
   which joins it to the word before with a no-break space. */
p, li, figcaption { text-wrap: pretty; }

/* 52ch, not the 66ch --measure. ch is the width of the "0", and in Atkinson
   that is wider than the average letter, so at 1440px 66ch (898px) ran 84 to
   96 characters a line; 52ch (708px) runs 68 to 77. At that width it is also
   the width of the 24ch title above it (708px), so the two share a right
   edge. The two font sizes shrink at different rates, so narrower they part
   (703 against 696px at 1100, 597 against 656 at 768) until the container
   sets both. */
.section-intro {
  max-width: 52ch;
  font-size: var(--fs-lead);
  line-height: 1.55;
}

/* A second, plain paragraph under the intro had no cap at all and ran the
   full container, 1180px on one line. A classed one (.section-intro, .todo)
   keeps its own width. */
.section-head > p:not([class]) { max-width: var(--measure); }

.section-head { margin-bottom: clamp(2rem, 1.5rem + 2vw, 3rem); }

.lead { font-size: var(--fs-lead); line-height: 1.55; }

.small-print { font-size: var(--fs-small); color: var(--slate-muted); }

/* ---- base/layout.css ---- */
/* ---- layout primitives ---------------------------------------------------
   Grids use auto-fit + minmax(min(…, 100%)), so they reflow at every width
   and a column can never be wider than its container. No media queries.
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: calc(var(--container) + 2 * var(--gutter));
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.section { padding: var(--space-section) 0; }
/* A section that is a link target (#calculator, #extras, #ai on /pricing/)
   opens with its title, so a jump has to clear the sticky header: this puts
   the title 24px under the stuck header and its 1px rule at every width,
   whatever the section's top padding is there. Without it the title landed
   under the header on phones, where the padding is smaller than the header. */
.section[id] { scroll-margin-top: calc(var(--header-h-stuck) + 1px + 24px - var(--space-section)); }
.section--tight { padding: var(--space-section-tight) 0; }
.section--stone { background: var(--stone-light); }

.grid { display: grid; gap: var(--gap); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-2), 100%), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-3), 100%), 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-4), 100%), 1fr)); }

/* Two columns that stack on their own below ~760px. */
.split {
  display: grid;
  gap: clamp(2rem, 1rem + 4vw, 4rem);
  align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
}
.split--center { align-items: center; }

/* ---- components/button.css ---- */
/* ---- buttons ---------------------------------------------------------------
   One action colour. Every "Request a quote" on the site is the green of a
   phone's answer key, and nothing else is: green means "start here". */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 12px 24px;
  border: 0;
  border-radius: var(--radius-control);
  font-family: var(--font-text);
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--transition), color var(--transition), box-shadow var(--transition);
}

.btn--go { background: var(--answer); color: var(--paper); }
.btn--go:hover { background: var(--answer-deep); color: var(--paper); }

.btn--ghost {
  background: transparent;
  color: var(--line-blue);
  box-shadow: inset 0 0 0 2px var(--line-blue);
}
.btn--ghost:hover { background: var(--line-blue); color: var(--paper); }

/* For the dark bands: an outline that reads on navy. */
.btn--on-dark {
  background: transparent;
  color: var(--paper);
  box-shadow: inset 0 0 0 2px var(--mist);
}
.btn--on-dark:hover { background: var(--paper); color: var(--harbour); }

.btn[disabled] { opacity: 0.55; cursor: not-allowed; }
.btn[disabled]:hover { background: var(--answer); }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 16px;
  margin-top: 28px;
}
/* On a phone a pair that wraps would stack at two different widths, each
   its own label's length. Growing to fill the row gives each stacked button
   the full width, and a pair that still fits on one line reaches the row's
   right edge. */
@media (max-width: 480px) {
  .btn-row > .btn { flex: 1 1 auto; }
}

/* ---- components/topbar.css ---- */
/* ---- top bar: who runs the service, the portal, the other language ------- */

.topbar {
  background: var(--harbour-deep);
  color: var(--mist);
  font-size: var(--fs-small);
}

.topbar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 40px;
}

.topbar-note { margin: 0; }

.topbar-links {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-left: auto;
}

.topbar-links a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  color: var(--paper);
  font-weight: 700;
  text-decoration: none;
}
.topbar-links a:hover { color: var(--mist); text-decoration: underline; }

.login-link svg { flex: none; }

@media (max-width: 600px) {
  .topbar-note { display: none; }
}

/* ---- components/header.css ---- */
/* ---- header & navigation -------------------------------------------------
   1099px is the one structural breakpoint: the nav becomes a drawer. Media
   queries cannot read custom properties, so it is written out here and in
   20-nav.js.

   Stacking: the sticky header is z-index 1000, and it caps everything inside
   it, including the open drawer. The backdrop sits OUTSIDE the header, so it
   must be below 1000 or it covers the drawer (CLAUDE.md §8). It is 900.
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 1000;
  background: var(--paper);
  border-bottom: 1px solid var(--stone);
  transition: box-shadow 0.25s ease;
}
.site-header.is-stuck { box-shadow: var(--shadow-stuck); }

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  min-height: var(--header-h);
  transition: min-height 0.25s ease;
}
.site-header.is-stuck .header-inner { min-height: var(--header-h-stuck); }

.brand { display: block; flex: none; }
.brand img { width: var(--logo-w); height: auto; }

.nav-toggle {
  display: none;
  width: 44px; height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  color: var(--harbour);
}
.nav-toggle svg { margin: 0 auto; }
/* A hamburger while the drawer is shut, a cross while it is open. The label
   stays "Menu": whether it is open is aria-expanded's job, which 20-nav.js
   flips, so the icon keys off that attribute and cannot disagree with it.
   Without JavaScript it stays "false" and the hamburger shows. */
.nav-toggle-close { display: none; }
.nav-toggle[aria-expanded="true"] .nav-toggle-open { display: none; }
.nav-toggle[aria-expanded="true"] .nav-toggle-close { display: inline; }

.main-nav { margin-left: auto; }
.nav-list {
  display: flex;
  align-items: center;
  gap: clamp(14px, 1.6vw, 28px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav-list > li > a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--harbour);
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  box-shadow: inset 0 -3px 0 transparent;
  transition: color var(--transition), box-shadow var(--transition);
}
.nav-list > li > a:hover { color: var(--line-blue); }
.nav-list > li.is-active > a {
  color: var(--line-blue);
  box-shadow: inset 0 -3px 0 var(--line-blue);
}

.header-cta { flex: none; }
.nav-quote { display: none; }

.nav-backdrop {
  position: fixed;
  inset: 0;
  z-index: 900;
  background: rgba(21, 40, 56, 0.5);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s;
}

/* ---- drawer ------------------------------------------------------------ */
@media (max-width: 1099px) {
  /* Above the open drawer (1200), so the same button that opened it closes it. */
  .nav-toggle { display: inline-flex; align-items: center; justify-content: center; order: 3; position: relative; z-index: 1300; }
  .header-inner { min-height: 68px; }
  .header-cta { display: none; }

  .main-nav {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(320px, 86vw);
    z-index: 1200;
    /* 112px clears the toggle, which stays on top of the open drawer: at the
       top of the page it ends at y 96 (the 40px top bar, then centred in the
       68px bar), or 101 where the top bar's note wraps (about 601-650px).
       At 84 it covered the right end of the first link, so a tap there
       closed the drawer. */
    padding: 112px 24px 40px;
    background: var(--paper);
    transform: translateX(100%);
    /* Shut, the drawer is hidden as well as off-screen, or its eight links
       stay in the tab order as invisible stops. On closing, visibility waits
       out the 0.3s slide before it hides (CLAUDE.md §8). */
    visibility: hidden;
    transition: transform 0.3s ease, visibility 0s linear 0.3s;
    overflow-y: auto;
  }
  /* The shadow only when open: on the closed, off-screen drawer it shows as
     a dark band down the right edge of the page.
     Opening, visibility is not transitioned: it switches at once, so focus
     can move in straight away. Under reduced motion it takes about two
     frames (36ms measured), because the rule in utilities.css gives every
     element, the drawer's links included, a 0.01ms transition of whatever
     changes, inherited visibility too. Nothing moves focus in that soon. */
  .main-nav.is-open {
    transform: none;
    box-shadow: var(--shadow-drawer);
    visibility: visible;
    transition: transform 0.3s ease;
  }

  .nav-list { flex-direction: column; align-items: stretch; gap: 0; }
  .nav-list > li { border-bottom: 1px solid var(--stone); }
  .nav-list > li > a { width: 100%; padding: 8px 0; }
  .nav-list > li.is-active > a { box-shadow: inset 3px 0 0 var(--line-blue); padding-left: 12px; }

  .nav-quote { display: flex; margin-top: 24px; }

  .nav-backdrop.is-open { opacity: 1; visibility: visible; }
}

/* ---- components/page-banner.css ---- */
/* ---- inner-page banner: the page's h1 on the navy band, with breadcrumb --- */

.page-banner {
  padding: clamp(2.5rem, 1.6rem + 3.6vw, 4.5rem) 0 clamp(2.25rem, 1.5rem + 3vw, 3.75rem);
  background: var(--harbour);
  color: var(--mist);
}

.page-banner h1 {
  max-width: 22ch;
  margin: 0;
  color: var(--paper);
  font-size: var(--fs-banner);
  line-height: var(--lh-tight);
}

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin: 0 0 18px;
  padding: 0;
  list-style: none;
  font-size: var(--fs-small);
}
/* The build writes the breadcrumb after the h1; show it above. */
.page-banner .container { display: flex; flex-direction: column-reverse; align-items: flex-start; }
.breadcrumb li + li::before { content: "/"; margin-right: 10px; color: var(--line-blue); }
/* A bigger target for the crumb links. 'Home' was 37x18px and met WCAG
   2.5.8's 24px minimum only through the spacing exception; inline-block with
   3px above and below makes it 37x28. The matching negative margin keeps the
   line box, so the row stays 22.4px, the h1 does not move, and the link's
   text stays on the same baseline as the crumbs that are not links; without
   it the link's text sat 3px below theirs. */
.breadcrumb a {
  display: inline-block;
  margin-block: -3px;
  padding-block: 3px;
  color: var(--mist);
}
.breadcrumb a:hover { color: var(--paper); }

/* ---- components/hero.css ---- */
/* ---- homepage hero, and the call diagram ---------------------------------
   The one bold thing on the site. Where the reference shows a photo of a
   phone, this shows what the service does: one number coming in, the hosted
   system in the middle, and the phones that ring. The lines draw once on
   load, in order, and the lamps light. With reduced motion, or no animation
   support, the finished diagram is what shows: the base styles below ARE the
   end state, and the keyframes only describe how to get there.
   -------------------------------------------------------------------------- */

.hero {
  padding: clamp(3rem, 1.5rem + 6vw, 6rem) 0 clamp(3.5rem, 2rem + 6vw, 6.5rem);
  background: var(--harbour);
  color: var(--mist);
}

.hero-inner {
  display: grid;
  gap: clamp(2.5rem, 1rem + 5vw, 4.5rem);
  align-items: center;
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
}

.hero h1 {
  max-width: 13ch;
  margin-bottom: 24px;
  color: var(--paper);
  font-size: var(--fs-h1);
  line-height: var(--lh-tight);
  letter-spacing: -0.02em;
}

/* The headline follows the column it sits in, not the viewport. --fs-h1
   grows with the viewport, but at 969px the hero splits in two and the text
   column halves, so from there to about 1210 a near-full-size headline
   broke into six lines of one or two words (425px tall at 1100). 12.2cqi is
   the ratio the widest layout already has — 68px in a 554px column is
   12.27%, taken a little under — so every two-column width now breaks the
   way 1440 does: five lines, measured from 969 to 1440 (1440 itself goes
   from 68 to 67.6px). The clamp keeps --fs-h1's own floor and ceiling;
   where the column is the full width (stacked) --fs-h1 is
   the smaller term and nothing changes. Inside @supports because the value holds a var(): a browser
   without container units would otherwise throw the whole declaration out
   at computed-value time, and the headline would inherit body-text size. */
@supports (width: 1cqi) {
  .hero-inner > div { container-type: inline-size; }
  .hero h1 { font-size: clamp(2.375rem, 12.2cqi, var(--fs-h1)); }
}

/* French words are longer: at 13ch the headline was six lines at every
   width, with "téléphonique" alone on one. 17ch gives it four from 768 up
   (432 to 288px tall at 1440). On a phone the column is narrower than 17ch
   and is the limit, so it stays at six there. */
.hero h1:lang(fr) { max-width: 17ch; }

.hero-lead {
  max-width: 34rem;
  font-size: var(--fs-lead);
  line-height: 1.55;
}

.hero .todo { margin-top: 28px; }

/* ---- the diagram ------------------------------------------------------- */
/* A panel, so it takes the panels' padding: the bill sheet further down the
   home page is inset by the same clamp, and now the two line up. */
.call-map {
  margin: 0;
  padding: var(--pad-panel);
  border: 1px solid var(--line-blue);
  border-radius: var(--radius-panel);
  background: var(--harbour-deep);
}
/* Only when the hero stacks (below 969px) does the diagram need a cap: the
   SVG fills its column and its text is sized in viewBox units, so an 836px
   column drew 27.6px labels beside a 20px lead paragraph (measured at 900).
   34rem holds it near 1:1 and lines it up with the lead and the TODO above
   it, which are 34rem too. From 969 up the column is at most 554px. */
@media (max-width: 968px) {
  .call-map { max-width: 34rem; }
}
.call-map svg { width: 100%; }
.call-map figcaption {
  margin-top: 18px;
  font-size: var(--fs-small);
  color: var(--mist);
}
.call-map figcaption .todo { margin-top: 12px; }

.cm-route {
  fill: none;
  stroke: var(--mist);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-dasharray: 1 1;
  stroke-dashoffset: 0;
}
.cm-node { fill: var(--harbour); stroke: var(--mist); stroke-width: 1.5; }
.cm-hub { fill: var(--line-blue); stroke: var(--paper); stroke-width: 2; }
.cm-lamp { fill: var(--lamp); }
.cm-label { fill: var(--paper); font-family: var(--font-text); font-size: 17px; font-weight: 700; }
.cm-sub { fill: var(--mist); font-family: var(--font-text); font-size: 14px; }
.cm-hub-label { fill: var(--paper); font-family: var(--font-display); font-stretch: var(--display-stretch); font-size: 19px; font-weight: 800; }
.cm-icon { fill: none; stroke: var(--paper); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* On a phone the drawing is shrunk to about two thirds, which took the
   sub-labels down to 9.5px at 390. Larger viewBox sizes bring the three
   styles to 13.8 / 11.9 / 14.5px there (12.5 / 10.7 / 13.1 at 360). Each
   label still fits inside its box: the tightest, "Desk phones" and
   "OneConnect", keep about 12 units either side. */
/* The same sizes where the hero is two columns but the diagram's column is
   narrow (969-1099px): it drops to about 0.75 scale there, and its
   sub-labels were 10.5px — smaller than on a phone. It steps back at 1100,
   the same kind of step as the one at 480/481. */
@media (max-width: 480px), (min-width: 969px) and (max-width: 1099px) {
  .cm-label { font-size: 21px; }
  .cm-sub { font-size: 18px; }
  .cm-hub-label { font-size: 22px; }
}

@keyframes cm-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes cm-light { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: no-preference) {
  .cm-route--in { animation: cm-draw 0.6s ease-out 0.3s both; }
  .cm-route--out { animation: cm-draw 0.5s ease-out 0.9s both; }
  .cm-route--out:nth-of-type(2) { animation-delay: 1.05s; }
  .cm-route--out:nth-of-type(3) { animation-delay: 1.2s; }
  .cm-lamp { animation: cm-light 0.25s ease-out 1.5s both; }
}

/* ---- components/tiles.css ---- */
/* ---- service tiles: what OneConnect covers, each linking to its page ----- */

.tiles {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-4), 100%), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A row with one tile left over reads as a mistake, so a set of four or of
   three says how many it has, and the columns jump straight between counts
   that divide it evenly instead of letting auto-fit pass through 3 + 1 or
   2 + 1 on the way down. steps.css does the same for the numbered steps, at
   the same widths, so a page's tiles and steps change together.

   Four: 4 across from 1073px, 2 x 2 down to 561px, 1 column at 560px and
   below. 1073 is the first width where four columns are each still
   --col-min-4 (230px) wide — 4 x 230 + 3 gaps of 29.4 + 2 gutters of 32 —
   and is where the plain auto-fit grid switched to four on its own
   (measured: 3 columns at 1072, 4 of 230.2px at 1073). From 806 to 1072
   that grid showed 3 + 1. The 560 is the phone layout's own breakpoint
   (below). If --col-min-4, --gap or --gutter changes, re-measure the 1073. */
.tiles--four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 1073px) {
  .tiles--four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.tile {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 28px 24px;
  border: 1px solid var(--stone);
  border-radius: var(--radius-panel);
  background: var(--paper);
  color: var(--slate);
  text-decoration: none;
  transition: border-color var(--transition);
}
.tile:hover { border-color: var(--line-blue); color: var(--slate); }

.tile-icon {
  width: 44px;
  height: 44px;
  margin-bottom: 20px;
  color: var(--line-blue);
}

/* The title states its own face rather than taking it from its tag. The tag
   follows the page outline (an h3 under the home page's section title; it
   was an h2 on the old /hospitality/ page, where the tiles were the first
   headings), and the base h2 rule is Archivo 800 while h3 is Atkinson 700 —
   so the same tile used to render in two typefaces. Atkinson 700 is the h3
   set, the same as the step titles further down the home page. */
.tile-title {
  margin: 0 0 8px;
  color: var(--harbour);
  font-family: var(--font-text);
  font-size: var(--fs-h3);
  font-stretch: normal;
  font-weight: 700;
  letter-spacing: 0;
}

.tile-text { margin: 0 0 20px; }

/* On a phone, four tall tiles are a lot of scrolling: put the icon beside
   the text instead of above it. A set of four goes to one column here:
   auto-fit still fitted two of these side by side from 524 to 560, which
   left the text a 135px strip beside the icon (measured at 540). */
@media (max-width: 560px) {
  .tiles--four { grid-template-columns: minmax(0, 1fr); }
  .tile { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 0 16px; padding: 20px; }
  .tile-icon { grid-row: span 3; margin-bottom: 0; }
  .tile-text { margin-bottom: 10px; }
}

/* In the flex layout (.tile above, which is what a browser without subgrid
   gets) margin-top: auto pins the link to the foot of a tall tile. The
   subgrid block at the end of this file lines the links up by row. */
/* One line always: at 1073-1096px "About moving numbers" split in two, and
   its first line sat 27px above the other tiles' links. */
.tile-more {
  white-space: nowrap;
  margin-top: auto;
  color: var(--line-blue);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Tiles in a row share their rows: icon, title, text, link. Without this
   each tile is its own flex column, so when one title wraps to two lines its
   text starts a line lower than its neighbours' (26px at 1440 on the home
   page). With subgrid every tile in the row takes the tallest title's height,
   so the text and the links line up across the row.

   row-gap: 0 because a subgrid otherwise inherits .tiles' gap BETWEEN its
   own rows, which would open var(--gap) of space under the icon, the title
   and the text. The spacing inside a tile stays the margins above.

   From 561px only: at 560 and below the tile is its own icon-beside-text
   grid, and the site's one set (.tiles--four, on the home page) is one tile
   per row there, with nothing beside it to line up with. */
@supports (grid-template-rows: subgrid) {
  @media (min-width: 561px) {
    .tiles > li,
    .tile {
      display: grid;
      grid-row: span 4;
      grid-template-rows: subgrid;
      /* One column that may shrink: an implicit auto column let a nowrap
         link widen its tile past its siblings. */
      grid-template-columns: minmax(0, 1fr);
      row-gap: 0;
    }
    /* The link sits at the foot of its row, so when one link wraps to two
       lines ('About moving numbers', 1073 to 1097px on the home page) the
       one-line links line up with its last line. margin-top: auto already
       does this inside a grid area too (measured: the same positions with
       or without this line); it is stated in grid terms so the row layout
       does not depend on a margin written for the flex layout. */
    .tile-more { align-self: end; }
  }
}

/* ---- components/bill.css ---- */
/* ---- the bill ---------------------------------------------------------------
   OneConnect is not one fixed package, so the price is shown the way a phone
   bill shows it: one line per thing you pay for, a dotted leader, the price
   in its own column. Below 560px the detailed bills drop the leader and put
   the price under its line's description; the compact bill and the
   calculator's estimate, whose lines are short, keep the name, leader and
   amount on one row at every width.
   -------------------------------------------------------------------------- */

.bill-sheet {
  padding: var(--pad-panel);
  border: 1px solid var(--stone);
  border-radius: var(--radius-panel);
  background: var(--paper);
}

/* A sheet with no head starts straight on its first line, and one that ends
   on a line ends on the sheet's own border rather than on a rule drawn a
   padding's width above it. Measured 2026-09-24: the first name sat 16px
   lower in a headless sheet than under a head (53px against 37px at
   1440px). A sheet that ends on .bill-total keeps its last rule, which
   separates the lines from the total. */
.bill-sheet > .bill:first-child > .bill-line:first-child { padding-top: 0; }
.bill-sheet > .bill:last-child > .bill-line:last-child { border-bottom: 0; padding-bottom: 0; }

.bill-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px 16px;
  padding-bottom: 14px;
  border-bottom: 2px solid var(--harbour);
}

/* The bill's heading, in the display face whatever its tag: it is an h3 on
   the homepage and an h2 on /pricing/ and /contact/, and typography.css sets
   the face by tag, which made the homepage's the odd one out in Atkinson.
   It also heads blocks that are not bills (the calculator, the contact
   aside), so it carries its own space below; inside .bill-head the head's
   padding and rule space it instead. */
.bill-title {
  margin: 0 0 12px;
  font-family: var(--font-display);
  font-stretch: var(--display-stretch);
  font-size: var(--fs-h3);
  font-weight: 800;
  letter-spacing: -0.01em;
}
.bill-head .bill-title { margin: 0; }

.bill-cols { font-size: var(--fs-small); color: var(--slate-muted); }
/* When the title wraps and "Price" drops to a line of its own, it stays at
   the right, over the price column, rather than starting the line. */
.bill-head .bill-cols { margin-left: auto; }

.bill {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One line of the bill, as named areas: the name, its leader and the price
   along the top, and the description under the name and leader. Everything
   sits on the first line's baseline, so the amount reads level with the name
   and the dots sit on that same baseline: the leader is an empty span, and an
   empty box's baseline is its bottom edge, which is where its dotted border
   is drawn. (It was a flex row aligned by the bottom, which put the dots on
   the description's line and ran them to the unit, not the amount.) */
.bill-line {
  display: grid;
  grid-template-columns: auto minmax(24px, 1fr) auto;
  grid-template-areas:
    "name leader price"
    "desc desc   price";
  column-gap: 12px;
  align-items: baseline;
  padding: 16px 0;
  border-bottom: 1px solid var(--stone);
}

/* The name and description's wrapper groups them in the markup only; the
   grid places each on its own. It has no role, so taking its box away
   changes nothing a screen reader hears. */
.bill-item { display: contents; }
.bill-name {
  grid-area: name;
  color: var(--harbour);
  font-size: 1.125rem;
  font-weight: 700;
}
.bill-desc {
  grid-area: desc;
  margin-top: 2px;
  font-size: var(--fs-small);
}

.bill-leader {
  grid-area: leader;
  border-bottom: 2px dotted var(--stone);
}

/* The price column. Its min-width is the widest thing the column holds in
   these bills, so every leader in a bill ends at the same x however wide the
   amount on its own line: "per phone number" is 114px at 14px, 6.7em of the
   17px price (measured 2026-09-24); the amounts, "$1,119.00" at the most, are
   84px. A wider price still fits — its own column grows, and only that
   line's leader ends sooner. */
.bill-price {
  grid-area: price;
  min-width: 7em;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--harbour);
}

/* What a price is charged for — "per phone number", "per user". It sits under
   the amount rather than beside it, in every bill: the amounts are the column
   a reader scans down, right-aligned to one edge, and a unit set after each
   amount would push each one in by its own length. One line always, so a
   unit never breaks inside the column. */
.bill-unit {
  display: block;
  margin-top: 2px;
  color: var(--slate-muted);
  font-size: var(--fs-small);
  font-weight: 400;
  white-space: nowrap;
}

/* The compact bill (the pricing guide on the homepage and beside the
   calculator) is names and prices only. */
.bill--compact .bill-line { padding: 10px 0; }
.bill--compact .bill-name { font-size: var(--fs-body); }

/* Its column is sized to the AMOUNTS, not the units under them. At 7em the
   114px "per phone number" set the column, and every dotted leader stopped
   73-84px short of a $2.70 or a $30.00 — a leader that does not reach its
   price. The unit's negative left margin lets it extend back under the
   leader, right-aligned beneath its amount, without widening the column:
   there is no description in a compact bill for it to run into. 4em holds
   the widest compact amount, "$30.00" (58px). This also let the guide fit
   320px, where the 7em column had pushed one row into the sheet's padding. */
.bill--compact .bill-price { min-width: 4em; }
.bill--compact .bill-unit { margin-left: -4em; }

/* The calculator's estimate, written by 45-price-builder.js, has amounts and
   no units, so its column only needs to hold an amount: four figures of
   dollars, "$0,000.00", is 84px, 4.9em (measured 2026-09-24). Its other
   styles are in builder.css. */
.bill--estimate .bill-price { min-width: 5em; }
/* On a phone an estimate line whose name wraps keeps its amount on the right,
   but its leader is a 24px stub a long way from the name's first line. The
   gap reads as a mistake, so the leader goes and the space stays. */
@media (max-width: 560px) {
  .bill--estimate .bill-leader { visibility: hidden; }
}

.bill-total {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-top: 20px;
}
/* A measure and a basis, so the sentence wraps beside the button instead of
   running one bold line the width of the sheet and pushing the button under
   it (131 characters on one line at 1440px, measured 2026-09-24). */
.bill-total p {
  flex: 1 1 30ch;
  max-width: 62ch;
  margin: 0;
  color: var(--harbour);
  font-weight: 700;
}

/* The "See the options" link that moreLink() in tools/build.js puts after a
   description, in these bills and on /features/. Kept whole: two or three
   words split over two lines read as two links. The longest, "See the paging
   hardware", is 187px; the narrowest column it sits in is 260px, on
   /features/ at 320px (measured 2026-09-24). */
.more-link { white-space: nowrap; }

@media (max-width: 560px) {
  /* The detailed bills stack: name, description, then the price, left-aligned
     under them. Their descriptions need the whole width at this size. */
  .bill:not(.bill--compact, .bill--estimate) .bill-line {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "name" "desc" "price";
  }
  .bill:not(.bill--compact, .bill--estimate) .bill-leader { display: none; }
  .bill:not(.bill--compact, .bill--estimate) .bill-price { margin-top: 6px; text-align: left; }
  /* The "Price" label goes too: over a stacked bill there is no column for
     it to head, and the compact guide's amounts carry their own $. */
  .bill-cols { display: none; }
}

/* ---- components/plans.css ---- */
/* ---- the two plans ----------------------------------------------------------
   OneConnect Unlimited and OneConnect UnSquare, side by side. Neither is marked
   "most popular" or given a coloured border: the client sells them to
   different-sized businesses, not as a good/better pair, so the cards are
   deliberately equal and the thing that distinguishes them is the number of
   users each is for.
   -------------------------------------------------------------------------- */

.plan-grid {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-2), 100%), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

.plan-card {
  display: flex;
  flex-direction: column;
  padding: var(--pad-panel);
  border: 1px solid var(--stone);
  border-radius: var(--radius-panel);
  background: var(--paper);
}

/* The plan's name, in the display face whatever its tag. typography.css sets
   the face by tag, and these were h3s (Atkinson) until they became the page's
   first h2s; stating it here means the outline can change without the look
   changing. A step below .section-title at every width (that is 42px on a
   wide screen and 28px on a phone), because a card title should not outrank
   the headings of the sections around it. At 24px "OneConnect UnSquare"
   (303px) still fits the 316px a card leaves at 390px; at 28px it broke onto
   two lines. The names still wrap below about 380px, and from where the
   cards go side by side (about 890px) to about 1010px, where each card is at
   its narrowest; there the rows below them stay aligned through the
   subgrid. */
.plan-name {
  margin: 0;
  font-family: var(--font-display);
  font-stretch: var(--display-stretch);
  font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.25rem);   /* 24 -> 36 */
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
}

/* 32rem is the width the summary has beside its twin at 1440 — about 65
   characters a line. The card's text column is widest not at 1440 but in the
   single column just below the switch to two (891px), where uncapped it ran
   98-103 characters; the old 34ch cap held it to 47 and left a quarter of
   the card empty. Measured 2026-09-24. */
.plan-summary {
  margin: 8px 0 0;
  max-width: 32rem;
}

.plan-price {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  margin: 24px 0 0;
}
.plan-amount {
  font-family: var(--font-display);
  /* font-stretch, as every other display element sets it. This used to be
     font-variation-settings: "wdth" var(--display-stretch), which is invalid —
     the token is a percentage and that property wants a number — so the
     figures rendered at Archivo's normal width beside wide headings. */
  font-stretch: var(--display-stretch);
  font-size: var(--fs-banner);
  font-weight: 700;
  line-height: 1;
  color: var(--harbour);
  font-variant-numeric: tabular-nums;
}
.plan-unit { color: var(--slate-muted); font-size: var(--fs-small); }

.plan-facts {
  margin: 20px 0 24px;
  padding-top: 16px;
  border-top: 1px solid var(--stone);
}
/* Label and value on one baseline. They are different sizes (14px and 17px),
   and stretched flex items put their text 3px apart. */
.plan-facts > div {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 16px;
  padding: 8px 0;
}
.plan-facts dt { color: var(--slate-muted); font-size: var(--fs-small); }
.plan-facts dd { margin: 0; font-weight: 700; color: var(--harbour); }

/* Without subgrid: pushed to the bottom so the two buttons line up even when
   one card's summary wraps to an extra line. */
.plan-card .btn { margin-top: auto; align-self: flex-start; }

/* Side by side, the two cards share their rows: each card spans five rows of
   the plan grid — one per child that planCardsHtml() in tools/build.js writes
   (name, summary, price, facts, button) — and lays its children on them, so
   the price, the facts rule and the button sit at the same height in both
   cards however much longer one summary is. Measured 2026-09-24: at 1440px
   the prices were 54px apart, because only the button was pinned. Stacked in
   one column each card has its own five rows, and nothing changes.

   Without subgrid the card stays the flex column above, with its button
   pushed to the bottom by the rule just above: the buttons still line up,
   the rows above them do not. */
@supports (grid-template-rows: subgrid) {
  .plan-card {
    display: grid;
    grid-row: span 5;
    grid-template-rows: subgrid;
    row-gap: 0;
  }
  /* Its own row, so no auto margin to push it; start and end keep it the
     width of its label at the foot of that row, not stretched to the card. */
  .plan-card .btn { margin-top: 0; justify-self: start; align-self: end; }
}

/* "What's a line?" — the client's own explanation, under the plan cards here
   and under the lines question in the calculator. A line is the one term on
   this page a visitor cannot be expected to know already.

   Its left rule is 4px, the weight of the calculator's plan panel (.plan-fit
   in builder.css) and of the 911 notice, so the site's callouts share one
   rule weight. It is a tinted note rather than an outlined panel like
   .plan-fit, because it explains; the panel is the answer. */
.line-explain {
  max-width: var(--measure);
  margin: var(--gap) 0 0;
  padding: 12px 16px;
  border-left: 4px solid var(--line-blue);
  border-radius: 0 var(--radius-control) var(--radius-control) 0;
  background: var(--stone-light);
  font-size: var(--fs-small);
}
.line-explain strong { color: var(--harbour); }
.builder-step .line-explain { margin-top: 16px; }

/* Under the plan cards it stays inside the first card's column. At 66ch it
   ran 25px past that card into the gutter at 1440px (measured 2026-09-24).

   The second term is the plan grid's own two-column rule, restated from the
   same tokens so the two cannot disagree: two columns exist when two
   --col-min-2 columns and the gap fit, and then one column is half the width
   less half the gap. Below that the multiplied term is large and positive,
   max() takes it, and --measure is the only limit. */
.plan-grid + .line-explain {
  max-width: min(
    var(--measure),
    max((100% - var(--gap)) / 2, (2 * var(--col-min-2) + var(--gap) - 100%) * 999)
  );
}

/* ---- components/industries.css ---- */
/* ---- industries: one panel per industry, on /industries/ -------------------
   Hospitality, education and non-profits, each its own panel (the client's
   email, 2026-10-05: "individual sections/cards"). A panel has two halves:
   who it is for and why on the left, what they get and the next step on the
   right, which is also the order they come in on a phone.

   Stacked one above the other rather than three across. Three across, a
   panel's text column would be about 298px at 1440 (a third of the 1180px
   container, less two gaps, the padding and the border), narrower than its
   button: "Contact us for non-profit pricing" is 306px on one line
   (measured). Stacked, every panel has the same two columns, so the halves
   line up down the page without a subgrid.
   -------------------------------------------------------------------------- */

.industries {
  display: grid;
  gap: var(--gap);
}

/* scroll-margin-top: each panel has an id, so a link can open the page on it
   (/industries/#non-profit), and a jump has to clear the sticky header — the
   24px under its stuck height that layout.css gives .section[id]. */
.industry {
  display: grid;
  gap: 28px clamp(2rem, 1rem + 3vw, 4rem);
  padding: var(--pad-panel);
  border: 1px solid var(--stone);
  border-radius: var(--radius-panel);
  background: var(--paper);
  scroll-margin-top: calc(var(--header-h-stuck) + 1px + 24px);
}
/* From 900, where the left half is 335px and the right 402. The two longer
   names take two lines there, and "Non-profit organizations" still does at
   1100; at 1440 the left half is 476px and all three fit on one (measured
   2026-10-05). */
@media (min-width: 900px) {
  .industry {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    align-items: start;
  }
}

.industry-icon {
  display: block;
  width: 44px;
  height: 44px;
  margin-bottom: 16px;
  color: var(--line-blue);
}

/* The plan cards' title face and size (plans.css .plan-name): the two are
   the site's card titles, and both are a page's first h2s. */
.industry-name {
  margin: 0;
  font-family: var(--font-display);
  font-stretch: var(--display-stretch);
  font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.25rem);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
}

/* Who it is for, the way the calculator labels a plan's user range
   (.plan-fit-range): small, bold, muted. */
.industry-who {
  margin: 8px 0 0;
  color: var(--slate-muted);
  font-size: var(--fs-small);
  font-weight: 700;
}

.industry-lead {
  max-width: 34rem;
  margin: 16px 0 0;
  font-size: var(--fs-lead);
  line-height: 1.55;
}

/* What they get, on a tinted sheet beside the intro: the panel's answer to
   "what does OneConnect do for us". */
.industry-detail {
  padding: clamp(18px, 2vw, 24px);
  border-radius: var(--radius-control);
  background: var(--stone-light);
}

.industry-points {
  display: grid;
  gap: 16px;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Each capability with a lit lamp beside it — the site's mark for "this is
   on for you" (tokens.css --lamp), as on the features page's included
   list. Centred on the first line: half the difference between that line's
   height and the 14px lamp. */
.industry-points li {
  position: relative;
  padding-left: 28px;
}
.industry-points li::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc((var(--fs-body) * var(--lh-text) - 14px) / 2);
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--lamp);
}
.industry-points strong {
  display: block;
  color: var(--harbour);
}

.industry-action { margin: 24px 0 0; }
/* On a phone the button takes the width, as every other button on the site
   does there (button.css, form.css). It also lets "Contact us for non-profit
   pricing", 306px on one line, wrap inside the sheet at 320 instead of
   running past it. */
@media (max-width: 480px) {
  .industry-action > .btn { width: 100%; }
}

/* ---- components/steps.css ---- */
/* ---- switching steps: a real sequence, so it is numbered ------------------ */

.steps {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-4), 100%), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

/* No lonely last step: four go 4 across, 2 x 2, then 1, at the same widths
   as .tiles--four, whose comment in tiles.css has the arithmetic, so the
   tiles and the steps on the home page change column count together.

   Three (on /industries/) go 3 across from 705px, where each column is still
   200px wide (705 - 2 x 28 gutter - 2 x 24 gap = 601, over three), and 1
   column below that. 200 rather than --col-min-4's 230, because three
   narrower columns read better than a lone third step under two wide ones,
   which is what auto-fit gave from 524 to 805. The tiles had a three-column
   set at these widths until the old /hospitality/ page went (2026-10-05). */
.steps--four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 1073px) {
  .steps--four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .steps--four { grid-template-columns: minmax(0, 1fr); }
}

.steps--three { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 705px) {
  .steps--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.step {
  counter-increment: step;
  padding-top: 20px;
  border-top: 3px solid var(--line-blue);
}
.step::before {
  content: counter(step);
  display: block;
  margin-bottom: 10px;
  color: var(--line-blue);
  font-family: var(--font-display);
  font-stretch: var(--display-stretch);
  font-size: 2.25rem;
  font-weight: 800;
  line-height: 1;
}

.step-title { margin: 0 0 8px; font-size: var(--fs-h3); }
.step p { margin: 0; }

/* Steps in a row share their three rows: the number (the ::before, which is
   a grid item like any child), the title and the text. Without this a step
   whose title wraps starts its text 26px lower than its neighbours'. row-gap:
   0 keeps .steps' gap from opening between a step's own rows; the margins
   above still space them. A step that ever gains a fourth child needs
   span 4. */
@supports (grid-template-rows: subgrid) {
  .step {
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
    row-gap: 0;
  }
}

/* ---- components/hardware.css ---- */
/* ---- the hardware catalogue -------------------------------------------------
   Fifty products, filtered by manufacturer, type and a search box. A grid of
   cards rather than a table: every product has a photograph, and the photograph
   is what a visitor recognises their own phone by.

   The filters are hidden until <html> carries the `js` class, because filtering
   is the only thing they do. Without JavaScript the whole catalogue is on the
   page, which is the complete answer.
   -------------------------------------------------------------------------- */

.hardware { margin-top: var(--gap); }

/* The filters sit in a rail down the left, with the results beside them.
   The grid is scoped to `.js` on purpose: without JavaScript the rail is
   `display: none`, and a `display: none` element creates no grid item at all,
   so an unscoped two-column grid would drop the results into the rail's
   column. */
.js .hardware {
  display: grid;
  gap: var(--gap);
}

@media (min-width: 900px) {
  /* --rail-w rather than a literal: .hardware-ask, under the results, starts
     where the results do, and the two have to agree on where that is. */
  .js .hardware {
    grid-template-columns: var(--rail-w) minmax(0, 1fr);
    align-items: start;
  }
  /* The rail follows the reader down a fifty-card list. It scrolls inside
     itself if it ever outgrows the screen, so a long facet list can never push
     its own bottom out of reach. */
  .js .hardware-filters {
    position: sticky;
    top: calc(var(--header-h-stuck) + 16px);
    max-height: calc(100vh - var(--header-h-stuck) - 32px);
    overflow-y: auto;
  }
}

.hardware-filters { display: none; }

.js .hardware-filters {
  display: grid;
  gap: 18px var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
  align-items: start;
  padding: clamp(18px, 2.5vw, 24px);
  border: 1px solid var(--stone);
  border-radius: var(--radius-panel);
  background: var(--stone-light);
}
.hardware-filters .field { margin-bottom: 0; }

/* In the rail the groups stack, each under a rule. Below the breakpoint the
   rail becomes a band above the results and the groups sit side by side
   instead; on a phone, where the band is one column, each group's choices
   run in columns of their own (below). */
@media (min-width: 900px) {
  .js .hardware-filters { grid-template-columns: minmax(0, 1fr); gap: 0; }
  /* The rule and the space above it belong to the legend, not the fieldset.
     A fieldset draws its border-top through the middle of its legend, so the
     rule struck through each heading, and the fieldset's padding sat between
     the heading and its first choice instead: 24px there, against the 6px a
     label keeps from its control everywhere else. */
  .js .hardware-filters .facet { margin-top: 18px; }
  .js .hardware-filters .facet legend {
    width: 100%;
    padding-top: 18px;
    border-top: 1px solid var(--stone);
  }
}

/* The band, below the breakpoint. Manufacturer (seven brands against Type's
   four) is much the tallest group, so the band's first row was as tall as it,
   and Type started a second row below a 176px hole under the search box: the
   band was 477px tall at 768px. Spanning Manufacturer over two rows moves Type
   up into that space instead (301px). No `dense` is needed: the next free
   cell after Manufacturer already is the one under the search box, so on
   screen the groups still read Search, Manufacturer, Type, the same as the
   source and the tab order. Keyed on the attribute build.js sets, so it
   follows the group rather than its position in the markup. */
@media (max-width: 899px) {
  .js .hardware-filters .facet[data-facet-group="brand"] { grid-row: span 2; }
}

/* On a phone the band is one column, so the groups stack after all, and they
   made a 574px wall of one-per-line checkboxes before the first product. The
   choices are short — the longest, "Grandstream 9", fits a 135px column — so
   each group runs in as many 135px columns as it has room for: two at 360 to
   390px, three at 560px, and the band is 414px tall at 390px. At 320px there
   is room for one, so the list stays as it was; min() only stops a group
   narrower than 135px from overflowing. The legend is not a grid item, so it
   stays above the choices. */
@media (max-width: 599px) {
  .js .hardware-filters .facet {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(135px, 100%), 1fr));
    column-gap: 20px;
  }
}

/* The results column. `min-width: 0` is load-bearing in a grid: without it a
   grid item refuses to shrink below its content, and the card grid would push
   the rail off the side instead of reflowing. */
.hardware-results { min-width: 0; }

/* Above the grid: how many of the fifty are showing. It describes the results,
   so it lives with them rather than in the rail. */
.hardware-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  margin-bottom: var(--gap);
  padding-bottom: 12px;
  border-bottom: 1px solid var(--stone);
}

.facet {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}
.facet legend {
  padding: 0;
  margin-bottom: 6px;
  color: var(--harbour);
  font-weight: 700;
}

.facet-choice {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 32px;
  font-size: var(--fs-small);
  cursor: pointer;
}
/* margin: 0 undoes the browser's own checkbox margin (3px 3px 3px 4px in
   Chrome), which set the box 4px in from the legend and the search box above
   it and made the gap to its label 11px rather than the 8px set here. */
.facet-choice input { width: 18px; height: 18px; flex: none; margin: 0; accent-color: var(--line-blue); }

/* How many products sit behind each filter, so the visitor can see that Poly
   is three and Yealink twenty-one before clicking either. */
.facet-count {
  margin-left: auto;
  padding-left: 8px;
  color: var(--slate-muted);
  font-variant-numeric: tabular-nums;
}

.hardware-count {
  margin: 0;
  color: var(--slate-muted);
  font-size: var(--fs-small);
}

.hardware-list {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fill, minmax(min(var(--col-min-card), 100%), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

.hw-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--stone);
  border-radius: var(--radius-panel);
  background: var(--paper);
}

/* THE IMAGE AREA IS THE SAME SIZE ON EVERY CARD. A FIXED HEIGHT, NOT AN
   ASPECT RATIO, AND THE REASON IS WORTH KEEPING.

   These photographs are wildly different shapes: the Grandstream WP822 is
   555x1500 and the Yealink CP925 is wider than it is tall. This box used to be
   `aspect-ratio: 4 / 3` with the image capped at `max-height: 100%`, and
   measured on the staged site it did not hold at all — the boxes came out
   between 278px and 952px tall, in sixteen different heights.

   Two reasons, and both had to be fixed:

   1. `.hw-photo` is a flex ITEM of `.hw-card` (a grid item where subgrid is
      supported, and the same default applies), and a flex item's default
      `min-height: auto` means its content can push it past any height the box
      asks for. `aspect-ratio` sets a preferred size, not a cap. Hence
      `min-height: 0` and `flex: none` below.
   2. A percentage `max-height` needs a definite parent height to resolve
      against. Under `aspect-ratio` alone it resolved to nothing, so the image
      was never capped. A fixed `height` gives it something real.

   `overflow: hidden` is the backstop: if either rule above is ever lost, the
   image is clipped to the box rather than shoving the card open again. */
.hw-photo {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 220px;
  min-height: 0;
  overflow: hidden;
  padding: 14px;
  background: var(--stone-light);
}
.hw-photo img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  /* The supplied shots sit on white rather than being cut out, so multiply
     drops that white into the panel instead of showing a paler rectangle. */
  mix-blend-mode: multiply;
}

.hw-nophoto {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px;
  color: var(--slate-muted);
  font-size: var(--fs-small);
  text-align: center;
}

/* Where subgrid is unsupported, the body fills whatever height the row
   settles on and the "Full specification" link is pushed to its foot, so the
   link at least sits on one line across a row. With subgrid the cards share
   row tracks instead: see the @supports block after the card rules. */
.hw-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 18px 20px 20px;
}

/* Two lines reserved for the name and three for the description, whether or
   not this product uses them, so "Grandstream HT802 ATA" and "Snom M500 Pro
   DECT Mulitcell Base Station" start their chips and text on the same lines,
   and a row of short ones is no shorter than the rest of the catalogue.
   Where subgrid is unsupported, this reservation is all that lines the chips
   and the text up from card to card.

   Long text is clamped rather than allowed to run on, and a description is
   only ever clamped with the whole of it in the <details> underneath, so
   nothing is lost: build.js (spec()) gives a panel to every description over
   67 characters, the length measured to fit three lines at the narrowest
   this text column gets (198px: a 240px card less its border and padding). */
.hw-model {
  margin: 0 0 8px;
  min-height: calc(2 * 1.3em);
  font-size: 1.0625rem;
  line-height: 1.3;
  color: var(--harbour);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* A product name wraps the ordinary way, not balanced like other headings
     (typography.css): balance moved every break in a long name. A hyphenated
     compound ("High-Performance", "Multi-cell") is kept whole by a
     .nowrap span that hardwareHtml() puts round it — ordinary wrapping still
     breaks at a typed hyphen, and no CSS property turns that off. */
  text-wrap: wrap;
}

.hw-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0 0 12px;
}
.hw-chip {
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  background: var(--stone);
  color: var(--harbour);
  font-size: 0.75rem;
  font-weight: 700;
}
.hw-sku {
  color: var(--slate-muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

.hw-desc {
  margin: 0;
  min-height: calc(3 * 1.6em);
  font-size: var(--fs-small);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Manufacturer copy runs to a thousand characters on some models. Every
   description too long to be sure of fitting the card's three lines gets this
   panel (build.js, spec()): the card shows a lead and keeps the whole text in
   here, so the grid stays scannable and nothing the client supplied is thrown
   away. */
/* Without subgrid, margin-top: auto pushes the link to the foot of the body,
   so it lands on one line across the row; with subgrid it sits in a row track
   of its own instead (below). The 6px above the summary plus the summary's
   own 6px padding put the link text where a 12px padding-top used to, while
   making the summary a 34px target rather than a 22px one: WCAG 2.2
   asks for 24. */
.hw-spec { margin-top: auto; padding-top: 6px; }
.hw-spec summary {
  padding-block: 6px;
  color: var(--line-blue);
  font-size: var(--fs-small);
  font-weight: 700;
  cursor: pointer;
}
.hw-spec summary:hover { color: var(--line-blue-deep); }
.hw-spec p {
  margin: 8px 0 0;
  font-size: var(--fs-small);
  color: var(--slate);
}
/* An open panel makes its whole row taller, not just its own card: the other
   cards in the row stretch to match. That is fine — it is a deliberate act by
   the reader, not the default state — and with subgrid their own links stay
   where they were, at the top of the row's spec track. */
.hw-spec[open] { padding-bottom: 4px; }

/* EVERY CARD IN A ROW SHARES ONE SET OF ROW TRACKS: photo, name, chips,
   description, spec link. Laid out alone, a card whose chip line wrapped
   (the GRP2612W's chips need 229px; the text column is 200px at 1140px)
   pushed its own description 25px below its neighbours', which it did in two
   or three rows at 600, 899, 900 and 1140 to 1160px. Now the wrap grows the
   chips track for the whole row.

   The spans count children: a card is the photo and the body, and the body
   is the name, the chips, the description and the spec. Add an element to
   either and its span has to change with it. A card with no spec takes its
   description across the last two tracks, because an empty track would
   still cost it half a row gap of blank card.

   The chips hold to the top of their track. Stretched to a track that a
   neighbour's wrap has made taller, the chip row (a wrapping flex line with
   align-items: center) centred its one line in it, 13px below the chips
   beside it.

   The list's row gap is what separates one row of cards from the next, so
   inside a card it is set back to 0. With the tracks shared, the name may run
   to a third line: that grows the name track for its own row only, and the
   two-line reservation above still sets the minimum. The link drops
   margin-top: auto, because inside a shared track it would push a closed
   link to the foot of a row whose neighbour's panel is open (measured: 460px
   down).

   After the card rules on purpose: the selectors match theirs, so order
   decides. Browsers without subgrid keep the flex layout above. A filtered
   card still disappears: [hidden] is display: none !important in
   utilities.css. */
@supports (grid-template-rows: subgrid) {
  .hw-card { display: grid; grid-row: span 5; grid-template-rows: subgrid; row-gap: 0; }
  .hw-body { display: grid; grid-row: span 4; grid-template-rows: subgrid; row-gap: 0; }
  .hw-meta { align-self: start; }
  .hw-desc:last-child { grid-row: span 2; }
  .hw-spec { margin-top: 0; }
  .hw-model { -webkit-line-clamp: 3; }
}

.hardware-empty {
  padding: 32px 0;
  color: var(--slate-muted);
}

/* The way out of the catalogue when the answer is not in it. The button sits
   inside the sentence rather than under a heading of its own, because it
   answers that sentence and nothing else on the page. */
.hardware-ask {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 20px;
  margin-top: var(--gap);
  padding-top: var(--gap);
  border-top: 1px solid var(--stone);
}
/* The rule closes the list, so it spans what the list spans. Beside the rail
   that is the results column alone: the line starts where the results do
   (the same --rail-w and gap as the grid above) rather than under the empty
   rail column. No max-width, for the same reason: the rule has to reach the
   list's right edge, and the copy is one short sentence. Relies on the line
   directly following .hardware in phones.html. */
@media (min-width: 900px) {
  .js .hardware + .hardware-ask { margin-left: calc(var(--rail-w) + var(--gap)); }
}

/* ---- components/features.css ---- */
/* ---- feature lists ----------------------------------------------------------
   The mark beside each item says which list it is in, so it carries meaning:
   a lit lamp for "included in every plan", a lit diamond for "an
   integration, also included", an unlit ring for "optional extra", a blue
   square for "a service we handle". The diamond was outlined until the client
   said the integrations are included in every plan (2026-10-28); it now takes
   the lamp's colour, and its shape still sets the group apart. */

/* auto-fill, not auto-fit: a group shorter than a row keeps the empty
   columns, so its items stay the width of every other group's and start at
   the same x. With auto-fit the two integrations stretched across three
   columns' width and sat out of line with the lists above and below them
   (measured at 1440, 2026-10-05). */
.feature-list {
  display: grid;
  gap: 18px var(--gap);
  grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

.feature { display: flex; align-items: flex-start; gap: 14px; }
/* min-width: 0 lets a long description wrap instead of pushing the row wide. */
.feature-text { min-width: 0; }

.feature-mark {
  flex: none;
  width: 14px;
  height: 14px;
  margin-top: 5px;
  border-radius: 50%;
}
.feature-list--included .feature-mark { background: var(--lamp); }
/* A 10px square turned 45 degrees spans 14px corner to corner, the size of
   the other marks, and these margins put its centre where theirs is: 7px in
   and 12px down. */
.feature-list--integrations .feature-mark {
  width: 10px;
  height: 10px;
  margin: 7px 2px 0;
  border-radius: 1px;
  background: var(--lamp);
  transform: rotate(45deg);
}
.feature-list--optional .feature-mark { border: 2px solid var(--line-blue); }
.feature-list--services .feature-mark { border-radius: 3px; background: var(--line-blue); }

.feature-name { color: var(--harbour); font-weight: 700; }
.feature-desc { display: block; }

.feature-group + .feature-group { margin-top: clamp(2.5rem, 1.8rem + 3vw, 4rem); }
/* The page's section headings, so they take the h2 face from typography.css
   (Archivo 800 on its wide axis) rather than the feature names' Atkinson 700,
   which made them read as one more item. The size is .prose h2's
   (prose.css): one size for an h2 inside a section, site-wide. Change both
   together. */
.feature-group-title { margin-bottom: 1.25rem; font-size: clamp(1.5rem, 1.25rem + 1vw, 2rem); }

/* ---- components/faq.css ---- */
/* ---- FAQ: native <details>, so it opens with or without JavaScript -------- */

/* One column. Each question opens on its own, so in two columns an open
   answer left a hole beside it and pushed both columns down, and the reading
   order zigzagged.
   60ch is the long-form column's width (.prose, prose.css): 661px at 17px.
   Inside the card's padding the answers then run about 73 characters a line,
   81 at most. Atkinson's ch is its wide '0', not an average letter, so 48rem
   (768px) ran them to 92. */
.faq-list {
  display: grid;
  gap: 14px;
  grid-template-columns: minmax(0, 1fr);
  max-width: 60ch;
}

.faq {
  border: 1px solid var(--stone);
  border-radius: var(--radius-control);
  background: var(--paper);
}

/* The radius is for the focus ring: Chrome and Safari draw the outline on
   the summary's own corners, so without it a square ring sat on the rounded
   card. */
.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 56px;
  padding: 14px 20px;
  border-radius: var(--radius-control);
  color: var(--harbour);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
/* The circle is 28px because width and height say so. line-height 24px, not
   28, centres the glyph in it: at 28 both + and − sat 2px low. */
.faq summary::after {
  content: "+";
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--stone-light);
  color: var(--line-blue);
  font-size: 1.25rem;
  line-height: 24px;
  text-align: center;
}
.faq[open] summary::after { content: "\2212"; }

.faq-answer { padding: 0 20px 18px; }
/* A TODO in an answer runs the width of the answer text. Its own max-width
   is 66ch at its smaller size, which stopped it about 10px short of the
   paragraph above it. */
.faq-answer .todo { max-width: none; }

/* ---- components/form.css ---- */
/* ---- both forms, and the grid and aside around them -------------------------
   The quote form on /pricing/ and the contact form on /contact/ share these
   field styles and the same two-column layout. What only the quote form has —
   the numbered steps, the steppers, the pinned total — is builder.css.
   -------------------------------------------------------------------------- */

/* One column, form above aside, until 1000px. Not auto-fit: that made two
   EQUAL columns as soon as two 360px tracks fitted, so from about 834px to
   999px the form had half the row — narrower than it is at 830px, every
   two-up row of fields stacked, and the quote form 3,400px tall beside a
   short aside. The form needs the width more than the aside needs to sit
   beside it. */
.contact-grid {
  display: grid;
  gap: clamp(2rem, 1rem + 4vw, 3.5rem);
  align-items: start;
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 1000px) {
  .contact-grid { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }

  /* Side by side, the aside's first heading should sit level with the form
     card's first line. An aside that opens with a .bill-sheet card (the
     pricing guide on /pricing/) does that by itself, because the two cards
     share a border and --pad-panel. One that opens with a bare heading (on
     /contact/) sat 31-37px higher, so it gets the card's padding and 1px
     border as its own top padding. Without :has() it stays where it was. */
  .contact-aside:not(:has(> .bill-sheet:first-child)) {
    padding-top: calc(var(--pad-panel) + 1px);
  }
}

/* The single column below 1000px is up to 935px wide, so the prose in the
   card and the aside, and the hints under the fields, keep the site's line
   length rather than the column's. */
.form-card > p,
.contact-aside p,
.field-hint { max-width: var(--measure); }

.form-card {
  padding: var(--pad-panel);
  border: 1px solid var(--stone);
  border-radius: var(--radius-panel);
  background: var(--paper);
}

/* For the Turnstile widget, which is a fixed 300px wide. Below 375px the
   card's 20px padding leaves it less than that, and the widget ran out of the
   card and widened the page. 12px leaves exactly 300px at 358px. Narrower
   than that it runs into the padding, past the card's edge below 346px, and
   off the page below 329px, which no padding can prevent: there it needs
   Turnstile's compact size, rendered from script. Not done here, because
   that is the anti-bot path and can only be tested on staging. */
@media (max-width: 374px) {
  .form-card { padding-inline: 12px; }
}

/* The space between a card's intro and its form. */
.contact-form { margin-top: 24px; }

/* Fields are spaced by their row's gap and the row's own margin, not by a
   margin on each field: inside a grid row a field's margin is part of the
   row's height, so a row that ended a step left 18px more under it than any
   other control did. */
.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: 18px;
  margin-bottom: 18px;
}

.field { margin-bottom: 18px; }
.form-row > .field { margin-bottom: 0; }
.field label,
.choice-group legend {
  display: block;
  margin-bottom: 6px;
  color: var(--harbour);
  font-weight: 700;
}
/* A legend's own 2px of inline padding set it 2px in from every label. */
.choice-group legend { padding: 0; }
.field .req,
.form-key .req { color: var(--alert); }
/* 6px off the control above, as a label is 6px off the control below. At 0
   a hint touched the input's or the stepper's border. */
.field-hint { display: block; margin-top: 6px; font-size: var(--fs-small); color: var(--slate-muted); }

/* A hint under a legend describes the whole question, so it sits under the
   legend the way a control sits under its label (the legend's 6px), and the
   choices follow 12px below. */
.choice-group > .field-hint { margin: 0 0 12px; }

/* What the red * beside a label means. The whole key is hidden from screen
   readers, as the stars in the labels are: each of those fields is
   `required` and announces itself so, and a lone "Required" read before the
   first field would refer to nothing. */
.form-key {
  margin: 0 0 12px;
  font-size: var(--fs-small);
  color: var(--slate-muted);
}

.field input,
.field textarea,
.field select {
  width: 100%;
  min-height: 48px;
  padding: 11px 14px;
  border: 1px solid var(--slate-muted);
  border-radius: var(--radius-control);
  background: var(--paper);
  font-family: inherit;
  /* 16px minimum: anything smaller makes iOS Safari zoom on focus. */
  font-size: 1rem;
  color: var(--harbour);
}
.field input:focus,
.field textarea:focus,
.field select:focus { border-color: var(--line-blue); }
/* Block, because an inline textarea sits on its line's baseline and leaves a
   descender gap of about 8px under it — which made the space above the
   Turnstile widget 28px. */
.field textarea { display: block; min-height: 140px; resize: vertical; }

.choice-group {
  margin: 0 0 18px;
  padding: 0;
  border: 0;
}
.choices { display: flex; flex-wrap: wrap; gap: 10px; }
.choice {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 8px 16px;
  border: 1px solid var(--stone);
  border-radius: var(--radius-control);
  cursor: pointer;
  transition: border-color var(--transition), background-color var(--transition);
}
/* flex: none, or a label that wraps squeezes the box (14px wide at 390).
   margin: 0 drops the browser's own 3-5px, so the gap to the text is the
   8px above. */
.choice input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--line-blue); }
/* The pill shows the answer, not only the 18px control inside it: a chosen
   "Yes" looked exactly like "No". */
.choice:hover { border-color: var(--slate-muted); }
.choice:has(input:checked) { border-color: var(--line-blue); background: var(--stone-light); }

.form-status {
  margin-top: 16px;
  padding: 12px 16px;
  display: none;
}
/* The two results of sending, told apart by the stripe and by the sentence
   itself. Not the yellow the error used to borrow: that is the TODO chrome,
   which marks a client placeholder and goes when the last client question
   is answered. */
.form-status.is-ok { display: block; background: var(--stone-light); color: var(--harbour); border-left: 4px solid var(--lamp); }
.form-status.is-err { display: block; background: var(--stone-light); color: var(--harbour); border-left: 4px solid var(--alert); }

/* The honeypot's wrapper: a class, never a style attribute, so it stays
   hidden under a strict style-src. Off-canvas rather than display:none,
   because some bots skip what is not displayed. */
.form-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Turnstile renders its own iframe into this div and sizes it itself. The
   only thing the page decides is the space around it: 20px above, and 20px
   down to the button. line-height: 0 removes the descender gap under its
   inline iframe, which made the box 73px tall for a 65px widget. The
   widget's own text is inside its iframe, so it does not inherit this. */
.cf-turnstile { margin: 20px 0 0; line-height: 0; }

.form-actions { margin-top: 20px; }

/* ---- beside the form --------------------------------------------------- */
.contact-aside > * + * { margin-top: 24px; }
.contact-facts { margin: 0; padding: 0; list-style: none; }
.contact-facts li { padding: 10px 0; border-bottom: 1px solid var(--stone); }
.contact-facts strong { display: block; color: var(--harbour); }
/* Every link in the list at the 28px the phone link already gets from
   utilities.css, so a row with a link is one height whichever link it is. */
.contact-facts a { display: inline-flex; align-items: center; min-height: 28px; }

/* On a phone every button in a .btn-row fills the width (button.css). The
   contact form's send button and the aside's lone button are not in one, and
   were the only buttons left at their own width. Not the quote form's: its
   button sits in the pinned total bar beside the total. */
@media (max-width: 480px) {
  .contact-form:not(.builder) .form-actions > .btn,
  .contact-aside p > .btn:only-child { width: 100%; }
}

/* ---- components/builder.css ---- */
/* ---- the price calculator ---------------------------------------------------
   The quote form on /pricing/, read as numbered steps with a running total
   pinned to the bottom of the screen.

   The steps ARE numbered here, and that is not decoration: the first answer
   decides what the rest of the form asks. The number of users picks the plan,
   and only OneConnect UnSquare has a step asking how many simultaneous calls.
   That step is `display: none` on Unlimited, and an element that is not
   rendered does not increment a CSS counter — so the steps after it renumber
   themselves rather than skipping a number.
   -------------------------------------------------------------------------- */

/* No top margin: the form opens its card, and the first step starts at the
   card's padding, level with the pricing guide's title beside it. */
.builder-steps {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: builder;
}

.builder-step {
  counter-increment: builder;
  padding: clamp(20px, 2.5vw, 28px) 0;
  border-top: 1px solid var(--stone);
}
.builder-step:first-child { border-top: 0; padding-top: 0; }
/* A step's padding is the space under it. Whatever ends the step brings no
   18px of its own, or the step it ends is 18px deeper than the rest. */
.builder-step > :is(.form-row, .field, .choice-group):last-child { margin-bottom: 0; }

.builder-title {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 0 0 16px;
  font-size: var(--fs-h3);
}
.builder-title::before {
  content: counter(builder);
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-pill);
  background: var(--harbour);
  color: var(--paper);
  font-size: var(--fs-small);
  line-height: 30px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* ---- the "– [ 1 ] +" control ------------------------------------------- */

/* The field is a real number input and does the whole job on its own; the two
   buttons are 43-stepper.js's, so they exist only once <html> has the `js`
   class. Without it the visitor gets the browser's own number field, spinner
   and all. With it, the spinner goes — two sets of up/down controls on one
   field is one too many — and the buttons join the field into one control. */
.stepper {
  display: inline-flex;
  align-items: stretch;
  max-width: 100%;
}
/* 6.5rem holds six digits: ?users= accepts up to 100,000. */
.stepper input {
  width: 6.5rem;
  min-height: 48px;
  padding: 8px 10px;
  border: 1px solid var(--slate-muted);
  border-radius: var(--radius-control);
  background: var(--paper);
  font-family: inherit;
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--harbour);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.stepper input:focus { border-color: var(--line-blue); }

.stepper-btn { display: none; }
.js .stepper-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 48px;
  min-height: 48px;
  padding: 0;
  border: 1px solid var(--slate-muted);
  background: var(--stone-light);
  color: var(--harbour);
  font-family: inherit;
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background var(--transition);
}
.js .stepper-btn:hover { background: var(--stone); }
.js .stepper-btn:first-child { border-right: 0; border-radius: var(--radius-control) 0 0 var(--radius-control); }
.js .stepper-btn:last-child { border-left: 0; border-radius: 0 var(--radius-control) var(--radius-control) 0; }
/* At the minimum: still focusable (see 43-stepper.js for why it is
   aria-disabled rather than disabled), visibly spent. */
.js .stepper-btn[aria-disabled="true"] {
  background: var(--paper);
  color: var(--slate-muted);
  cursor: default;
}
.js .stepper input {
  border-radius: 0;
  -moz-appearance: textfield;
  appearance: textfield;
}
.js .stepper input::-webkit-inner-spin-button,
.js .stepper input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* ---- step 1: the plan the users fit ------------------------------------ */

/* One panel per plan. With JavaScript only the one that fits is shown; without
   it both are, each headed by the range it is for. */
.plan-fits {
  display: grid;
  gap: 12px;
  margin-top: 20px;
}
.plan-fit {
  padding: 16px 18px;
  border: 1px solid var(--line-blue);
  border-left-width: 4px;
  border-radius: var(--radius-control);
  background: var(--stone-light);
}
.plan-fit p { margin: 0; }
.plan-fit-range {
  color: var(--slate-muted);
  font-size: var(--fs-small);
  font-weight: 700;
}
.plan-fit-name {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 12px;
  margin-top: 2px;
  color: var(--harbour);
  font-size: 1.125rem;
  font-weight: 700;
}
.plan-fit-price {
  color: var(--slate);
  font-size: var(--fs-small);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}
.plan-fit-text { margin-top: 6px; }

/* ---- step: numbers and optional services ------------------------------- */

.choices--stack { flex-direction: column; align-items: stretch; }
/* An add-on's label can wrap, and a box centred on two lines sits between
   them. Top-aligned instead, and nudged down by half the difference between
   a line of text and the 18px box, so it is centred on the FIRST line. Not
   1lh: on the input that is the input's own line height. */
.choices--stack .choice { justify-content: flex-start; align-items: flex-start; }
.choices--stack .choice input { margin-top: calc((var(--fs-body) * var(--lh-text) - 18px) / 2); }
.field--ai { margin-top: 16px; margin-bottom: 0; }

/* ---- step: the estimate, line by line ---------------------------------- */

/* Written by 45-price-builder.js, so it is not rendered at all without
   JavaScript — an empty numbered step would be a promise with nothing behind
   it. A class rule under `.js` rather than the hidden attribute, because the
   step must appear with JavaScript without any script having to remove
   anything: utilities.css makes [hidden] display:none !important, which no
   rule here could override. Either way it is display:none, so the step
   counter skips it. */
.builder-step--estimate { display: none; }
.js .builder-step--estimate { display: block; }

.bill--estimate .bill-line { padding: 10px 0; }
.bill--estimate .bill-name { font-size: var(--fs-body); }

/* The total on the right, as on a bill, and the cost per user under it in
   the same column: smaller, lighter and muted, so the total stays the result
   and the per-user figure reads as a note on it. A grid rather than the
   wrapping flex row this was, because a wrapped row put the amount at the
   left and would have left the per-user line under nothing. */
.estimate-total {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 2px 16px;
  padding-top: 14px;
  border-top: 2px solid var(--harbour);
  color: var(--harbour);
  font-weight: 700;
}
.estimate-total p { margin: 0; }
.estimate-total-amount {
  font-size: 1.25rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.estimate-total-per {
  grid-column: 2;
  color: var(--slate-muted);
  font-size: var(--fs-small);
  font-weight: 400;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.estimate-total-per:empty,
.builder-total-per:empty { display: none; }

/* What happens next, under the result. */
.estimate-next {
  max-width: var(--measure);
  margin: 16px 0 0;
}

/* ---- the running total --------------------------------------------------- */

/* Sticky, so the total stays in view while the steps scroll under it — with
   JavaScript only, and not on short or narrow screens (the rules below). */
.builder-total {
  position: sticky;
  bottom: 0;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px var(--gap);
  margin-top: var(--gap);
  padding: 14px clamp(16px, 2vw, 24px);
  border-radius: var(--radius-panel);
  background: var(--harbour);
  color: var(--mist);
}

/* Label, amount and cost per user, one over the other at every width, so
   the block is only as wide as its longest line and the actions fit beside
   it (the measurements are with the container query below). */
.builder-total-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.builder-total-label {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--mist);
}
.builder-total-amount {
  margin: 0;
  font-family: var(--font-display);
  /* font-stretch, as every other display element sets it. This used to be
     font-variation-settings: "wdth" var(--display-stretch), which is invalid —
     the token is a percentage and that property wants a number — so the
     figures rendered at Archivo's normal width beside wide headings. */
  font-stretch: var(--display-stretch);
  /* Deliberately smaller than --fs-h2: this line is inside a bar pinned over
     the form, and every pixel it grows is a field the visitor cannot see. */
  font-size: 1.75rem;
  font-weight: 700;
  line-height: var(--lh-tight);
  color: var(--paper);
  font-variant-numeric: tabular-nums;
}
/* Under the amount, smaller, and mist rather than paper: the client asked
   for the cost per user to be secondary to the total. */
.builder-total-per {
  margin: 0;
  color: var(--mist);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
}
.builder-total .form-actions { margin-top: 0; }

/* Book a demo, and "or request a quote" centred under it: the button is the
   strong call to action the client asked for, the quote its quieter
   alternative. */
.builder-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.builder-alt {
  margin: 0;
  color: var(--mist);
  font-size: var(--fs-small);
}
/* A real submit button dressed as a link. min-height 28px keeps it a tap
   target above WCAG 2.2's 24px minimum, the size utilities.css gives a
   phone link. The white focus ring comes from reset.css's rule for the
   navy surfaces, which names .builder-total. */
.builder-alt-btn {
  min-height: 28px;
  padding: 0 2px;
  border: 0;
  background: none;
  color: var(--paper);
  font: inherit;
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.builder-alt-btn:hover { color: var(--mist); }
.builder-alt-btn[disabled] { opacity: 0.55; cursor: not-allowed; }

/* The total and the actions side by side, which is what keeps the pinned
   bar short: 106px at every width where it is pinned, against 173px where
   the actions wrapped under the total (measured 2026-10-05 at 1440, 1100,
   1020, 1000, 999, 900, 600 and 561, with one, 400 and 1,000 users). It was
   80px before the cost per user and the second action, both of which the
   client asked for.

   What has to fit beside the 153px actions is the total block, as wide as its
   amount: "$127.40 a month" is 249px in the display face at 1.75rem,
   "$4,882.40 a month" (400 users) 277 and "$12,082.40 a month" (1,000 users)
   296; at 1.5rem, 214, 237 and 254. The bar has 570px at 1440, 491 at 1100
   and 440 at 1000, where the form shares its row with the pricing guide
   (form.css), and 833 at 999, 478 at 600 and 442 at 561 in the single
   column. So in a bar under 480px the amount steps down to 1.5rem, which
   keeps even a five-figure total beside the actions in the narrowest bars
   that are pinned, 440 and 442. Narrower still — a phone, where the bar is
   not pinned (below) and is 188px tall — the actions take the full width
   under the total, the button as wide as the bar, like every other button on
   a phone. The queries are on the bar's own width, not the screen's: the
   form is in a narrower column from 1000px than just below it. */
.js .builder-total { container-type: inline-size; }
@container (max-width: 479px) {
  .js .builder-total-amount { font-size: 1.5rem; }
}
@container (max-width: 435px) {
  .js .builder-actions { flex: 1 1 100%; align-items: stretch; }
  .js .builder-alt { text-align: center; }
}

/* The standing caveat lives below the bar, not in it. */
.builder-note {
  margin: 12px 0 0;
  max-width: var(--measure);
  color: var(--slate-muted);
  font-size: var(--fs-small);
}

/* Sticky is worth having on a long form and worth losing on a short screen:
   below 700px tall the bar would eat most of the viewport. And without
   JavaScript there is no running total to keep in view. */
@media (max-height: 700px), (max-width: 560px) {
  .builder-total { position: static; }
}
html:not(.js) .builder-total { position: static; }
/* Without JavaScript the "amount" is the sentence "We will work this out with
   you.", and a sentence is not a figure: at the display face's width it ran
   to two lines and made the bar taller than it was before. */
html:not(.js) .builder-total-amount { font-stretch: normal; }

/* While the bar is pinned it covers the bottom 106px of the screen (measured
   at every pinned width, above), and a browser tabbing to a field there
   counts it as already on screen and does not scroll. The margin makes the browser keep every control in the steps
   clear of the bar (WCAG 2.2, 2.4.11 Focus Not Obscured). Only where the bar
   is actually pinned: the same conditions as the two rules above, turned
   round. */
@media (min-height: 701px) and (min-width: 561px) {
  .js .builder-steps :is(input, select, textarea, button, a, iframe) { scroll-margin-bottom: 150px; }
}

/* The quote form opens its card: its title and intro are the section head
   above the card, so there is nothing inside the card for it to be spaced
   from, and it takes back the .contact-form margin that the form on
   /contact/ keeps. The class is also the hook 45-price-builder.js looks for
   (#contact-form.builder). The numbered steps and the pinned total come from
   .builder-step* and .builder-total, which the plain form on /contact/ simply
   does not use. */
.builder { margin-top: 0; }

/* For a sentence that is only true while 45-price-builder.js runs — "The
   estimate updates as you go" in the calculator's intro on /pricing/ —
   inside copy that is true either way. Without JavaScript no estimate is
   shown, so neither is the sentence. */
.js-only { display: none; }
.js .js-only { display: inline; }

/* ---- components/prose.css ---- */
/* ---- long-form pages: Business VoIP, 911, privacy, and the two no-JS form
   landings (thank-you, form-problem) -------------------------------------------
   .prose sits INSIDE a .container, never on it. On the same element its
   max-width replaced the container's and `margin: 0 auto` centred the column,
   so the text started up to 258px right of the banner h1 above it (1440) and
   20px off it at 768. Nested, the column starts at the container's edge, like
   the content of every other inner page. */

/* 60ch, not --measure: --measure (66ch = 727px) used to include the
   container's 2 x 32px gutters, so the text itself was 663px. Inside the
   container 60ch at 17px is 661px, the same line length as before. */
.prose { max-width: 60ch; }
/* The same size as .feature-group-title (features.css): one size for an h2
   inside a section, site-wide. Change both together. */
.prose h2 { margin-top: clamp(2.25rem, 1.8rem + 2vw, 3.25rem); font-size: clamp(1.5rem, 1.25rem + 1vw, 2rem); }
.prose h2:first-child { margin-top: 0; }
/* Bullet-list styling for bare lists only. A list with a class of its own
   (.contact-facts on /form-problem/) brings its own spacing, and `.prose ul`
   outranked it: its rows were pushed 19px in and 6px apart. :where() keeps
   the ul rule at the old (0,1,1), so .compare-col ul below, also (0,1,1)
   and later, still sets the compare lists' own 1.1rem indent. */
.prose ul:where(:not([class])) { padding-left: 1.2rem; }
.prose :is(ul, ol):not([class]) > li + li { margin-top: 6px; }
/* max-width: none so a TODO runs the full width of the text above it; its
   own 66ch is counted at 14px and stopped 54px short. */
.prose .todo { max-width: none; margin: 1rem 0 1.5rem; }

/* Two ways to run a phone system, side by side. */
.compare {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  margin: 1.5rem 0 0;
}
.compare-col {
  padding: 24px;
  border: 1px solid var(--stone);
  border-radius: var(--radius-panel);
  background: var(--paper);
}
.compare-col--hosted { border-color: var(--line-blue); box-shadow: inset 0 4px 0 var(--line-blue); }
/* On the site's h3 size; left alone it was the browser's 1.17em (19.89px),
   the one off-scale heading. */
.compare-col h3 { margin-bottom: 12px; font-size: var(--fs-h3); }
.compare-col ul { margin: 0; padding-left: 1.1rem; }

/* Side by side, the two cards share a title row and a list row, so both
   lists start level whatever length either title is. row-gap: 0 keeps
   .compare's --gap out from between a card's own title and list; the h3's
   12px margin is the space there. Stacked, each card spans its own two rows
   and --gap separates the cards, as before. Without subgrid the cards fall
   back to plain blocks, and the lists start level only while both titles run
   the same number of lines: today, two each wherever the cards sit side by
   side (about 680px up). */
@supports (grid-template-rows: subgrid) {
  .compare-col {
    display: grid;
    grid-row: span 2;
    grid-template-rows: subgrid;
    row-gap: 0;
  }
}

/* The emergency notice, in the alert colour — which elsewhere marks only
   required fields and a form that was not sent (tokens.css). The rule is 4px, the
   weight of the calculator's .plan-fit rule (builder.css), rather than a
   third weight of its own. The margin keeps the lead paragraph off the box,
   which it otherwise touched. */
.notice-911 {
  margin-bottom: 1.5rem;
  padding: 20px 24px;
  border-left: 4px solid var(--alert);
  background: var(--stone-light);
}
.notice-911 p { margin: 0; color: var(--harbour); font-weight: 700; }

/* ---- components/cta-band.css ---- */
/* ---- closing call to action: the navy band at the foot of a page --------- */

.cta-band {
  padding: clamp(3rem, 2rem + 4vw, 5rem) 0;
  background: var(--harbour);
  color: var(--mist);
}

/* The band is laid out like a line on the bill: the words on the left, the
   buttons in a column of their own on the right, where the price column
   would be. It used to be a wrapping flex row, so whether the buttons sat
   beside the text or dropped under it depended on how long each page's text
   and buttons were: at 1100 they sat beside it on four pages and under it
   on three, and at 1440 the buttons started at six different x positions
   across the seven pages that have the band.

   The column is 17rem because the longest button in a band, "Estimate your
   monthly cost" on /business-voip/, is 265px ("Hotels, motels and schools",
   the same width, went with the old /hospitality/ page). auto lets it grow
   if a longer label ever arrives, rather than overflow; the column then
   moves on that page only. From 900 up, where the text beside it still gets
   about 516px; below that the buttons go under the text on every page. */
.cta-inner {
  display: grid;
  gap: 24px 48px;
  align-items: center;
}
@media (min-width: 900px) {
  .cta-inner { grid-template-columns: minmax(0, 1fr) minmax(17rem, auto); }
  .cta-band .btn-row { flex-direction: column; align-items: stretch; }
}

.cta-band h2 {
  max-width: 20ch;
  margin: 0 0 10px;
  color: var(--paper);
  font-size: var(--fs-h2);
}
/* balance: the home band's sentence otherwise ends with "you." alone on its
   second line. */
.cta-band p { max-width: 44ch; margin: 0; text-wrap: balance; }
.cta-band .btn-row { margin-top: 0; }

/* On a phone every button takes the full width, so a pair stacks as two
   equal bars instead of two ragged widths (133 over 265px on /pricing/). */
@media (max-width: 480px) {
  .cta-band .btn-row > .btn { flex: 1 1 100%; }
}

/* ---- components/footer.css ---- */
/* ---- footer and back-to-top -------------------------------------------------- */

.site-footer {
  background: var(--harbour-deep);
  color: var(--mist);
  font-size: var(--fs-small);
}

/* Four blocks, so the columns go 4, 2 or 1 and never 3: an auto-fit grid made
   three columns from about 712 to 946px and left "Get started", the footer's
   one action, alone on a second row. That grid went to four at 947px; 950 is
   the round number beside it (four columns of 201px). At 441px the two
   columns are 193px, still wider than the 178px button. */
.footer-main {
  display: grid;
  gap: 32px var(--gap);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  padding-top: clamp(3rem, 2rem + 3vw, 4.5rem);
  padding-bottom: clamp(2.5rem, 2rem + 2vw, 3.5rem);
}
@media (min-width: 950px) {
  .footer-main { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 440px) {
  .footer-main { grid-template-columns: minmax(0, 1fr); }
}

.footer-brand img { width: var(--logo-w); margin-bottom: 16px; }
.footer-brand p { max-width: 28ch; }

.footer-heading {
  margin: 0 0 12px;
  color: var(--paper);
  font-family: var(--font-text);
  font-stretch: normal;
  font-size: var(--fs-body);
  font-weight: 700;
}

.footer-nav ul { margin: 0; padding: 0; list-style: none; }
.footer-nav a,
.footer-login {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  color: var(--mist);
  text-decoration: none;
}
.footer-nav a:hover,
.footer-login:hover { color: var(--paper); text-decoration: underline; }

.footer-action .btn { margin-bottom: 12px; }
.footer-login { display: flex; }

.footer-bottom {
  padding-top: 20px;
  padding-bottom: 28px;
  border-top: 1px solid var(--line-blue);
}
.footer-bottom p { margin: 0; }

/* ---- back to top ------------------------------------------------------- */
/* It only works with JavaScript, so without it the button does not show. */
.to-top {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 800;
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  border: 0;
  border-radius: var(--radius-control);
  background: var(--line-blue);
  color: var(--paper);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.25s ease, visibility 0.25s, background var(--transition);
}
.js .to-top { display: inline-flex; }
.to-top.is-visible { opacity: 1; visibility: visible; }
.to-top:hover { background: var(--harbour-deep); }
/* It is fixed over the page, and a keyboard only reaches it at the bottom,
   where it sits on the navy footer: the blue ring was 2.13:1 there. It is
   not inside the footer, so the dark-surface rule in reset.css never
   matched. A white ring drawn inside its own blue fill reads 7.08:1 on
   whatever it is over — the approach reset.css takes for .skip-link. */
.to-top:focus-visible { outline-color: var(--paper); outline-offset: -6px; }

/* On /pricing/ the quote form's running total pins to the bottom of the
   screen (builder.css: at least 561px wide and 701px tall). Below 1000px the
   form is one column (form.css), so the pinned bar spans it and this button
   sat on the bar. The bar is 106px at every width where it is pinned
   (builder.css, measured 2026-10-05; it was 80 and 118 before the cost per
   user and the second action were added to it), and 126 clears it by the
   button's usual 20px. It moves up everywhere on that page, not only beside
   the form, which costs nothing. */
@media (min-width: 561px) and (max-width: 999px) and (min-height: 701px) {
  body:has(.builder-total) .to-top { bottom: 126px; }
}

/* ---- components/prototype.css ---- */
/* ---- prototype chrome ---------------------------------------------------------
   TODO boxes, stand-in notes and "Sample" badges. None of this is design: it
   marks what the client still has to supply (CLIENT-QUESTIONS.md) and which
   parts are still unanswered. The stand-in notes are gone — this is the client
   repo now. Since 2026-10-28 no page carries a TODO box (the operator had
   them all removed; the questions are kept in CLIENT-QUESTIONS.md), so the
   .todo rules here and in hero, faq and prose.css style nothing; they stay
   so a box can come back without restyling it.
   -------------------------------------------------------------------------- */

.todo {
  display: block;
  max-width: var(--measure);
  margin: 1rem 0;
  padding: 12px 16px;
  border: 2px dashed var(--todo-line);
  border-radius: var(--radius-control);
  background: var(--todo-bg);
  color: var(--todo-ink);
  font-size: var(--fs-small);
  font-weight: 700;
  line-height: 1.45;
}

/* After a grid, a TODO sits --gap below it: the gap between the tiles, the
   steps and the plan cards, and well clear of a FAQ list's 14px rows. At the
   default 1rem it sat closer to the last row than the rows sit to each other,
   and read as one more card. */
:is(.tiles, .steps, .plan-grid, .faq-list) + .todo { margin-top: var(--gap); }
/* In the hero, no wider than the lead above it (34rem, as .hero-lead in
   hero.css). At its own 66ch it ran to 609px under a 544px lead. */
.hero .todo { max-width: 34rem; }

.todo--inline {
  display: inline-block;
  margin: 0;
  padding: 2px 8px;
  border-width: 1px;
  font-size: 0.8125rem;
  white-space: normal;
}

.sample-badge {
  display: inline-block;
  margin: 0 0 0 8px;
  padding: 1px 10px;
  border-radius: var(--radius-pill);
  background: var(--stone);
  color: var(--harbour);
  font-size: 0.8125rem;
  font-weight: 700;
  vertical-align: 2px;
}
.phone-body > .sample-badge { margin-left: 0; }

/* ---- utilities.css ---- */
/* ---- utilities, motion, print ------------------------------------------- */

/* The `hidden` attribute is how the calculator takes a plan's step and panel
   off the page, and how the hardware filter hides a card. Its UA rule is only
   `display: none`, which ANY later display declaration beats — and .hw-card
   sets one. Without this line a filtered-out card stays on screen, so this is
   load-bearing rather than defensive; the calculator's li.builder-step and
   .plan-fit set no display of their own today, and would need it the day one
   of them does. utilities.css is concatenated last, which is what lets it
   win. */
[hidden] { display: none !important; }

/* Keeps a hyphenated compound on one line. Headings are balanced
   (typography.css), and a typed hyphen is a place the browser may break, so
   "Request a Large-Scale Quote" came out as "Request a Large- / Scale Quote".
   No CSS property turns that break off, so a compound that must stay whole
   goes in a span with this class. */
.nowrap { white-space: nowrap; }

/* Any phone link anywhere is a tap target, including one written directly into
   page content: enforce the WCAG 2.2 minimum. */
a[href^="tel:"] {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

@media print {
  .topbar,
  .site-header,
  .nav-backdrop,
  .site-footer,
  .to-top,
  .skip-link { display: none; }

  body { color: #000; background: #fff; }
  .section { padding: 1rem 0; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.75em; }
}
