/* ==========================================================================
   Nexvy, the company's own site
   --------------------------------------------------------------------------
   Separate from demo.css on purpose. That file is the renderer for client
   sites and must never mention a business or a niche; this one is a single
   marketing page and is allowed to be opinionated.

   Reuses the Inter file demo.css already self hosts, so this page costs one
   extra stylesheet and no extra font download.

   The look has a job. A company selling websites is judged on its own website
   before a word of the copy is read, and the whole pitch is that automated does
   not mean cheap looking.

   The palette is warm rather than cool. Blue-grey neutrals with one bright
   accent is the default look of every SaaS landing page built in the last five
   years, and a reader has seen it a thousand times. Stone neutrals against a
   deep forest green read as considered and editorial instead, which is what an
   agency should look like. Restraint does the work here: one accent, one font,
   a real spacing scale, and hairlines instead of heavy borders.
   ========================================================================== */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/inter-latin-var.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* Warm near-black through to warm stone. Nothing here is a pure grey. */
  --ink:        #14161a;
  --ink-soft:   #4e5560;
  /* 5.20:1 on --paper, 4.94:1 on --paper-warm and 4.52:1 on --accent-wash.
     This token carries every ledger note, form hint and footer line, so it is
     body text and has to clear 4.5:1 on every ground it lands on. The third
     ground is the one the old #6b727d was never checked against: a ledger row
     paints --accent-wash under itself on hover and took the note down to
     4.34:1 while the cursor was on the row a reader was reading. Do not
     lighten it. */
  --ink-faint:  #656c77;
  --line:       #e7e4de;
  --line-soft:  #f1efea;
  --paper:      #ffffff;
  --paper-warm: #faf9f6;

  /* One accent. Deep forest green stays legible on both the light and the dark
     ground without needing a second tone, and it is dark enough to use as text
     colour, which a brighter green would not be. */
  --accent:      #0d6349;
  --accent-lift: #12805e;
  --accent-soft: #35c88d;
  --accent-wash: #edf4f0;
  --accent-line: #cfe2d8;

  --night:      #0b0f16;
  --night-soft: #141a24;
  --night-line: #242d3b;
  --night-text: #a9b3c1;
  /* 5.87:1 on --night and 4.93:1 on the lifted panel the dark theme swaps in
     below. The old value was certified against a ground the price card never
     actually had: three animated gradient lobes drifted under it and took
     every label on it down to 3.71:1 at the blue lobe's maximum. The lobes
     are gone and the panel is opaque, so this number is now a value that can
     be checked rather than one that drifts for 26 seconds at a time. */
  --night-faint:#838f9f;

  /* The one dark object on a light page: the price card in the hero, the
     closing panel on the home page, and the ledger's total row. Declared once
     so all three are the same object rather than three rectangles that happen
     to be the same colour, and so the dark theme can lift them off a near
     black page instead of drawing black on black. */
  --panel:      #0b0f16;
  --panel-line: #242d3b;
  /* The list of things the monthly fee covers, set on the panel. 12.94:1 on
     the light theme's panel and 10.92:1 on the lifted one the dark theme
     swaps in. Brighter than --night-text because these are the six lines the
     card is actually selling, not its captions. */
  --on-panel:   #ccd5e0;

  /* The 3px halo a focused field draws. Mixed from the accent rather than
     written out as one theme's green at ten percent, so the dark theme's
     brighter green brings its own halo instead of inheriting a forest one. */
  --accent-halo: color-mix(in srgb, var(--accent) 10%, transparent);
  /* The disc behind a tick on the price card, same reasoning. */
  --accent-disc: color-mix(in srgb, var(--accent-soft) 15%, transparent);

  --font: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;

  --shell: 1180px;
  --gutter: clamp(20px, 5vw, 40px);

  /* One spacing scale, so vertical rhythm is a decision made once rather than a
     number picked again at every block. */
  --s1: 6px;
  --s2: 10px;
  --s3: 16px;
  --s4: 24px;
  --s5: 36px;
  --s6: 56px;
  --s7: 88px;

  --radius-sm: 10px;
  --radius: 14px;
  --radius-lg: 20px;
  --radius-xl: 26px;
  /* Every small rounded tile: the hamburger, the card and trust marks. They
     were hand set to 9, 11, 12 and 11 pixels and two of them sit within four
     hundred pixels of each other in the masthead, so the inconsistency was
     visible side by side. The radius scale is now a decision made once, the
     way the spacing scale already was. */
  --radius-tile: 11px;

  /* Layered and soft. A single hard shadow is the tell of a template. */
  --shadow-sm: 0 1px 2px rgba(20, 22, 26, .04), 0 2px 6px rgba(20, 22, 26, .04);
  --shadow-md: 0 2px 6px rgba(20, 22, 26, .04), 0 12px 28px rgba(20, 22, 26, .06);
  --shadow-lg: 0 4px 12px rgba(20, 22, 26, .05), 0 24px 60px rgba(20, 22, 26, .10);

  --ease: cubic-bezier(.22, .61, .36, 1);
}

/* --------------------------------------------------------------- resets --- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 92px;
  /* clip, not hidden. overflow-x: hidden makes an element a scroll
     container, and position: sticky sticks to its nearest scrolling
     ancestor, so with hidden here the masthead was sticky against a
     box that never scrolls and rode away with the page. clip does the
     same clipping without creating the container. */
  overflow-x: clip;
}

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

body {
  margin: 0;
  font-family: var(--font);
  color: var(--ink);
  background: var(--paper);
  font-size: 17px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-feature-settings: 'cv05' 1, 'cv11' 1;
}

/* There is no overflow-x: clip here any more.
   It was hiding whatever went past the right edge rather than stopping it, so
   no screenshot of this site could prove the layout was sound at 375: only
   that any breakage was being cut off quietly. Every page is measured at 320,
   360 and 375 with it gone, and the one thing that genuinely does not fit, the
   cost ledger, scrolls inside its own box on purpose. Put it back and the next
   regression is invisible too. */

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

/* Display type is set tight. Loose tracking at large sizes is the single most
   common thing that makes a heading look amateur. */
h1, h2, h3, h4 {
  margin: 0;
  font-weight: 700;
  line-height: 1.06;
  letter-spacing: -0.03em;
  text-wrap: balance;
}

h1 { font-size: clamp(2.6rem, 6vw, 4.6rem); line-height: 1.01; letter-spacing: -0.038em; }
h2 { font-size: clamp(1.9rem, 3.7vw, 2.85rem); letter-spacing: -0.032em; }
h3 { font-size: clamp(1.08rem, 1.8vw, 1.28rem); letter-spacing: -0.018em; }

p { margin: 0 0 1.05em; text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

a { color: inherit; }

/* The UA default for b and strong is font-weight: bolder, which resolves
   against whatever weight it is nested inside rather than against 400. Inside
   a block already set at 650 or 800 it computes to 900, and the one face this
   page loads is declared 400 to 800, so the request falls outside the range.
   Naming the weight means no element anywhere can quietly ask for a weight
   that does not exist and get a synthesised one back. */
b, strong { font-weight: 700; }

/* Numbers line up in columns everywhere on this page, because most of the page
   is an argument made out of numbers. */
table, .is-cost, .stat b, .pricecard__amount b, .card__price b {
  font-variant-numeric: tabular-nums;
}

/* The words in a table are not numbers. Inter draws the hyphen at a figure
   width under tnum, so the row label inherited from the table above rendered
   "follow-up" as "follow - up" with a visible gap each side. The figures in
   the cells beside it still line up; only the prose in the row header and its
   note opts out. */
.ledger tbody th b,
.ledger tbody th small,
.ledgerrow__label,
.ledgerrow__vals i { font-variant-numeric: normal; }

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

/* Keyboard users get to skip the nav. Costs nothing and its absence is the
   single most common accessibility failure on a marketing page. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--night);
  color: #fff;
  padding: 12px 18px;
  z-index: 100;
  border-radius: 0 0 var(--radius) 0;
}
.skip:focus { left: 0; }

/* ------------------------------------------------------------- reveals --- */
/* The .anim class is added by a few lines of inline script, so with no
   JavaScript nothing is ever hidden. The script also checks for reduced motion
   before adding it. Movement is small on purpose: 10px and a fade reads as the
   page settling, anything larger reads as a slideshow. */

.anim .reveal {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .6s var(--ease), transform .6s var(--ease);
}
.anim .reveal.is-in { opacity: 1; transform: none; }

/* --------------------------------------------------------------- layout --- */

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

.section { padding-block: clamp(68px, 9.5vw, 128px); }
.section--tight { padding-block: clamp(48px, 6vw, 80px); }
.section--warm { background: var(--paper-warm); border-block: 1px solid var(--line-soft); }

/* ------------------------------------------------------ section openers --- */
/* Fourteen sections used to open with the same three elements in the same
   order: an uppercase accent label, a heading, a grey paragraph. That is the
   structural tell this site exists to argue against, reproduced on the site
   making the argument, and no amount of good copy survives being stamped out
   of the same die eleven times on one scroll.

   So there are four openers and a section picks one. They differ in structure
   rather than in decoration: where the heading sits, whether the supporting
   line is under it or beside it, and what marks the entry. The partial that
   prints them is site/partials/intro.blade.php, and at most one section on a
   page is allowed the label. */

.section__intro { max-width: 720px; margin-bottom: clamp(38px, 5vw, 64px); }
.section__intro p { color: var(--ink-soft); font-size: 1.1rem; margin-top: var(--s3); }

/* Heading left, supporting line in its own narrower column to the right, with
   the rule running the full measure above both. The reading order is unchanged
   and the page stops looking like a stack of centred blocks. */
.section__intro--wide { max-width: none; }
.section__intro--wide > .section__head {
  border-top: 1px solid var(--line);
  padding-top: clamp(20px, 2.6vw, 30px);
  display: grid;
  gap: var(--s4) clamp(32px, 6vw, 72px);
}
@media (min-width: 860px) {
  .section__intro--wide > .section__head { grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); align-items: start; }
  .section__intro--wide h2 { margin-top: -.14em; }
}
.section__intro--wide p { margin-top: 0; max-width: 46ch; }

/* There used to be an index variant here: a display numeral standing in for
   the label, drawn in --line-strong. No section ever asked for it, and at
   1.35:1 on --paper-warm it would have shipped a two and a half rem number
   nobody could read on the day one did. A variant with no caller is a promise
   the file cannot keep, so it is gone rather than waiting. */

/* A rule with the heading sitting on it, and the supporting line run in
   underneath at reading size rather than blown up to 1.1rem. */
.section__intro--rule > .section__head { border-top: 2px solid var(--ink); padding-top: var(--s3); }
.section__intro--rule p { font-size: 1rem; max-width: 58ch; }

/* A short rule before the label. Kept, because one section on a page earns a
   label, and used exactly once per page. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: .76rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--s3);
}
.eyebrow::before {
  content: '';
  width: 22px;
  height: 1.5px;
  background: var(--accent);
  flex: none;
  opacity: .55;
}

/* -------------------------------------------------------------- masthead -- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--bar);
  /* Kept deliberately cheap. A backdrop-filter on a sticky bar re-blurs what
     is behind it on every scroll frame, which is the usual cause of scroll
     stutter on Windows. 8px is visually almost identical to 16 and costs far
     less, and dropping the saturate pass removes a second full-width filter. */
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-bottom: 1px solid transparent;
  transition: border-color .2s var(--ease), background-color .2s var(--ease);
}
/* The rule only appears once there is content behind the bar. At the top of the
   page it would be a line drawn across the hero for no reason. */
