/* ==========================================================================
   SolidStack Labs — styles.css
   Single stylesheet. Tokens first (PRD §4.1), then base, then components.
   Budget: ≤ 30 KB uncompressed. No preprocessor, no framework.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Fonts — self-hosted, latin subset, variable weight
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: optional;
  src: url("fonts/inter-latin-wght-normal.woff2") format("woff2-variations");
  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+2074, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 100 800;
  font-display: optional;
  src: url("fonts/jetbrains-mono-latin-wght-normal.woff2") format("woff2-variations");
  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+2074, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Metric-matched fallbacks.

   With plain `font-display: swap`, the fallback and the real font set text at
   different widths, so the swap reflows the line and costs CLS. Measured on
   the live deploy that was 0.016 on the footer's mono email link, and the
   exposure grows with every mono price and metric added later.

   These faces re-shape the local fallback to match the real font's metrics,
   so the swap is invisible and CLS stays 0. Numbers are derived, not guessed:
   size-adjust is the measured rendered-width ratio (Inter/Arial 1.0688,
   JetBrains Mono/Courier New 0.9998), and each override is the font's own
   ascent/descent over unitsPerEm divided by that ratio — Inter UPM 2048
   asc 1984 desc -494, JetBrains Mono UPM 1000 asc 1020 desc -300.

   To recompute after replacing a font file: measure the same string in the
   real font and in the local fallback at a large size, take the width ratio,
   then verify by forcing the fallback and diffing every element's geometry.
   Do NOT derive the ratio from OS/2 xAvgCharWidth — fonts disagree on how it
   is calculated, and for Inter it suggests 138%, which is wildly wrong.

   Ignored by browsers without size-adjust support, or where local() is
   blocked, which simply get the old swap behaviour.

   Paired with `font-display: optional` above, and the pairing is the point.
   Uniform size-adjust matches a font's AVERAGE width, not its per-glyph
   advances, so any line of text sitting exactly on a wrap boundary can still
   break differently in the fallback. On /services/ one checklist item did
   exactly that and pushed 187 elements down by 26.4px. `optional` removes
   the swap entirely — the browser either has the font by first paint or uses
   the fallback for that whole page load — so a reflow becomes impossible
   rather than merely unlikely. The metric matching is what makes that
   acceptable: the fallback lays out identically, only the letterforms
   differ. Preloading Inter is what keeps the real font winning that race in
   the common case.

   KNOWN LIMIT — do not try to tune this away. Swept size-adjust against every
   page and measured whole-document geometry, not just heroes. 106.88% (the
   measured ratio) leaves /, /work/, /work/solid-perfumes-website/ perfectly
   stable and up to 60px of potential reflow elsewhere; 109% fixed the worst
   page but destabilised body copy on seven. No single value can be clean,
   because uniform scaling matches average width, never per-glyph advances.
   106.88% is both the honest number and the better of the two, so it stays.

   What actually holds CLS at 0 is `font-display: optional` on all four faces
   above: whichever font wins at first paint is used for the whole load, so
   these differences never become a mid-load reflow. One 0.028 shift was
   still observed on /work/solid-perfumes-ordering-pwa/ in roughly one run in
   four — see the handover notes. If it recurs, the fix is to reserve the
   taller line count on that specific heading, NOT to re-tune size-adjust. */

@font-face {
  font-family: "Inter Fallback";
  /* Without this the face defaults to font-display:auto, so text can paint
     in system-ui first and re-lay-out when the local() face resolves — an
     intermediate swap that `optional` on the real font never covered. That
     re-layout moved the h1 by one line and cost 0.028 CLS at 412px. */
  font-display: optional;
  src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
  size-adjust: 106.88%;
  ascent-override: 90.64%;
  descent-override: 22.57%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "JetBrains Mono Fallback";
  /* Without this the face defaults to font-display:auto, so text can paint
     in system-ui first and re-lay-out when the local() face resolves — an
     intermediate swap that `optional` on the real font never covered. That
     re-layout moved the h1 by one line and cost 0.028 CLS at 412px. */
  font-display: optional;
  src: local("Courier New"), local("Liberation Mono");
  size-adjust: 99.98%;
  ascent-override: 102.02%;
  descent-override: 30.01%;
  line-gap-override: 0%;
}

