/* ==========================================================================
   nayfla.css — everything shared by every landing page.

   Extracted from the three campaign.css files, which carried 64 identical
   rule blocks between them. Per-page styles live in assets/<slug>.css and
   load after this file.

   Colour, type, spacing, and grid tokens still come from ACSS. This file adds
   only what ACSS does not ship: the self-hosted font faces, layout primitives,
   and the v2 components.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Fonts — self-hosted, deliberately

   etch-typography.css points at /wp-content/uploads/*.woff2, which resolves
   only inside WordPress. Pointing at https://nayfla.com/wp-content/uploads/
   does not work either: that origin serves woff2 with no
   Access-Control-Allow-Origin header, and @font-face is always a CORS
   request, so every cross-origin load fails and the page silently falls back
   to a system font. Easy to miss on a Mac with Pangea installed locally.

   Adding Access-Control-Allow-Origin: * to woff2 responses on nayfla.com is
   worth doing regardless.
   -------------------------------------------------------------------------- */

@font-face { font-family: "Pangea"; src: url("/fonts/Pangea-Bold.woff2") format("woff2"); font-style: normal; font-weight: 700; font-display: swap; }
@font-face { font-family: "Pangea"; src: url("/fonts/Pangea-BoldItalic.woff2") format("woff2"); font-style: italic; font-weight: 700; font-display: swap; }
@font-face { font-family: "Pangea"; src: url("/fonts/Pangea-Medium.woff2") format("woff2"); font-style: normal; font-weight: 500; font-display: swap; }
@font-face { font-family: "Pangea"; src: url("/fonts/Pangea-MediumItalic.woff2") format("woff2"); font-style: italic; font-weight: 500; font-display: swap; }

@font-face { font-family: "Pangea Text"; src: url("/fonts/PangeaText-Regular.woff2") format("woff2"); font-style: normal; font-weight: 400; font-display: swap; }
@font-face { font-family: "Pangea Text"; src: url("/fonts/PangeaText-RegularItalic.woff2") format("woff2"); font-style: italic; font-weight: 400; font-display: swap; }
@font-face { font-family: "Pangea Text"; src: url("/fonts/PangeaText-Medium.woff2") format("woff2"); font-style: normal; font-weight: 500; font-display: swap; }
@font-face { font-family: "Pangea Text"; src: url("/fonts/PangeaText-MediumItalic.woff2") format("woff2"); font-style: italic; font-weight: 500; font-display: swap; }
@font-face { font-family: "Pangea Text"; src: url("/fonts/PangeaText-Bold.woff2") format("woff2"); font-style: normal; font-weight: 700; font-display: swap; }
@font-face { font-family: "Pangea Text"; src: url("/fonts/PangeaText-BoldItalic.woff2") format("woff2"); font-style: italic; font-weight: 700; font-display: swap; }

/* --------------------------------------------------------------------------
   2. Foundation
   -------------------------------------------------------------------------- */

/* Theme. Every ACSS token is already light-dark(), so the entire palette
   follows the used color-scheme and no second set of colours is needed here.
   Bare :root follows the OS; data-theme is set by the toggle in the sticky
   bar and pins the choice. The attribute is applied by an inline script in
   base.njk before first paint, so a returning visitor never sees a flash of
   the wrong theme. */
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

:root {
  /* --accent (#CB6C1A) fails WCAG AA as small text on this light palette
     (3.68:1 on white, needs 4.5). Same hue and chroma, dark enough to pass.
     Applied only where the colour carries small text; the palette itself is
     untouched. Same reasoning for --success and --danger. The dark sides are
     the same hues lightened to clear 4.5 on --base-light's dark value; these
     three are the only colours in the project that are not already
     light-dark(), which is why they need stating twice. */
  --accent-readable: light-dark(oklch(0.52 0.1478 54.4), oklch(0.8 0.1478 54.4));
  --success-readable: light-dark(oklch(0.48 0.175 147), oklch(0.82 0.175 147));
  --danger-readable: light-dark(oklch(0.5 0.202 21), oklch(0.75 0.202 21));

  /* Body prose. ACSS's scale jumps from --text-s (14–15px, a label size) to
     --text-m (18–20px, a lede size) with nothing between, so every paragraph
     on these pages was being set at 14–15px: FAQ answers, step bodies, tier
     features, section subtitles, the lot. These two sit in that gap. --prose
     is the default paragraph, --prose-s is genuine fine print (helper text
     under an input, the footer, tier feature lists). Same fluid slope as the
     ACSS tokens so they scale in step with everything around them. */
  --prose: clamp(1.0625rem, calc(0.0862069vw + 1.0431034rem), 1.125rem);
  --prose-s: clamp(0.9375rem, calc(0.0862069vw + 0.9181034rem), 1rem);

  --hairline: color-mix(in oklch, var(--base) 14%, transparent);
  --panel: color-mix(in oklch, var(--white) 70%, transparent);
  /* Image edges want pure black/white at low alpha. A tinted neutral picks up
     the surface colour under it and reads as dirt along the edge. */
  --img-outline: light-dark(oklch(0 0 0 / 0.1), oklch(1 0 0 / 0.1));
  --bar-h: 3.5rem;
  /* Light enough to let the progressive blur read, opaque enough that the bar
     text still passes AA over whatever scrolls under it. */
  --bar-tint: color-mix(in oklch, var(--base-light) 86%, transparent);
  /* Keeps anchored sections clear of the sticky bar. */
  --offset: calc(var(--bar-h) + var(--space-s));
  --scroll-margin: var(--offset);
}