.masthead { border-bottom-color: var(--line-soft); }

.masthead__inner {
  display: flex;
  align-items: center;
  gap: var(--s4);
  min-height: 70px;
  width: 100%;
  /* The bar itself is full bleed, so the padding lives here rather than
     coming from .shell. Slightly wider than the content gutter, because a
     navigation that hugs the window edge reads as cramped. */
  padding-inline: clamp(18px, 3.4vw, 44px);
}

/* A mark, not just a word. An agency with no logo reads as unfinished, and the
   mark is drawn in CSS-friendly SVG so it costs no request.

   It used to be an ascending chart stroke on a gradient rounded tile, which is
   the stock mark of every growth pitch ever made and one abstraction up from
   the initials-in-a-box logos this site argues against. What is here now is
   the page's own argument drawn at 26 pixels: two bars of unequal length, the
   long one what a website costs elsewhere and the short one what it costs
   here. One ink, one accent, no gradient, no tile. */
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: 1.3rem;
  font-weight: 800;
  letter-spacing: -0.045em;
  text-decoration: none;
  margin-right: auto;
}
.wordmark__text span { color: var(--accent); }

.wordmark__mark {
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  flex: none;
  color: var(--ink);
}
.wordmark__mark svg { width: 26px; height: 26px; }
.wordmark__mark .is-them { fill: currentColor; }
.wordmark__mark .is-us { fill: var(--accent); }

.masthead__nav {
  display: flex;
  gap: var(--s4);
  font-size: .94rem;
  font-weight: 500;
}
.masthead__nav a {
  color: var(--ink-soft);
  text-decoration: none;
  padding-block: 4px;
  border-bottom: 1.5px solid transparent;
  transition: color .15s var(--ease), border-color .15s var(--ease);
}
.masthead__nav a:hover { color: var(--ink); border-bottom-color: var(--accent); }

@media (max-width: 920px) { .masthead__nav { display: none; } }

/* Two links, not a dropdown. A dropdown for two options is a click that buys
   nothing, and a crawler follows a plain link. */
.langs {
  display: inline-flex;
  border: 1px solid var(--control-line);
  border-radius: 999px;
  overflow: hidden;
  font-size: .8rem;
  font-weight: 600;
  background: var(--paper);
}
.langs a {
  padding: 6px 13px;
  text-decoration: none;
  color: var(--ink-faint);
  transition: color .15s var(--ease), background-color .15s var(--ease);
}
.langs a:hover { color: var(--ink); }
/* --paper, not white. The selected language is ink-on-paper inverted, and in
   dark mode --ink is near-white, so a literal white here was white on white. */
.langs a[aria-current="true"] { background: var(--ink); color: var(--paper); }

/* --------------------------------------------------------------- buttons -- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  padding: 14px 26px;
  border-radius: 999px;
  font-size: 1rem;
  font-weight: 650;
  letter-spacing: -0.011em;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform .16s var(--ease), background-color .16s var(--ease),
              border-color .16s var(--ease), box-shadow .16s var(--ease);
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }

.btn--primary {
  background: var(--accent);
  /* Not white. The dark theme's accent is a bright green and the readable text
     on it is near-black, which is what --on-accent carries. */
  color: var(--on-accent);
}
.btn--primary:hover {
  background: var(--accent-lift);
}

/* Every edge a reader can click is drawn with --control-line, not --line.
   WCAG 1.4.11 wants a control's boundary at 3:1 against what is behind it, and
   --line is a hairline between blocks: #e7e4de measures 1.27:1 on --paper and
   1.21:1 on --paper-warm. The token existed for exactly this and was reaching
   only the text inputs and the payment methods, so the language switch, the
   theme toggle, the hamburger, every filter chip, the chat input and this
   button were all drawing an edge nobody with imperfect sight could find. */
.btn--ghost { background: transparent; color: var(--ink); border-color: var(--control-line); }
.btn--ghost:hover { border-color: var(--ink); background: var(--paper-warm); }

.btn--onnight { background: #fff; color: var(--night); }
/* A shade off white, mixed towards the slab it sits on rather than written out
   as a grey that belongs to no scale in the file. */
.btn--onnight:hover { background: color-mix(in srgb, #fff 94%, var(--night)); }

/* rgba(255,255,255,.26) composites to #4a4d53 on --panel, which is 2.26:1, and
   this is the second most clicked control on the site. .42 composites to
   #6e7076 for 3.05:1, which clears it. */
.btn--onnight-ghost { color: #fff; border-color: var(--on-panel-line); box-shadow: none; }
.btn--onnight-ghost:hover { border-color: rgba(255, 255, 255, .72); background: rgba(255, 255, 255, .07); }

.btn--wide { width: 100%; }
.btn--sm { padding: 9px 18px; font-size: .9rem; }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }

/* Below the nav breakpoint the header is the wordmark, the languages and this
   button, and the nav links are gone, so this is the only way forward that is
   always on screen. It gets tighter on a narrow phone rather than being
   dropped: a sticky call to action on mobile is worth more than the whitespace
   it costs. */
@media (max-width: 480px) {
  .masthead__inner { gap: var(--s2); min-height: 64px; }
  .masthead__cta { padding: 8px 13px; font-size: .85rem; }
  .wordmark { font-size: 1.14rem; gap: 7px; }
  .wordmark__mark { width: 26px; height: 26px; border-radius: 8px; }
  .wordmark__mark svg { width: 15px; height: 15px; }
  .langs { font-size: .76rem; }
  .langs a { padding: 5px 9px; }
}

/* ----------------------------------------------------------------- hero --- */

/* The hero used to be three radial gradient lobes drifting for 26 seconds over
   a near black ground, with a 68 pixel grid masked across it. That is the
   default landing page of the last five years, built here in green rather than
   blue, and this file's own opening paragraph argues against it.

   What replaces it is nothing. The hero is the warm paper the rest of the page
   is set on, the headline is as large as the measure allows, and the only dark
   object above the fold is the price card. One dark rectangle on a light page
   is a composition; a dark page with a dark card on it was a gradient with a
   rectangle lost in it. It also ends the contrast problem outright: the labels
   on that card sat over a moving blue lobe and measured 3.71:1 at its peak,
   and a ratio that changes every frame is not a ratio anybody can certify. */
.hero {
  position: relative;
  background: var(--paper-warm);
  color: var(--ink);
  border-bottom: 1px solid var(--line);
  padding-block: clamp(64px, 9vw, 124px);
}

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.06fr) minmax(0, .94fr);
  gap: clamp(40px, 6vw, 80px);
  align-items: center;
}
@media (max-width: 980px) { .hero__grid { grid-template-columns: 1fr; } }

.hero__lede {
  margin-top: var(--s4);
  font-size: clamp(1.08rem, 1.9vw, 1.24rem);
  line-height: 1.55;
  color: var(--ink-soft);
  max-width: 34em;
}
.hero .btn-row { margin-top: var(--s5); }

.hero__note {
  margin-top: var(--s4);
  font-size: .89rem;
  line-height: 1.6;
  color: var(--ink-faint);
  max-width: 46em;
}

/* Three figures under a hairline. They are the claims a reader checks first, so
   they sit above the fold and next to nothing else.

   A GRID OF THREE, NOT A WRAPPING FLEX. As a flex row with wrap and no column
   control the third figure dropped to a line of its own with the whole right
   half of the row empty: at 375px in both languages, and at 1440 in English
   too, where the longer labels pushed "Same day" underneath. Three items in a
   row of three is a grid, and the only question left is what it does when
   three columns stop fitting, which is two and then one, on purpose. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(20px, 3vw, 44px);
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
}
/* Below this the three labels are each two or three words in a 100px column,
   so the row becomes a single column of three rather than a comb. */
@media (max-width: 430px) {
  .stat-row { grid-template-columns: 1fr; gap: var(--s4); }
}
.stat b {
  display: block;
  font-size: 1.62rem;
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.1;
}
.stat span { font-size: .85rem; color: var(--ink-faint); }

/* ------------------------------------------------------------- pricecard -- */
/* The price in the hero rather than on a pricing page behind a click. Hiding it
   is what the whole market does and it is the thing buyers say they distrust. */

.pricecard {
  position: relative;
  /* Opaque, and its own object. The film of white over a moving gradient was
     what put every label on this card below 4.5:1. */
  background: var(--panel);
  color: #fff;
  border: 1px solid var(--panel-line);
  border-radius: var(--radius-xl);
  padding: clamp(26px, 3.4vw, 36px);
  box-shadow: var(--shadow-lg);
}
/* A hairline of light along the top edge. It is the difference between a card
   that sits on the background and a rectangle drawn on it. */
.pricecard::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .34), transparent);
}

.pricecard__label {
  margin: 0 0 var(--s2);
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--night-faint);
}
.pricecard__label--tight { margin-top: var(--s4); margin-bottom: var(--s3); }

.pricecard__amount {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  margin: 0;
  font-weight: 800;
  letter-spacing: -0.045em;
  line-height: 1;
}
/* The <b> gets its weight said out loud. Left alone it inherits 800 and then
   resolves the UA default font-weight: bolder against it, which computes to
   900, and the face is declared 400 to 800. Chrome clamps the axis back and it
   happens to look right; an engine that treats the request as unmatched
   synthesises the bold instead, and it would do it on the two most important
   numbers on the site. See the b, strong rule in the reset. */
.pricecard__amount b { font-size: clamp(2.8rem, 6vw, 3.6rem); font-weight: 800; }
.pricecard__amount span {
  font-size: .96rem;
  font-weight: 550;
  color: var(--night-faint);
  letter-spacing: 0;
}
.pricecard__amount--sm b { font-size: clamp(1.75rem, 3.4vw, 2.1rem); font-weight: 800; }

.pricecard__sub { margin: var(--s2) 0 0; font-size: .9rem; color: var(--night-faint); }

/* The monthly sits below a rule rather than beside the one-off. Side by side
   reads as two options to choose between, and they are not: you pay both. */
.pricecard__split {
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--night-line);
}

.pricecard ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.pricecard li {
  position: relative;
  display: flex;
  gap: var(--s2);
  align-items: flex-start;
  font-size: .95rem;
  line-height: 1.5;
  color: var(--on-panel);
}
/* Disc and glyph, the same construction the order summary's ticks use. The
   glyph was a data URI with the green written into the URL, and a colour
   inside a url() cannot be a token; painted through a mask it is one. */
.pricecard li::before {
  content: '';
  flex: none;
  width: 18px; height: 18px;
  margin-top: 3px;
  border-radius: 50%;
  background-color: var(--accent-disc);
}
.pricecard li::after {
  content: '';
  position: absolute;
  width: 11px; height: 11px;
  margin: 6px 0 0 4px;
  background-color: var(--accent-soft);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
}

.pricecard .btn { margin-top: var(--s4); }
.pricecard__foot { margin-top: var(--s3); font-size: .83rem; color: var(--night-faint); text-align: center; }

/* ---------------------------------------------------------------- cards --- */

.group-label {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--s4);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}
.group-label--spaced { margin-top: clamp(40px, 5.5vw, 64px); }

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(272px, 1fr));
  gap: clamp(14px, 2vw, 20px);
}

/* auto-fit stretches a lone card across the whole shell, and a 1180px card
   holding one line of text and a 42px icon reads as a row that failed to fill. */
.cards:has(> :only-child) { grid-template-columns: minmax(0, 460px); }