/* --------------------------------------------------------------------------
   2. Tokens — the only place raw values are allowed
   -------------------------------------------------------------------------- */

:root {
  /* color */
  --ink:        #0B1220;  /* near-black — headings, primary text */
  --body:       #1F2937;  /* body text on light */
  --muted:      #64748B;  /* secondary text, labels */
  --accent:     #0E7490;  /* deep cyan — links, buttons, rules */
  --accent-ink: #155E75;  /* hover/active accent */
  --accent-soft:#ECFEFF;  /* callout tint */
  --bg:         #FFFFFF;
  --bg-alt:     #F1F5F9;  /* zebra sections */
  --rule:       #CBD5E1;  /* hairlines */
  --dark-bg:    #0B1220;  /* dark sections: hero, footer */
  --dark-body:  #CBD5E1;  /* body text on dark */
  --ok:         #15803D;  /* success/verified marks */

  /* accent on dark — the only accent variant, for eyebrows on --dark-bg */
  --accent-on-dark: #67E8F9;

  /* type */
  --font-body:  "Inter", "Inter Fallback", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono:  "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, Consolas, monospace;

  /* scale (fluid, clamp-based) */
  --text-xs:  0.79rem;
  --text-sm:  0.89rem;
  --text-md:  1rem;                                /* 16px base, 1.65 line-height */
  --text-lg:  clamp(1.13rem, 1rem + 0.5vw, 1.27rem);
  --text-xl:  clamp(1.42rem, 1.2rem + 1vw, 1.8rem);
  --text-2xl: clamp(1.8rem, 1.4rem + 2vw, 2.55rem);
  --text-hero:clamp(2.25rem, 1.6rem + 3.2vw, 3.6rem);

  /* rhythm */
  --space-1: 0.5rem;  --space-2: 1rem;  --space-3: 1.5rem;
  --space-4: 2.5rem;  --space-5: 4rem;  --space-6: 6.5rem;
  --container: 72rem;  /* max content width */
  --radius: 6px;       /* subtle — this brand is not bubbly */

  /* motion */
  --ease: cubic-bezier(0.2, 0, 0.2, 1);
  --dur:  150ms;
}

/* --------------------------------------------------------------------------
   3. Reset and base
   -------------------------------------------------------------------------- */

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--body);
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: 1.65;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  margin: 0 0 var(--space-2);
  color: var(--ink);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

h1 { font-size: var(--text-hero); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
h4 { font-size: var(--text-lg); }

p, ul, ol { margin: 0 0 var(--space-2); }
p { max-width: 68ch; text-wrap: pretty; }

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
a:hover { color: var(--accent-ink); }

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

hr {
  height: 1px;
  margin: var(--space-4) 0;
  border: 0;
  background: var(--rule);
}

::selection {
  background: var(--accent);
  color: var(--bg);
}

/* --------------------------------------------------------------------------
   4. Focus and skip-link — accessibility floor (WCAG 2.2 AA)
   -------------------------------------------------------------------------- */

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

/* --accent is only 3.49:1 on --dark-bg. That clears the 3:1 floor for a focus
   indicator, but barely — dark surfaces get the brighter cyan instead. */
.on-dark :focus-visible,
.site-header :focus-visible,
.site-footer :focus-visible {
  outline-color: var(--accent-on-dark);
}

.skip-link {
  position: absolute;
  left: var(--space-2);
  top: var(--space-2);
  z-index: 10;
  padding: var(--space-1) var(--space-2);
  background: var(--bg);
  color: var(--accent);
  font-weight: 600;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; }

/* --------------------------------------------------------------------------
   5. Layout primitives
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--space-3);
}

.section {
  padding-block: var(--space-5);
}

.section--alt  { background: var(--bg-alt); }
.section--soft { background: var(--accent-soft); }

/* Dark surfaces are `.on-dark` — and only `.on-dark`. Every child override
   below (headings, links, eyebrow, chips, tiles, secondary button) is keyed
   to it, so a second class name that merely darkened the background would
   silently leave light-surface text on a dark panel. */
.on-dark {
  background: var(--dark-bg);
  color: var(--dark-body);
}

.on-dark h1,
.on-dark h2,
.on-dark h3,
.on-dark h4 { color: var(--bg); }

.on-dark a { color: var(--accent-on-dark); }

/* --------------------------------------------------------------------------
   6. Typographic utilities
   -------------------------------------------------------------------------- */

/* Section eyebrow — mono, uppercase, letterspaced. Signature of the brand. */
.eyebrow {
  display: block;
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  line-height: 1.4;
  text-transform: uppercase;
  color: var(--accent);
}

.on-dark .eyebrow { color: var(--accent-on-dark); }

/* Every number, price, date, metric and code accent on this site is mono. */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.lede {
  font-size: var(--text-lg);
  max-width: 60ch;
}

.muted { color: var(--muted); }

.rule-under {
  padding-bottom: var(--space-2);
  border-bottom: 2px solid var(--accent);
}

/* Vertical nudge for an element following a badge or chip row. */
.stack { margin-top: var(--space-2); }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   7. Buttons — solid accent (primary) / outlined (secondary). No shadows.
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: 44px;
  padding: 0.6rem var(--space-3);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font: inherit;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}