/* Kept so the markup imports cleanly into Etch, which expects these. */
[data-etch-element="section"] { inline-size: 100%; display: flex; flex-direction: column; align-items: center; }
[data-etch-element="container"] { inline-size: 100%; max-inline-size: var(--content-width); margin-inline: auto; }

body { background: var(--body-bg-color); }

/* Layout primitives. This ACSS build ships no flex/grid/gap utilities. */
.n-stack { display: flex; flex-direction: column; gap: var(--gap, var(--space-m)); }
.n-row { display: flex; flex-wrap: wrap; gap: var(--space-xs); align-items: center; }
.n-center { text-align: center; margin-inline: auto; }
.n-pad { padding-inline: var(--gutter); }

.n-eyebrow {
  font-family: var(--heading-font-family);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-readable);
}

.n-eyebrow:has(.n-ico),
.n-eyebrow:has(svg) {
  display: flex;
  align-items: center;
  gap: 0.5em;
}

.n-eyebrow .n-ico { --icon-color: currentColor; font-size: 0.95em; }

.n-trust { font-size: var(--prose-s); color: var(--text-dark-muted); }

/* --------------------------------------------------------------------------
   2b. Section header

   One grid for all three variants, so the markup is identical everywhere and
   only the modifier changes. Pairs with section-head.njk. Empty props render
   nothing, which is why there is no --no-subtitle or --no-eyebrow variant.
   -------------------------------------------------------------------------- */

.n-head { display: grid; gap: var(--space-s); }
.n-head__main { display: flex; flex-direction: column; gap: var(--space-xs); }
.n-head__main > * { margin: 0; }
.n-head__sub { font-size: var(--prose); line-height: 1.55; color: var(--text-dark-muted); max-inline-size: 60ch; }

.n-head--center {
  justify-items: center;
  text-align: center;
  max-inline-size: 52ch;
  margin-inline: auto;

  & .n-head__main { align-items: center; }
  & .n-head__sub { margin-inline: auto; }
}

/* Two columns only where there is room for them. Below this the subtitle
   falls under the title and the variant is indistinguishable from --left,
   which is the correct result rather than a fallback. */
@media (min-width: 768px) {
  .n-head--split {
    grid-template-columns: 2fr 1fr;
    gap: var(--grid-gap);
    align-items: end;
  }
}

/* --------------------------------------------------------------------------
   3. Sticky offer bar
   The biggest structural change from v1: price and CTA never leave the screen.
   -------------------------------------------------------------------------- */

.n-bar {
  position: sticky;
  top: 0;
  z-index: 50;
  inline-size: 100%;
  min-block-size: var(--bar-h);
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding-inline: var(--gutter);
  padding-block: 0.5rem;
}

/* Progressive blur. One flat backdrop-filter stops at a hard edge you can see
   against a photo or a heading scrolling under it; four stacked ones, each
   blurrier and each masked to fade out sooner, do not. The wrapper overhangs
   the bar by 2.5rem so the whole transition happens below the bar rather than
   across the text. z-index: -1 keeps it behind the content without needing to
   position every sibling. The tint (::after, painted last) fades on the same
   curve — a solid bar background would hide the effect entirely, which is what
   the old blur(12px) under a 90% fill was doing. */
.n-bar__blur {
  position: absolute;
  inset: 0;
  bottom: -2.5rem;
  z-index: -1;
  pointer-events: none;
}

.n-bar__blur > span {
  position: absolute;
  inset: 0;
  backdrop-filter: blur(var(--b));
  -webkit-backdrop-filter: blur(var(--b));
  mask-image: var(--m);
  -webkit-mask-image: var(--m);
}