/* A GROUP FILLS ITS ROWS OR IT DOES NOT USE THREE COLUMNS.
   ---------------------------------------------------------------------------
   The four groups on the services page hold three, five, four and four cards,
   and a three column grid turned that into 3, then 3+2, then 3+1, then 3+1:
   three ragged half rows on one scroll, which is what a page looks like when
   the grid was chosen before the content was counted.

   The grid counts instead. Three across is the base and is what a group of
   three or six gets. Four becomes two rows of two, which is a full rectangle
   rather than a row and an orphan. Five keeps three on the first row and
   widens the remaining pair to half the measure each, so the second row is
   full as well and the two cards that got the extra width are the two the
   group ends on. Anything else falls through to the base, because auto-fit
   handles the rest.

   940px is where three columns of 272 plus their gaps first fit inside the
   shell, measured, so above it the counting is worth doing and below it the
   grid is already two or one and every row is full by definition. */
@media (min-width: 940px) {
  .cards:has(> :nth-child(4)):not(:has(> :nth-child(5))) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .cards:has(> :nth-child(5)):not(:has(> :nth-child(6))) {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
  .cards:has(> :nth-child(5)):not(:has(> :nth-child(6))) > * { grid-column: span 2; }
  .cards:has(> :nth-child(5)):not(:has(> :nth-child(6))) > :nth-child(n+4) { grid-column: span 3; }
}

.card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(22px, 2.6vw, 28px);
  transition: transform .2s var(--ease), border-color .2s var(--ease);
}
.card:hover {
  transform: translateY(-3px);
  border-color: var(--line-strong);
}
.card h3 { margin-bottom: var(--s1); }
.card p { color: var(--ink-soft); font-size: .96rem; line-height: 1.58; }

.card__mark {
  width: 42px; height: 42px;
  border-radius: var(--radius-tile);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  color: var(--accent);
  display: grid;
  place-items: center;
  margin: 0 0 var(--s3);
}
.card__mark svg { width: 20px; height: 20px; }

/* The three things you keep, marked because ownership is the argument. The
   monthly cards beside them are the things you stop paying for. */
.card--owned {
  border-color: var(--accent-line);
  background: linear-gradient(170deg, var(--accent-wash), var(--paper) 58%);
}
/* Border colour comes from the retrofit block at the foot of the file, with
   the other four literals that predate the dark theme. This one was missed:
   #b8d5c7 measures 11.65:1 on the dark theme's --paper, so hovering an owned
   card painted a near white edge on a near black card. */
.card--owned:hover { border-color: var(--accent); }

/* The add-on price sits where the icon does on the other cards, so the rows
   line up even though they carry different information. */
.card__price {
  display: flex;
  align-items: baseline;
  gap: var(--s1);
  margin: 0 0 var(--s3);
  height: 42px;
  color: var(--accent);
}
.card__price b { font-size: 1.55rem; font-weight: 800; letter-spacing: -0.035em; }
.card__price span { font-size: .86rem; font-weight: 600; color: var(--ink-faint); }

/* ------------------------------------------------------------ the ledger -- */
/* The centrepiece. Nobody in this market shows their own cost structure, so
   showing it is the argument. It is a table because it is a comparison, and a
   table is what a comparison is. */

.ledger {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--paper);
}

.ledger table { width: 100%; border-collapse: collapse; font-size: .96rem; }

.ledger thead th {
  text-align: left;
  padding: 16px 22px;
  font-size: .73rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  background: var(--paper-warm);
  border-bottom: 1px solid var(--line);
}
/* Money is read off its last digit, so every value column is set right. The
   first column is the line item and stays left. Written as nth-child rather
   than :last-child because there are two value columns, not one. */
.ledger thead th:nth-child(n+2),
.ledger tbody td:nth-child(n+2),
.ledger tfoot td:nth-child(n+2) { text-align: right; }

/* The first cell of every body row is a <th scope="row">, not a <td>. Rules
   that name only td leave that column on the browser's own th defaults:
   centred, one pixel of padding, no rule beneath it, and the note running
   inline in full ink instead of dropping onto its own line. Name both. */
.ledger tbody th,
.ledger tbody td {
  padding: 16px 22px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
.ledger tbody th { text-align: left; font-weight: 400; }

.ledger tbody tr:last-child th,
.ledger tbody tr:last-child td { border-bottom: 0; }

.ledger th b,
.ledger td b { font-weight: 650; }
.ledger th small,
.ledger td small {
  display: block;
  color: var(--ink-faint);
  font-size: .855rem;
  line-height: 1.5;
  margin-top: 3px;
  max-width: 52ch;
}

.ledger .is-gone { color: var(--accent); font-weight: 650; white-space: nowrap; }
.ledger .is-cost { white-space: nowrap; }

.ledger tfoot td {
  padding: 19px 22px;
  background: var(--panel);
  color: #fff;
  font-weight: 700;
  font-size: 1.04rem;
}

/* A table cannot reflow, so on a phone it scrolls inside its own box rather
   than pushing the page sideways. */
/* At 360px the table is 560px wide, so two of every five columns are off the
   edge with nothing saying so. A box that scrolls and does not admit it reads
   as a table with the answer column missing, and the answer column is the
   whole argument of this page.

   Three things fix that: a shadow on the right edge that is painted by the
   scroll itself and disappears once the reader reaches the end, a tab stop so
   a keyboard can reach the part a mouse can drag to, and a line underneath
   that says it in words for anyone who does not read the shadow. */
.ledger__scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* Two layers per edge: an opaque cover pinned to the content, and the shade
     behind it. Scroll away from an edge and the cover moves off, revealing the
     shade; scroll back and it covers it again. No script. */
  /* The shade is mixed from --ink. Written out as a black it was a black shade
     on a near-black page in dark mode, which is to say no shade at all, and
     the affordance that tells a reader there is more table to the right was
     the one thing this construction exists for. */
  background:
    linear-gradient(to right, var(--paper) 30%, transparent) left center,
    linear-gradient(to left, var(--paper) 30%, transparent) right center,
    radial-gradient(farthest-side at 0 50%, color-mix(in srgb, var(--ink) 16%, transparent), transparent) left center,
    radial-gradient(farthest-side at 100% 50%, color-mix(in srgb, var(--ink) 16%, transparent), transparent) right center;
  background-repeat: no-repeat;
  background-size: 44px 100%, 44px 100%, 15px 100%, 15px 100%;
  background-attachment: local, local, scroll, scroll;
}
.ledger__scroll:focus-visible { outline: 2.5px solid var(--accent-lift); outline-offset: -2px; }

.ledger table { min-width: 560px; }

/* Below 620px the table stops being a table.

   Measured at 375px it was 562px wide inside a 333px box, so 229 pixels were
   off the edge and the "hos oss" column, which is the entire argument of the
   page, began 67 pixels past the right edge and was never on screen at rest.
   A scroll shadow and a line of text admitting it were an affordance bolted
   onto a layout that did not fit; the page's central claim should not require
   a horizontal drag on the device most visitors are holding.

   So there are two markup branches for the same data, and exactly one of them
   is in the accessibility tree at any width: display: none removes the other
   outright, so nothing is read twice. */
.ledger__hint { display: none; }

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

@media (max-width: 620px) {
  .ledger__scroll { display: none; }
  .ledgerlist { display: block; }
}

.ledgerrow { padding: 14px 18px; border-bottom: 1px solid var(--line-soft); }
.ledgerrow:last-child { border-bottom: 0; }
.ledgerrow__label { margin: 0; font-size: .96rem; }
.ledgerrow__label b { font-weight: 650; }
.ledgerrow__label small {
  display: block;
  color: var(--ink-faint);
  font-size: .855rem;
  line-height: 1.5;
  margin-top: 3px;
}
/* Both figures on one line under the label, each with the column head it came
   from, because a number with no heading beside it is a number a reader has to
   go back up the page to identify. */
.ledgerrow__vals {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s4);
  margin: var(--s2) 0 0;
  font-size: .92rem;
  font-variant-numeric: tabular-nums;
}
.ledgerrow__vals span { display: inline-flex; gap: 7px; align-items: baseline; }
.ledgerrow__vals i {
  font-style: normal;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* A price is one object. The thousands separator on this site is a space, so
   without this "1 200 till 1 800 kr/tim" broke after the second 1 at 375px and
   the row read as two numbers that do not exist. The table branch above has
   carried .is-cost for the same reason since it was written. */
.ledgerrow__num { white-space: nowrap; }
.ledgerrow__vals .is-gone { color: var(--accent); font-weight: 650; }
.ledgerrow--total {
  background: var(--panel);
  color: #fff;
  font-weight: 700;
}
.ledgerrow--total .ledgerrow__label b { font-weight: 700; }
.ledgerrow--total .ledgerrow__vals { margin-top: var(--s1); }
.ledgerrow--total .ledgerrow__vals i { color: var(--night-faint); }

/* The line that stops the column not adding up. It lives inside the ledger box,
   directly under the bottom line, so it is read with the total rather than with
   the sourcing note further down the section. Padded on the same 22px gutter as
   every cell above it, because the box has no padding of its own.

   --ink-faint on --paper, which is the ground .ledger paints: 5.30:1 in light
   (#656c77 on #ffffff) and 5.50:1 in dark (#838c9a on #12151b). Body text, so
   both clear 4.5:1. */
.ledger__sum-note {
  margin: 0;
  padding: 14px 22px 17px;
  font-size: .86rem;
  line-height: 1.6;
  color: var(--ink-faint);
  max-width: 62ch;
}

.ledger__note {
  margin-top: var(--s4);
  font-size: .89rem;
  line-height: 1.6;
  color: var(--ink-faint);
  max-width: 64ch;
}

/* The second, smaller ledger: what a site costs us rather than what it costs
   them. Same component, quieter, because it is the footnote to the argument and
   not the argument. */
.ours { margin-top: clamp(52px, 7vw, 88px); }
.ours .section__intro { margin-bottom: var(--s4); }
.ours .section__intro h2 { font-size: clamp(1.5rem, 2.6vw, 2rem); }

.ledger--tight table { min-width: 320px; }
.ledger--tight tfoot td { font-size: 1rem; }

/* A quiet block for the question the ledger provokes. Not a coloured callout:
   it is an admission, and dressing it up would undercut it. */
.aside {
  margin-top: clamp(36px, 4.5vw, 56px);
  padding-left: clamp(18px, 2vw, 24px);
  border-left: 2px solid var(--accent);
  max-width: 72ch;
}
.aside h3 { margin-bottom: var(--s1); }
.aside p { color: var(--ink-soft); font-size: .98rem; line-height: 1.6; }
/* Beside something rather than under it, so it keeps the rule and loses the
   stacking margin. */
.aside--flush { margin-top: 0; }

/* -------------------------------------------------------------- payment --- */
/* The ways to pay, and the promise about getting the money back, in one row.
   Only invoice is switched on, so the card grid on its own drew one 460px card
   into an 1100px shell and left two thirds of the row empty. Pairing the two
   fills the row at one method and still works at three, where the cards take
   the wider column and the promise the narrower one. */

.payment__grid { display: grid; gap: clamp(24px, 3.5vw, 44px); align-items: start; }
@media (min-width: 860px) {
  .payment__grid { grid-template-columns: minmax(0, 1.25fr) minmax(0, .75fr); }
  /* One card is a single object, not a column of them, so it takes the narrower
     side and the promise beside it gets the room to be read. */
  .payment__grid--one { grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); }
}

/* -------------------------------------------------------------- compare --- */

.compare table { min-width: 680px; }

/* The row label is a header for its row, which is what scope="row" means, and it
   is what lets a screen reader say "Nexvy, over three years, 22 900 kr" instead
   of reading a bare number with no idea which option it belongs to. */