.btn--primary {
  background: var(--accent);
  color: var(--bg);
}
.btn--primary:hover { background: var(--accent-ink); color: var(--bg); }

.btn--secondary {
  background: transparent;
  border-color: var(--ink);
  color: var(--ink);
}
.btn--secondary:hover { border-color: var(--accent); color: var(--accent); }

/* `.on-dark a` (0,2,0) out-specifies `.btn--primary`'s own colour (0,1,0), so
   a primary button on a dark section would take the cyan link colour and land
   at 3.7:1 on the accent fill. A button is not a link — restate it here.
   Deliberately not solved with `.on-dark a:not(.btn)`: that computes to
   (0,2,1) and would then beat `.on-dark .chip`, breaking chip colours. */
.on-dark .btn--primary {
  color: var(--bg);
}

.on-dark .btn--secondary {
  border-color: var(--rule);
  color: var(--bg);
}
.on-dark .btn--secondary:hover {
  border-color: var(--accent-on-dark);
  color: var(--accent-on-dark);
}

/* --------------------------------------------------------------------------
   8. Site header and navigation

   No-JS behaviour is the baseline: the nav is an ordinary wrapped list that
   is always visible. main.js then unhides the toggle button and the collapsed
   layout is enabled by the `.js` class, so a script failure degrades to a
   longer header rather than an unreachable site.
   -------------------------------------------------------------------------- */

.site-header {
  background: var(--dark-bg);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  min-height: 68px;
}

.logo {
  display: inline-flex;
  align-items: center;
  min-height: 44px;   /* tap-target floor */
  gap: 0.6rem;
  color: var(--bg);
  font-weight: 700;
  font-size: var(--text-md);
  letter-spacing: -0.01em;
  text-decoration: none;
}
.logo:hover { color: var(--accent-on-dark); }
.logo svg { flex: none; }

.site-nav__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-nav__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--dark-body);
  font-size: var(--text-sm);
  font-weight: 500;
  text-decoration: none;
  border-bottom: 2px solid transparent;
}

.site-nav__link:hover { color: var(--bg); }

/* Current page: an accent rule, not a colour swap — colour alone would fail
   WCAG 1.4.1 (use of colour). */
.site-nav__link[aria-current="page"] {
  color: var(--bg);
  border-bottom-color: var(--accent-on-dark);
}

.nav-toggle {
  display: none;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0 var(--space-2);
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: var(--radius);
  color: var(--bg);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}
.nav-toggle:hover { border-color: var(--accent-on-dark); color: var(--accent-on-dark); }
.nav-toggle svg { width: 18px; height: 18px; }

/* The two icon states are both in the DOM; aria-expanded picks one. */
.nav-toggle .nav-toggle__close,
.nav-toggle[aria-expanded="true"] .nav-toggle__open { display: none; }
.nav-toggle[aria-expanded="true"] .nav-toggle__close { display: block; }

@media (max-width: 53.99em) {
  .js .nav-toggle { display: inline-flex; }

  .js .site-nav {
    flex-basis: 100%;
    display: none;
  }
  .js .site-nav.is-open { display: block; }

  .js .site-nav__list {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding-bottom: var(--space-2);
  }

  .js .site-nav__link {
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  }
  .js .site-nav__link[aria-current="page"] {
    border-bottom-color: var(--accent-on-dark);
  }

  .js .site-nav .btn { margin-top: var(--space-2); }
}