.n-bar__blur > span:nth-child(1) { --b: 2px;  --m: linear-gradient(to bottom, #000 0, #000 70%, transparent 100%); }
.n-bar__blur > span:nth-child(2) { --b: 4px;  --m: linear-gradient(to bottom, #000 0, #000 48%, transparent 78%); }
.n-bar__blur > span:nth-child(3) { --b: 8px;  --m: linear-gradient(to bottom, #000 0, #000 26%, transparent 56%); }
.n-bar__blur > span:nth-child(4) { --b: 16px; --m: linear-gradient(to bottom, #000 0, #000 4%,  transparent 34%); }

.n-bar__blur::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, var(--bar-tint) 0, var(--bar-tint) 58%, transparent 100%);
}

/* Inlined SVG, so its var(--primary, #7198F7) fills resolve against this page
   and follow light/dark with the rest of the palette. */
.n-bar__brand { display: flex; align-items: center; }
.n-bar__brand svg { display: block; block-size: 1.5rem; inline-size: auto; }
.n-bar__price { margin-inline-start: auto; font-size: var(--text-s); color: var(--text-dark-muted); }

.n-bar__price strong { color: var(--text-dark); font-family: var(--heading-font-family); }
/* min-block-size rather than more padding: the label stays bar-sized while the
   tap target reaches 40px. At 0.6em padding this button was 33px tall, which is
   the smallest thing on the page and also the one people are meant to press. */
.n-bar .btn--primary { --btn-font-size: var(--text-s); --btn-padding-block: 0.6em; min-block-size: 2.5rem; }

/* Wordmark, price, CTA. nowrap on the last two is the point: if a longer
   label ever stops fitting it has to show as overflow here rather than
   quietly doubling the bar's height on a phone. */
@media (max-width: 30rem) {
  .n-bar { gap: var(--space-xs); }
  .n-bar__brand svg { block-size: 1.25rem; }
  .n-bar__price { font-size: var(--text-xs); white-space: nowrap; }

  .n-bar .btn--primary {
    --btn-width: auto;
    --btn-min-width: 0;
    --btn-font-size: var(--text-xs);
    --btn-padding-inline: 0.85em;
    white-space: nowrap;
  }

  /* The price is the offer, so it stays. Only the longer label goes, which is
     what was actually overflowing the row. */
  .n-bar__label { display: none; }
}

/* --------------------------------------------------------------------------
   4. Hero shell
   -------------------------------------------------------------------------- */

/* Grid + grain, lifted from the /free-strategy-call hero. A pseudo-element
   rather than the two spare divs that page uses, so the markup stays as is.
   --black is light-dark(), so the lines invert with the palette. */
.n-hero {
  position: relative;
  isolation: isolate;
}

.n-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    url("data:image/svg+xml,%3Csvg viewBox='0 0 1746 1746' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
    linear-gradient(to right, color-mix(in oklch, var(--black) 25%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in oklch, var(--black) 25%, transparent) 1px, transparent 1px);
  background-size: 64px 64px;
  background-blend-mode: overlay, normal;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 40%, black 30%, transparent 80%);
  -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 40%, black 30%, transparent 80%);
  opacity: 0.25;
}

.n-hero__title {
  margin: 0;

  & em { color: var(--primary); font-style: normal; }
}

/* One centred hero, shared by all three campaigns. Every page ran its own
   copy of this in its own stylesheet, and Campaigns 01 and 03 ran a two-column
   split with the montage beside the argument. One column reads the same at
   every width and puts the featured piece — the montage, or Campaign 02's
   speed checker — directly under the sentence it is evidence for.

   The measure is held on the text rather than on the box, so the featured
   piece is full width while the copy stays readable. */
.n-hero__inner {
  max-inline-size: 62rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-m);
  text-align: center;
}

.n-hero__lede {
  font-size: var(--text-l);
  line-height: 1.5;
  max-inline-size: 52ch;
  color: var(--text-dark-muted);
}

/* --------------------------------------------------------------------------
   4b. Work montage

   Every slide occupies the same grid cell, so the track is as tall as its
   tallest image and that height never changes as it cycles — no reflow of the
   hero, and no jump in whatever sits below it. Each image is width-driven with
   block-size: auto, so images of any aspect ratio can be dropped in and each
   keeps its own proportions. Pairs with slider.njk and slider.js.
   -------------------------------------------------------------------------- */

.n-slider {
  display: grid;
  inline-size: 100%;
  /* Nothing here is load-bearing for the offer, so it is never the LCP fight. */
  contain: paint;
}

.n-slide {
  grid-area: 1 / 1;
  place-self: center;
  inline-size: 100%;
  margin: 0;
  opacity: 0;
  transition: opacity 0.8s var(--ease-smooth, ease);
}

.n-slide.is-on { opacity: 1; }

.n-slide img {
  inline-size: 100%;
  block-size: auto;
  display: block;
  border-radius: var(--radius-xl);
  border: 1px solid var(--img-outline);
}

.n-slide figcaption {
  margin-block-start: var(--space-xs);
  font-size: var(--text-xs);
  color: var(--text-dark-muted);
}

/* Without JS, or with reduced motion on, the first slide is simply the image.
   Nothing announces itself as broken. */
@media (prefers-reduced-motion: reduce) {
  .n-slide { transition: none; }
}

/* --------------------------------------------------------------------------
   4c. Hero stats, and the Google rating that sits with them

   Last thing in every hero: two numbers the offer can stand on, plus the
   rating. The rating's figures come from site.json, so all three pages move
   together when the review count does.
   -------------------------------------------------------------------------- */

.n-stats {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-end;
  gap: var(--space-s) var(--space-l);
  margin-block-start: var(--space-xs);
}

.n-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15em;
  max-inline-size: 20ch;
}

.n-stat__num {
  font-family: var(--heading-font-family);
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--heading-color);
}

.n-stat__label { font-size: var(--prose-s); line-height: 1.4; color: var(--text-dark-muted); }

/* Review stars. Gold rather than --primary: everyone reads gold stars as a
   rating, and a brand-coloured star reads as decoration. The same value in
   both themes, which is the point of a rating. */
.n-rating {
    gap: 0.45em;

    & .n-rating a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 2px; }
}

.n-rating__stars {
  color: #f5b301;
  font-size: var(--text-l);
  line-height: 1.1;
  letter-spacing: 0.08em;
}

/* --------------------------------------------------------------------------
   5. Booking card
   -------------------------------------------------------------------------- */

/* Base card. Campaign 01 overrides this to sit sticky inside the hero grid;
   the other two centre it in a section of its own. */