.compare tbody th {
  text-align: left;
  font-weight: 400;
  padding: 16px 22px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
.compare tbody th b { font-weight: 650; }
.compare tbody th small {
  display: block;
  color: var(--ink-faint);
  font-size: .855rem;
  line-height: 1.5;
  margin-top: 3px;
  max-width: 48ch;
}

.compare tbody td { text-align: right; }
.compare .is-total { font-weight: 650; }

/* Our own row, marked but not shouted. The table's job is to be checkable, and
   a reader who feels steered stops checking. */
.compare tr.is-us th,
.compare tr.is-us td { background: var(--accent-wash); }
.compare tr.is-us th b,
.compare tr.is-us .is-total { color: var(--accent); }
.compare tbody tr:last-child th,
.compare tbody tr:last-child td { border-bottom: 0; }

/* ---------------------------------------------------------------- steps --- */

.steps { display: grid; gap: clamp(14px, 2vw, 20px); counter-reset: step; }
@media (min-width: 900px) { .steps { grid-template-columns: repeat(4, 1fr); } }

.step {
  position: relative;
  padding: clamp(24px, 2.6vw, 30px);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper);
}
.step::before {
  counter-increment: step;
  content: '0' counter(step);
  display: block;
  font-size: .8rem;
  font-weight: 800;
  letter-spacing: .08em;
  color: var(--accent);
  margin-bottom: var(--s3);
}
.step h3 { font-size: 1.04rem; margin-bottom: var(--s1); }
.step p { color: var(--ink-soft); font-size: .94rem; line-height: 1.58; }

/* ------------------------------------------------------------------ faq --- */

/* The questions beside their heading rather than under it. Fifteen rows at
   840px in an 1100px measure left the right third of the section empty for the
   whole of it, and the heading on top of them was the fourth 2px rule on the
   page. The head column sticks, so the reader always knows what they are
   inside while the answers go past. */
.faqblock { display: grid; gap: clamp(28px, 4vw, 56px); align-items: start; }
@media (min-width: 940px) {
  .faqblock { grid-template-columns: minmax(0, .78fr) minmax(0, 1.22fr); }
  .faqblock__head { position: sticky; top: calc(var(--header-h, 72px) + 28px); }
}
.faqblock__head { border-top: 2px solid var(--ink); padding-top: var(--s3); }
.faqblock__head h2 { font-size: clamp(1.7rem, 3vw, 2.3rem); }

.faq { display: grid; gap: var(--s2); max-width: none; }

.faq details {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  overflow: hidden;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.faq details:hover { border-color: var(--ink-soft); }
.faq details[open] { border-color: var(--accent-line); }

.faq summary {
  cursor: pointer;
  list-style: none;
  padding: 18px 22px;
  font-weight: 650;
  font-size: 1.01rem;
  letter-spacing: -0.012em;
  display: flex;
  align-items: center;
  gap: var(--s3);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: '';
  margin-left: auto;
  flex: none;
  width: 10px; height: 10px;
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(45deg) translateY(-3px);
  transition: transform .2s var(--ease), border-color .2s var(--ease);
}
.faq details[open] summary::after { transform: rotate(225deg) translateY(-3px); border-color: var(--accent); }

/* The inline way in to the assistant. It reads as a sentence with a control at
   the end of it rather than as a component, which is the point: the question
   is being asked where the reader ran out of answers. */
.faq__ask {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
  margin: var(--s4) 0 0;
  font-size: .95rem;
  color: var(--ink-soft);
}
.faq__ask span { max-width: 46ch; }

.faq__body {
  padding: 0 22px 20px;
  color: var(--ink-soft);
  font-size: .97rem;
  line-height: 1.62;
  max-width: 68ch;
}

/* ----------------------------------------------------------------- trust -- */

.trust { display: grid; gap: clamp(14px, 2vw, 20px); }
@media (min-width: 800px) { .trust { grid-template-columns: repeat(2, 1fr); } }

.trust__item {
  display: flex;
  gap: var(--s3);
  padding: clamp(20px, 2.4vw, 26px);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper);
}
.trust__item h3 { font-size: 1.02rem; margin-bottom: var(--s1); }
.trust__item p { color: var(--ink-soft); font-size: .94rem; line-height: 1.58; }
.trust__mark {
  flex: none;
  width: 38px; height: 38px;
  border-radius: var(--radius-tile);
  background: var(--accent-wash);
  border: 1px solid var(--accent-line);
  display: grid;
  place-items: center;
}
.trust__mark svg { width: 19px; height: 19px; stroke: var(--accent); }

/* ------------------------------------------------------------------ form -- */

.split {
  display: grid;
  gap: clamp(30px, 5vw, 64px);
  align-items: start;
}
@media (min-width: 960px) { .split { grid-template-columns: minmax(0, .86fr) minmax(0, 1.14fr); } }

.lede { font-size: clamp(1.05rem, 1.7vw, 1.15rem); color: var(--ink-soft); max-width: 62ch; margin-top: var(--s3); }

.formcard {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-xl);
  padding: clamp(24px, 3vw, 34px);
  box-shadow: var(--shadow-sm);
}

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

.field { display: grid; gap: var(--s1); }
.field__label { font-size: .89rem; font-weight: 650; }

.field input,
.field textarea {
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: var(--paper-warm);
  border: 1px solid var(--control-line);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  width: 100%;
  transition: border-color .16s var(--ease), background-color .16s var(--ease), box-shadow .16s var(--ease);
}
/* Hover darkens the edge, it does not erase it. --line-strong is a hairline
   between blocks at 1.42:1 on --paper, so using it here took every control on
   the site from a passing 3.41:1 at rest down to 1.42:1 the moment a pointer
   landed on it, which made a control harder to see by pointing at it.
   --ink-soft measures 7.52:1 on --paper and 7.14:1 on --paper-warm in the
   light theme, and 8.22:1 and 8.46:1 on the dark theme, so the edge gets
   stronger on hover in both. */
.field input:hover,
.field textarea:hover { border-color: var(--ink-soft); }
.field input:focus,
.field textarea:focus {
  border-color: var(--accent);
  background: var(--paper);
  /* Mixed from the accent rather than written out as the light theme's green
     at 10%, so the halo is the theme's own green in dark mode too. */
  box-shadow: 0 0 0 3px var(--accent-halo);
  outline: none;
}
.field textarea { resize: vertical; min-height: 88px; }

.field small,
.field__hint { font-size: .83rem; color: var(--ink-faint); }
.field__hint { margin: calc(var(--s1) * -1) 0 0; }

.field-pair { display: grid; gap: var(--s3); }
@media (min-width: 520px) { .field-pair { grid-template-columns: 1fr 1fr; } }

/* Off screen rather than display:none. Some bots skip anything hidden outright,
   and the point is that they fill it in. aria-hidden and tabindex keep it away
   from anyone using a keyboard or a screen reader. */
.trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form__privacy { font-size: .83rem; line-height: 1.55; color: var(--ink-faint); margin: 0; }
.form__privacy a { color: var(--ink-soft); }

.alert { border-radius: var(--radius); padding: 20px 22px; }
.alert h3 { font-size: 1.04rem; margin-bottom: var(--s1); }
.alert p, .alert li { font-size: .94rem; }
.alert ul { margin: var(--s1) 0 0; padding-left: 1.1rem; }

.alert--ok { background: var(--ok-bg); border: 1px solid var(--ok-line); color: var(--ok-ink); }
.alert--bad { background: var(--bad-bg); border: 1px solid var(--bad-line); color: var(--bad-ink); margin-bottom: var(--s3); }

/* ------------------------------------------------------------------ cta --- */

/* ------------------------------------------------------------ the close --- */
/* Five of seven pages used to end in a byte-identical dark rounded slab with a
   radial glow behind it, which was also the same construction as the hero, so
   every page ended in a smaller copy of how it began. A reader who has come
   down the pricing page and a reader who has come down the examples page are
   at different points in the same decision and the next step is not the same
   sentence, so there are now two closes and each page names its own copy.

   .cta is the slab, and exactly one page uses it: the home page, where it is
   the second and last dark object after the price card. Everywhere else the
   close is .closer, which is a rule, a line and the action. */

.cta {
  background: var(--panel);
  color: #fff;
  border-radius: var(--radius-xl);
  padding: clamp(40px, 6vw, 76px) clamp(28px, 5vw, 64px);
  text-align: center;
  position: relative;
  overflow: hidden;
}
.cta h2 { color: #fff; }
.cta p { color: var(--night-text); margin: var(--s3) auto 0; max-width: 52ch; }
.cta .btn-row { justify-content: center; margin-top: var(--s5); }

.closer {
  border-top: 2px solid var(--ink);
  padding-top: clamp(22px, 3vw, 32px);
  display: grid;
  gap: var(--s4) clamp(32px, 6vw, 72px);
}
@media (min-width: 860px) {
  .closer { grid-template-columns: minmax(0, 1fr) minmax(0, auto); align-items: end; }
}
.closer h2 { font-size: clamp(1.6rem, 3vw, 2.2rem); max-width: 22ch; }
.closer p { color: var(--ink-soft); margin: var(--s3) 0 0; max-width: 54ch; }
.closer .btn-row { margin-bottom: 4px; }

/* --------------------------------------------------------------- footer --- */

.footer {
  background: var(--paper-warm);
  border-top: 1px solid var(--line);
  padding-block: clamp(48px, 6vw, 72px) var(--s5);
  font-size: .93rem;
  color: var(--ink-soft);
}

.footer__grid {
  display: grid;
  gap: clamp(28px, 4vw, 56px);
  /* Three link columns beside the brand: the pages, the questions people
     arrive with, and the contact details. */
  grid-template-columns: minmax(0, 1.3fr) repeat(3, minmax(0, 1fr));
}
@media (max-width: 760px) { .footer__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 500px) { .footer__grid { grid-template-columns: 1fr; } }

.footer__brand { grid-column: 1 / -1; }
@media (min-width: 761px) { .footer__brand { grid-column: auto; } }

.footer__tagline { margin-top: var(--s3); max-width: 34ch; color: var(--ink-faint); font-size: .9rem; }

.footer h2 {
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--s3);
}

.footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.footer a { color: var(--ink-soft); text-decoration: none; transition: color .15s var(--ease); }
.footer a:hover { color: var(--accent); }

/* On a phone these ten links reported an 18px hit box 10px apart, so centre to
   centre was 28px against the 44px a thumb actually needs, and the two a
   person reaches for on a small screen are the legal ones at the bottom. Sixty
   pixels of footer height buys a 30px target and 42px of clearance. */
@media (max-width: 620px) {
  .footer ul { gap: var(--s3); }
  .footer li a { display: block; padding-block: 6px; }
}

.footer__base {
  margin-top: clamp(36px, 4vw, 52px);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  font-size: .85rem;
  color: var(--ink-faint);
}
.footer__base > :last-child { margin-left: auto; }
@media (max-width: 620px) { .footer__base > :last-child { margin-left: 0; } }

/* ----------------------------------------------------------------- prose --- */
/* One narrow column for the privacy policy. Long text at the full page width is
   unreadable, and that page exists to be read rather than skimmed. */

.prose { max-width: 68ch; }
.prose h1 { font-size: clamp(2rem, 4vw, 2.9rem); margin-bottom: var(--s3); }
.prose h2 { font-size: clamp(1.18rem, 2vw, 1.38rem); margin-top: var(--s6); margin-bottom: var(--s2); }
/* The gap above a heading is the gap between it and the paragraph before it.
   The first heading has no paragraph before it, so it was adding 56px to the
   80 the page head already ended in and the 128 the section opened with: about
   260px of empty paper between the rule that introduces the page and the first
   word of it, on both prose pages. */