/* --------------------------------------------------------------------------
   9. Hero
   -------------------------------------------------------------------------- */

.hero {
  padding-block: var(--space-6) var(--space-5);
}

.hero h1 { max-width: 20ch; }

.hero__sub {
  font-size: var(--text-lg);
  max-width: 58ch;
  margin-bottom: var(--space-4);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* --------------------------------------------------------------------------
   10. Components
   -------------------------------------------------------------------------- */

/* --- Section head: eyebrow + heading + the letterhead rule ---------------- */

.section-head {
  margin-bottom: var(--space-4);
}

.section-head h2 {
  display: inline-block;
  margin-bottom: 0;
  padding-bottom: var(--space-1);
  border-bottom: 2px solid var(--accent);
}

.on-dark .section-head h2 { border-bottom-color: var(--accent-on-dark); }

.section-head__sub {
  margin-top: var(--space-2);
  font-size: var(--text-lg);
}

/* --- Stat chips: mono, verifiable facts. Never decorative. ---------------- */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0.4rem 0.8rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--body);
  text-decoration: none;
}

.chip svg {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--accent);
}

.on-dark .chip {
  border-color: rgba(255, 255, 255, 0.18);
  color: var(--dark-body);
}
.on-dark .chip svg { color: var(--accent-on-dark); }

a.chip { transition: border-color var(--dur) var(--ease); }
a.chip:hover { border-color: var(--accent); color: var(--body); }
.on-dark a.chip:hover { border-color: var(--accent-on-dark); color: var(--bg); }

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

.card-grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  padding: 0;
  margin: 0;
  list-style: none;
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.card:hover { border-color: var(--accent); transform: translateY(-2px); }

/* Keyboard parity: the card lifts when the link inside it is focused. */
.card:has(a:focus-visible) { border-color: var(--accent); transform: translateY(-2px); }

.card__body { padding: var(--space-3); }
.card__body > :last-child { margin-bottom: 0; }

/* Card titles are h3 under a section heading (home) but h2 on /work/, where
   the projects are the page's top-level items and an h3 would skip a level.
   The card looks identical either way. */
.card h2,
.card h3 { font-size: var(--text-lg); margin-bottom: var(--space-1); }

.card__outcome {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  margin-bottom: var(--space-2);
}

/* Whole card is the target; the link keeps the accessible name. */
.card__link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
}
.card__link { text-decoration: none; color: inherit; }
.card__link:focus-visible { outline: none; }

/* Block, not inline: sharing a line box meant the badge's baseline depended
   on the parent's 16px strut metrics, which nudged it 1px on font swap.
   `width: fit-content` keeps it hugging its text. (The old `align-self` was
   inert anyway — .card__body is not a flex container.) */
.badge {
  display: block;
  width: fit-content;
  line-height: 1.4;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --muted is 3.9:1 on --dark-bg, and the status badge sits on the case-study
   hero. Fourth time this pattern has bitten — see .section--dark, .btn--primary
   and .step__num. Any component that can appear on both surfaces needs a light
   default plus an .on-dark override. */
.on-dark .badge {
  border-color: rgba(255, 255, 255, 0.24);
  color: var(--dark-body);
}

.badge + h1 { margin-top: var(--space-2); }

/* --- Feature tiles -------------------------------------------------------- */

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

.tile__icon {
  width: 28px;
  height: 28px;
  margin-bottom: var(--space-2);
  color: var(--accent);
}
.on-dark .tile__icon { color: var(--accent-on-dark); }

.tile h3 { font-size: var(--text-lg); }
.tile p { margin-bottom: 0; }

/* --- CTA band ------------------------------------------------------------- */

.cta {
  background: var(--accent-soft);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.cta h2 { margin-bottom: var(--space-2); }
.cta p { margin-bottom: var(--space-3); }

.cta__contact {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  max-width: none;
}

.cta__contact a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* Copy button is injected by main.js — it never exists without a working
   clipboard API, so the mono email beside it is always the real fallback. */
.copy-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 44px;
  padding: 0 var(--space-2);
  background: transparent;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}
.copy-btn:hover { border-color: var(--accent); color: var(--accent); }
.copy-btn svg { width: 16px; height: 16px; }

/* --------------------------------------------------------------------------
   11. Site footer
   -------------------------------------------------------------------------- */

.site-footer {
  background: var(--dark-bg);
  color: var(--dark-body);
  padding-block: var(--space-4);
  font-size: var(--text-sm);
}

.site-footer a { color: var(--accent-on-dark); }
.site-footer a:hover { color: var(--bg); }

/* Note: --muted on --dark-bg is only 3.9:1 — never use it on dark surfaces.
   All footer text stays on --dark-body (12.6:1). */
.site-footer__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  align-items: center;
  margin: 0;
  max-width: none;
  color: var(--dark-body);
}