.n-book {
  /* border: 1px solid var(--hairline);
  border-radius: var(--radius-xxl);
  background: var(--white);
  overflow: hidden;
  max-inline-size: 46rem;
  margin-inline: auto;
  inline-size: 100%; */
}

/* Set beside the calendar, not above it. Nothing is allowed to render
   between the top of .n-book and the embed. */

/* The capacity pill sits between the embed and the fallback link. It is
   fit-content, so this is what centres it on the card — the same alignment
   .n-book__alt below it already has. */
.n-book > .n-slots { margin-inline: auto; margin-block-start: var(--space-s); }
/* --------------------------------------------------------------------------
   Slot counter (slots.njk)

   One row: how many slots are left, and what the month costs. It wraps rather
   than shrinks, so in the booking column and on a narrow phone the count and
   the price stack in that order.

   The only decorated element on these pages. It carries scarcity and a
   struck-through price, which is the one place on a landing page where a
   plain line reads as an afterthought — but the decoration is a texture and a
   shadow, not motion. An animated border on a static claim is attention spent
   on nothing, and it is on screen for the whole scroll.
   -------------------------------------------------------------------------- */
.n-slots {
  --n-slots-accent: var(--primary);
  /* Coloured, not white: a tint of whatever the state's accent is. */
  --n-slots-tint: color-mix(in oklch, var(--n-slots-accent) 8%, var(--white));

  position: relative;
  /* Keeps the texture layer's negative z-index inside this element. */
  isolation: isolate;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.35rem 0.9rem;
  margin: 0;
  padding: 0.75em 1.25em;
  border-radius: 1.25rem;
  font-size: var(--text-s);
  line-height: 1.4;
  color: var(--text-dark-muted);
  /* Numbers that change on a redeploy should not shift the text around them. */
  font-variant-numeric: tabular-nums;
  background-color: var(--n-slots-tint);

  /* Structure from a hairline, lift from layered shadow — the accent-tinted
     third layer is the glow, sized to read as light coming off the chip
     rather than a second border. */
  border: 1px solid color-mix(in oklch, var(--n-slots-accent) 20%, transparent);
  box-shadow:
    0 1px 2px light-dark(oklch(0 0 0 / 0.04), oklch(0 0 0 / 0.2)),
    0 8px 20px -12px light-dark(oklch(0 0 0 / 0.25), oklch(0 0 0 / 0.5)),
    0 0 24px -10px color-mix(in oklch, var(--n-slots-accent) 45%, transparent);

  /* fit-content rather than align-self, so the pill follows whichever
     container it lands in: centred in the hero and the closing CTA, hard left
     in the booking column. A definite width also stops the flex parent
     stretching it to full width — and it still shrinks and wraps below it. */
  inline-size: fit-content;
  max-inline-size: 100%;
}

/* The dot grid, on its own layer so it can be masked. Full strength at the
   leading edge and gone by two thirds, which keeps the texture off the price
   and stops the grid colliding with the section background behind it. */
.n-slots::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background-image: radial-gradient(
    circle at center,
    color-mix(in oklch, var(--n-slots-accent) 30%, transparent) 1px,
    transparent 1.5px
  );
  background-size: 9px 9px;
  /* One ellipse anchored off the left edge: the grid is strongest where the
     count is and gone before the price, and it fades away from the border on
     every side rather than stopping against it. One mask layer, so there is no
     mask-composite support to worry about. */
  mask-image: radial-gradient(
    62% 125% at 3% 50%,
    #000 0%,
    color-mix(in srgb, #000 45%, transparent) 52%,
    transparent 100%
  );
  pointer-events: none;
}

.n-slots strong { color: var(--text-dark); font-family: var(--heading-font-family); }

/* The price. Struck-through original first, the way the tier cards do it. */
.n-slots__price { display: inline-flex; align-items: baseline; gap: 0.4em; white-space: nowrap; }
.n-slots__price s { color: var(--text-dark-muted); }
.n-slots__price strong { color: var(--n-slots-accent); }

/* Three or fewer left is the only state that earns a second colour, and it
   recolours the tint, the border, the glow and the price through one
   variable. */
.n-slots.is-low { --n-slots-accent: var(--accent-readable); }

.n-book__sub { font-size: var(--prose-s); color: var(--text-dark-muted); }
.n-book__embed { min-block-size: 26rem; }
.n-book__embed > * { inline-size: 100%; }
.n-book__loading { padding: var(--space-l) var(--space-m); text-align: center; font-size: var(--prose-s); color: var(--text-dark-muted); }

/* The fallback stays until the embed reports itself ready, so a blocked or
   slow cal.com never leaves an empty box where the calendar should be. */
.n-book[data-cal="ready"] .n-book__loading { display: none; }

/* The fallback booking route, and the only way through if cal.com is blocked,
   so its two links get a real tap target rather than bare inline text. */
.n-book__alt a { display: inline-flex; align-items: center; min-block-size: 2.5rem; }

/* A footnote under the calendar, so it gets a hairline and nothing else. The
   tinted panel it used to sit on gave two text links the weight of a section.
   Row gap is zero because each link already carries a 2.75rem tap target,
   which is all the separation a wrapped row needs. */
.n-book__alt {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-xs);
  align-items: center;
  justify-content: center;
  padding: var(--space-s) var(--space-xs);
  font-size: var(--prose-s);
}