.prose > :first-child { margin-top: 0; }
.prose p { color: var(--ink-soft); }
.prose .btn { margin-top: var(--s3); }
.prose__meta { font-size: .86rem; color: var(--ink-faint); margin-top: var(--s6); }

/* THE ABOUT PAGE IS PROSE WITH A COLUMN OF FACTS BESIDE IT.
   ---------------------------------------------------------------------------
   A 68ch measure in an 1100px shell leaves the right 45 percent of the
   viewport empty for the whole height of the page, and what was missing from
   it is exactly what the prose is arguing about: the two prices, the notice
   period, who owns the site, and the two real addresses. The column is read
   off the price list rather than typed out, so it cannot drift from the rest
   of the site, and it sticks while the argument scrolls past it. */
.prose-split { display: grid; gap: clamp(36px, 5vw, 64px); align-items: start; }
@media (min-width: 940px) {
  .prose-split { grid-template-columns: minmax(0, 1fr) minmax(0, 300px); }
  .factcol { position: sticky; top: calc(var(--header-h, 72px) + 24px); }
}

.factcol {
  border-top: 2px solid var(--ink);
  padding-top: var(--s3);
}
.factcol__title {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  /* --ink-faint is 5.62:1 on --paper and 5.34:1 on --paper-warm, and 5.9:1 on
     the dark theme, so the label clears 4.5:1 everywhere it is drawn. */
  color: var(--ink-faint);
  margin-bottom: var(--s3);
}
.factcol__title--spaced { margin-top: var(--s6); }

.factlist { margin: 0; display: grid; }
.factlist__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s3);
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
}
.factlist__row:last-child { border-bottom: 0; }
.factlist dt { margin: 0; font-size: .92rem; color: var(--ink-soft); }
.factlist dd {
  margin: 0;
  font-weight: 700;
  font-size: .98rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.reflist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); }
.reflist li { display: grid; gap: 2px; }
.reflist a { font-weight: 650; }
.reflist span { font-size: .88rem; color: var(--ink-faint); }

.founder__contact { margin-top: var(--s3); }

/* ==========================================================================
   Dark theme
   --------------------------------------------------------------------------
   Three ways in, in order of authority.

   1. `[data-theme]` on <html>, set by a blocking script in the head. That is
      the reader's saved choice, or failing that the hour on their own clock.
   2. `prefers-color-scheme`, guarded so an explicit light choice always wins.
   3. Neither, which is plain light.

   The whole stylesheet above is written against tokens, so the theme is a
   redefinition of about twenty custom properties and not a second stylesheet.
   The one thing that is NOT just an inversion is the accent: a deep forest
   green is unreadable on a dark ground, so dark mode uses a brighter green and
   flips the text on top of it to near-black.
   ========================================================================== */

:root {
  --line-strong: #dcd8d1;
  --on-accent: #ffffff;
  --bar: rgba(255, 255, 255, .82);
  --ok-bg: var(--accent-wash);
  --ok-line: var(--accent-line);
  --ok-ink: #10422f;
  --bad-bg: #fdf3f2;
  --bad-line: #f2d3d0;
  --bad-ink: #7a2320;
  /* Waiting on someone. An invoice that is out is neither done nor wrong, and
     showing it in the neutral default made it read as "nothing happened yet". */
  --warn-bg: #fdf6e8;
  --warn-line: #f0dfb8;
  --warn-ink: #6b4a06;
  /* The edge of something you type in or click. WCAG 1.4.11 wants that at 3:1
     against its background. --line is a hairline between blocks and carries no
     such requirement, and darkening that to satisfy this would make every rule
     on the page heavier to fix a problem only the controls have. */
  /* 3.41:1 on --paper and 3.21:1 on --paper-warm. */
  --control-line: #918a80;
  /* The same requirement for a control drawn on the dark panel. .26 white
     composited to #4a4d53 for 2.26:1; .42 clears 3:1 on both the light
     theme's panel and the lifted one the dark theme uses. */
  --on-panel-line: rgba(255, 255, 255, .42);
}

/* THE TWO DARK BRANCHES BELOW MUST STAY IDENTICAL, property for property and
   value for value. CSS gives no way to write one declaration list and select
   it both by an attribute and by a media query, so there are two copies, and
   the two had already drifted once: --accent-soft was declared in the
   attribute branch and missing from the media branch, which happened to be
   harmless only because the value it fell through to was the same. The next
   one would not have been. tests/Feature/SiteThemeTokensTest.php now parses
   this file and fails the build if the two lists differ, so the duplication is
   still duplication but it can no longer rot quietly.

   The one thing that is NOT an inversion is the accent: a deep forest green is
   unreadable on a dark ground, so dark mode uses a brighter green and flips
   the text on top of it to near-black. The second is --panel: the light theme
   paints the price card in near black on white paper, and in dark mode near
   black on near black is not an object, so the panel lifts instead. */

:root[data-theme="dark"] {
  --ink:        #e9ecf1;
  --ink-soft:   #a4adba;
  --ink-faint:  #838c9a;
  --line:       #262c35;
  --line-soft:  #1c212a;
  --line-strong:#333b46;
  --paper:      #12151b;
  --paper-warm: #0d1015;

  --accent:      #35c88d;
  --accent-lift: #4edea2;
  --accent-soft: #35c88d;
  --accent-wash: #12241d;
  --accent-line: #1f4133;
  --on-accent:   #06120d;

  --panel:      #1a212b;
  --panel-line: #333d4b;

  --bar: rgba(18, 21, 27, .82);

  --ok-bg: #12241d;
  --ok-line: #1f4133;
  --ok-ink: #a9e7c9;
  --bad-bg: #251618;
  --bad-line: #45262a;
  --bad-ink: #f0b4b0;
  --warn-bg: #241d10;
  --warn-line: #453a1f;
  --warn-ink: #f0d9a4;
  --control-line: #636e7e;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3), 0 2px 6px rgba(0, 0, 0, .26);
  --shadow-md: 0 2px 6px rgba(0, 0, 0, .3), 0 12px 28px rgba(0, 0, 0, .34);
  --shadow-lg: 0 4px 12px rgba(0, 0, 0, .34), 0 24px 60px rgba(0, 0, 0, .46);
}

/* The system preference, but never over an explicit light choice. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink:        #e9ecf1;
    --ink-soft:   #a4adba;
    --ink-faint:  #838c9a;
    --line:       #262c35;
    --line-soft:  #1c212a;
    --line-strong:#333b46;
    --paper:      #12151b;
    --paper-warm: #0d1015;

    --accent:      #35c88d;
    --accent-lift: #4edea2;
    --accent-soft: #35c88d;
    --accent-wash: #12241d;
    --accent-line: #1f4133;
    --on-accent:   #06120d;

    --panel:      #1a212b;
    --panel-line: #333d4b;

    --bar: rgba(18, 21, 27, .82);

    --ok-bg: #12241d;
    --ok-line: #1f4133;
    --ok-ink: #a9e7c9;
    --bad-bg: #251618;
    --bad-line: #45262a;
    --bad-ink: #f0b4b0;
    --warn-bg: #241d10;
    --warn-line: #453a1f;
    --warn-ink: #f0d9a4;
    --control-line: #636e7e;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3), 0 2px 6px rgba(0, 0, 0, .26);
    --shadow-md: 0 2px 6px rgba(0, 0, 0, .3), 0 12px 28px rgba(0, 0, 0, .34);
    --shadow-lg: 0 4px 12px rgba(0, 0, 0, .34), 0 24px 60px rgba(0, 0, 0, .46);
  }
}

/* There used to be a block of eight rules here restating .masthead, .langs,
   .card:hover, .faq details:hover, the field hover, .btn--primary and the two
   alerts in tokens, because each of those was written with a literal before
   the dark theme existed and was patched from a distance rather than at
   source. A rule that declares the wrong colour and a second rule four hundred
   lines away that corrects it is two places to read and one place to forget:
   .card--owned:hover was in that group and was the one that got forgotten.
   Every one of those rules now names its token where the rule is, so there is
   nothing left here to retrofit.

   The hero and the closing panel no longer need a retrofitted edge in dark
   mode: the hero is the page's own paper in both themes and carries a
   hairline under it either way, and --panel lifts off the page rather than
   matching it. */

/* --------------------------------------------------------- theme toggle --- */

.themetoggle {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  flex: none;
  padding: 0;
  border: 1px solid var(--control-line);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink-soft);
  cursor: pointer;
  transition: color .15s var(--ease), border-color .15s var(--ease);
}
.themetoggle:hover { color: var(--ink); border-color: var(--ink-soft); }
.themetoggle svg { width: 16px; height: 16px; }

/* One icon at a time: the one that shows is the mode you would switch TO. */
.themetoggle__moon { display: none; }
:root[data-theme="dark"] .themetoggle__sun { display: none; }
:root[data-theme="dark"] .themetoggle__moon { display: block; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .themetoggle__sun { display: none; }
  :root:not([data-theme="light"]) .themetoggle__moon { display: block; }
}
:root[data-theme="light"] .themetoggle__sun { display: block; }
:root[data-theme="light"] .themetoggle__moon { display: none; }

/* ==========================================================================
   Multi-page navigation
   ========================================================================== */

.masthead__tools { display: flex; align-items: center; gap: var(--s2); flex: none; }

.masthead__nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }

/* ----------------------------------------------------------- hamburger --- */
/* A <details> rather than a scripted drawer, so the navigation opens even if
   the script never runs. On a phone the nav is the one thing that must not be
   allowed to depend on JavaScript. */

.menu { display: none; position: relative; }
@media (max-width: 920px) { .menu { display: block; } }

.menu__button {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--control-line);
  border-radius: var(--radius-tile);
  background: var(--paper);
  cursor: pointer;
  list-style: none;
}
.menu__button::-webkit-details-marker { display: none; }
.menu__button:hover { border-color: var(--ink-soft); }

/* Three bars from one element: the middle is the box, the others are its
   pseudo elements. They cross into an X when the panel is open. */
.menu__bars,
.menu__bars::before,
.menu__bars::after {
  display: block;
  width: 17px;
  height: 2px;
  border-radius: 2px;
  background: var(--ink);
  transition: transform .2s var(--ease), opacity .2s var(--ease);
}
.menu__bars { position: relative; }
.menu__bars::before,
.menu__bars::after { content: ''; position: absolute; left: 0; }
.menu__bars::before { top: -5.5px; }
.menu__bars::after { top: 5.5px; }

.menu[open] .menu__bars { background: transparent; }
.menu[open] .menu__bars::before { transform: translateY(5.5px) rotate(45deg); }
.menu[open] .menu__bars::after { transform: translateY(-5.5px) rotate(-45deg); }

.menu__panel {
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  min-width: 210px;
  padding: var(--s2);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: 60;
}
.menu__panel nav { display: grid; }
.menu__panel nav a {
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  font-weight: 550;
  color: var(--ink-soft);
}
.menu__panel nav a:hover { background: var(--paper-warm); color: var(--ink); }
.menu__panel nav a[aria-current="page"] { background: var(--accent-wash); color: var(--accent); font-weight: 650; }

@media (max-width: 480px) {
  .masthead__cta { display: none; }
  .menu__button { width: 36px; height: 36px; }
}

/* ------------------------------------------------------------- pagehead --- */

.pagehead h1 { font-size: clamp(2.1rem, 4.6vw, 3.4rem); }
.pagehead .lede { margin-top: 0; }

/* Headline and supporting line side by side, with the rule running under both.
   On a phone it is simply one column, which is the same reading order. */