/* Standalone links in the footer row are targets, not prose — hold them to
   the 44px floor rather than the 24px WCAG 2.5.8 inline-link exception. */
/* .site-footer__built is included deliberately: the PageSpeed link is the one
   the copy invites the reader to tap ("Check it yourself"), so it gets the
   same floor as every other footer link rather than relying on the WCAG
   2.5.8 exemption for links inside a sentence. */
.site-footer__meta a,
.site-footer__nav a,
.site-footer__contact a,
.site-footer__built a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.site-footer__cols {
  display: grid;
  gap: var(--space-3) var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-3);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.site-footer h2 {
  margin-bottom: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-on-dark);
}

.site-footer__nav ul,
.site-footer__contact ul {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-footer__built {
  margin: var(--space-2) 0 0;
  max-width: none;
  color: var(--dark-body);
}

/* --------------------------------------------------------------------------
   12. Home page
   -------------------------------------------------------------------------- */

.hero__grid {
  display: grid;
  gap: var(--space-4);
  align-items: center;
}

@media (min-width: 54em) {
  .hero__grid { grid-template-columns: 1.15fr 1fr; }
}

/* Decorative. width/height on the <svg> plus aspect-ratio here means the box
   is reserved before paint, so it can never contribute to CLS. */
.hero__art {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 11;
}

.hero__art .art-grid   { stroke: var(--accent-on-dark); opacity: 0.16; }
.hero__art .art-frame  { stroke: var(--accent-on-dark); opacity: 0.55; }
.hero__art .art-bar    { fill: var(--accent-on-dark); opacity: 0.75; }
.hero__art .art-bar--dim { fill: var(--dark-body); opacity: 0.3; }
.hero__art .art-cursor { fill: var(--accent-on-dark); }

/* --- Proof bar ----------------------------------------------------------- */

.proof {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.proof li {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

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

/* aspect-ratio reserves the screenshot box up front. Real AVIF/WebP art
   drops in at M4 with explicit width/height and the geometry will not move. */
.media {
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 10;
  padding: var(--space-2);
  background: var(--bg-alt);
  border-bottom: 1px solid var(--rule);
  border-radius: var(--radius) var(--radius) 0 0;
  text-align: center;
}

/* --muted would be 4.34:1 here — it clears 4.5 on white but not on --bg-alt,
   which is what the media well is filled with. */
/* Card thumbnails DO keep one ratio so the grid stays tidy. Cover-cropped from
   the top, which is the identifiable part of a tall phone screenshot. The
   full, uncropped image is on the case study itself. */
.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  border-radius: var(--radius) var(--radius) 0 0;
}

.media__note {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--body);
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0 0 var(--space-2);
  padding: 0;
  list-style: none;
}

.tag {
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
}

.section__more {
  margin: var(--space-4) 0 0;
  font-weight: 600;
}

/* A standalone section CTA, not a link inside prose — hold it to 44px. */
.section__more a,
.link-cta a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* --- Process strip ------------------------------------------------------- */

.steps {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 10.5rem), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Default to the LIGHT-surface colours and let .on-dark override, the same
   way .eyebrow works. Written dark-first, this read 1.45:1 on /process/,
   where the stages sit on white. */
.step__num {
  display: block;
  margin-bottom: var(--space-1);
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: 0.14em;
  color: var(--accent);
}

.on-dark .step__num {
  border-bottom-color: rgba(255, 255, 255, 0.16);
  color: var(--accent-on-dark);
}

.step h3 {
  margin-bottom: var(--space-1);
  font-size: var(--text-md);
}

.step p {
  margin-bottom: 0;
  font-size: var(--text-sm);
}

/* --- Services teaser ----------------------------------------------------- */

.offers {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--rule);
}