/* --------------------------------------------------------------------------
   5b. Tick list

   Only the three decisive inclusions sit in a hero. The full list goes
   further down; seven bullets up top would slow the decision. Pairs with
   tick.njk. Campaigns 01 and 03 both use it, so it lives here.
   -------------------------------------------------------------------------- */

.n-checks { display: flex; flex-direction: column; gap: 0.6em; list-style: none; padding: 0; margin: 0; }

/* Centred hero, left-aligned rows: a checklist whose ticks do not share a
   left edge is a paragraph with icons in it. */
.n-checks--center { inline-size: fit-content; max-inline-size: 52ch; margin-inline: auto; text-align: start; }

.n-checks li {
  display: grid;
  grid-template-columns: 1.15em 1fr;
  gap: 0.7em;
  align-items: start;
  font-size: var(--text-m);
  line-height: 1.45;
}

.n-checks svg { inline-size: 1.15em; block-size: 1.15em; margin-block-start: 0.2em; color: var(--primary); flex-shrink: 0; }

/* --------------------------------------------------------------------------
   6. Proof strip
   -------------------------------------------------------------------------- */

.n-proof { padding-block: var(--section-space-xs); border-block: 1px solid var(--hairline); background: var(--white); }
.n-proof__inner { display: flex; flex-direction: column; gap: var(--space-s); align-items: center; }

.n-logos {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-s);
  align-items: center;
  inline-size: 100%;

  @media (min-width: 768px) { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

.n-logos img { inline-size: 100%; block-size: auto; }

/* --------------------------------------------------------------------------
   7b. Radio-group controls

   Shared: Campaign 03's volume pills and scope cards, and Campaign 01's
   industry picker in the planner. Moved here from design-retainer.css when
   the second page started using .n-choice.
   -------------------------------------------------------------------------- */

.n-field { display: flex; flex-direction: column; gap: 0.4em; }
/* Splits the volume question from the cost assumptions it feeds. */
.n-field--rule { padding-block-end: var(--space-m); border-block-end: 1px solid var(--hairline); }
/* The other two edges, for questions that sit beside or under a rule instead
   of above one. Both are desktop-only: on a phone every field is stacked and
   already carries the horizontal rule from design-retainer.css. */
@media (min-width: 700px) {
  .n-field--rule-inline { padding-inline-start: var(--space-l); border-inline-start: 1px solid var(--hairline); }
  .n-field--rule-top { padding-block-start: var(--space-m); border-block-start: 1px solid var(--hairline); }
}
/* The radio groups cannot use a <label for>, so their heading is a span. */
.n-field > label,
.n-field__label { font-family: var(--heading-font-family); font-size: calc(0.9 * var(--text-m)); font-weight: 700; }
.n-field small { font-size: var(--prose-s); color: var(--text-dark-muted); line-height: 1.45; }

.n-field input[type="number"],
.n-field select {
  inline-size: 100%;
  padding: 0.6em 0.75em;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-l);
  background: var(--base-ultra-light);
  font-family: var(--text-font-family);
  /* 1rem minimum or iOS zooms the page on focus. */
  font-size: max(1rem, var(--text-s));
  color: var(--text-dark);

  &:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
}

/* The default range track is a 16px-tall target, and on this page the slider
   is the primary interaction. Padding gives the thumb a 44px band to be
   grabbed in without making the track itself look chunky. */
.n-field input[type="range"] {
  inline-size: 100%;
  accent-color: var(--primary);
  block-size: 2.75rem;
  cursor: pointer;

  &:focus-visible { outline: 2px solid var(--primary); outline-offset: 4px; }
}
.n-field__val { font-family: var(--heading-font-family); font-size: var(--text-l); font-weight: 700; line-height: 1; }

/* Equal columns filling the row, however many pills the front matter lists.
   auto-fit drops the empty tracks so the row always spans the full width; on
   a phone it falls to a tidy grid of whole rows instead of one long scrap. */
.n-pills {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(4.5rem, 1fr));
  gap: var(--space-xs);
  margin-block-start: 0.2em;
}

.n-pill,
.n-choice__opt {
  cursor: pointer;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-l);
  background: var(--base-ultra-light);
  transition: background 0.15s var(--ease-smooth, ease), border-color 0.15s var(--ease-smooth, ease);

  & input { position: absolute; opacity: 0; inline-size: 0; block-size: 0; }
  &:hover { border-color: color-mix(in oklch, var(--primary) 45%, transparent); }
  &:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 2px; }
  &:has(input:checked) { background: var(--primary-light); border-color: var(--primary); color: var(--primary-dark); }
}

.n-pill {
  padding: 0.65em 0.5em;
  text-align: center;
  font-family: var(--heading-font-family);
  font-size: var(--text-s);
  font-weight: 700;
  /* Tabular figures stop the pills reflowing between 4 and 40. */
  font-variant-numeric: tabular-nums;
}

.n-choice { display: grid; grid-template-columns: var(--grid-1); gap: var(--space-s); }
@media (min-width: 700px) { .n-choice { grid-template-columns: var(--grid-2); } }

/* Two columns is the default because the first question built with this was a
   two-way one. These are for the groups that are not: --4 goes two-up before
   it goes four-up, because four cards at 700px leaves no room for the
   description under each title. */