.pagehead__grid {
  display: grid;
  gap: var(--s4) clamp(32px, 6vw, 72px);
  padding-bottom: clamp(18px, 2.4vw, 26px);
  border-bottom: 1px solid var(--line);
}
@media (min-width: 860px) {
  .pagehead__grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); align-items: end; }
  .pagehead .lede { margin-bottom: .2em; }
}

/* ---------------------------------------------------------------- work --- */
/* The examples. Real generated sites for invented businesses, and the page
   says so above the grid rather than below it. */

.work__item { display: flex; flex-direction: column; }

/* There was a band of the demo's own accent here, for a card that had nothing
   else to tell it apart from its neighbours. The card now shows a photograph
   of the site, so what tells one from another is the site, which is the
   difference a visitor came to see. */

.work__niche {
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 var(--s1);
}
.work__open { margin-top: auto; align-self: flex-start; padding-top: var(--s2); }
.work__item h3 + p { margin-bottom: var(--s4); }

/* Bigger builds are quoted rather than priced, so the card is marked as a
   different kind of thing from the ones with a number on them. */
.card--quoted { background: var(--paper-warm); border-style: dashed; }
.card--quoted:hover { border-style: solid; }

/* The call to action inside the mobile menu. The header button is hidden at
   this width, so without this the primary action is unreachable on a phone,
   which is where most of these visitors are. */
.menu__cta {
  margin-top: var(--s3);
  padding-block: 12px;
}

/* ==========================================================================
   Motion and depth
   --------------------------------------------------------------------------
   The brief was "futuristic". The trap in that word is neon and noise, and a
   company selling websites is judged on its own restraint before a line of its
   copy is read. So this layer is built out of light rather than decoration: an
   aurora drifting behind the hero, a filament travelling the edge of the price
   card, a sheen crossing a button as the cursor lands, glass that reacts.
   Nothing bounces, nothing spins for its own sake, and no element moves more
   than a few pixels.

   Everything here animates transform, opacity or filter only, so it stays on
   the compositor and never triggers layout. The page argues that we build fast
   sites, and an animation costing frames would contradict the argument.

   The global prefers-reduced-motion rule near the top of this file collapses
   every duration to nothing, and the effects that would look wrong frozen
   mid-way are switched off outright at the bottom.
   ========================================================================== */

/* ------------------------------------------------------- scroll progress -- */
/* A filament along the bottom edge of the bar, driven by scroll position with
   no JavaScript at all. Browsers without scroll-driven animations never grow it
   from zero, which is why the resting state is invisible rather than full. */

.progress {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: 100%;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--accent), var(--accent-soft));
  pointer-events: none;
}

@supports (animation-timeline: scroll()) {
  .progress {
    animation: progress-grow linear both;
    animation-timeline: scroll(root block);
  }
}

@keyframes progress-grow { to { transform: scaleX(1); } }

/* ---------------------------------------------------------------- glass --- */

.masthead {
  border-bottom-color: transparent;
  box-shadow: 0 1px 0 var(--line-soft), 0 6px 24px -18px rgba(0, 0, 0, .5);
}

/* The wordmark answers the cursor: the short bar, the one that is our price,
   grows a little towards the long one. It is the only animation on the mark
   and it says the same thing the mark says. */
.wordmark__mark .is-us { transition: transform .3s var(--ease); transform-box: fill-box; transform-origin: 0 50%; }
.wordmark:hover .wordmark__mark .is-us { transform: scaleX(1.35); }

/* An underline that grows from the left instead of appearing all at once. */
.masthead__nav a {
  position: relative;
  border-bottom-color: transparent !important;
}
.masthead__nav a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1.5px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .28s var(--ease);
}
.masthead__nav a:hover::after,
.masthead__nav a[aria-current="page"]::after { transform: scaleX(1); }

/* ----------------------------------------------------------- hero aurora -- */
/* There is no aurora any more. Three radial lobes drifted here on a 26 second
   infinite alternate, which is the default hero of every SaaS landing page
   built since 2021, and it was also the reason the price card's labels had no
   fixed contrast ratio: the blue lobe was centred on that column. The hero is
   now the page's own paper and the only motion left on it is the reader's. */

/* ------------------------------------------------------- the price card --- */
/* There was a filament of light travelling the card border here: a conic
   gradient rotated through an xor mask on hover. It was the most recognisable
   ornament on the page, and the same ornament is on thousands of generated
   landing pages, which is the opposite of what an ornament is for. The card
   holds itself up with type and a hairline. */

.pricecard { position: relative; }

/* -------------------------------------------------------------- buttons --- */
/* A sheen used to sweep across the primary buttons on arrival. It is the most
   copied hover effect on the web and it made a plain button read as a template
   component. Buttons answer with colour, which is what everything else here
   does. */
/* ---------------------------------------------------------------- cards --- */
/* Lift plus a tinted glow, so a card reads as lit rather than merely raised. */

.trust__item,
.step {
  transition: transform .22s var(--ease), border-color .22s var(--ease);
}

.card:hover,
.trust__item:hover,
.step:hover {
  transform: translateY(-4px);
  border-color: var(--accent-line);
}

/* The icon tile answers too, one level down from the card. */
.card__mark,
.trust__mark { transition: transform .25s var(--ease), background-color .25s var(--ease); }
.card:hover .card__mark,
.trust__item:hover .trust__mark { transform: scale(1.07) rotate(-3deg); }

/* Ledger rows light under the cursor, which makes a long table scannable by
   hand as well as by eye. */
.ledger tbody tr { transition: background-color .18s var(--ease); }
.ledger tbody tr:hover { background: var(--accent-wash); }

/* --------------------------------------------------- staggered reveals --- */
/* Grids arrive one item at a time rather than as a slab. The parent is pinned
   open so only the children move, otherwise the two animations fight.

   All of this sits under .anim, which the layout script adds only after the
   observer has proved it delivers callbacks. No .anim, nothing hidden, ever. */

.anim .cards.reveal,
.anim .steps.reveal,
.anim .trust.reveal { opacity: 1; transform: none; }

.anim .cards.reveal > *,
.anim .steps.reveal > *,
.anim .trust.reveal > * {
  opacity: 0;
  transform: translateY(16px);
}

.anim .cards.reveal.is-in > *,
.anim .steps.reveal.is-in > *,
.anim .trust.reveal.is-in > * {
  opacity: 1;
  transform: none;
  transition: opacity .5s var(--ease), transform .5s var(--ease);
}

.anim .reveal.is-in > *:nth-child(2) { transition-delay: .07s; }
.anim .reveal.is-in > *:nth-child(3) { transition-delay: .14s; }
.anim .reveal.is-in > *:nth-child(4) { transition-delay: .21s; }
.anim .reveal.is-in > *:nth-child(5) { transition-delay: .28s; }

/* The machinery above is careful and the application was not. Fourteen blocks
   on one page carried .reveal, so an entire page arrived as a sequence of 10px
   fade-ups with a 70ms cascade, which is the one behaviour a reader has seen
   on every template built site. What makes it read as a setting rather than a
   decision is that it was on everything without exception. It is now on two
   things per page at most, both of them places where arrival means something:
   the ledger, where the total is the argument, and the monthly report, where
   the point is that a real run exists. Everything else is simply there when
   the reader gets to it. */

/* ------------------------------------------------------------- controls --- */

.themetoggle,
.langs a,
.menu__button {
  transition: transform .2s var(--ease), background-color .2s var(--ease), color .2s var(--ease);
}
.themetoggle:hover { transform: rotate(-18deg); }

.faq summary { transition: color .18s var(--ease); }
.faq summary:hover { color: var(--accent); }

/* The panel drops in rather than appearing. */
.menu[open] .menu__panel { animation: panel-in .22s var(--ease) both; }

@keyframes panel-in {
  from { opacity: 0; transform: translateY(-8px) scale(.98); }
}

/* --------------------------------------------------------- calmed down --- */
/* Anything that would look wrong frozen at an arbitrary frame is switched off
   rather than merely shortened. The global reduced-motion rule collapses
   durations, and a half-swept sheen or a filament stopped mid-edge is worse
   than no effect at all. */

@media (prefers-reduced-motion: reduce) {
  .menu[open] .menu__panel { animation: none; }

  .progress { display: none; }

  .card:hover,
  .trust__item:hover,
  .step:hover { transform: none; }

  .wordmark:hover .wordmark__mark .is-us,
  .themetoggle:hover,
  .card:hover .card__mark,
  .work__item:hover .work__thumb img,
  .flagship:hover .flagship__thumb img,
  .trust__item:hover .trust__mark { transform: none; }
}

/* ==========================================================================
   Ordering
   --------------------------------------------------------------------------
   The order form and the page after it. The money sits on the left, written
   out in full including VAT before a field is filled in; the whole site
   argues against hidden numbers and this page cannot have one.
   ========================================================================== */

@media (min-width: 960px) { .split--order { grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); } }

.ordersummary {
  position: sticky;
  top: 90px;
  padding: clamp(22px, 2.6vw, 28px);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper-warm);
}

.olines { margin: 0; display: grid; }
.oline {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s3);
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}
.oline:last-child { border-bottom: 0; }
.oline dt { margin: 0; }
.oline dt b { display: block; font-weight: 650; }
.oline dt small { display: block; color: var(--ink-faint); font-size: .85rem; line-height: 1.45; max-width: 30ch; }
.oline dd { margin: 0; font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
.oline--quiet { color: var(--ink-soft); font-size: .95rem; }
.oline--total { font-weight: 800; font-size: 1.15rem; padding-top: 16px; }
.oline--total dd { color: var(--accent); font-size: 1.35rem; letter-spacing: -0.03em; }

.ordersummary__note { margin-top: var(--s3); font-size: .88rem; line-height: 1.55; color: var(--ink-faint); }
.ordersummary__trust { list-style: none; margin: var(--s4) 0 0; padding: var(--s3) 0 0; border-top: 1px solid var(--line); display: grid; gap: var(--s2); }
.ordersummary__trust li { position: relative; display: flex; gap: var(--s2); align-items: flex-start; font-size: .9rem; line-height: 1.5; color: var(--ink-soft); }
/* A mask rather than a coloured data URI. The glyph's stroke cannot be a
   token inside a URL, so it was frozen at the light theme accent and measured
   2.24:1 on the dark theme's --accent-wash: the three reassurance lines on the
   order page were effectively invisible to anyone reading at night, which is
   the one page where those three lines are doing work. Painted through a mask
   the colour comes from --accent and follows the theme like everything else. */
.ordersummary__trust li::before {
  content: '';
  flex: none;
  width: 16px; height: 16px;
  margin-top: 3px;
  border-radius: 50%;
  background-color: var(--accent-wash);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.ordersummary__trust li::after {
  content: '';
  position: absolute;
  width: 10px; height: 10px;
  margin: 6px 0 0 3px;
  background-color: var(--accent);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
}

.form__group {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: var(--s2) 0 calc(var(--s2) * -1);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}
.form__group:first-child { margin-top: 0; }

.field select {
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: var(--paper-warm);
  border: 1px solid var(--control-line);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  width: 100%;
}
.field select:focus { border-color: var(--accent); background: var(--paper); box-shadow: 0 0 0 3px var(--accent-halo); outline: none; }

/* Payment methods as cards you can click anywhere on. */
.methods { display: grid; gap: var(--s2); }
.method {
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
  padding: 14px 16px;
  border: 1px solid var(--control-line);
  border-radius: var(--radius);
  background: var(--paper);
  cursor: pointer;
  transition: border-color .15s var(--ease), background-color .15s var(--ease);
}
.method:hover { border-color: var(--ink-soft); }
.method:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
.method input { margin: 4px 0 0; accent-color: var(--accent); flex: none; }
.method__body b { display: block; font-weight: 650; }
.method__body small { display: block; color: var(--ink-soft); font-size: .88rem; line-height: 1.5; }

.check { display: flex; gap: var(--s2); align-items: flex-start; font-size: .92rem; line-height: 1.5; color: var(--ink-soft); }
.check input { margin: 4px 0 0; accent-color: var(--accent); flex: none; }
.check a { color: var(--ink); }

.pill {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: .8rem;
  font-weight: 650;
  background: var(--paper-warm);
  border: 1px solid var(--line);
}
.pill--paid { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok-ink); }
.pill--cancelled { background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad-ink); }
/* Invoice is the only payment method switched on today, so 'invoiced' is the
   state every real order lands in. It had no rule at all and fell through to
   the neutral default, which made a sent invoice look identical to an order
   nothing had happened to yet. */