/* `align-items: baseline` aligns across three different font sizes, and that
   cross-item baseline resolution rounds differently on font swap — worth 1px
   of movement on the description. Centre alignment is metric-independent. */
.offer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  min-height: 44px;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--rule);
  color: inherit;
  text-decoration: none;
}

.offer:hover .offer__name { color: var(--accent); }

.offer__name {
  flex: 1 1 12rem;
  font-weight: 600;
  font-size: var(--text-lg);
  color: var(--ink);
}

.offer__desc {
  flex: 2 1 20rem;
  margin: 0;
  color: var(--body);
}

.offer__price {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* --------------------------------------------------------------------------
   13. Inner pages — services, process, about, contact
   -------------------------------------------------------------------------- */

/* --- Service blocks ------------------------------------------------------ */

.offering {
  padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
}
.offering + .offering { margin-top: var(--space-4); }

.offering__price {
  margin: var(--space-3) 0;
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* Delivery estimate under each price. Must stay in step with the "how long
   does it take?" FAQ answer — if one changes, change both. */
.offering__delivery {
  margin: calc(var(--space-3) * -1 + var(--space-1)) 0 var(--space-3);
  font-size: var(--text-sm);
  color: var(--muted);
}

/* Sits on the dark hero, so --muted (3.9:1 there) is not available. */
.stamp {
  margin-top: calc(var(--space-1) * -1);
  font-size: var(--text-sm);
  color: var(--dark-body);
}

.field__optional {
  font-weight: 400;
  color: var(--muted);
}

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

.checklist li {
  display: flex;
  gap: var(--space-1);
  align-items: flex-start;
}

/* The tick is decorative — the list already reads as "typically includes". */
.checklist svg {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: 0.28em;
  color: var(--ok);
}

/* --- Care plans table ---------------------------------------------------- */

/* The table scrolls inside its own box; the page never scrolls sideways. */
.table-scroll {
  overflow-x: auto;
  margin-bottom: var(--space-3);
}

.plans {
  width: 100%;
  min-width: 34rem;
  border-collapse: collapse;
  text-align: left;
}

.plans th,
.plans td {
  padding: var(--space-2);
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}

.plans thead th {
  border-bottom-width: 2px;
  border-bottom-color: var(--accent);
  color: var(--ink);
  font-size: var(--text-md);
}

.plans tbody th {
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

.plans .plans__price {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* --- FAQ — native details/summary, works with JS off --------------------- */

.faq {
  border-top: 1px solid var(--rule);
}

.faq details {
  border-bottom: 1px solid var(--rule);
}

.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-2) 0;
  color: var(--ink);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }

.faq summary::after {
  content: "+";
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  color: var(--accent);
}
.faq details[open] summary::after { content: "\2212"; }

.faq details > :not(summary) { margin-bottom: var(--space-2); }

/* --- Callout ------------------------------------------------------------- */

.callout {
  padding: var(--space-3);
  background: var(--accent-soft);
  border-left: 2px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.callout > :last-child { margin-bottom: 0; }

/* --- Process detail ------------------------------------------------------ */

.stage {
  padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
}
.stage + .stage { margin-top: var(--space-4); }

.stage__pair {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  margin-top: var(--space-3);
}

.stage__pair h3 {
  margin-bottom: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.stage__pair ul { margin-bottom: 0; padding-left: 1.1rem; }

/* --- About --------------------------------------------------------------- */

.about-grid {
  display: grid;
  gap: var(--space-4);
  align-items: start;
}
@media (min-width: 54em) {
  .about-grid { grid-template-columns: 1fr 18rem; }
}

/* aspect-ratio reserves the portrait box before the image exists, so adding
   the real headshot later cannot shift the page. */
.portrait {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--bg-alt);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  text-align: center;
}

/* The source is square and cropped to 4:5 at build time, so cover crops
   nothing — the box was reserved at this ratio before the image existed. */
.portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.facts {
  margin: var(--space-3) 0 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}

.facts div {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--rule);
}

.facts dt {
  flex: 0 0 8.5rem;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  line-height: 1.9;
}

.facts dd {
  flex: 1 1 12rem;
  margin: 0;
}

/* --- Contact ------------------------------------------------------------- */

.contact-grid {
  display: grid;
  gap: var(--space-5);
  align-items: start;
}
@media (min-width: 54em) {
  .contact-grid { grid-template-columns: 1.4fr 1fr; }
}

.field { margin-bottom: var(--space-3); }

.field label {
  display: block;
  margin-bottom: var(--space-1);
  color: var(--ink);
  font-weight: 600;
}

.field .hint {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--text-sm);
  color: var(--body);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: 44px;
  padding: 0.6rem var(--space-2);
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--body);
  font: inherit;
}