@media (min-width: 700px) { .n-choice--3 { grid-template-columns: var(--grid-3); } }
@media (min-width: 1024px) { .n-choice--4 { grid-template-columns: var(--grid-4); } }

.n-choice__opt { padding: var(--space-s) var(--space-m); display: flex; flex-direction: column; gap: 0.25em; }
.n-choice__title { font-family: var(--heading-font-family); font-size: var(--prose-s); font-weight: 700; }
.n-choice__desc { font-size: var(--prose-s); color: var(--text-dark-muted); line-height: 1.45; }
.n-choice__opt:has(input:checked) .n-choice__desc { color: var(--primary-dark); }


/* --------------------------------------------------------------------------
   7. Icons, steps, cards, Q&A, asides
   -------------------------------------------------------------------------- */

.n-ico {
  --icon-color: var(--primary);
  --icon-size: 1em;
  font-size: var(--icon-size);
  color: var(--icon-color);
  flex-shrink: 0;

  /* Lucide SVGs, inlined by the `icon` shortcode. Sized in em against the
     element's own font-size rather than --icon-size directly, so the handful
     of rules that already tune these with font-size keep working untouched. */
  inline-size: 1em;
  block-size: 1em;
  vertical-align: -0.125em;
  stroke: currentColor;
  fill: none;
}

.n-label {
  display: flex;
  align-items: center;
  gap: 0.55em;
  font-family: var(--heading-font-family);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dark-muted);
}

.n-num {
  font-family: var(--heading-font-family);
  font-weight: 700;
  font-size: clamp(2.75rem, 2rem + 2.4vw, 4rem);
  line-height: 0.85;
  letter-spacing: -0.04em;
  color: color-mix(in oklch, var(--primary) 22%, transparent);
  font-variant-numeric: tabular-nums;
}

.n-card {
  padding: var(--space-m);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-xl);
  background: var(--panel);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  transition: box-shadow 0.25s var(--ease-smooth, ease), border-color 0.25s var(--ease-smooth, ease);

  /* No lift. These cards are not links, and a card that rises under the cursor
     reads as clickable, so people click it and nothing happens. The border and
     shadow still respond, which is enough to feel alive. */
  &:hover {
    border-color: color-mix(in oklch, var(--primary) 35%, transparent);
    box-shadow: 0 14px 34px color-mix(in oklch, var(--base) 12%, transparent);
  }
}

.n-card__ico { font-size: 1.35rem; color: var(--primary); align-self: flex-start; inline-size: 1em; block-size: 1em; stroke: currentColor; fill: none; }
.n-card h3 { margin: 0; }
.n-card__rule { inline-size: 2.5rem; block-size: 2px; background: var(--primary); border: 0; margin: 0.1em 0 0.2em; }
.n-card p { font-size: var(--prose); color: var(--text-dark-muted); line-height: 1.6; }

.n-steps {
  display: grid;
  grid-template-columns: var(--grid-1);
  gap: var(--space-l);
  list-style: none;
  padding: 0;
  margin: 0;

  @media (min-width: 768px) { grid-template-columns: var(--grid-3); gap: var(--grid-gap); }
}

.n-step {
  display: flex;
  flex-direction: column;
  gap: 0.5em;
  padding-block-start: var(--space-s);
  border-block-start: 1px solid var(--hairline);
  position: relative;

  /* Short primary tick sitting on the hairline. */
  &::before {
    content: "";
    position: absolute;
    inset-block-start: -1px;
    inset-inline-start: 0;
    inline-size: 2.5rem;
    block-size: 2px;
    background: var(--primary);
  }
}

.n-step__title { margin: 0; }
.n-step__body { font-size: var(--prose); color: var(--text-dark-muted); line-height: 1.6; }

.n-qa {
  display: grid;
  grid-template-columns: var(--grid-1);
  gap: var(--space-l);
  counter-reset: qa;

  @media (min-width: 768px) { grid-template-columns: var(--grid-2); gap: var(--space-l) var(--grid-gap); }
}

.n-qa__item {
  display: flex;
  flex-direction: column;
  gap: 0.4em;
  padding-block-start: var(--space-s);
  border-block-start: 1px solid var(--hairline);
  counter-increment: qa;
  position: relative;

  &::before {
    content: counter(qa, decimal-leading-zero);
    font-family: var(--heading-font-family);
    font-weight: 700;
    font-size: var(--text-xs);
    letter-spacing: 0.1em;
    color: var(--primary);
    font-variant-numeric: tabular-nums;
  }
}

.n-qa__q { margin: 0; }
.n-qa__a { font-size: var(--prose); color: var(--text-dark-muted); line-height: 1.65; }

.n-aside {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-s);
  align-items: start;
  padding: var(--space-m);
  border: 1px dashed color-mix(in oklch, var(--base) 22%, transparent);
  border-radius: var(--radius-xl);
  font-size: var(--prose);
  color: var(--text-dark-muted);
  line-height: 1.6;
}

.n-aside .n-ico { --icon-color: var(--accent-readable); font-size: 1.1rem; margin-block-start: 0.1em; }
.n-aside strong { color: var(--text-dark); }

/* --------------------------------------------------------------------------
   8. Pricing tiers

   Used by website-speed and design-retainer, not by starter-website, which
   has a single price. Kept here anyway rather than in a third shared file —
   one unused rule set is cheaper than two files that drift apart. The
   .n-tiers grid itself is per-page, since one page has three tiers and the
   other has two.
   -------------------------------------------------------------------------- */