.pill--invoiced { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn-ink); }
.pill--pending { background: var(--paper-warm); border-color: var(--line-strong); color: var(--ink-soft); }

.swishbox { display: grid; gap: var(--s3); margin-bottom: var(--s5); }
.swishbox__qr { width: 220px; height: 220px; margin: 0 auto; border-radius: var(--radius); border: 1px solid var(--line); background: #fff; }

.steps--stacked { grid-template-columns: 1fr; }

/* ==========================================================================
   The assistant
   --------------------------------------------------------------------------
   A button in the corner and a panel above it. Same tokens as everything
   else, so it follows the theme, and the header says "AI" before a word is
   exchanged.
   ========================================================================== */

/* There was a bright green pill fixed to the bottom right corner of every
   page. It is the single most recognisable template component on the web, its
   shadow was a literal rgba of the accent so it never followed the theme, and
   at 375px it sat permanently over the copy: over the stat row in the hero,
   over the last line of the price card further down.

   On a five page marketing site the question a visitor wants to ask occurs in
   two places, the FAQ and the contact page, so the control to ask it lives in
   those two places and nowhere else. The panel is unchanged; only the way in
   moved, from floating over the page to sitting in the sentence that prompted
   the question. */
.chat { position: fixed; right: 18px; bottom: 18px; z-index: 70; }
.chat:not(.is-open) { display: none; }
@media (max-width: 420px) { .chat { right: 10px; left: 10px; bottom: 10px; } }

.chat__panel {
  width: min(380px, calc(100vw - 20px));
  max-height: min(600px, calc(100vh - 36px));
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.chat__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--paper-warm);
}
.chat__title { margin: 0; font-weight: 700; font-size: .98rem; }
.chat__sub { margin: 2px 0 0; font-size: .8rem; color: var(--ink-faint); }
.chat__close {
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--ink-soft); cursor: pointer;
}
.chat__close:hover { background: var(--line-soft); color: var(--ink); }
.chat__close svg { width: 18px; height: 18px; }
.chat__log {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: grid;
  gap: var(--s2);
  align-content: start;
  min-height: 220px;
}
.chat__msg {
  max-width: 88%;
  padding: 10px 14px;
  border-radius: var(--radius);
  font-size: .93rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin: 0;
}
.chat__msg--bot { background: var(--paper-warm); border: 1px solid var(--line); justify-self: start; border-bottom-left-radius: 4px; }
.chat__msg--me { background: var(--accent); color: var(--on-accent); justify-self: end; border-bottom-right-radius: 4px; }
.chat__msg--wait { color: var(--ink-faint); font-size: .85rem; justify-self: start; padding-left: 4px; }
.chat__form { display: flex; gap: var(--s2); padding: 12px; border-top: 1px solid var(--line); }
.chat__form input {
  flex: 1; min-width: 0;
  font: inherit; font-size: 16px;
  color: var(--ink);
  background: var(--paper-warm);
  border: 1px solid var(--control-line);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
}
.chat__form input:focus { outline: none; border-color: var(--accent); }
.chat__send {
  width: 44px; height: 44px; flex: none;
  display: grid; place-items: center;
  border: 0; border-radius: var(--radius-sm);
  background: var(--accent); color: var(--on-accent); cursor: pointer;
}
.chat__send svg { width: 18px; height: 18px; }
.chat__note { margin: 0; padding: 0 14px 12px; font-size: .74rem; line-height: 1.45; color: var(--ink-faint); }

/* ==========================================================================
   Examples, the report, the reference
   ========================================================================== */

/* The two real sites.
   ---------------------------------------------------------------------------
   This block used to be a two column split: a three line heading and a lede in
   the left column, two short text cards in the right, and about 250px of empty
   paper under the lede at 1440. The heading now runs across the measure the
   way every other section opener does, and the cards sit under it in the same
   grid the examples use, with the same screenshot treatment, because they are
   the same kind of object and the only difference is that these two are real.

   auto-fit rather than the gallery's auto-fill: with two cards, auto-fill
   would lay out four tracks and leave two of them empty. */
/* .work comes later in the file at the same specificity, so this has to name
   both or the gallery's auto-fill wins and lays two cards out in four tracks. */
.work.reference__cards { grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); }
.reference__cards .work__item {
  border-color: var(--accent-line);
  background: linear-gradient(170deg, var(--accent-wash), var(--paper) 58%);
}
.reference__cards h3 { font-size: 1.3rem; }
.reference__cards .work__thumb { background: var(--accent-wash); }

.work__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  margin: clamp(28px, 4vw, 44px) 0 var(--s4);
}
.work__count { margin: 0; font-size: .9rem; color: var(--ink-faint); }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  font: inherit;
  font-size: .86rem;
  font-weight: 600;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--control-line);
  background: var(--paper);
  color: var(--ink-soft);
  cursor: pointer;
  transition: border-color .15s var(--ease), background-color .15s var(--ease), color .15s var(--ease);
}
.chip:hover { border-color: var(--ink-soft); color: var(--ink); }
.chip.is-on { background: var(--ink); border-color: var(--ink); color: var(--paper); }

.work { grid-template-columns: repeat(auto-fill, minmax(256px, 1fr)); }
.work__item { padding: 0; overflow: hidden; }
.work__item > :not(.work__thumb) { margin-inline: clamp(18px, 2.4vw, 24px); }
.work__item h3 { margin-top: 0; }
.work__item .work__niche { margin-top: var(--s3); }
.work__item .work__open { margin-bottom: clamp(18px, 2.4vw, 24px); }
.work__item[hidden] { display: none; }

/* The thumbnail is a photograph of the page, taken by a headless browser at
   1440 by 900 and filed under public/img/examples. It used to be the demo's
   own hero photograph, which meant the page that answers "are these any good"
   showed a prospect a picture of a pizza, and the flagship card for an
   architecture practice showed a stock sunrise over pine trees.

   Anchored to the top edge, because the top of a page is the part being
   judged, and cover rather than contain so the card is filled rather than
   letterboxed. A demo with no capture yet falls back to its photograph, which
   is why the box still carries a ground colour. */
.work__thumb,
.flagship__thumb {
  position: relative;
  display: block;
  aspect-ratio: 16 / 10;
  background: var(--paper-warm);
  border-bottom: 1px solid var(--line);
  overflow: hidden;
}
.work__thumb img,
.flagship__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  transition: transform .5s var(--ease);
}
.work__item:hover .work__thumb img,
.flagship:hover .flagship__thumb img { transform: scale(1.03); }

/* THE PHONE CROP IS GONE, AND THE CARD SHOWS THE WHOLE PAGE AT EVERY WIDTH.
   ---------------------------------------------------------------------------
   Below 760 these two used to go to 5:4 with the capture blown up to 185% and
   anchored top left, on the argument that a whole desktop page shrunk into a
   335px card is a grey texture rather than a photograph of a website. The zoom
   bought bigger type and paid for it by cutting the headline in half: at 375
   the cards read "Ritningar till h" and "Valkommen ti", and a portfolio whose
   own copy says to open these on a phone was clipping the one line a visitor
   reads to judge the work.

   The home page wall answers the same question with .wall__shot: 16/10, no
   zoom, the whole hero at the same card width, and it is legible. Two
   treatments of the same photograph on the same site, one of them clipping,
   is one treatment too many. These now keep the 16/10 and the full width they
   have above 760, so the card and the wall agree. */

/* The tint over the lower half of every thumbnail is gone. Thirty
   photographs in thirty different colour casts made the grid read as a colour
   chart, and the wash sat exactly over the part of the image a reader looks
   at. The card still tells one demo from another, with the accent band the
   file already defines for that purpose. */

/* The layout name, on the card's own meta line rather than floating over the
   screenshot. As a pill pinned to the bottom left of the image its background
   was whatever that site happened to have at that corner: on one card the word
   KRAFTFULL sat directly on top of the heading underneath it, and the crop
   point moved with every recapture, so which cards collided was luck. On the
   meta line it is this page's paper, it is measured once, and it reads as what
   it is, which is a label about the card and not part of the photograph. */
.work__layout {
  margin-left: var(--s2);
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--paper);
  /* --ink-soft is 7.52:1 on --paper and 8.22:1 on the dark theme's. */
  color: var(--ink-soft);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  white-space: nowrap;
}

.report {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper);
  overflow: hidden;
}
.report__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s4);
  padding: clamp(18px, 2.4vw, 24px);
  background: var(--paper-warm);
  border-bottom: 1px solid var(--line);
}
.report__score { display: flex; align-items: baseline; gap: var(--s2); }
.report__score b { font-size: 2.6rem; font-weight: 800; letter-spacing: -0.04em; color: var(--accent); line-height: 1; }
.report__score span { font-size: .8rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .1em; font-weight: 700; }
.report__meta p { margin: 0; font-size: .92rem; color: var(--ink-soft); }
.report__head .btn { margin-left: auto; }
.report__list { list-style: none; margin: 0; padding: 0; }
.report__list li { display: flex; gap: var(--s3); padding: 12px clamp(18px, 2.4vw, 24px); border-bottom: 1px solid var(--line-soft); font-size: .93rem; align-items: baseline; }
.report__list li:last-child { border-bottom: 0; }
.report__state { flex: none; min-width: 84px; font-size: .74rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.is-ok .report__state { color: var(--accent); }
.is-bad .report__state { color: var(--bad-ink); }
.report__detail { color: var(--ink-soft); }
.report__more { margin: 0; padding: 12px clamp(18px, 2.4vw, 24px); font-size: .85rem; color: var(--ink-faint); background: var(--paper-warm); border-top: 1px solid var(--line); }

.founder { max-width: 62ch; }
.founder h2 { margin-top: var(--s1); }
.founder__title { color: var(--ink-faint); font-weight: 600; margin-top: var(--s1); }

/* The hidden attribute must win over the panel display, or the panel opens by itself. */
.chat__panel[hidden] { display: none; }

.pill--refunded { background: var(--paper-warm); border-color: var(--line-strong); color: var(--ink-soft); }

/* -------------------------------------------------------- the flagships -- */
/* Six examples shown large, above the gallery of the rest. Thirty examples of
   equal weight is a catalogue, and a catalogue gets browsed rather than
   believed. These are the ones to send somebody who asked whether we are any
   good, so they get the room to answer. */

/* Two across, not three. Now that the thumbnail is a photograph of the page
   rather than of a pizza, the card's width decides whether a reader can tell
   anything about the site inside it: at three across a 1440px capture lands in
   a 340px box and the whole page is a smudge. Two across gives it 570px, which
   is enough to read the shape of the layout, the type and the colour, which is
   the entire reason this block is separate from the gallery below it. */
.flagships {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 430px), 1fr));
  gap: clamp(20px, 3vw, 34px);
  margin-top: var(--s6);
}