.field textarea {
  min-height: 8rem;
  resize: vertical;
}

.field input:hover,
.field select:hover,
.field textarea:hover { border-color: var(--accent); }

.req {
  font-family: var(--font-mono);
  color: var(--accent);
}

.contact-methods {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--rule);
}

.contact-methods li {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--rule);
}

.contact-methods a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* --- Work listing filters -----------------------------------------------
   Ships `hidden` in the markup and is unhidden by main.js, so with JS off
   there is no dead UI and every project stays visible. Same pattern as the
   nav toggle. */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  align-items: center;
  margin-bottom: var(--space-4);
}

.filters__label {
  margin: 0 var(--space-1) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.filter {
  min-height: 44px;
  padding: 0 var(--space-2);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--body);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.filter:hover { border-color: var(--accent); color: var(--accent); }

/* Pressed state carries a border AND a weight change — not colour alone. */
.filter[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

/* --- Case study ---------------------------------------------------------- */

.case__outcome {
  margin-bottom: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  color: var(--accent-on-dark);
}

.meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  padding: var(--space-2) 0 0;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  list-style: none;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.meta dt {
  display: inline;
  color: var(--accent-on-dark);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-size: var(--text-xs);
}
.meta dd { display: inline; margin: 0 0 0 0.4rem; }
.meta div { display: flex; align-items: center; }

/* The live-project link is a standalone target in this row, not a link inside
   a sentence, so it takes the 44px floor rather than the 24px inline
   exception. Centre-aligning the row keeps dt/dd level against it. */
.meta a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* Screenshots: the box is reserved at 16:10 now, so dropping the real AVIF
   in later cannot move anything below it. */
.shots {
  display: grid;
  gap: var(--space-3);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

/* Screenshots vary from 1400x733 desktop frames to 540x1128 phone frames, so
   the box is NOT forced to one ratio. Each <img> carries width/height
   attributes and the browser reserves the correct box from them before the
   file arrives — that is what keeps CLS at 0, not a hard-coded aspect-ratio. */
.shot {
  overflow: hidden;
  background: var(--bg-alt);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

/* <figure> carries a default 16px/40px UA margin. Left unreset it squeezed
   every screenshot to 282px inside a 364px slot on mobile, and Lighthouse
   read the result as "media element lacking an explicit size". */
.shot figure {
  margin: 0;
}

.shot img {
  display: block;
  width: 100%;
  height: auto;
}

.shot figcaption {
  padding: var(--space-2);
  border-top: 1px solid var(--rule);
  font-size: var(--text-sm);
  color: var(--body);
}

/* Placeholder variant keeps a reserved box while an image is still missing. */
.shot--placeholder {
  aspect-ratio: 16 / 10;
  display: grid;
  place-items: center;
  padding: var(--space-2);
  text-align: center;
}

.case-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  justify-content: space-between;
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}

.case-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-weight: 600;
}

/* --- 404 ----------------------------------------------------------------- */

.notfound {
  display: grid;
  align-content: center;
  min-height: 60vh;
}

.notfound__code {
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  letter-spacing: 0.14em;
  color: var(--accent-on-dark);
}

/* --------------------------------------------------------------------------
   14. Motion — one reveal, killed entirely under reduced-motion
   -------------------------------------------------------------------------- */

/* Reveal is opt-in from JS: `.js` is set on <html> by main.js before paint.
   With JS off (or broken) the rule never applies and content is simply there. */
.js .reveal {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 400ms var(--ease), transform 400ms var(--ease);
}

.js .reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  /* Must out-specify `.js .reveal` (0,2,0) — a media query adds no
     specificity of its own, so a bare `.reveal` here would lose. */
  .js .reveal,
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