.n-tier {
  position: relative;
  display: flex; flex-direction: column; gap: var(--space-s);
  padding: var(--space-l) var(--space-m);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-xxl);
  background: var(--panel);
}

.n-tier--pick { border-color: color-mix(in oklch, var(--primary) 45%, transparent); background: var(--white); box-shadow: 0 12px 40px color-mix(in oklch, var(--primary) 12%, transparent); }

.n-tier__badge {
  position: absolute; inset-block-start: -0.7rem; inset-inline-start: var(--space-m);
  padding: 0.25em 0.8em; border-radius: 99px;
  font-family: var(--heading-font-family); font-size: var(--text-xs); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--white); background: var(--primary);
}

.n-tier__name { margin: 0; }
.n-tier__desc { font-size: var(--prose-s); color: var(--text-dark-muted); line-height: 1.5; }
.n-tier__price { display: flex; align-items: baseline; gap: 0.3rem; flex-wrap: wrap; }

.n-tier__amt {
  font-family: var(--heading-font-family);
  font-size: clamp(2rem, 1.4rem + 1.6vw, 2.6rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--heading-color);
}

/* "From" belongs in front of the price, not stranded before "Usually". */
.n-tier__from {
  font-family: var(--heading-font-family);
  font-size: var(--text-s);
  font-weight: 700;
  color: var(--text-dark-muted);
  align-self: center;
}

.n-tier__per { font-size: var(--text-s); color: var(--text-dark-muted); }
.n-tier__was { font-size: var(--text-s); color: var(--text-dark-muted); }
.n-tier__was s { text-decoration-thickness: 1px; text-underline-offset: 2px; }
.n-tier ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.55em; flex-grow: 1; }
.n-tier li { display: grid; grid-template-columns: 1.05em 1fr; gap: 0.6em; align-items: start; font-size: var(--prose-s); line-height: 1.5; }
.n-tier li svg { inline-size: 1.05em; block-size: 1.05em; margin-block-start: 0.2em; color: var(--primary); flex-shrink: 0; }

/* --------------------------------------------------------------------------
   9. Booking layout

   The heading ran full width while the calendar sat in a narrow centred
   column, so the two read as unrelated blocks. Pairing them in one grid ties
   the ask to its reason and fills the space the calendar cannot.
   -------------------------------------------------------------------------- */

.n-booklayout {
  display: grid;
  grid-template-columns: var(--grid-1);
  gap: var(--grid-gap);
  align-items: start;

  /* 1:2 in the calendar's favour. At a fixed 26rem the embed was forced into
     its narrow single-column mode, which read as an afterthought next to the
     copy; given two thirds it lays the month out beside the times. */
  @media (min-width: 1024px) {
    grid-template-columns: 1fr 2fr;
    gap: calc(1.5 * var(--grid-gap));
  }
}

/* The grid column sets the width now. */
.n-booklayout .n-book { max-inline-size: none; }

.n-agenda {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  counter-reset: ag;
}

.n-agenda li {
  display: grid;
  grid-template-columns: 2rem 1fr;
  gap: var(--space-xs);
  align-items: baseline;
  padding-block: var(--space-s);
  border-block-start: 1px solid var(--hairline);
  counter-increment: ag;
  font-size: var(--prose-s);
  color: var(--text-dark-muted);
  line-height: 1.55;
}

.n-agenda li:last-child { border-block-end: 1px solid var(--hairline); }

.n-agenda li::before {
  content: counter(ag, decimal-leading-zero);
  font-family: var(--heading-font-family);
  font-weight: 700;
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  color: var(--primary);
  font-variant-numeric: tabular-nums;
}

.n-agenda strong { display: block; color: var(--text-dark); font-family: var(--heading-font-family); }

/* --------------------------------------------------------------------------
   10. Closing CTA and footer
   -------------------------------------------------------------------------- */

.n-close {
  background: var(--base-ultra-light);
  background-image: radial-gradient(ellipse 60% 60% at 50% 0%, color-mix(in oklch, var(--primary) 18%, transparent), transparent 70%);
  border-block-start: 1px solid var(--hairline);
}

.n-close__inner { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--space-m); max-inline-size: 46ch; margin-inline: auto; }
.n-close__title { margin: 0; }
.n-close__title em { color: var(--primary); font-style: normal; }

.n-foot {
  padding-block: var(--section-space-xs);
  font-size: var(--prose-s);
  color: var(--text-dark-muted);
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 0.4em;
}

.n-foot__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25em;
  align-items: center;
  justify-content: center;
  font-size: var(--prose-s);
}

/* Padding rather than a bare inline link: these were 21px tall, which is under
   every touch-target floor, and they are the only links in the footer. */
.n-foot__legal a { display: inline-flex; align-items: center; min-block-size: 2.75rem; padding-inline: 0.5em; }

.n-foot__theme { display: flex; justify-content: center; }

/* One icon, not a sun/moon swap: the page you are looking at already tells
   you which mode you are in, so a second indicator is noise. Down here it can
   carry its label as text instead of only as an aria-label, which is what it
   wanted all along — the bar had no room for the word. Both are rewritten on
   toggle. */