.flagship {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper);
  overflow: hidden;
}

.flagship__body {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: clamp(18px, 2.2vw, 26px);
  /* Pushes the button down so a row of cards has its buttons on one line
     whatever the tagline length. */
  flex: 1;
}
.flagship__body h3 { font-size: 1.22rem; }
.flagship__body > p:last-of-type { color: var(--ink-soft); font-size: .95rem; line-height: 1.55; }
.flagship__body .btn { margin-top: auto; align-self: flex-start; }

/* ==========================================================================
   Art direction
   --------------------------------------------------------------------------
   Three objects, and every one of them is made out of something the company
   actually has: the price ledger drawn to scale, the captures of its own work
   used large, and the page an SEO report is about shown next to the report.

   Nothing here is stock, nothing here is a person who does not exist, and
   nothing here is a photograph of an office. The rule the file works to is
   that an image on this site has to be evidence or a drawing of data, because
   an agency with no clients decorating itself with photographs of clients is
   the one thing the copy promises it will never do.
   ========================================================================== */

/* -------------------------------------------- the ledger, drawn to scale -- */
/* The argument of the whole site was a grid of numerals, which is the one form
   an argument about proportion cannot be read in. Two bars at the same scale
   say it before a word is read: the agency's bill split into the lines it is
   made of, and this site's price against the same ruler.

   Blade draws it from the same rows the table below draws, so the picture
   cannot drift from the figures under it. */
.scale {
  margin: 0 0 clamp(30px, 4vw, 48px);
  display: grid;
  gap: clamp(12px, 1.6vw, 18px);
}

/* Below 720 the label and the total sit on one line and the bar runs the full
   measure underneath them, because a 150px label column in a 335px viewport
   leaves the bar 170px to carry five blocks in. */
.scale__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "key sum"
    "track track";
  gap: 8px var(--s3);
  align-items: baseline;
}
@media (min-width: 720px) {
  .scale__row {
    grid-template-columns: minmax(150px, 210px) minmax(0, 1fr) minmax(0, auto);
    grid-template-areas: "key track sum";
    align-items: center;
  }
}

.scale__key {
  grid-area: key;
  margin: 0;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  /* --ink-soft, 7.52:1 on --paper and 7.14:1 on --paper-warm; 8.22:1 and
     8.46:1 on the dark theme's two grounds. */
  color: var(--ink-soft);
}

.scale__sum {
  grid-area: sum;
  margin: 0;
  text-align: right;
  white-space: nowrap;
  font-weight: 700;
  font-size: 1rem;
  font-variant-numeric: tabular-nums;
  /* --ink measures 16.92:1 on --paper-warm in light and 16.09:1 in dark. */
  color: var(--ink);
}
/* --accent measures 6.88:1 on --paper-warm, and the dark theme's brighter
   green 8.89:1 on its own ground. Both clear 4.5:1 as text. */
.scale__sum--us { color: var(--accent); }

/* The track is the full scale on both rows, always. A short bar drawn in a
   short box is not a comparison, so the second row's remainder stays as empty
   ruled space rather than being cropped away.

   --line at 1.27:1 is a hairline and carries no contrast requirement: it marks
   where the scale ends, and the thing that has to be legible, the blocks, is
   drawn in --ink against it at better than 15:1. */
.scale__track {
  grid-area: track;
  display: flex;
  gap: 2px;
  height: clamp(30px, 3.4vw, 40px);
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--line-soft);
}

/* flex-grow carries the value, so the gaps between blocks come out of the
   track rather than pushing the last block past the end of it. */
.scale__seg {
  flex: 0 1 0;
  min-width: 0;
  display: grid;
  place-items: center;
  border-radius: 3px;
  background: var(--ink);
  /* White on near black measures 17.83:1, and the dark theme's inversion, near
     black on off white, 15.44:1. The figure inside a block is the hour count
     for that line of the table. */
  color: var(--paper);
  font-size: .74rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  overflow: hidden;
}
.scale__seg--us { background: var(--accent); }
.scale__rest { flex: 0 1 0; min-width: 0; }

.scale__caption {
  margin: 0;
  max-width: 64ch;
  font-size: .85rem;
  line-height: 1.55;
  /* --ink-faint, 4.94:1 on --paper-warm. */
  color: var(--ink-faint);
}

/* ------------------------------------------------------------- the wall -- */
/* The home page argued a price, answered the objections to it and closed, and
   never once showed the thing being sold. The only photography this company
   owns is photographs of its own pages, so five of them go on the wall and the
   first is drawn large.

   Five is not arbitrary: one tile across the full measure and four underneath
   it fills the grid exactly, at two columns and at four. Any other number
   leaves a hole in the bottom corner, which is the ragged last row this site
   has been marked down for twice, so a short wall runs flat instead.

   NOTHING HERE IS CROPPED AT ALL. A capture is a page with words on it, and
   the last round was marked down for a thumbnail rule that blew the picture up
   and cut headlines mid-word. A wider box crops only the bottom, which sounds
   safe until the bottom edge lands halfway through an h1, so every box on the
   wall is 16:10, which is the shape of the capture, and every picture is the
   whole of it. */
.wall {
  display: grid;
  gap: clamp(14px, 1.8vw, 22px);
  grid-template-columns: minmax(0, 1fr);
}

/* Two across from 620, four from 960, and in both the feature tile runs the
   full measure on its own row so the four that follow close the rectangle.
   At 1440 that is 1100 pixels wide for a page captured at 1024, which is the
   whole page at slightly over its own size, uncropped. */
@media (min-width: 620px) {
  .wall { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wall--feature .wall__tile:first-child { grid-column: 1 / -1; }
}

@media (min-width: 960px) {
  .wall--feature { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .wall--flat { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
}

.wall__tile {
  display: grid;
  gap: 10px;
  align-content: start;
  text-decoration: none;
  color: inherit;
}

/* A frame, not a floating rectangle. The tile is a link to a live page, so its
   edge is drawn with --control-line at 3.41:1 on --paper rather than with the
   1.27:1 hairline this file uses between blocks of text. */
.wall__shot {
  display: block;
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 10;
  border: 1px solid var(--control-line);
  border-radius: var(--radius);
  background: var(--paper-warm);
}
.wall__shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  transition: transform .5s var(--ease);
}
/* --ink-soft at 7.52:1 on --paper, so pointing at a tile strengthens its edge
   rather than tinting it. */
.wall__tile:hover .wall__shot { border-color: var(--ink-soft); }
.wall__tile:hover .wall__shot img { transform: scale(1.02); }

.wall__cap {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 3px 10px;
  font-size: .9rem;
  line-height: 1.4;
}
.wall__cap b { font-weight: 650; color: var(--ink); }
/* Not italic. It is the trade and the town, set quieter than the name, and an
   italic in a caption this small is decoration standing in for hierarchy. */
.wall__cap i { font-style: normal; font-size: .82rem; color: var(--ink-faint); }
@media (min-width: 620px) {
  .wall--feature .wall__tile:first-child .wall__cap b { font-size: 1.12rem; }
}

.wall__note {
  margin: clamp(20px, 2.6vw, 30px) 0 0;
  max-width: 70ch;
  font-size: .9rem;
  line-height: 1.6;
  color: var(--ink-faint);
}
.wall__note a { color: var(--ink); font-weight: 650; }

/* ---------------------------------------- the report, next to its page --- */
/* The monthly report is twenty-five checks on a real page, and the page it is
   about was nowhere near it: the one visual object on the services page was a
   table about a site the reader had never seen. The capture is the same file
   the examples page serves, of the demo the audit names. */
/* The picture sits above the report rather than beside it. Side by side, the
   report is a column of twenty-five sentences and the photograph is a single
   object, so the picture ran out about 330 pixels before the list did and left
   a hole in the bottom of its own column. Stacked, the page is a plate across
   the full measure and the report is the document under it, which is also the
   order a person reads them in. */
.proof { display: grid; gap: clamp(20px, 3vw, 34px); }

.proof__shot { margin: 0; display: grid; gap: var(--s2); }
.proof__shot a {
  display: block;
  overflow: hidden;
  aspect-ratio: 16 / 10;
  border: 1px solid var(--control-line);
  border-radius: var(--radius-lg);
  background: var(--paper-warm);
}
/* 16:10 is the shape of the capture, so the picture is the whole page. A
   wider box was tried and it cut the h1 of the page it was proving in half,
   which is a worse advertisement for the work than no picture. */
.proof__shot a:hover { border-color: var(--ink-soft); }
.proof__shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}
.proof__shot figcaption {
  margin: 0;
  font-size: .85rem;
  line-height: 1.5;
  /* --ink-faint, 5.20:1 on --paper. */
  color: var(--ink-faint);
}


/* ==========================================================================
   Landing pages
   --------------------------------------------------------------------------
   A page per trade, per question and per city (config/landings.php). They
   are built out of the same objects as the rest of the site: the hero and the
   price card from the home page, the cards, the example plates, the fact
   column. What is here is only what those pages have that no other does: the
   trail above the headline, the price on a real site's card, and the lists of
   links that connect the pages to each other.
   ========================================================================== */

/* Where the page sits. Small and quiet, because it is orientation and not
   content, and it is the first thing in the section so it carries the gap. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px var(--s2);
  margin-bottom: clamp(22px, 3vw, 36px);
  font-size: .86rem;
  color: var(--ink-faint);
}
.crumbs a { color: var(--ink-soft); text-decoration: none; }
.crumbs a:hover { color: var(--accent); text-decoration: underline; }
.crumbs [aria-current] { color: var(--ink); font-weight: 600; }

/* The home page headline is one short sentence. These are longer and there
   are forty of them, so they are set a step down and the section is a little
   shallower, which keeps the price card in the first screen on a laptop. */
.hero--landing { padding-block: clamp(40px, 6vw, 84px) clamp(56px, 8vw, 104px); }
.hero--landing h1 { font-size: clamp(2.2rem, 4.8vw, 3.6rem); line-height: 1.04; }
.hero--landing .hero__grid { align-items: start; }

/* What a real site cost its owner. The figure is the point, so it is the
   only heavy thing on the line. */
.work__cost { margin: 0 0 var(--s4); font-size: .95rem; color: var(--ink-soft); }
.work__cost b { color: var(--ink); font-weight: 750; white-space: nowrap; }
.work__item h3 + p:has(+ .work__cost) { margin-bottom: var(--s1); }

/* Lists of links to the other landing pages. Columns of plain links under a
   small label: nothing here is a card, because nothing here is content. */
.linkgroups {
  display: grid;
  gap: clamp(28px, 4vw, 48px) clamp(24px, 4vw, 56px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
}
.linkgroups--two { grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }

.linkgroup__title {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding-bottom: var(--s2);
  margin-bottom: var(--s3);
  border-bottom: 1px solid var(--line);
}

.linklist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.linklist a { color: var(--ink); font-weight: 550; text-decoration: none; }
.linklist a:hover { color: var(--accent); text-decoration: underline; }

/* The same thumb-sized targets the footer links were given, and for the same
   reason: on a phone these are a column of short links a few pixels apart. */
@media (max-width: 620px) {
  .linklist { gap: var(--s1); }
  .linklist a { display: block; padding-block: 8px; }
}

/* Most trades have one example. Alone in an auto-fit grid it took the whole
   measure and drew its screenshot seven hundred pixels tall, which is a
   poster and not a card. One example is held to the width it has when there
   are two. */
.flagships:has(> :only-child) { grid-template-columns: minmax(0, 620px); }