.n-theme {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  min-block-size: 2.75rem;
  padding-inline: 0.75em;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--panel);
  color: var(--text-dark-muted);
  font-family: inherit;
  font-size: var(--prose-s);
  cursor: pointer;

  &:hover { color: var(--text-dark); }
  & svg { inline-size: 1rem; block-size: 1rem; display: block; flex-shrink: 0; }
}

/* The footnotes every page used to print under the thing they qualified. They
   are disclosure, not argument, and inline they were the longest paragraph in
   a section whose job was a number. Rendered from `footnotes:` in the front
   matter — a page without the block renders nothing. */
.n-foot__notes {
  display: flex;
  flex-direction: column;
  gap: 0.6em;
  max-inline-size: 80ch;
  margin-inline: auto;
  margin-block-start: var(--space-s);
  padding-block-start: var(--space-s);
  border-block-start: 1px solid var(--hairline);
  font-size: var(--text-xs);
  line-height: 1.5;
  text-align: start;
}

.n-foot__notes b { color: var(--text-dark); font-family: var(--heading-font-family); font-weight: 700; }

/* --------------------------------------------------------------------------
   11. Motion
   -------------------------------------------------------------------------- */

/* Press feedback. Every CTA on these pages is an <a>, which gives no pressed
   state of its own, so on a phone a tap had no acknowledgement until the page
   finished scrolling. scale() only, so it composites and cannot be interrupted
   into a layout shift. 0.96 is the smallest value that still registers. */
.btn--primary,
.btn--base,
.n-theme {
  transition: scale 0.12s var(--ease-smooth, ease-out), background-color 0.15s var(--ease-smooth, ease-out), border-color 0.15s var(--ease-smooth, ease-out), color 0.15s var(--ease-smooth, ease-out);

  &:active { scale: 0.96; }
}

/* Buttons carry the same 40px floor as the sticky bar's, so a row of CTAs is
   one height whether it uses btn--s or not. */
.btn--s { min-block-size: 2.5rem; }

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

/* --------------------------------------------------------------------------
   Client quotes

   Shared by Campaigns 01 and 02 (see _includes/quotes.njk). Campaign 03 shows
   its quotes inside the work cards instead, since on that page the quote and
   the piece it is about are one argument.
   -------------------------------------------------------------------------- */

.n-quotes {
  display: grid;
  /* Three cards on a desktop, two on a tablet, one on a phone, without a
     breakpoint per count — a fourth review needs no CSS change. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: var(--grid-gap);
  font-size: calc(0.9 * var(--text-m));
}

.n-quote {
  /* Same token-blanking as Campaign 03's card quotes: ACSS ships a blockquote
     component behind :where(), and blanking its tokens is the supported way
     out of the grey panel and left bar it would otherwise draw. */
  --blockquote-background: none;
  --blockquote-padding: 0;
  --blockquote-border-width: 0;
  --blockquote-border-radius: 0;
  --blockquote-box-shadow: none;

  margin: 0;
  padding: var(--space-m);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-xl);
  background: var(--white);
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.n-quote p { font-size: var(--text-m); line-height: 1.6; }

.n-quote__stars {
  color: #f5b301;
  font-size: var(--text-m);
  line-height: 1;
  letter-spacing: 0.1em;
}

/* Pushed to the bottom so the attributions line up across cards of unequal
   quote length. */
.n-quote footer {
  margin-block-start: auto;
  padding-block-start: var(--space-s);
  border-block-start: 1px solid var(--hairline);
  font-size: var(--prose-s);
  color: var(--text-dark-muted);
}

.n-quote footer cite {
  color: var(--text-dark);
  font-style: normal;
  font-family: var(--heading-font-family);
  font-weight: 700;
}

/* --------------------------------------------------------------------------
   Optional email capture. Shared: the speed report on Campaign 02 and the
   calculator breakdown on Campaign 03 both use it.
   -------------------------------------------------------------------------- */

.n-lead {
  display: flex; flex-direction: column; gap: 0.5em;
  margin-block-start: var(--space-xs);
  padding-block-start: var(--space-s);
  border-block-start: 1px solid var(--hairline);
}

.n-lead__label { font-size: var(--prose-s); color: var(--text-dark-muted); }

.n-lead__row {
  display: flex; gap: var(--space-xs); flex-wrap: wrap;

  & button { flex-shrink: 0; }
  @media (max-width: 30rem) { & > * { inline-size: 100%; } }
}

.n-lead__input {
  flex: 1; min-inline-size: 12rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-l);
  background: var(--white);
  padding: 0.6em var(--space-s);
  font-family: var(--text-font-family);
  /* Under 16px iOS zooms the page on focus, which throws the result off screen. */
  font-size: max(1rem, var(--prose-s));
  color: var(--text-dark);

  &::placeholder { color: color-mix(in srgb, var(--text-dark) 45%, transparent); }
  &:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 18%, transparent); }
}

.n-lead__input[aria-invalid="true"] { border-color: var(--danger-readable); }
.n-lead[data-invalid] .n-lead__note { color: var(--danger-readable); }

.n-lead__note { font-size: var(--text-xs); color: var(--text-dark-muted); line-height: 1.45; max-inline-size: 52ch; }
.n-lead__done { font-size: var(--prose-s); color: var(--text-dark); }
