/* ===== SELF-HOSTED FONTS — generated by scripts/build-fonts.mjs — DO NOT EDIT BY HAND ===== */
/* vietnamese */
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/fraunces-italic-vietnamese.woff2?v=1') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/fraunces-italic-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/fraunces-italic-latin.woff2?v=1') 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;
}
/* vietnamese */
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/fraunces-italic-vietnamese.woff2?v=1') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/fraunces-italic-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/fraunces-italic-latin.woff2?v=1') 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;
}
/* vietnamese */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/fraunces-normal-vietnamese.woff2?v=1') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin.woff2?v=1') 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;
}
/* vietnamese */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/fraunces-normal-vietnamese.woff2?v=1') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin.woff2?v=1') 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;
}
/* vietnamese */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/fraunces-normal-vietnamese.woff2?v=1') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin.woff2?v=1') 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;
}
/* vietnamese */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/fraunces-normal-vietnamese.woff2?v=1') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin.woff2?v=1') 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;
}
/* greek */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/syne-normal-greek.woff2?v=1') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* latin-ext */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/syne-normal-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/syne-normal-latin.woff2?v=1') 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;
}
/* greek */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/syne-normal-greek.woff2?v=1') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* latin-ext */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/syne-normal-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/syne-normal-latin.woff2?v=1') 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;
}
/* greek */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/syne-normal-greek.woff2?v=1') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* latin-ext */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/syne-normal-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/syne-normal-latin.woff2?v=1') 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;
}
/* greek */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/syne-normal-greek.woff2?v=1') format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* latin-ext */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/syne-normal-latin-ext.woff2?v=1') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Syne';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/syne-normal-latin.woff2?v=1') 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;
}
/* ===== END SELF-HOSTED FONTS ===== */

:root {
  /* The palette is warm paper + warm ink and there is no dark variant — say so,
     so a phone or OS in dark mode doesn't auto-invert form controls, scrollbars
     and the like against a page that stays light. Pairs with the
     `<meta name="theme-color" content="#F7F5F0">` on all 35 pages, which tints
     the mobile browser chrome to the paper instead of framing it in white. */
  color-scheme: light;

  --paper: #F7F5F0;
  --paper-alt: #EEECE6;
  --paper-card: #FAF9F6;
  --ink: #1C1C1A;
  /* THREE TIERS OF INK, AND ALL THREE PASS WCAG AA (Aug 2026).
     --ink-tertiary used to be #9B9A96, which is 2.38–2.68:1 as text on the three
     paper tones — below the 4.5:1 minimum in every one of its 3,260 uses (blog
     figure legends .fig-key / .fig-axis, .spec-list dt, and ~84 small caps
     labels). It was a real legibility defect, not a lint opinion: at the sizes
     it is used, small tracked caps, it is the hardest text on the site to read.

     THE ARITHMETIC IS WHY BOTH TOKENS MOVED, not just one. Against the darkest
     paper (--paper-alt #EEECE6) 4.5:1 admits nothing lighter than about
     #6B6B6B — which is where --ink-secondary already sat. So a passing tertiary
     necessarily lands on the old secondary, and the only way to keep three
     distinct tiers rather than two is to step secondary down with it. The new
     ladder is roughly even in perceptual lightness (L* 10 · 31 · 45, against the
     old 10 · 45 · 63) and keeps the warm neutral hue of the originals (R>G>B by
     the same +1/-4).

     Measured on --paper-alt, the binding background: secondary 7.70:1,
     tertiary 4.62:1. On --paper: 8.09:1 and 4.86:1. Dark surfaces are unaffected
     — they have their own --dark-text-secondary. */
  --ink-secondary: #4A4945;
  --ink-tertiary: #6B6A66;
  --border: #E2E0DA;
  --border-strong: #C5C3BC;
  --dark-bg: #1A1A18;
  --dark-surface: #222220;
  --dark-text: #E8E6E1;
  --dark-text-secondary: #A3A19C;
  --dark-border: #333330;

  /* THE HOUSE MAROON. The red of the sheet of paper the company is named after
     (about.html: "It begins with a sheet of red paper"), and the backdrop of the
     founder portrait on the home page. Same token, same value, as the Instagram
     system's --eagle-red — do not re-sample it from a photograph, that mistake
     was already made once and produced #6F141E off a lit hotspot.

     IT IS NOT AN ACCENT COLOUR AND THIS SITE STILL HAS TWO. It is reachable from
     exactly one place, `.social-mark-tile` on socials.html, where the platform
     marks are drawn in it on the owner's instruction. Everything else on every
     page is warm paper and warm ink. 11.9:1 on --paper-card. */
  --eagle-red: #67111B;

  /* Widths: --section-max-width drives .container, --wide-width drives
     .measure-wide, and .measure is a plain 65ch. Don't add a fourth width
     token — the two that used to sit here (--max-width, --content-width) were
     never referenced and just gave the container two competing names. */
  /* Widths are in rem, not px, so the big-screen root scaling below moves them
     with the type. In px these froze the layout at a fixed pixel size while the
     text around them grew, which is the one way that scaling goes wrong.
     80rem = 1280px · 60rem = 960px at the default 16px root — unchanged for
     every normal screen. */
  --wide-width: 60rem;
  --gap: 2rem;
  --gap-lg: 4rem;
  --gap-xl: 8rem;

  /* THE CARD PADDING TOKEN. A card padded with a flat --gap-lg spends 128px of a
     320px phone on its own inset — the reading column inside a .blog-featured-card
     came out at 144px, which forced the card wider than the grid track and gave
     the blog index a sideways scroll. This is the same 4rem on a laptop and 1.5rem
     on the smallest phone. Use it for any card that was padded --gap-lg;
     .card keeps the smaller flat --gap and needs no token. */
  --card-padding-lg: clamp(1.5rem, 5.5vw, 4rem);
  /* Measured, not guessed: the fixed bar renders 93px on a phone, 94px through
     the 768–1023px tablet band and 103px from 1024px up. This token used to say
     4.5rem (72px) — 31px short of the truth — which is why .page-hero-booking's
     `nav-height + 2.5rem` left the contact page's h1 sitting under the bar.
     Anything that has to clear the nav derives from this; keep it honest. */
  --nav-height: 6.5rem;   /* 104px — clears the tallest state of the fixed nav */
  --transition-fast: 150ms ease;
  --transition-base: 300ms ease;
  --transition-slow: 600ms ease;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

  /* Section rhythm — ONE consistent vertical padding for every section.
     Height is driven by content, never forced to a viewport height. This is
     what makes every section the "same paper size" down the page. Do NOT add
     per-section min-heights or viewport-height (vh) sizing to content sections. */
  --section-padding-y: clamp(3.5rem, 7vw, 6rem);   /* every section, top & bottom */
  --section-padding-y-compact: clamp(2.25rem, 4vw, 3.5rem); /* .section-compact only */
  /* The horizontal gutter is a clamp for the same reason the vertical one is, but
     it guards a different failure. A flat 1.5rem triples for a reader whose
     browser font is 32px, so on a 320px phone 96px — nearly a third of the
     screen — went to empty margin and the content had nowhere left to go.

     The vw term is tuned so the DEFAULT is unchanged everywhere: at 16px root
     7.5vw is exactly 24px at 320px and is capped at 1.5rem above that, which is
     what a flat 1.5rem already produced at every width. It only ever bites when
     the reader's own font size would otherwise inflate the gutter past what a
     narrow screen can spare. The max stays in rem so a 4K TV still scales it. */
  --section-padding-x: clamp(0.75rem, 7.5vw, 1.5rem);
  --section-max-width: 80rem;   /* 1280px at the default root — see --wide-width */
  /* No --hero-min-height token: heroes are one full screen via the .hero,
     .page-hero rule (Rule 2), not via a clamp. */

  /* Vertical rhythm scale. --space-xl is in use; the rest of the rungs are kept
     deliberately so the scale is complete.

     THE MARKUP-FACING SPACING LADDER (the only values a structural inline margin
     may use — Rule 4's exception is "a margin using a TOKEN", never a raw rem):
       --space-xs (0.5rem) · --space-sm (1rem) · --element-gap (1.5rem)
       · --gap (2rem) · --gap-lg (4rem)
     The 67 raw-rem inline margins were moved onto these in Aug 2026. If a value
     you want isn't on the ladder, snap to the nearest rung — don't invent one. */
  --space-xs: 0.5rem;
  --space-sm: 1rem;
  --space-md: 2rem;      /* --gap */
  --space-lg: 4rem;      /* --gap-lg */
  --space-xl: 6rem;
  --space-2xl: 8rem;     /* --gap-xl */
  --space-3xl: 12rem;
  
  /* Professional Typography Scale - Fixed sizes.
     --text-xs is 12px, NOT the 11.2px it used to be. 11.2px is under the legible
     floor on a phone, and it is the size of every uppercase label on the site
     (.meta, .label, .service-number, .social-handle, .tag, .read-more) — ~200
     rendered instances measured at 11.2px on a 320–768px screen. Do not take it
     back below 0.75rem; if a label looks too big, cut the words, not the size. */
  --text-xs: 0.75rem;     /* 12px - labels, meta (accessibility floor) */
  --text-sm: 0.8rem;      /* 12.8px - small body, captions */
  --text-base: 1.0625rem; /* 17px - body text (enlarged for full-screen sections) */
  --text-lg: 1.1875rem;   /* 19px - large body */
  --text-xl: 1.375rem;    /* 22px - lead paragraphs */
  --text-2xl: 1.5rem;     /* 24px - small headings */
  --text-3xl: 1.875rem;   /* 30px - h3 */
  --text-4xl: 2.25rem;    /* 36px - h2 */
  --text-5xl: 3.5rem;     /* 56px - the h2 / blog-h1 ceiling (the clamps' maximum) */
  /* The top two rungs are kept so the scale is complete, but NOTHING should set a
     heading to them directly. Display headings are clamps whose maxima happen to
     equal these values — h1 tops out at 6rem, h2 at --text-5xl. `.hero-title` used
     to set --text-7xl flat in the ≥900px block and that switched the h1 clamp off;
     see the note in that block. Raise a clamp's maximum, never assign these. */
  --text-6xl: 5rem;       /* 80px - unassigned rung */
  --text-7xl: 6rem;       /* 96px - the h1 clamp's maximum; do not assign directly */
  
  /* Line heights - fixed for professional rhythm */
  --leading-tight: 1.05;
  --leading-snug: 1.15;
  --leading-normal: 1.5;
  --leading-relaxed: 1.7;
  --leading-loose: 1.8;
  
  /* Letter spacing scale */
  --tracking-tight: -0.03em;
  --tracking-normal: 0;
  --tracking-wide: 0.02em;
  --tracking-wider: 0.06em;
  --tracking-widest: 0.12em;
  
  /* Section rhythm (--gap / --gap-lg own the between-block spacing) */
  --element-gap: 1.5rem;
  --element-gap-lg: 2.5rem;
}

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

html {
  /* 100%, NOT a hard 16px. The whole type scale is in rem, so a pixel value here
     pins it to 16px and silently discards the reader's own browser font-size —
     the setting people with low vision actually use (WCAG 1.4.4, Resize Text).
     100% resolves to 16px for everyone on the default, so nothing changes for
     the vast majority; it just stops overriding the ones who changed it. */
  font-size: 100%;
  /* Stop iOS Safari and Android Chrome inflating the type on their own. Both
     run "text autosizing": they pick the blocks they think are the main content
     and multiply the font size, most visibly when a phone is turned landscape.
     The whole scale here is hand-tuned clamp() values, so a browser scaling
     some of them and not others pulls the page out of proportion — and no
     desktop-Chrome render can see it happen, because desktop never does it.
     100%, NOT none: `none` also blocks the reader's own pinch/zoom, which
     trades a layout bug for a WCAG 1.4.4 failure. 100% disables only the
     automatic inflation. Inert on every desktop browser. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Keep the layout stable when a page is short enough not to scroll — without
     this the gutter appears and disappears between pages on a desktop browser
     and every centred block shifts ~8px sideways as you navigate. */
  scrollbar-gutter: stable;
}

/* ------------------------------------------------------------------ *
   VERY LARGE SCREENS — 1800px and up (big monitors, TVs, conference displays).

   Above ~1800px every clamp has hit its maximum and the container has hit its
   80rem cap, so the page stops growing while the screen keeps going: on a 4K TV
   the whole site is a small panel of 17px text in the middle of the wall, read
   from three metres away instead of half a metre.

   Scaling the ROOT rather than adding another set of font sizes is what makes
   this proportional — the type scale, the spacing ladder, the container, the
   reading column and the clamps' own maxima are all in rem, so every one of them
   grows by the same factor and the design is identical, just larger. Nothing here
   touches a screen below 1800px.

   The percentages multiply the reader's own default (see `html` below), so
   someone browsing at 20px still gets their 20px, scaled.
 * ------------------------------------------------------------------ */
@media (min-width: 1800px) { html { font-size: 112.5%; } }   /* ~18px */
@media (min-width: 2400px) { html { font-size: 131.25%; } }  /* ~21px */
@media (min-width: 3200px) { html { font-size: 150%; } }     /* ~24px */

/* ------------------------------------------------------------------
   THE LAYOUT BREAKPOINTS ARE IN em, AND THESE THREE ARE THE EXCEPTION.

   Every breakpoint below is written in em — 48em, not 768px. In a media query
   `em` resolves against the READER'S default font size, not against `html`, so:

     default 16px  →  48em = 768px   (identical to the old px value: nothing
                                      changes for anyone on the default)
     default 24px  →  48em = 1152px  (the desktop nav waits until its bigger
                                      text actually fits)

   A px breakpoint asks "how wide is the screen". An em breakpoint asks "how much
   text fits", which is the question every one of these rules is really asking.
   Measured at a 24px default: px breakpoints put the full desktop nav into a
   768px tablet and pushed "Contact" 18px off the right edge on all 35 pages.

   The three rules ABOVE stay in px on purpose — they are about the physical
   screen and how far away it is, not about the reader's text size. A 4K TV is
   watched from a sofa whether or not its owner has raised their font size.

   `max-height: 560px` (the landscape-phone panel) stays px for the same reason.

   THE TRAP: `main.js` matches `(min-width: 48em)` to close the mobile panel. If
   you change 48em here, change it there too, or the panel will close while the
   hamburger is still the only nav on screen.
 * ------------------------------------------------------------------ */

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

body {
  font-family: 'Syne', sans-serif;
  font-weight: 400;
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  color: var(--ink);
  background: var(--paper);
  letter-spacing: var(--tracking-normal);
  overflow-x: hidden;
}

/* Every content section — same top/bottom rhythm. */
main > section,
main > .marquee,
main > article > section {
  position: relative;
  padding: var(--section-padding-y) 0;
  box-sizing: border-box;
  width: 100%;
  max-width: 100%;
}

/* ------------------------------------------------------------------ *
   FULL-SCREEN SECTION SYSTEM (brand direction)
   One big headline per screen. Every narrative section fills at least one
   viewport and vertically centres its content, so each headline gets its own
   clean screen instead of the next section peeking in at the bottom. Height
   still GROWS with content (min-height, never a fixed height), so a long
   section like the FAQ simply runs taller than one screen — it is never clipped.
     • Heroes are one screen tall via the .hero,.page-hero rule below.
     • .cta-section stays auto-height so the closing CTA + footer share the
       final screen (the CTA is never half cut off).
     • .section-flush opts a section out — used for listing grids and utility
       rows (filters, pagination) where a forced empty screen would look broken.
 * ------------------------------------------------------------------ */
main > section:not(.hero):not(.page-hero):not(.cta-section):not(.section-flush) {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
main > section.section-flush { min-height: 0; }

/* Listing pages (blog, knowledge) keep a full-screen hero, but their content
   sections flow normally — a grid of post cards or a filter/pagination row must
   not each be stretched to a full empty screen. */
body.listing-page main > section:not(.hero):not(.page-hero):not(.cta-section) {
  min-height: 0;
  display: block;
}

/* Legacy height classes stay neutralised (no-ops on old markup). */
.section-hero,
.section-narrative,
.section-grid,
.section-content {
  min-height: 0;
}

/* .section-compact — the ONE approved way to make a section tighter. */
.section-compact {
  padding-top: var(--section-padding-y-compact);
  padding-bottom: var(--section-padding-y-compact);
}

/* Hero — the first full screen of every page. One headline, vertically
   centred, clearing the fixed nav with top padding. Grows taller if content
   needs it, but never shorter than one viewport. */
.hero,
.page-hero {
  display: flex;
  align-items: center;
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  padding-top: calc(var(--section-padding-y) + var(--nav-height));
  padding-bottom: var(--section-padding-y);
}

/* (The marquee band is styled in ONE place — search "Marquee — Compact section"
   further down. A second .marquee rule used to sit here setting a 1.25rem
   padding that the later rule silently overrode.) */

/* Blog post articles - normal flow. Deliberately a DIRECT-child selector: every
   post is <main><article>, and a descendant selector used to leak this 860px
   reading column + 8rem padding onto any nested <article> (e.g. the social cards
   on socials.html). Keep the ">". */
/* THIS RULE OWNS THE BLOG READING COLUMN for all 26 posts — measured Aug 2026:
   860px minus the article's own padding = an 812px text column, ~81ch, ~97 characters
   a line. A post must never carry `.measure` or its own max-width; one rule here is
   why the posts are consistent. `main > article p { max-width: none }` below is what
   cancels the global `p { max-width: 65ch }` and is deliberate — do not "restore" it,
   it would narrow every post. (`.lead` keeps its own 65ch, so a post's opening
   paragraph is intentionally narrower than its body.) See site/CLAUDE.md → Blog Post
   Standards, which used to describe a `.measure` system that never existed. */
main > article {
  display: block;
  width: 100%;
  max-width: 53.75rem;   /* 860px at the default root; scales with it on a TV */
  margin: 0 auto;
  padding: 8rem var(--section-padding-x) 4rem;
  box-sizing: border-box;
}

/* Blog post headings — fluid, same rule as the marketing pages.
   These were flat 56 / 30 / 24px. A 56px h1 needs ~350px of line box for a single
   long word, and the reading column on a 320px phone is 272px, so titles like
   "Process Documentation: A Practical Guide" printed "Documentatio" and ran the
   rest off-screen. The clamp maxima are the original desktop sizes — a laptop and
   above renders exactly as before. */
main > article h1 {
  font-size: clamp(2.125rem, 8.5vw, var(--text-5xl));
  font-weight: 300;
  line-height: 1.1;
  margin-bottom: var(--element-gap);
  max-width: none;
}

main > article h2 {
  font-size: clamp(1.5rem, 5.2vw, var(--text-3xl));
  font-weight: 400;
  line-height: var(--leading-snug);
  margin-top: var(--element-gap-lg);
  margin-bottom: var(--element-gap);
  max-width: none;
}

main > article h3 {
  font-size: clamp(1.25rem, 4.2vw, var(--text-2xl));
  font-weight: 400;
  line-height: var(--leading-snug);
  margin-top: var(--element-gap-lg);
  margin-bottom: var(--element-gap);
  max-width: none;
}

main > article p {
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  margin-bottom: var(--element-gap);
  max-width: none;
}

main > article .lead {
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
}

main > article .meta {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin-bottom: var(--element-gap-lg);
}

main > article .label {
  font-size: var(--text-xs);
  margin-bottom: var(--element-gap);
}

main > article ol,
main > article ul {
  margin-bottom: var(--element-gap);
  padding-left: 1.5rem;
}

main > article li {
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  margin-bottom: 0.5rem;
}

main > article table {
  width: 100%;
  border-collapse: collapse;
  margin: var(--element-gap-lg) 0;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-sm);
}

main > article th,
main > article td {
  padding: 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
}

main > article th {
  font-weight: 600;
  border-bottom: 2px solid var(--ink);
}

main > article blockquote {
  border-left: 3px solid var(--ink);
  padding-left: var(--gap);
  margin: var(--element-gap-lg) 0;
  font-style: italic;
  color: var(--ink-secondary);
}

/* Blog post code blocks */
main > article pre {
  background: var(--dark-bg);
  color: var(--dark-text);
  padding: var(--gap);
  overflow-x: auto;
  margin: var(--element-gap-lg) 0;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

main > article code {
  font-family: 'Syne', sans-serif;
  font-size: var(--text-sm);
  background: var(--paper-alt);
  padding: 0.15rem 0.4rem;
  border-radius: 2px;
}

main > article pre code {
  background: none;
  padding: 0;
}

/* Blog post table of contents */
main > article .toc {
  background: var(--paper-alt);
  border: 1px solid var(--border);
  padding: var(--gap);
  margin: var(--element-gap-lg) 0;
}

main > article .toc .label {
  margin-bottom: var(--element-gap);
}

/* Blog post body links — visible as links without per-link inline styles.
   Scoped to prose + TOC so it never touches .btn or .meta. */
main > article p a,
main > article li a,
main > article .toc a {
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 3px;
  transition: text-decoration-color var(--transition-fast), opacity var(--transition-fast);
}
main > article p a:hover,
main > article li a:hover,
main > article .toc a:hover {
  text-decoration-color: var(--ink);
}

/* Ensure content fits within section - for regular pages only */
main > section > .container,
main > .hero > .container,
main > .page-hero > .container,
main > .marquee > .container {
  width: 100%;
  max-width: var(--section-max-width);
  margin: 0 auto;
  padding: 0 var(--section-padding-x);
  box-sizing: border-box;
}

/* Typography — fluid scale.
   EVERY display heading is a clamp, not a fixed size. h1 always was; h2 and h3
   were `--text-5xl` (56px) and `--text-3xl` (30px) flat, which is why a heading
   like "Content OS or Business OS — which do you need?" ran off the right edge of
   a 320–390px phone. The clamp's minimum is the phone size, the vw term does the
   scaling, and the maximum is the original desktop size — so nothing about the
   desktop rendering changes. Read the three numbers as: never smaller than · grows
   with the screen · never larger than. */
h1, h2, h3, h4, h5, h6 {
  font-family: 'Fraunces', serif;
  font-weight: 400;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
  /* A long unbreakable token ("keep/update/merge/cut", a URL, a hyphen-free
     compound) is the other way a heading overflows. Break it rather than let it
     punch through the edge of the screen. */
  overflow-wrap: break-word;
}

h1 {
  font-size: clamp(2.75rem, 7vw, 6rem); /* fluid: never overflows a phone, never dwarfs a desktop */
  font-weight: 300;
  line-height: 1.02;
  letter-spacing: -0.04em;
}

h2 {
  font-size: clamp(2rem, 6.2vw, var(--text-5xl));
  font-weight: 350;
  line-height: var(--leading-tight);
}

h3 {
  font-size: clamp(1.5rem, 3.6vw, var(--text-3xl));
  font-weight: 400;
  line-height: var(--leading-snug);
}

h4 {
  font-size: var(--text-sm);
  font-weight: 500;
  font-family: 'Syne', sans-serif;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  line-height: var(--leading-normal);
}

h5 {
  font-size: var(--text-base);
  font-weight: 500;
  font-family: 'Syne', sans-serif;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  line-height: var(--leading-normal);
}

h6 {
  font-size: var(--text-sm);
  font-weight: 500;
  font-family: 'Syne', sans-serif;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  line-height: var(--leading-normal);
}

/* Body text — fixed professional sizes */
p {
  margin-bottom: var(--element-gap);
  max-width: 65ch;
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  letter-spacing: var(--tracking-normal);
}

/* Prose can carry a token no phone column can fit — a long URL, an em-dash
   compound, a slash list. Break it instead of letting it widen the page.

   `summary` and `.social-handle` are here because neither is prose and both were
   missed: a <summary> is the FAQ question itself, and `.social-handle` is a bare
   handle ("in/shreyasdave1a19") with no space in it at all — the single hardest
   kind of token to fit. Both held at the default and broke the page open at a
   32px reader font. If you add a text element that is not a <p>, ask whether its
   longest unbreakable token fits a 320px column before assuming it does. */
p, li, dd, dt, blockquote, figcaption, td, th, .lead, .social-handle {
  overflow-wrap: break-word;
}

/* `summary` needs `anywhere`, not `break-word`, and the difference is the whole
   reason the FAQ was the last page still overflowing at a 32px reader font.

   A summary is `display: flex` (the question on the left, the marker on the
   right), so its text lives in an ANONYMOUS flex item — nothing you can put
   `min-width: 0` on. A flex item's automatic minimum is min-content, and
   `break-word` does not reduce min-content; it only breaks once a line would
   already overflow. `anywhere` is the one value that does both, so the box can
   finally be narrower than its longest word. Identical rendering at the default:
   like break-word, it only ever breaks a word that genuinely does not fit. */
summary {
  overflow-wrap: anywhere;
}

/* The panel's links are `display: block`, so break-word is enough here — but they
   are set large (a deliberate, thumb-sized target), and at a 32px reader font one
   word like "Services" wants 247px inside a 224px panel. */
.mobile-nav a {
  overflow-wrap: break-word;
}

.lead {
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
  color: var(--ink-secondary);
  max-width: 65ch;
}

.small-text {
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--ink-secondary);
}

.tiny-text {
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--ink-tertiary);
}

/* Link styles */
a {
  color: inherit;
  text-decoration: none;
  transition: opacity var(--transition-fast);
}

a:hover {
  opacity: 0.6;
}

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

/* Focus ring on dark surfaces. The base rings (here and on .btn) are var(--ink),
   which is invisible on --ink (.cta-section) and --dark-bg (.dark-section, .footer).
   Without this the closing CTA button on all 35 pages and every footer link have no
   keyboard focus indicator at all — WCAG 2.4.7 / 2.4.11. Specificity means placement
   in the file doesn't matter, but keep it beside the base rule so it stays findable. */
.dark-section a:focus-visible,
.dark-section .btn:focus-visible,
.cta-section a:focus-visible,
.cta-section .btn:focus-visible,
.footer a:focus-visible {
  outline: 2px solid var(--dark-text);
  outline-offset: 2px;
}

/* Skip link — WCAG 2.4.1 (Bypass Blocks). Every page opens with a fixed nav of 8
   focusable elements; without this a keyboard user tabs all of them on every page
   load. Off-screen until focused, then pinned top-left above the nav (z-index 100). */
.skip-link {
  position: absolute;
  top: 0;
  left: -9999px;
  z-index: 200;
  padding: 0.75rem 1.25rem;
  background: var(--ink);
  color: var(--paper);
  font-family: 'Syne', sans-serif;
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.skip-link:focus {
  left: 1rem;
  top: 1rem;
  outline: 2px solid var(--paper);
  outline-offset: 2px;
}

/* The skip target carries tabindex="-1" so focus really moves into <main>;
   suppress the ring it would otherwise draw on a non-interactive element. */
main:focus {
  outline: none;
}

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

.container {
  width: 100%;
  max-width: var(--section-max-width);
  padding: 0 var(--section-padding-x);
  margin: 0 auto;
  box-sizing: border-box;
}

/* .section is a plain marker class. It deliberately sets NO padding so the
   shared `main > section` rhythm applies. (It used to force padding:0, which is
   why sections had to fake their spacing with viewport heights.) */

.label {
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-secondary);
  margin-bottom: var(--element-gap);
  display: block;
}

/* Eyebrow labels & hero taglines are removed site-wide (brand direction):
   one headline per section, with nothing small stacked above the big
   headline. Blog-post internal labels (inside main > article, e.g. the table
   of contents) are intentionally NOT affected by this rule. */
main > section .label,
.hero .hero-tagline,
.page-hero .hero-tagline { display: none; }

/* ==========================================================================
   Layout utilities — reach for THESE before writing style="..." on an element.
   Every value comes from a design token, so anything built with them stays on
   the system automatically. This is the vocabulary that keeps pages consistent.
   ========================================================================== */

/* Readable line length. .measure matches the paragraph max-width (65ch). */
.measure { max-width: 65ch; }
.measure-wide { max-width: var(--wide-width); }

/* Text colour + alignment without re-declaring values inline. */
.text-muted { color: var(--ink-secondary); }
.text-faint { color: var(--ink-tertiary); }
.text-center { text-align: center; }

/* Even vertical spacing between stacked children. */
.stack > * + * { margin-top: var(--element-gap); }

/* ------------------------------------------------------------------ *
   THE SPACING UTILITIES — the classes that replaced 91 inline styles.

   Rule 4 used to allow one exception: a structural margin written inline
   using a token, e.g. style="margin-bottom:var(--gap);". That exception was
   retired in Aug 2026 because it was the last thing forcing 'unsafe-inline'
   into the Content-Security-Policy's style-src (SECURITY.md, S-07). These
   seven classes cover every value those 91 attributes used, and the markup
   now writes `class="mb-lg"` where it used to write the declaration.

   ⚠ THEY CARRY !important, AND THE REASON IS EXACT, NOT DEFENSIVE.
   An inline style has specificity (1,0,0,0): it beat every normal author
   rule on the page, including contextual ones. Eight of the headings being
   converted sit inside a `.card`, where `.card h3` (0,2,0) sets a DIFFERENT
   value (--space-xs) — so a plain single-class utility (0,1,0) would lose to
   it and would silently halve the gap under those eight headings. An author
   `!important` declaration outranks an inline style, which makes this layer
   the one thing that reproduces the old rendering BY CONSTRUCTION rather
   than by checking each of the 91 sites and hoping none moves later.

   That equivalence was measured, not assumed: 114 full-page screenshots at
   390/768/1440 before and after the conversion, byte-identical everywhere
   except the one band noted on `.mobile-nav .btn` below.

   The values are the five rungs of the markup-facing spacing ladder and
   nothing else. If a value you want is not a rung, snap to the nearest —
   do not add an eighth class. */
.mb-xs { margin-bottom: var(--space-xs) !important; }
.mb-sm { margin-bottom: var(--space-sm) !important; }
.mb-md { margin-bottom: var(--element-gap) !important; }
.mb-lg { margin-bottom: var(--gap) !important; }
.mb-xl { margin-bottom: var(--gap-lg) !important; }
.mt-xl { margin-top: var(--gap-lg) !important; }
.pt-0 { padding-top: 0 !important; }

/* Responsive column grids: one column on mobile, N columns from 48em (768px) up. */
.grid-2,
.grid-3 { display: grid; grid-template-columns: 1fr; gap: var(--gap); }
@media (min-width: 48em) {
  .grid-2 { grid-template-columns: 1fr 1fr; }
  .grid-3 { grid-template-columns: repeat(3, 1fr); }
}

/* ------------------------------------------------------------------ *
   GRID AND FLEX ITEMS MAY SHRINK — the one rule that stops sideways scroll.

   `1fr` is shorthand for `minmax(auto, 1fr)`, and `auto` as a track MINIMUM means
   "never narrower than this item's min-content". So a card with wide padding, a
   long word, or a fixed-width child silently pushes its track — and therefore the
   whole page — wider than the screen, with no single element looking wrong. Flex
   items have the identical default (`min-width: auto`).

   Setting the automatic minimum to 0 on the items lets the track do what the
   layout already says it should: fit the column it was given, and wrap or scroll
   its own content. Applied to the direct children of every grid/flex container on
   the site rather than one card at a time, because the failure is structural and
   the next new component would inherit it too.

   The one deliberate exception is `.table-scroll`, which owns its own horizontal
   scroll and whose table is MEANT to be wider than the column.
 * ------------------------------------------------------------------ */
.grid-2 > *,
.grid-3 > *,
.services-grid > *,
.services-os > *,
.beliefs-grid > *,
.content-grid > *,
.blog-grid > *,
.blog-featured > *,
.socials-grid > *,
.stats-grid > *,
.footer-grid > *,
.approach-item > *,
.nav-inner > *,
.hero-actions > *,
.hero-grid > *,
.social-card > *,
summary > * {
  min-width: 0;
}

/* Bordered surface card — the standard container for a grouped block. */
.card {
  padding: var(--gap);
  border: 1px solid var(--border);
  background: var(--paper-card);
}
/* h3 is the card title (h4 kept for the footer columns, which are genuine h4s). */
.card h3, .card h4 { margin-bottom: var(--space-xs); }
.card p { color: var(--ink-secondary); }
.card p:last-child { margin-bottom: 0; }

/* Cards inside a dark section flip to the dark palette automatically, so the
   two site colours stay in sync instead of clashing. */
.dark-section .card {
  background: var(--dark-surface);
  border-color: var(--dark-border);
  color: var(--dark-text);
}
.dark-section .card p { color: var(--dark-text-secondary); }

/* Navigation */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  padding: 1.5rem 0;
  /* OPAQUE, and it has to stay opaque. This was rgba(…, 0.85) with a 12px
     backdrop-filter — a frosted bar. The owner, reading the site on his phone:
     "the words scroll in my phone on the website, that is overlapping — which is
     on the home page or the first page of any section."

     He was describing exactly this. Fifteen per cent of everything that scrolls
     under a fixed bar shows through it, and what scrolls under it first on every
     page is the hero h1 at up to 96px. Reproduced at 390px, scrollY 985 on the
     home page: "Two Operating Systems." reads clean through the bar and collides
     with the wordmark. A phone makes it worse three ways — the type is huge
     relative to the screen, some Android browsers skip backdrop-filter entirely
     and render the raw 85%, and iOS "Reduce Transparency" does the same on
     purpose.

     It costs nothing to fix, because --paper is also the body background: at the
     top of every page the bar is already indistinguishable from the page behind
     it, so at rest nothing about the design changes. The frosting was only ever
     visible in the state where it was doing damage. .mobile-nav has been solid
     --paper all along, so the two now agree.

     THE AUDIT CANNOT CATCH THIS. `npm run gate` renders every page at the top —
     2,240 clean renders had nothing to say about it, and never will. Check it by
     eye after touching the nav: scratchpad/scroll-shot.mjs. */
  background: var(--paper);
  border-bottom: 1px solid transparent;
  transition: border-color var(--transition-base), background var(--transition-base), padding var(--transition-base);
}

/* The scrolled state is now carried by the hairline and the tighter padding
   alone — the background was already opaque before the page moved. Do not
   reintroduce an alpha here either; 5% is still enough to ghost a 96px serif. */
.nav.scrolled {
  border-bottom-color: var(--border);
  padding: 1rem 0;
}

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* space-between alone puts nothing between the logo and the first nav link
     once the row is full — at the 48em swap "papereagle AI" and "ABOUT" ran together
     with no space at all. A minimum gap makes the row degrade by tightening
     rather than by colliding. */
  gap: clamp(1rem, 2.5vw, 2.5rem);
}

.nav-logo {
  display: inline-flex;
  align-items: center;
  gap: min(0.5em, 2.2vw);
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1.1rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  transition: opacity var(--transition-fast);
}

/* THE ORIGAMI MARK — the one coloured thing on this website.
   A red paper eagle in flight, cut out of the owner's photograph by
   scripts/build-brand-mark.mjs. It appears in exactly two places: here, and in
   the browser tab. It is a WORDMARK, not an accent colour: it never becomes a
   border, a rule, a heading, a button or a hover state, and no other element on
   the site is allowed to go red because this one is. Same standing the red has
   on an Instagram `brandmark` frame.

   Sized by HEIGHT in `em`, so the mark and the wordmark scale as one object —
   at the root step-ups (1800/2400/3200px) the bird grows with the type instead
   of staying a 27px stamp beside it.

   The `min(…, 7vw)` is the guard the rest of this file keeps learning the hard
   way. `em` here resolves against 1.1rem, and rem follows the READER's default
   font size: at a 32px default the bird would be 55px wide inside a 320px
   phone, and the nav row — bird + wordmark + hamburger — would not fit. The vw
   term caps it at 22px on that phone and never binds on anything wider, so a
   laptop, a tablet and a TV all get the full-size mark. Height, not width: the
   source is 1.20:1, and a width cap would let a tall reader font stretch the
   row height instead. */
.nav-mark {
  height: min(1.8em, 7vw);
  width: auto;
  flex: none;
}

/* The wordmark holds one line wherever the desktop nav is what's showing — a
   wrapped logo doubles the nav height and pushes the hero's h1 under the fixed
   bar, and `flex-shrink: 0` stops the row squeezing it before the hamburger.

   BELOW that breakpoint it is allowed to wrap, as a last resort and only when it
   genuinely does not fit. At the default font size it always fits, so nothing
   changes for anyone on it. For a reader browsing at 32px it is the difference
   between a nav row 399px wide inside a 320px phone — every page scrolling
   sideways — and a wordmark on two lines, which is what reflow is supposed to
   look like. Shrinking the text back down instead would be overriding the
   setting they deliberately chose. */
@media (min-width: 48em) {
  .nav-logo {
    white-space: nowrap;
    flex-shrink: 0;
  }
}

.nav-logo:hover {
  opacity: 0.6;
}

/* `.nav-wordmark span`, NOT `.nav-logo span`. The wordmark is "paper" set in
   Syne butted straight against an italic Fraunces "eagle AI" with no space
   between them — that join is the logo. Once `.nav-logo` became a flex row to
   carry the origami mark, "paper" and the italic span turned into two separate
   flex items and the row `gap` opened a hole in the middle of the word:
   "paper eagle AI". `.nav-wordmark` wraps both so the join is one flex item and
   the gap only ever sits between the bird and the type. Scoping the italic to
   the inner span is what stops the wrapper inheriting it. */
.nav-wordmark span {
  font-family: 'Fraunces', serif;
  font-weight: 300;
  font-style: italic;
}

.nav-links {
  display: none;
  align-items: center;
  /* Fluid gap: six items + the logo + the CTA button must fit the moment the
     desktop nav appears at 48em, without wrapping. Not a flat 2rem. */
  gap: clamp(1.1rem, 2.2vw, 2rem);
  list-style: none;
}

.nav-links a {
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: 0.85rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-secondary);
  transition: color var(--transition-fast);
}

.nav-links a:hover,
.nav-links a.active {
  color: var(--ink);
  opacity: 1;
}

/* Below 48em — 768px on the default font size, wider for a reader who has raised
   it — the header CTA is hidden: the hamburger owns the mobile route and the same
   button is the last item in the mobile panel.

   THE SELECTOR IS `.nav .nav-cta` ON PURPOSE, and so is its ≥48em counterpart.
   A bare `.nav-cta { display: none }` has the same specificity as `.btn
   { display: inline-block }` further down the file, and the later rule won: the
   full-size black "Start a Conversation" button rendered in the middle of the
   mobile header on all 35 pages, clipped to "START A CONVERSAT…", squeezing the
   wordmark onto two lines and growing the bar to 125px — tall enough to sit on
   top of the contact page's h1. Never drop the `.nav ` prefix from either rule. */
.nav .nav-cta {
  display: none;
}

.nav-toggle {
  background: none;
  border: none;
  cursor: pointer;
  /* 44×44 minimum touch target (WCAG 2.5.8) — the three 24px bars alone are
     24×17, which is a miss on a phone as often as a hit. */
  padding: 0.75rem 0.5rem;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;
  flex-shrink: 0;
  gap: 5px;
}

.nav-toggle span {
  display: block;
  width: 24px;
  height: 1.5px;
  background: var(--ink);
  transition: transform var(--transition-base), opacity var(--transition-base);
  transform-origin: center;
}

.nav-toggle.active span:nth-child(1) {
  transform: rotate(45deg) translate(4px, 5px);
}

.nav-toggle.active span:nth-child(2) {
  opacity: 0;
}

.nav-toggle.active span:nth-child(3) {
  transform: rotate(-45deg) translate(4px, -5px);
}

@media (min-width: 48em) {
  .nav-links {
    display: flex;
  }
  /* `.nav ` prefix required — see the comment on `.nav .nav-cta` above. */
  .nav .nav-cta {
    display: inline-block;
  }
  .nav-toggle {
    display: none;
  }
}

/* 768–1023px: the logo, six nav items and the CTA button have to share one row.
   The button runs compact through this band and returns to full size at 1024px.
   If a seventh nav item is ever proposed, re-measure this band first.

   `.nav .nav-cta`, not `.nav-cta` — the bare selector ties with `.btn` below and
   loses on source order, so this whole band was dead: the button rendered at its
   full 1rem/2rem size, wrapped onto two lines, and sat on top of the "Contact"
   link on every iPad-portrait render. Same trap as the display rule above. */
@media (min-width: 48em) and (max-width: 63.9375em) {
  .nav-links a {
    font-size: 0.78rem;
    letter-spacing: 0.02em;
  }
  .nav .nav-cta {
    padding: 0.8rem 1.25rem;
    font-size: 0.75rem;   /* 12px — the legibility floor; 0.72rem rendered 11.5px */
  }
}

/* Buttons */
.btn {
  display: inline-block;
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* Horizontal padding is a clamp for the same reason the page gutter is: a flat
     2rem doubles to 64px a side for a reader at 32px, and 128px of padding does
     not fit a 320px phone before the label has had a single pixel. 10vw is exactly
     2rem at 320px and is capped above it, so the DEFAULT is unchanged at every
     width, and the rem max keeps it scaling on a 4K TV. */
  padding: 1rem clamp(1rem, 10vw, 2rem);
  border: none;
  cursor: pointer;
  transition: all var(--transition-base);
  text-align: center;
  position: relative;
  overflow: hidden;
  /* An inline-block is floored at its own min-content width, so without these the
     button sizes to "CONVERSATION" + padding no matter how narrow the screen is —
     351px inside a 320px phone, on every page, and `overflow: hidden` above meant
     the label was clipped rather than visibly wrong. max-width caps the box;
     break-word lets the label wrap instead of being cut off. */
  max-width: 100%;
  overflow-wrap: break-word;
}

.btn:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.btn::before {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
  transition: left var(--transition-slow);
}

.btn:hover::before {
  left: 100%;
}

.btn-primary {
  background: var(--ink);
  color: var(--paper);
}

.btn-primary:hover {
  background: var(--ink-secondary);
  opacity: 1;
}

.btn-outline {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--border-strong);
}

.btn-outline:hover {
  border-color: var(--ink);
  opacity: 1;
}

.btn-outline-light {
  background: transparent;
  color: var(--dark-text);
  border: 1px solid var(--dark-border);
}

.btn-outline-light:hover {
  border-color: var(--dark-text);
  opacity: 1;
}

/* Hero — layout only. Height/padding live in the consolidated .hero,.page-hero
   rule near the top of this file. (The old duplicate here referenced
   --hero-height / --hero-height-min, which were never defined.) */
.hero-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap-lg);
  align-items: center;
  width: 100%;
  max-width: var(--section-max-width);
  margin: 0 auto;
}

.hero-content {
  position: relative;
  z-index: 2;
}

.hero-tagline {
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-secondary);
  margin-bottom: var(--element-gap);
  display: block;
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 0.8s var(--ease-out) forwards;
}

.hero-title {
  margin-bottom: var(--element-gap);
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 0.8s var(--ease-out) 0.1s forwards;
}

.hero-desc {
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
  color: var(--ink-secondary);
  margin-bottom: var(--element-gap-lg);
  max-width: 55ch;
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 0.8s var(--ease-out) 0.2s forwards;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 0.8s var(--ease-out) 0.3s forwards;
}

/* THE PORTRAIT SHOWS ON A PHONE. It used to be `display: none` below 900px, so
   the owner's photo existed only on a laptop — his words: "in the laptop screen
   I can see my image on the right hand side, but not in phone when I scroll it…
   at least when I scroll, that should be managed."

   A phone cannot have a right-hand side, so the grid's single column stacks it
   under the copy and you meet it on the way down.

   IT IS CENTRED IN THAT COLUMN, AND THAT REVERSES THE ORIGINAL CALL (Aug 2026,
   the owner: "on the home page I can see the logo, but my logo is only on the
   right hand side — it has to be in the center… and you have to increase the
   size of the image. It looks totally off."). The first version left-aligned it
   to the headline's edge, reasoning that a centred photo under left-aligned copy
   reads as a stray element. On a real phone it reads as the opposite: at 15rem in
   a ~342px column it was a small picture pushed against one edge with a band of
   dead paper beside it, which looks like a layout that failed rather than one
   that chose. Big enough to own the column and centred in it is what makes it a
   deliberate part of the hero — the same thing the two-column layout does above
   56.25em, which is why this declaration is now unconditional.

   Note for the socials hero, which is this same element (`.hero-visual
   .hero-socials`): `.hero-socials` sets `flex-direction: column`, so
   `justify-content` there is the VERTICAL axis and this is a no-op for it. Its
   horizontal alignment comes from its own `align-items`, and is unchanged —
   still left under the copy on a phone, centred in its own half from 56.25em.

   The hero is `min-height: 100svh`, not `height`, so it simply grows: on a 390px
   phone the first screen still opens on the headline and the photo arrives on the
   first scroll. */
.hero-visual {
  display: flex;
  position: relative;
  justify-content: center;
  align-items: center;
  /* Image stays fixed in place — it must never parallax/scroll down over the
     marquee. !important guarantees it even if a cached older main.js runs. */
  transform: none !important;
}

/* Home hero photo: kept shorter than the left-hand text block so it never
   spills down into the rotating marquee band below the hero. The image is also
   fixed in place (scroll parallax is deliberately not implemented — see the
   note in main.js) so it never moves down over the marquee/footer. */
.hero-visual img {
  /* Mobile first: 20rem, then 26.25rem from 56.25em up (see that block below).
     20rem = 320px on a phone — about 82% of a 390px screen. It was 15rem/240px,
     which the owner called "totally off" (Aug 2026): 240px centred in a 342px
     column still leaves the photo looking like a thumbnail that got left behind,
     and 240px was chosen for a bandwidth reason, not a design one — it is exactly
     what founder-portrait-420.jpg covers at 1.75× on a DPR-1 screen.

     THAT BANDWIDTH REASON IS PAID FOR IN THE MARKUP, NOT HERE. A 320px box wants
     640px of real pixels on a DPR-2 phone, so index.html's phone <source> is now
     a 420w/840w srcset with `sizes: 320px` and a retina phone fetches the 840
     (137KB) instead of the 420 (37KB). That is the same trade the about-page mark
     already makes and for the same reason: the alternative is a photo drawn at
     0.66× device pixels — visibly soft — on every phone made in the last decade.
     A DPR-1 phone still gets the 420. **If this number changes, change `sizes` in
     index.html AND about.html** — scripts/tests/semantics.test.js re-derives
     about.html's from this rule and fails on drift.

     `max-width: 100%` is what keeps 20rem safe: on a 320px phone, or for a reader
     browsing at a 32px default font, the box yields to the column instead of
     pushing the page sideways.

     26.25rem = the original 420px, but in rem so it grows with the root on a big
     monitor or a TV instead of staying a 420px stamp beside 24px type.
     `aspect-ratio` replaces the old fixed `height: 420px`: the source is square
     (1254×1254), so a hard height meant that once `max-width: 100%` narrowed the
     box on a phone, `object-fit: contain` letterboxed the photo inside a
     still-420px-tall frame. The ratio keeps the height following the width. */
  width: 20rem;
  max-width: 100%;
  aspect-ratio: 1;
  height: auto;
  object-fit: contain;
}

.hero-statement {
  display: flex;
  align-items: center;
  justify-content: center;
  /* rem, not px: above 1800px the root font-size steps up and everything around
     this block grows with it. A frozen 280px floor would shrink relative to its
     own contents on a 4K TV. */
  min-height: 17.5rem;
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 1s var(--ease-out) 0.4s forwards;
  will-change: transform, opacity;
}

/* THE "SCROLL" CUE IS GONE FROM THE WHOLE SITE — permanent (Aug 2026).
   The owner: "I can still see the scroll button. Just remove the scroll button
   from the about section… from the service section also remove the scroll…
   and also remove the scroll button from the FAQ page."

   It was already off the home page; about, services and faq were the last three.
   Nobody needs telling that a page scrolls, and a pulsing hairline in the corner
   of a hero is exactly the kind of decoration Rule 11 exists to cut. `.hero-scroll`,
   `.hero-scroll-line` and `@keyframes scrollPulse` are deleted rather than left
   unused — a rule with no markup is a hook that looks live and is not. `initHeroScroll`
   in main.js went with them. **Do not reintroduce it on a new page.** */

@keyframes fadeSlideUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeIn {
  to { opacity: 1; }
}

@media (min-width: 56.25em) {
  .hero-grid {
    grid-template-columns: 1fr 1fr;
  }
  /* Two columns, so the photo gets its own half at full size. It is centred at
     every width now, so the `justify-content: center` that used to live here is
     gone — it sits unconditionally on `.hero-visual` above. Below this breakpoint
     the same element is a 20rem box centred under the copy. */
  .hero-visual img {
    width: 26.25rem;
  }
  /* NO font-size here. `.hero-title { font-size: var(--text-7xl) }` used to sit in
     this block, and because `.hero-title` (0,1,0) outranks `h1` (0,0,1) it switched
     the global h1 clamp off from 900px up — pinning the headline to a flat 96px
     exactly where this rule halves the column it lives in. Measured: 880px gave
     61.6px over 2 lines in an 832px column; 900px gave 96px over FIVE lines in a
     394px one. Twenty pixels of window, and the hero fell apart.
     The h1 clamp `clamp(2.75rem, 7vw, 6rem)` already ends at the same 6rem/96px,
     so from ~1371px up nothing changed at all — it just gets there by growing
     instead of by snapping. If a bigger hero is ever wanted, raise the clamp's
     maximum on `h1`; do not reintroduce a flat size on `.hero-title`. */
}

/* Marquee — Compact section. The ONLY .marquee ruleset; keep it that way. */
.marquee {
  width: 100%;
  overflow: hidden;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  padding: 1.5rem var(--section-padding-x);
  margin-bottom: 0;
}

.marquee-inner {
  display: flex;
  gap: 3rem;
  animation: marquee 30s linear infinite;
  width: max-content;
}

.marquee-item {
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-tertiary);
  white-space: nowrap;
}

@keyframes marquee {
  0% { transform: translateX(0); }
  100% { transform: translateX(-50%); }
}

/* Story Section — Fixed typography */
.story {
  position: relative;
}

.story-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap-lg);
}

.story-quote {
  font-family: 'Fraunces', serif;
  font-size: var(--text-3xl);
  font-weight: 300;
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  position: relative;
  padding-left: 2rem;
  border-left: 2px solid var(--ink);
  opacity: 0;
  transform: translateX(-30px);
  transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out);
}

.story-quote.visible {
  opacity: 1;
  transform: translateX(0);
}

.story-quote em {
  font-style: italic;
}

.story-text {
  color: var(--ink-secondary);
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.8s var(--ease-out) 0.2s, transform 0.8s var(--ease-out) 0.2s;
}

.story-text.visible {
  opacity: 1;
  transform: translateY(0);
}

.story-link {
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: var(--element-gap);
  color: var(--ink);
  border-bottom: 1px solid var(--ink);
  padding-bottom: 0.25rem;
  transition: gap var(--transition-fast);
}

.story-link:hover {
  gap: 0.75rem;
  opacity: 1;
}

@media (min-width: 48em) {
  .story-layout {
    grid-template-columns: 1fr 1fr;
  }
}

/* Services */
.services-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}

.service-card {
  padding: var(--gap-lg) 0;
  border-bottom: 1px solid var(--border);
  position: relative;
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
  will-change: opacity, transform;
}

.service-card.visible {
  opacity: 1;
  transform: translateY(0);
}

.service-card:last-child {
  border-bottom: none;
}

.service-number {
  font-family: 'Fraunces', serif;
  font-size: var(--text-xs);
  font-weight: 400;
  color: var(--ink-tertiary);
  margin-bottom: 1rem;
  display: block;
}

.service-card h3 {
  margin-bottom: 0.75rem;
}

.service-card p {
  color: var(--ink-secondary);
  margin-bottom: var(--element-gap);
}

.service-link {
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--ink);
  transition: gap var(--transition-fast);
}

.service-link:hover {
  gap: 0.75rem;
  opacity: 1;
}

/* THE ARROW IS DRAWN, NOT TYPED — and it has to be (Aug 2026).
   These six links used to end in a literal "→" (U+2192). Measured in a real
   browser against the real font files: it is THE ONLY character on the whole
   site that neither Fraunces nor Syne contains — 89 of the site's 90 characters
   come from our fonts, and that one fell through to whatever the device happened
   to have. Segoe UI Symbol on Windows, Noto on Android, Apple Symbols on a Mac:
   a different weight and width in every visitor's browser, in the one spot the
   eye follows to the call to action. Google does not publish a subset carrying
   it for either family, so there was nothing to self-host.

   Drawn from `currentColor` it is identical everywhere, inherits the link colour
   and every hover state for free, and scales with `em` so it still tracks the
   reader's font size.

   ⚠ It also REVIVES THE HOVER. `.service-link` is an inline-flex with a `gap`
   and a transition on it, but the text and the arrow were one text node — a
   single anonymous flex item, with no gap to animate. The declaration had been
   doing nothing. As a pseudo-element the arrow is a second flex item, so the
   arrow now slides on hover, which is plainly what the transition was written
   for. Removing the character without adding this rule would delete the
   affordance silently.

   The @supports guard is cheap insurance: without clip-path the box would paint
   as a solid rectangle, so anything that lacks it gets "›" instead — a character
   both fonts DO carry, verified the same way. */
.service-link::after {
  content: '\203A';
  flex: none;
  line-height: 1;
}

@supports (clip-path: polygon(0 0)) {
  .service-link::after {
    content: '';
    width: 0.9em;
    height: 0.72em;
    background: currentColor;
    clip-path: polygon(0 39%, 52% 39%, 52% 8%, 100% 50%, 52% 92%, 52% 61%, 0 61%);
  }
}

.service-extensions {
  margin-top: var(--gap-lg);
  padding-top: var(--gap-lg);
  border-top: 1px solid var(--border);
}

.service-extensions h4 {
  margin-bottom: var(--element-gap);
}

@media (min-width: 48em) {
  .services-grid {
    grid-template-columns: 1fr 1fr;
    gap: var(--gap);
  }
  .service-card {
    padding: var(--gap-lg);
    border: 1px solid var(--border);
    border-radius: 0;
  }
  .service-card:nth-child(2) {
    margin-top: 2rem;
  }
}

/* Approach / How It Works — Fixed typography */
.approach-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  counter-reset: step;
}

.approach-item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1.5rem;
  padding: var(--gap) 0;
  border-bottom: 1px solid var(--border);
  align-items: start;
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
  will-change: opacity, transform;
}

.approach-item.visible {
  opacity: 1;
  transform: translateY(0);
}

.approach-item:last-child {
  border-bottom: none;
}

.approach-num {
  font-family: 'Fraunces', serif;
  font-size: var(--text-5xl);
  font-weight: 300;
  line-height: 1;
  /* --ink-tertiary, NOT --border-strong. These are the step numbers 01–09 of an
     ordered list of components, so they are content a reader is meant to read,
     and #C5C3BC on --paper is 1.62:1 against the 3:1 that large text owes —
     a ghost, not a numeral. --border-strong is a token for drawing edges; the
     moment it is used for words it is being asked to do a job it was never
     measured for. 4.86:1 here, and still the quietest of the three ink tiers,
     which is all the design ever wanted from it. */
  color: var(--ink-tertiary);
  min-width: 3rem;
  transition: color var(--transition-base);
}

.approach-item:hover .approach-num {
  color: var(--ink);
}

/* On a phone the numeral column stops being worth its width. A 56px "04" plus the
   1.5rem gap takes 81px out of a 272px row, leaving the copy 192px — measured at
   22 characters a line, roughly a third of a comfortable measure and narrow
   enough to read as a column of broken words. Stacked, the same copy gets the
   full width (~44 characters) and the number reads as the step label it is. */
@media (max-width: 37.5em) {
  .approach-item {
    grid-template-columns: 1fr;
    gap: 0.25rem;
  }
  .approach-num {
    font-size: var(--text-4xl);
    min-width: 0;
  }
}

/* The step title. It follows an <h2>, so it is an <h3> — it was an <h4> until Aug 2026,
   which skipped a heading level purely to borrow h4's small-label look. The look lives
   here instead; the LEVEL is set by the markup. Never pick a heading tag for its size. */
.approach-content h3 {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 0.5rem;
  line-height: var(--leading-normal);
}

.approach-content p {
  font-size: var(--text-base);
  color: var(--ink-secondary);
  line-height: var(--leading-relaxed);
  margin-bottom: 0;
}

@media (min-width: 48em) {
  .approach-list {
    grid-template-columns: 1fr 1fr;
    gap: var(--gap);
  }
  .approach-item {
    border: 1px solid var(--border);
    padding: var(--gap);
  }
}

/* Deliverable / meta caption under a component (Syne, small, uppercase).
   Block-level span so it never collides with .approach-content p / .card p. */
.deliverable {
  display: block;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--ink-tertiary);
  margin-top: 0.75rem;
}
.dark-section .deliverable { color: var(--dark-text-secondary); }

/* Data / comparison table (services, knowledge). Scrolls on narrow screens. */
.table-scroll {
  overflow-x: auto;
  margin-bottom: var(--gap);
  /* Momentum scrolling on iOS, and don't let a sideways flick inside the table
     trigger the browser's back-swipe gesture. */
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

/* SAY THAT IT SCROLLS. A 40rem table in a 272px phone column simply ends at the
   screen edge mid-word, and a reader has no way to know a third column exists —
   they see a cut-off table and assume it's broken, which is worse than a table
   they know to push. The label is a `::before` with `position: sticky; left: 0`,
   so it is pinned to the left edge of the scrollport and stays put while the
   table moves under it. Sticky rather than a wrapper element because there are
   28 of these across the site and none of them needs a markup change.
   Above 760px the whole table fits and the label would be a lie, so it is gone. */
@media (max-width: 47.5em) {
  .table-scroll::before {
    content: "Scroll sideways for the full table";
    position: sticky;
    left: 0;
    display: block;
    width: fit-content;
    font-family: 'Syne', sans-serif;
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-wider);
    text-transform: uppercase;
    color: var(--ink-tertiary);
    margin-bottom: 0.6rem;
  }
  .dark-section .table-scroll::before { color: var(--dark-text-secondary); }
}
.data-table {
  width: 100%;
  min-width: 40rem;
  border-collapse: collapse;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-sm);
}
.data-table th,
.data-table td {
  padding: 0.85rem 1rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
}
.data-table thead th { font-weight: 600; border-bottom: 2px solid var(--ink); }
.data-table tbody th { font-weight: 600; color: var(--ink); }
.data-table td { color: var(--ink-secondary); }

/* Content Grid / Knowledge Hub — Fixed typography */
.content-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  background: var(--border);
}

.content-card {
  background: var(--paper-card);
  padding: var(--gap);
  transition: background var(--transition-base), transform var(--transition-fast);
  will-change: transform, background;
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 0.6s var(--ease-out) forwards;
}

.content-card:hover {
  background: var(--paper-alt);
  transform: translateY(-4px);
}

.content-card .tag {
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin-bottom: 0.75rem;
  display: block;
}

.content-card h3 {
  font-size: var(--text-2xl);
  margin-bottom: 0.75rem;
}

.content-card p {
  font-size: var(--text-sm);
  color: var(--ink-secondary);
  margin-bottom: 0;
}

.content-card.featured {
  background: var(--ink);
  color: var(--dark-text);
}

.content-card.featured h3 {
  color: var(--dark-text);
}

.content-card.featured p {
  color: var(--dark-text-secondary);
}

.content-card.featured .tag {
  color: var(--dark-text-secondary);
}

@media (min-width: 48em) {
  .content-grid {
    grid-template-columns: 1fr 1fr;
  }
  .content-card.featured {
    grid-column: 1 / -1;
  }
}

/* CTA Section — auto height (NOT a full screen) so the closing CTA and the
   footer sit together on the final screen; the CTA is never half cut off. */
.cta-section {
  background: var(--ink);
  color: var(--dark-text);
  text-align: center;
  padding: var(--space-xl) 0;
}

/* Same reasoning as the .dark-section heading rule above, applied to the other
   dark surface on the site. Every CTA band carries only an h2 today, so this is
   a no-op right now and stays one — it is here so that the day someone adds an
   h3 to a CTA, it is not ink-on-ink at 1.02:1. */
.cta-section h1,
.cta-section h2,
.cta-section h3,
.cta-section h4,
.cta-section h5,
.cta-section h6 {
  color: var(--dark-text);
}

.cta-section h2 {
  margin-bottom: var(--element-gap);
}

.cta-section p {
  color: var(--dark-text-secondary);
  margin: 0 auto var(--element-gap-lg);
  max-width: 50ch;
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
}

/* Inline text links inside a CTA paragraph — the quiet fallback routes (email,
   the message form) that shouldn't compete with the one primary button. */
.cta-section p a {
  color: var(--dark-text);
  border-bottom: 1px solid var(--dark-border);
  transition: border-color var(--transition-fast);
}

.cta-section p a:hover {
  border-bottom-color: var(--dark-text);
}

.cta-section .btn-primary {
  background: var(--dark-text);
  color: var(--ink);
}

.cta-section .btn-primary:hover {
  background: var(--dark-text-secondary);
}

/* Footer — pairs with the CTA to fill the final screen without overflowing it. */
.footer {
  background: var(--dark-bg);
  color: var(--dark-text-secondary);
  padding: var(--space-xl) 0 var(--gap);
}

.footer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap-lg);
}

.footer-brand h3 {
  color: var(--dark-text);
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: var(--text-2xl);
  margin-bottom: 0.5rem;
}

/* The footer wordmark, and the exact counterpart of `.nav-wordmark span` above:
   "paper" set in Syne butted straight against an italic Fraunces "eagle AI".
   It was an inline style on all 39 pages until Aug 2026 (SECURITY.md S-07) —
   the same declaration, repeated 39 times, which is what a stylesheet is for.
   Scoped to the h3 so it can never reach the column headings beside it. */
.footer-brand h3 span {
  font-family: 'Fraunces', serif;
  font-style: italic;
}

.footer-brand p {
  font-size: var(--text-sm);
  color: var(--dark-text-secondary);
  line-height: var(--leading-relaxed);
}

.footer h4 {
  color: var(--dark-text);
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  margin-bottom: 1rem;
}

.footer ul {
  list-style: none;
}

.footer li {
  margin-bottom: 0.5rem;
}

.footer a {
  font-size: var(--text-sm);
  color: var(--dark-text-secondary);
  transition: color var(--transition-fast);
}

.footer a:hover {
  color: var(--dark-text);
  opacity: 1;
}

.footer-bottom {
  margin-top: var(--gap-lg);
  padding-top: var(--gap);
  border-top: 1px solid var(--dark-border);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  font-size: var(--text-xs);
  color: var(--dark-text-secondary);
}

/* THE PHONE FOOTER IS THREE COLUMNS, NOT A STACK (Aug 2026, owner's ask).

   "At the end of every page there is Products, Content OS, Business OS, All
   Extensions, Resources, Company — written line by line, and there is empty
   space. For phone, to save some space: three invisible columns."

   Stacked, the three link lists are 12 rows tall — and at pointer:coarse each
   row is floored at 44px for the touch target, so the footer alone runs ~700px
   on a phone while two thirds of every row is empty paper. Side by side they
   are 3 rows. The brand block spans the full width above them, so the reading
   order (brand → Products → Resources → Company) is unchanged in the DOM and
   for a screen reader.

   THE BREAKPOINT IS A BAND IN em, AND BOTH ENDS ARE LOAD-BEARING (Rule 6):
     · min-width 22em (352px) — MEASURED, not chosen. The binding word is the
       "RESOURCES" heading, which sets at 86px with the tracking below (81px
       with no tracking at all). Column width is 80px at a 320px viewport, 86px
       at 340px and 90px at 352px. So a 320px iPhone SE CANNOT hold three of
       these columns by any amount of tuning, and keeps the stack; 352px is the
       first width with real headroom. Below the bound the base `1fr` applies.
     · max-width 47.999em — at 48em the desktop `2fr 1fr 1fr 1fr` takes over.
   Because em resolves against the READER'S default font, not the root, the
   band closes on its own for anyone browsing large: at a 24px default 22em is
   528px and at 32px it is 704px, so a phone falls back to the stack exactly
   when three columns would stop fitting. That is the whole reason this is not
   a px query — no ladder of overrides, and nothing to re-tune per device.

   ⚠ A HEADING BREAKING MID-WORD IS INVISIBLE TO EVERY GATE WE HAVE. The first
   version of this rule started at 20em and rendered "PRODUCT/S" and
   "RESOURC/ES" on a 320px phone. Nothing overflowed, nothing scrolled
   sideways, no tap target shrank — responsive-audit called 320px clean,
   because a box that wraps grows DOWNWARD and downward growth breaks no
   geometric invariant. Same shape of blindness as the fig-checklist defect.
   It was caught by screenshotting the footer and looking at it, and the bound
   was then set from measured min-content widths. Measure the words.

   ⚠ THIS BLOCK MUST STAY BELOW `.footer h4` AND `.footer-grid`. Its selectors
   TIE them — `.footer h4` is (0,1,1) in both places — so source order is the
   only thing deciding, and a media query is not a tiebreaker. Sitting above
   them (where it was first written) the letter-spacing below silently did
   nothing, the heading kept --tracking-widest, and "RESOURCES" needed 94px it
   did not have. Same trap as `.nav-cta` vs `.btn`. Don't hoist it. */
@media (min-width: 22em) and (max-width: 47.999em) {
  .footer-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: var(--space-sm);
    row-gap: var(--gap);
  }

  /* Full width above the three columns — `1 / -1` spans the explicit grid. */
  .footer-brand {
    grid-column: 1 / -1;
  }

  /* Load-bearing, not cosmetic: --tracking-widest (0.12em) sets "RESOURCES" at
     94px, which does not fit the 93px column of a 360px Android phone. At
     0.05em it is 86px and fits from 352px up. The label still reads as one. */
  .footer h4 {
    letter-spacing: 0.05em;
    margin-bottom: 0.75rem;
  }
}

@media (min-width: 48em) {
  .footer-grid {
    grid-template-columns: 2fr 1fr 1fr 1fr;
  }
}

/* Dark Section Utility */
.dark-section {
  background: var(--dark-bg);
  color: var(--dark-text);
}

/* EVERY heading level, not just h2 — this was a real defect, reported by the
   owner: "when the background is black and at some places the text is also
   black, so the text is not visible… 'Running on effort' and 'Running on a
   system'." Both of those are `h3`s inside `.dark-section .card`, and they
   rendered #1C1C1A on #222220 — a contrast ratio of 1.07:1.

   THE RULE THIS BREAKS, WORTH KNOWING BECAUSE IT WILL COME BACK: a DIRECT
   declaration always beats an INHERITED one, however specific the inherited one
   is. `.dark-section .card { color: var(--dark-text) }` (0,2,0) is inherited by
   the card's paragraphs, so they flipped correctly — and it could never reach
   the headings, because `h1,h2,h3,h4,h5,h6 { color: var(--ink) }` sets colour on
   them directly. Specificity never entered into it. So: any element that sets
   its own colour needs its own dark-surface counterpart, and listing one level
   here was never enough. Caught by `npm run contrast`, which exists because of
   this bug. */
.dark-section h1,
.dark-section h2,
.dark-section h3,
.dark-section h4,
.dark-section h5,
.dark-section h6 {
  color: var(--dark-text);
}

.dark-section .label {
  color: var(--dark-text-secondary);
}

/* Mobile Nav Overlay — Fixed typography */
.mobile-nav {
  /* visibility, NOT display. An element cannot transition out of display:none —
     the browser applies both changes in the same frame, so the 300ms slide-in
     below never ran and the panel simply popped in. visibility is transitionable,
     and it still removes the links from the tab order and from assistive tech
     while closed, and takes no pointer events. */
  display: flex;
  visibility: hidden;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--paper);
  z-index: 99;
  padding: 6rem 2rem 2rem;
  flex-direction: column;
  gap: 1.5rem;
  transform: translateX(100%);
  transition: transform var(--transition-base) var(--ease-out),
              visibility var(--transition-base);
  will-change: transform;

  /* THE PANEL MUST SCROLL. Six 36px links plus the button stack 656px tall — more
     than an iPhone SE is high (568px) and nearly twice a phone held sideways
     (375px). With the default `overflow: visible` the last items simply fell off
     the bottom of the screen with nothing to scroll, so on the smallest phones
     "Start a Conversation" could not be reached from the menu at all, and the
     open menu also locks body scroll behind it.
     `overscroll-behavior: contain` stops a flick at the end of the list from
     scrolling the page underneath. */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}


.mobile-nav.active {
  visibility: visible;
  transform: translateX(0);
}

/* `:not(.btn)` is load-bearing. The last child of the panel is the "Start a
   Conversation" button, and a bare `.mobile-nav a` (0,2,0) outranks
   `.btn-primary { color: var(--paper) }` (0,1,0) — so the button drew its label
   in --ink on an --ink background and the site's primary conversion control was a
   blank black slab on every phone, 36px of Fraunces tall with a hairline under
   it. Keep the exclusion on every `.mobile-nav a` rule; the button is styled by
   .btn and needs nothing from here. */
.mobile-nav a:not(.btn) {
  font-family: 'Fraunces', serif;
  font-size: var(--text-4xl);
  font-weight: 300;
  color: var(--ink);
  border-bottom: 1px solid var(--border);
  padding-bottom: 1rem;
  transition: color var(--transition-fast);
}

.mobile-nav a:not(.btn):hover {
  color: var(--ink-secondary);
  opacity: 1;
}

/* The panel's CTA sits a rung below the last link. This was an inline
   style="margin-top:1rem" on all 39 pages until Aug 2026 (SECURITY.md S-07).

   ⚠ REMOVING IT UNCOVERED A DEAD RULE, AND THAT IS THE POINT OF THE EXERCISE.
   The landscape-phone block below sets `margin-top: 0.5rem` on this same
   button — and it had NEVER ONCE APPLIED, because an inline style (1,0,0,0)
   outranks `.mobile-nav .btn` (0,2,0) from inside a media query just as it
   does from outside one. A media query is not a specificity boost. So the
   tightening that block was written to perform silently did nothing on every
   phone held sideways since the day it shipped, in the one band where the
   panel is 375px tall and the whole design goal is "fits without scrolling".
   This rule is deliberately NOT !important so that block can now win. */
.mobile-nav .btn {
  margin-top: var(--space-sm);
}

/* Held sideways, a phone gives the panel ~375px of height. Tighten the type and
   the top inset so the whole menu fits without scrolling in the common case,
   and let the rule above catch the rest. */
@media (max-height: 560px) {
  .mobile-nav {
    padding: 5rem 2rem 2rem;
    gap: 1rem;
  }
  .mobile-nav a:not(.btn) {
    font-size: clamp(1.5rem, 7vh, var(--text-4xl));
    padding-bottom: 0.5rem;
  }
}

/* A phone held sideways is short but WIDE — one tall column wastes the whole
   right half of the screen and still needs scrolling. Two columns put every
   destination and the button on screen at once. Height-and-width gated, so a
   narrow phone in portrait never sees it. */
@media (max-height: 560px) and (min-width: 30em) {
  .mobile-nav {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-content: start;
    column-gap: 3rem;
    row-gap: 0.75rem;
  }
  .mobile-nav .btn {
    grid-column: 1 / -1;
    justify-self: start;
    margin-top: 0.5rem;
  }
}

/* Animations — Fixed timing */
.fade-in {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out);
  will-change: opacity, transform;
}

.fade-in.visible {
  opacity: 1;
  transform: translateY(0);
}

.slide-up {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
  will-change: opacity, transform;
}

.slide-up.visible {
  opacity: 1;
  transform: translateY(0);
}

.scale-in {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
  will-change: opacity, transform;
}

.scale-in.visible {
  opacity: 1;
  transform: scale(1);
}

/* About Page — Fixed typography */
.about-hero {
  padding-bottom: var(--gap-xl);
}

.about-story {
  font-family: 'Fraunces', serif;
  font-size: var(--text-xl);
  line-height: var(--leading-loose);
  font-weight: 300;
  max-width: var(--wide-width);
}

.about-story em {
  font-style: italic;
}

.about-story .highlight {
  font-weight: 400;
  font-style: italic;
  display: block;
  font-size: var(--text-3xl);
  margin: var(--element-gap) 0;
  color: var(--ink);
}

.services-os {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap);
}

.os-card {
  padding: var(--card-padding-lg) clamp(1.25rem, 4.5vw, var(--gap));
  border: 1px solid var(--border);
  position: relative;
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out), border-color var(--transition-base), box-shadow var(--transition-base);
  will-change: opacity, transform, border-color, box-shadow;
}

.os-card.visible {
  opacity: 1;
  transform: translateY(0);
}

.os-card:hover {
  border-color: var(--border-strong);
  box-shadow: 0 20px 40px -20px rgba(28, 28, 26, 0.1);
}

.os-card h3 {
  margin-bottom: 1rem;
}

.os-card .os-sub {
  font-size: var(--text-sm);
  color: var(--ink-secondary);
  margin-bottom: var(--element-gap-lg);
}

.os-card ul {
  list-style: none;
  margin-bottom: var(--element-gap-lg);
}

.os-card li {
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--border);
  font-size: var(--text-sm);
  color: var(--ink-secondary);
  display: flex;
  gap: 0.75rem;
  align-items: baseline;
  opacity: 0;
  transform: translateX(-10px);
  transition: opacity 0.4s var(--ease-out), transform 0.4s var(--ease-out);
}

.os-card.visible li {
  opacity: 1;
  transform: translateX(0);
}

.os-card li:nth-child(1) { transition-delay: 0.1s; }
.os-card li:nth-child(2) { transition-delay: 0.15s; }
.os-card li:nth-child(3) { transition-delay: 0.2s; }
.os-card li:nth-child(4) { transition-delay: 0.25s; }
.os-card li:nth-child(5) { transition-delay: 0.3s; }
.os-card li:nth-child(6) { transition-delay: 0.35s; }
.os-card li:nth-child(7) { transition-delay: 0.4s; }

.os-card li::before {
  content: "—";
  color: var(--ink-tertiary);
  flex-shrink: 0;
}

@media (min-width: 48em) {
  .services-os {
    grid-template-columns: 1fr 1fr;
  }
}

/* Contact Page — booking calendar, centred and stacked at the top of the page.
   The Calendly embed is a plain <iframe> (no third-party script) and it is the ONE
   place on the site allowed to load an external frame. Its chrome is ours: paper
   card, 1px border, sharp corners — the same as .card.

   Why this hero breaks the full-screen rule (Rule 2): Calendly scrolls INSIDE its
   own frame. If the page also has to be scrolled to reach the frame, a visitor
   scrolls twice before they can pick a time, and some give up. So this hero opts
   out of `min-height: 100vh`, sits close under the nav, and puts headline → one
   line → calendar in a single centred column. The page below runs uneven on
   purpose; booking is worth more than a tidy screen rhythm. */
.page-hero-booking {
  min-height: 0;                 /* the calendar decides the height, not the viewport */
  align-items: flex-start;
  padding-top: calc(var(--nav-height) + 2.5rem);
  padding-bottom: var(--section-padding-y-compact);
  text-align: center;
}

/* Reveals with a CSS keyframe, NOT the JS-driven .fade-in class. .fade-in starts
   at opacity 0 and needs main.js to add .visible — if the script ever fails, the
   site's most important conversion element must not be invisible. */
.booking-stack {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--element-gap);
  width: 100%;
  max-width: 62rem;
  margin: 0 auto;
  min-width: 0;
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 0.8s var(--ease-out) 0.1s forwards;
}

/* The one bold headline on the site. Capped well under the global h1 clamp (6rem)
   so it announces the page without pushing the calendar off the screen. */
.booking-headline {
  font-weight: 600;
  font-size: clamp(2.5rem, 6vw, 4rem);
  margin: 0;
}

.booking-lede {
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
  color: var(--ink-secondary);
  max-width: 60ch;
  margin: 0;
}

.booking-panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  width: 100%;
  padding: var(--element-gap);
  background: var(--paper-card);
  border: 1px solid var(--border);
}

/* On a phone the calendar goes edge to edge. The panel's 24px inset plus the
   container's 24px gutter left Calendly a 222px-wide frame on a 320px screen —
   its own date grid does not fit in that, so the visitor scrolls sideways inside
   an iframe to find a day. Trading the side border for ~80px of frame is the
   right trade on the page whose entire job is booking. Vertical padding and both
   horizontal borders elsewhere on the page are untouched. */
@media (max-width: 37.5em) {
  .booking-panel {
    align-self: stretch;
    width: auto;
    margin-left: calc(var(--section-padding-x) * -1);
    margin-right: calc(var(--section-padding-x) * -1);
    padding: 0.5rem;
    border-left: none;
    border-right: none;
  }
}

.booking-note {
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--ink-secondary);
  margin: 0;
}

/* The write-instead route is a quiet text link, never a second button — the
   calendar is the one action in this hero. */
.booking-note a {
  color: var(--ink);
  border-bottom: 1px solid var(--border-strong);
  transition: border-color var(--transition-fast);
}

.booking-note a:hover {
  border-bottom-color: var(--ink);
}

/* Tall on purpose. Calendly scrolls inside its own frame, so every pixel we give the
   frame is a scroll the visitor doesn't have to do.

   THIS HEIGHT IS THE ONE LENGTH ON THE SITE THAT IS DELIBERATELY IN px, NOT rem
   (Aug 2026). Everything else is rem so it grows with the reader's own font size —
   but what this box contains is a FOREIGN DOCUMENT. Calendly renders in its own
   root, at its own sizes, and does not know or care what our `html` font-size is.
   So a rem height here scales the frame while its contents stay put: at a 24px
   reader default the old `44rem` was a 1056px window onto 862px of calendar, i.e.
   ~200px of grey, and every attempt to fix the phone by raising the rem made that
   worse. The correct question is "how tall is Calendly", and that answer is in px.

   MEASURED, not guessed — Calendly rendered at each width this page actually gives
   it, with its cookie banner dismissed, reading the bottom of its lowest content:

     frame 304px (a 320px phone) ...... needs 862px   \
     frame 374px (a 390px phone) ...... needs 862px    | stacked: date picker
     frame 602px (a 700px tablet) ..... needs 830px    | above the time list
     frame 662–982px .................. needs 886px   /
     ----- frame crosses 1000px, Calendly switches layout -----
     frame 1002–1168px ................ needs 766px   -- side by side

   Hence two tiers and not three. 900px covers the worst stacked case (886) with a
   little slack for a six-row month; 800px covers the side-by-side case (766) with
   the same slack it has always had. The old three-tier 44/48/50rem was short in
   every one of them — 158px short on a phone, which clipped the last week of the
   month AND the timezone control and put the visitor back to scrolling inside the
   iframe, the exact thing this page was rebuilt to stop.

   IF THE BOOKING URL OR THE EVENT TYPE CHANGES, RE-MEASURE. A different event with
   more custom questions is a different height, and nothing here can detect that. */
.booking-embed {
  display: block;
  width: 100%;
  height: 900px;
  border: 1px solid var(--border);
  background: var(--paper);
  color-scheme: light;    /* Calendly is a light UI — don't let the OS invert it. */
}

@media (min-width: 56.25em) {
  /* Past ~1000px of frame width Calendly lays the date picker and the time list
     side by side instead of stacking them — far less scrolling inside the frame. */
  .booking-stack { max-width: 68rem; }
}

/* The frame reaches 1000px wide — where Calendly re-lays itself — at a 1100px
   viewport, measured on this page (1080 → 982, 1100 → 1002). Written in em like
   every other breakpoint here: at a large reader font it fires later than the real
   switch, which costs a laptop reader some grey below the calendar and nothing else.
   Getting it exactly right would need a px query, and a cosmetic gap is not worth
   being the one rule that opts out of rule 6. */
@media (min-width: 68.75em) {
  .booking-embed { height: 800px; }
}

/* Forms — the contact page has no message form (the Calendly panel is the only
   route in). These stay as the shared field vocabulary for any future form. */
.form-group {
  margin-bottom: var(--element-gap-lg);
}

.form-group label {
  display: block;
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  margin-bottom: 0.5rem;
  color: var(--ink-secondary);
}

.form-group input,
.form-group textarea,
.form-group select {
  width: 100%;
  padding: 1rem;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-base);
  border: 1px solid var(--border);
  background: var(--paper-card);
  color: var(--ink);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);
  -webkit-appearance: none;
}

.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
  outline: none;
  border-color: var(--ink);
  background: var(--paper);
  box-shadow: 0 0 0 3px rgba(28, 28, 26, 0.05);
}

.form-group textarea {
  min-height: 140px;
  resize: vertical;
}

/* (knowledge/index.html builds its listing from .content-grid / .content-card,
   which is why there is no .knowledge-list or .knowledge-item ruleset here.) */

.blog-featured {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap);
  padding: var(--gap-lg) 0;
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--gap-lg);
}

/* Both card types are <a> elements, so the global `a` rule above already gives them
   `color: inherit` + `text-decoration: none`. They used to carry that inline on all 26
   cards (Rule 4 violation); the attributes were removed and NOT replaced with a rule
   here, because a rule restating the global one is the duplicate CSS item 15 deleted. */
.blog-featured-card {
  padding: var(--card-padding-lg);
  border: 1px solid var(--border);
  transition: background var(--transition-base), transform var(--transition-fast), border-color var(--transition-base);
  will-change: transform, background, border-color;
  opacity: 0;
  transform: translateY(30px);
  animation: fadeSlideUp 0.8s var(--ease-out) forwards;
}

.blog-featured-card:hover {
  background: var(--paper-alt);
  border-color: var(--border-strong);
  transform: translateY(-4px);
}

.blog-featured-card .meta {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin-bottom: 1rem;
}

.blog-featured-card h2 {
  margin-bottom: 1rem;
}

.blog-featured-card p {
  color: var(--ink-secondary);
  margin-bottom: var(--element-gap);
}

.blog-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  background: var(--border);
}

.blog-card {
  background: var(--paper-card);
  padding: var(--gap);
  transition: background var(--transition-base), transform var(--transition-fast);
  will-change: transform, background;
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 0.6s var(--ease-out) forwards;
}

.blog-card:hover {
  background: var(--paper-alt);
  transform: translateY(-4px);
}

.blog-card .meta {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin-bottom: 0.75rem;
}

.blog-card h3 {
  font-size: var(--text-lg);
  margin-bottom: 0.5rem;
}

.blog-card p {
  font-size: var(--text-sm);
  color: var(--ink-secondary);
  margin-bottom: 0;
}

.blog-card .read-more {
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  display: inline-block;
  margin-top: 1rem;
  color: var(--ink);
  border-bottom: 1px solid var(--ink);
  padding-bottom: 0.15rem;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.blog-card .read-more:hover {
  color: var(--ink-secondary);
  border-color: var(--ink-secondary);
  opacity: 1;
}

.blog-sidebar {
  margin-top: var(--gap-lg);
  padding: var(--gap);
  border: 1px solid var(--border);
}

.blog-sidebar h4 {
  margin-bottom: 1rem;
}

.blog-sidebar p {
  font-size: var(--text-sm);
  color: var(--ink-secondary);
  margin-bottom: var(--element-gap-lg);
}

.blog-categories {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: var(--gap-lg);
  padding-bottom: var(--gap-lg);
  border-bottom: 1px solid var(--border);
}

/* These are <button>s, not links — they toggle a filter, they don't navigate. They were
   <a href="#"> until Aug 2026. The first five declarations neutralise the UA button
   styling so the appearance is byte-identical to the old anchor; everything below the
   blank line is the original rule. `line-height` matters: an <a> inherited the body's
   1.7 and a <button> defaults to `normal`, which would shrink the pill. */
.blog-cat {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  line-height: var(--leading-relaxed);
  text-align: center;

  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  padding: 0.4rem 1rem;
  border: 1px solid var(--border);
  color: var(--ink-secondary);
  transition: all var(--transition-fast);
  cursor: pointer;
}

/* A <button> is matched by neither `a:focus-visible` nor `.btn:focus-visible`, so
   without this the filters would have lost the focus ring item 2 added. */
.blog-cat:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.blog-cat:hover {
  border-color: var(--ink);
  color: var(--ink);
  opacity: 1;
}

.blog-cat.active {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}

@media (min-width: 48em) {
  .blog-featured {
    grid-template-columns: 1fr 1fr;
  }
  .blog-grid {
    grid-template-columns: 1fr 1fr;
  }
}

/* Beliefs Grid — Fixed typography */
.beliefs-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap);
}

.belief-card {
  padding: var(--gap);
  border: 1px solid var(--border);
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out), border-color var(--transition-base);
  will-change: opacity, transform, border-color;
}

.belief-card.visible {
  opacity: 1;
  transform: translateY(0);
}

.belief-card:hover {
  border-color: var(--border-strong);
}

.belief-card h3 {
  margin-bottom: 0.75rem;
}

.belief-card p {
  color: var(--ink-secondary);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  margin-bottom: 0;
}

@media (min-width: 48em) {
  .beliefs-grid {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

/* Process Steps */
.process-steps {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap);
}

@media (min-width: 48em) {
  .process-steps {
    grid-template-columns: 1fr 1fr;
  }
}

/* FAQ accordion — native details/summary, zero JS */
.faq-list {
  border-top: 1px solid var(--border);
  max-width: 54rem;
}

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

.faq-item summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap);
  padding: 1.5rem 0;
}

.faq-item summary::-webkit-details-marker {
  display: none;
}

.faq-item summary h3 {
  margin: 0;
  font-size: var(--text-xl);
  font-weight: 400;
  color: var(--ink);
  transition: color var(--transition-fast);
}

.faq-item summary:hover h3 {
  color: var(--ink-secondary);
}

.faq-item summary::after {
  content: "+";
  flex-shrink: 0;
  font-family: 'Syne', sans-serif;
  font-size: 1.5rem;
  line-height: 1.1;
  color: var(--ink-tertiary);
}

.faq-item[open] summary::after {
  content: "\2013";
}

.faq-item > p {
  margin: 0;
  max-width: 65ch;
  padding: 0 0 1.5rem;
  color: var(--ink-secondary);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
}

.dark-section .faq-list {
  border-top-color: var(--dark-border);
}

.dark-section .faq-item {
  border-bottom-color: var(--dark-border);
}

.dark-section .faq-item summary h3 {
  color: var(--dark-text);
}

.dark-section .faq-item summary:hover h3 {
  color: var(--dark-text-secondary);
}

.dark-section .faq-item summary::after {
  color: var(--dark-text-secondary);
}

.dark-section .faq-item > p {
  color: var(--dark-text-secondary);
}

/* Extension Cards — Fixed typography */
.ext-card {
  padding: var(--card-padding-lg);
  border: 1px solid var(--border);
  transition: background var(--transition-base), border-color var(--transition-base), transform var(--transition-fast);
  will-change: transform, background, border-color;
  opacity: 0;
  transform: translateY(20px);
  animation: fadeSlideUp 0.6s var(--ease-out) forwards;
}

.ext-card:hover {
  background: var(--paper-alt);
  border-color: var(--border-strong);
  transform: translateY(-4px);
}

.ext-card h3 {
  margin-bottom: 1rem;
}

.ext-card p {
  color: var(--ink-secondary);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  margin-bottom: var(--element-gap);
}

/* Socials — the profile cards on socials.html.
   The page is in two named groups, because the accounts belong to two different
   people: "Shreyas Dave" heads LinkedIn, which is the founder's personal account,
   and "Paper Eagle AI" heads Instagram and YouTube, which are the company's. The
   page used to describe the LinkedIn profile as though it were the company's —
   the owner's correction, Aug 2026: "that is my personal account."

   The platform marks are drawn in --eagle-red on his instruction, tiled inside a
   maroon hairline box. Everything else on the page and the site stays warm ink;
   see the token's own note. The grid is auto-fit, so a group holds one card or
   three without any markup or CSS change. */
.socials-group + .socials-group {
  margin-top: var(--gap-lg);
}

/* The owner of the accounts below. A label, not a section heading — it is set in
   the same small tracked caps as .social-handle so the platform names inside the
   cards stay the loudest thing in the group. */
.socials-group-name {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 0.5rem;
}

.socials-group-note {
  color: var(--ink-secondary);
  font-size: var(--text-base);
  margin-bottom: var(--element-gap);
  max-width: 55ch;
}

.socials-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  gap: var(--gap);
  max-width: var(--wide-width);
}

.social-card {
  display: flex;
  gap: var(--element-gap);
  padding: var(--card-padding-lg);
  border: 1px solid var(--border);
  background: var(--paper-card);
  transition: background var(--transition-base), border-color var(--transition-base), transform var(--transition-fast);
  will-change: transform, background, border-color;
}

.social-card:hover {
  background: var(--paper-alt);
  border-color: var(--border-strong);
  transform: translateY(-4px);
}

/* The maroon plate the platform mark sits in. `flex: none` plus explicit rem
   sides, because a flex item holding only an SVG will otherwise be squeezed by a
   long word in the paragraph beside it. The mark inherits `color`, so the box and
   the glyph can never disagree about which red they are. */
.social-mark-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.25rem;
  height: 3.25rem;
  flex: none;
  border: 1px solid var(--eagle-red);
  color: var(--eagle-red);
}

.social-mark {
  width: 1.6rem;
  height: 1.6rem;
  fill: currentColor;
}

.social-card h4 {
  font-size: var(--text-2xl);
  margin-bottom: 0.35rem;
}

.social-handle {
  display: block;
  font-family: 'Syne', sans-serif;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin-bottom: var(--element-gap);
}

.social-card p {
  color: var(--ink-secondary);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  margin-bottom: var(--element-gap);
  /* Rule 6, and it only ever binds on a group holding ONE card: `auto-fit`
     collapses the empty tracks, so the lone LinkedIn card stretches the full
     --wide-width and its paragraph would otherwise set ~81 characters a line.
     In a two-up group the column is ~340px and this never applies. */
  max-width: 65ch;
}

/* On a phone the mark stacks above the copy — a 4rem-padded row would leave the
   paragraph about 150px wide. */
@media (max-width: 37.5em) {
  .social-card {
    flex-direction: column;
    gap: 1rem;
    padding: var(--gap);
  }
}

/* THE SOCIALS HERO LOCKUP — the right-hand column of socials.html.
   Owner's ask, Aug 2026: "on the socials page on the right hand side, I just
   want my name to be written and just the logo of Instagram and YouTube and
   LinkedIn — understand, just the logo. The written part is when I scroll."

   So it is the two owners, named, with their marks beneath — and nothing else.
   It shares .hero-visual with the home portrait and the about mark, which is
   what makes it stack under the copy on a phone and take its own column from
   900px up without a single new breakpoint. The one thing it must add is a
   width: .hero-visual centres its child at ≥56.25em, and a child with no width
   of its own would collapse onto its longest name. */
/* CENTRED AT EVERY WIDTH, INCLUDING A PHONE (Aug 2026). This used to centre only
   from 56.25em up and sit at the column's left edge below it — deliberately, on the
   reasoning that a block of names and tiles is not one picture and would read as
   ragged if centred.

   The owner's instruction retires that reasoning. He reported the home portrait and
   the about mark both hugging one edge on his phone, and then said "check it again
   for mobile phones — I guess you know what type of error I'm talking about." This
   is the third instance of exactly that error: the same `.hero-visual` slot, the same
   empty half beside it. Centring the box AND its contents is what the ≥56.25em rule
   already did; it is now simply unconditional, so all three heroes behave the same
   way at every width instead of two of them changing at 900px.

   `width: fit-content` before `margin-inline: auto` for the reason the desktop rule
   found: auto-margins centre the 22rem BOX and leave its left-aligned contents short
   of true centre, so the box has to shrink to its content first. */
.hero-socials {
  flex-direction: column;
  align-items: center;
  gap: var(--gap-lg);
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
}

.hero-socials-group {
  display: flex;
  flex-direction: column;
  gap: var(--element-gap);
  width: 100%;
}

/* The same small tracked caps as .socials-group-name further down the page, on
   purpose: the hero names and the group labels are the same two names doing the
   same job, and setting the hero pair in display type would make them compete
   with the h1 two centimetres to their left. */
.hero-socials-name {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink);
  text-align: center;       /* centred with the lockup, at every width */
}

.hero-socials-marks {
  display: flex;
  flex-wrap: wrap;          /* two marks and a 32px-font reader still fit */
  justify-content: center;  /* centred with the lockup, at every width */
  gap: var(--space-sm);
  list-style: none;
  margin: 0;
  padding: 0;
}

.hero-socials-marks a {
  display: block;
  border: 0;                /* the tile draws the only edge */
}

/* Bigger than the card tiles, because these are the hero and there is no copy
   beside them to balance against — but a clamp, not a step, so the phone keeps
   the card's 3.25rem and a 4K TV grows with the root. The 3.25rem floor is also
   what keeps the tap target over 44×44 on a coarse pointer. */
.hero-socials .social-mark-tile {
  width: clamp(3.25rem, 6vw, 5.5rem);
  height: clamp(3.25rem, 6vw, 5.5rem);
  transition: background var(--transition-base), color var(--transition-base);
}

.hero-socials .social-mark {
  width: 50%;
  height: 50%;
}

/* Inverted on hover rather than tinted: --eagle-red is a mark, not a palette,
   so the one interaction it gets is the plate filling in behind it. */
.hero-socials-marks a:hover .social-mark-tile,
.hero-socials-marks a:focus-visible .social-mark-tile {
  background: var(--eagle-red);
  color: var(--paper);
}

/* ONCE THERE ARE TWO COLUMNS, THE LOCKUP CENTRES IN ITS OWN HALF — exactly as
   the home portrait and the about mark do. Owner's correction, Aug 2026:
   "the whole right side, take it as another page… it has to be in the center of
   the right side, just like my image on the home page."

   It was not. `.hero-visual` centres a child horizontally by being full-column
   width with `justify-content: center`; `.hero-socials` reverses the flex axis
   to stack the two groups, so that declaration now centres them VERTICALLY and
   nothing was centring the box itself. Being a grid item with `max-width: 22rem`
   it therefore sat at the column's start edge. Measured at 1440px: the portrait's
   centre x=1044, the lockup's x=928 — 116px adrift in a 640px column.

   `margin-inline: auto` alone would only centre the 22rem BOX, leaving its
   left-aligned contents ~80px left of true centre, so the box shrinks to its
   content first. Centring the contents as well is what makes it read as one
   designed unit rather than a ragged left edge: the single LinkedIn tile would
   otherwise hang at the left under a name half again its width.

   THIS BLOCK IS GONE AND THIS IS ITS HEADSTONE (Aug 2026). Every declaration it
   held moved up onto `.hero-socials` itself and now applies at EVERY width — see
   the note there. It said "below this breakpoint nothing changes: the lockup stays
   left-aligned under the copy," and that clause is what the owner then reported as
   a defect on the other two heroes: "my logo is only on the right hand side, it has
   to be in the center… check it again for mobile phones — I guess you know what type
   of error I'm talking about." A desktop-only correction to an off-centre lockup was
   always going to be half a fix. Do not re-add a breakpoint here. */

/* Responsive Typography — small phones. Note: section heights are driven by
   content everywhere, including mobile. Never reintroduce min-height:__vh here.

   The h2/h3 overrides that used to live here (flat --text-4xl / --text-2xl) are
   gone: h2 and h3 are clamps now, so the fluid rule already lands smaller than
   these did at 480px and keeps shrinking down to 320px, which the flat values
   could not. Re-adding a flat size here would silently switch the clamp off. */
@media (max-width: 30em) {
  .hero-desc { font-size: var(--text-base); }
}

/* ------------------------------------------------------------------ *
   TOUCH TARGETS — WCAG 2.5.8 (Target Size, Minimum): 24×24 CSS px is the AA
   floor and 44×44 is the AAA / Apple + Android platform guidance. This site is
   read on phones, so 44 is the number.

   Gated on `pointer: coarse` rather than a width, because that is the actual
   question — a finger, not a small window. A mouse keeps the tight, quiet
   layout the design intends; nothing about the desktop rendering changes.

   These use min-height + centring instead of extra padding so the visible pill,
   rule, and text stay exactly where they are — the target grows, the design
   doesn't move.
 * ------------------------------------------------------------------ */
@media (pointer: coarse) {
  /* The wordmark is the home link on every page — the most-tapped target on the
     site and, at 110×31, one of the smallest. (`display: inline-flex` and the
     centring moved into the base rule when the origami mark was added — they
     are how the mark sits beside the type now, not a touch-target concern. The
     min-height is the only part that belongs here.) */
  .nav-logo {
    min-height: 44px;
  }

  /* Every selector here is a CONTROL. `.social-handle` used to be in this list and
     is not — it is a plain <span> of static text ("in/shreyasdave1a19"); the card's
     actual link is the `.service-link` below it. A touch target on a label nobody
     can tap is meaningless, and it cost something real: inline-flex is floored at
     min-content, so at a 32px reader font that unbreakable handle became a 306px
     box that pushed the socials page 75px past a 320px screen. */
  .blog-cat,
  .service-link,
  .read-more,
  .pagination a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* The TOC is the one link list here whose entries are prose-length, and it
     must not buy a touch target with the ability to wrap.

     `overflow-wrap: break-word` does NOT reduce an element's min-content width —
     it only breaks a word once a line would overflow. So any shrink-to-fit box
     (inline-flex AND inline-block alike) is floored at its longest unbreakable
     token and overflows a narrow column. Measured, "The keep/update/merge/cut
     decision table" in a 114px box: inline 111px, inline-flex 196px,
     inline-block 196px, block 114px.

     A block box takes the width it is given and lets break-word do its job, so
     the 44px comes from padding rather than from centring. */
  .toc a {
    display: block;
    min-height: 44px;
    padding-block: 0.5rem;
  }

  /* Filter pills sit in a wrap-flow row; give them width as well as height. */
  .blog-cat {
    justify-content: center;
    min-width: 44px;
  }

  /* Footer and nav lists: the link is the whole row, so the target comes from
     the row's own spacing rather than from padding on the text. */
  .footer-links li,
  .footer-grid li {
    min-height: 44px;
    display: flex;
    align-items: center;
  }
  .footer-links li a,
  .footer-grid li a {
    display: block;
    width: 100%;
  }
}

::selection {
  background: var(--ink);
  color: var(--paper);
}

/* Stagger Animation Delays */
.delay-1 { animation-delay: 0.1s; }
.delay-2 { animation-delay: 0.2s; }
.delay-3 { animation-delay: 0.3s; }
.delay-4 { animation-delay: 0.4s; }
.delay-5 { animation-delay: 0.5s; }
.delay-6 { animation-delay: 0.6s; }
.delay-7 { animation-delay: 0.7s; }
.delay-8 { animation-delay: 0.8s; }

/* Focus Visible Polyfill */
:focus:not(:focus-visible) {
  outline: none;
}

/* Scrollbar Styling */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: var(--paper);
}

::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--ink-tertiary);
}

/* ==========================================================================
   THE FIGURE SYSTEM — blog-post diagrams, drawn from type and hairlines
   ==========================================================================

   WHY THIS EXISTS. A blog post is read by three audiences and only one of them
   reads: a person skimming for whether we understand their problem, a search
   engine deciding what the page is about, and an LLM deciding whether the page
   is worth citing. All three are served by the same thing — a page whose
   argument is visible in its structure. A figure is that structure made
   literal: it states one claim, in one shape, in about four seconds.

   THE RULES, and they are the site's existing rules, not new ones:

   1. MONOCHROME. Warm paper + warm ink, hairlines between them. No accent
      colour, no gradient of brand colour, no icon, no emoji, no image file.
      Every figure here is HTML, CSS and (twice) a bare SVG path.
   2. TEXT IS REAL TEXT. Nothing here is an image, so every figure is
      selectable, searchable, translatable, readable by a screen reader, and
      crawlable — which is the entire GEO argument for building them this way
      instead of exporting PNGs. Labels never go below --text-xs (12px, the
      accessibility floor the responsive audit enforces).
   3. IT REFLOWS. Every family is mobile-first: it stacks in one column and
      turns into its wide arrangement at 48em. Nothing is positioned in px,
      nothing is floored at min-content, every grid/flex child carries
      `min-width: 0`. This is what keeps `npm run gate` at 0 findings.
   4. NO INLINE STYLES. A post is held to `styles:2` — the footer brand span and
      the mobile-nav button, and nothing else. Anything a figure needs to vary
      per instance (a bar's length, a point's position) is a utility class from
      the --v / --x / --y ladders below, never a `style="width:63%"`.
   5. ONE FIGURE, ONE CLAIM. The figcaption states the claim in a sentence. If
      you cannot write that sentence, the figure is decoration and does not ship.

   ALIGNMENT IS FIXED AND DELIBERATE. Every figure is the full width of the
   reading column, its contents are left-aligned, and its caption sits under it
   on the left. Figures never float, never alternate sides, never sit half-width
   beside text. A reader scrolling a post sees the same rectangle in the same
   place every time, so the eye learns where the picture is and stops hunting.

   HOW A POST USES THEM: see docs/FIGURE-SYSTEM.md — the catalogue, the
   copy-paste blocks, and the ledger of which families each post has used (a
   post never repeats a family inside itself).
 * ------------------------------------------------------------------ */

/* A SECOND :root, ON PURPOSE — these four tokens are used by nothing above and
   belong beside the block that reads them, which is the whole point of the
   figure system living in one documented place at the bottom of this file.
   Hoisting them into the :root at line 1 would scatter it. */
/* stylelint-disable-next-line no-duplicate-selectors */
:root {
  /* Figure-local rhythm. The padding is a clamp for the same reason
     --card-padding-lg is: a flat rem triples for a reader at a 32px browser
     font and leaves a 320px phone nothing to draw in. */
  --fig-pad: clamp(0.875rem, 4vw, 1.75rem);
  --fig-gap: clamp(0.5rem, 2vw, 0.875rem);
  --fig-rule: 1px solid var(--border);
  --fig-rule-strong: 1px solid var(--border-strong);
}

/* ── The frame every figure shares ───────────────────────────────────── */

.fig {
  margin: var(--element-gap-lg) 0;
  padding: var(--fig-pad);
  border: var(--fig-rule);
  border-top: 2px solid var(--ink);
  background: var(--paper-card);
  box-sizing: border-box;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  /* THE TYPE IS SET HERE, ON THE CONTAINER, NOT ON p/li.
     A figure is mostly bare <strong> and <span> inside flex rows, and those
     inherit from whatever set a size last — which was `body` (--text-base).
     At a 32px reader font that is 34px of label text in a 216px box, and the
     stack figure bled 355px out of it. Setting the size on .fig means every
     descendant that doesn't ask for its own lands on --text-sm. */
  font-family: 'Syne', sans-serif;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--ink-secondary);
  /* Bare text inside a flex container is an anonymous flex item — there is no
     element to put `min-width: 0` on, and `break-word` does not move a
     min-content floor. `anywhere` is the only value that does. */
  overflow-wrap: anywhere;
}

.fig > * { min-width: 0; }

/* The drawing area. Everything above the caption lives in here. */
.fig-frame {
  display: flex;
  flex-direction: column;
  gap: var(--fig-gap);
  min-width: 0;
}

/* Figure copy resets. `main > article p|li|ol|ul` is (0,0,3); each of these is
   at least (0,1,1) and wins, so a figure never inherits body-paragraph rhythm. */
.fig p,
.fig li,
.fig dt,
.fig dd {
  margin: 0;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  max-width: none;
}

.fig ol,
.fig ul,
.fig dl {
  margin: 0;
  padding-left: 0;
  list-style: none;
}

.fig li { margin-bottom: 0; }

/* The small uppercase readouts — the counters, the axis names, the column
   heads. --text-xs is 12px, the floor; never take a label below it. */
.fig-key,
.fig-tag,
.fig-axis,
.fig-col {
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  line-height: var(--leading-normal);
}

/* An optional one-line heading inside the frame, above the drawing. */
.fig-head {
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-secondary);
  padding-bottom: var(--fig-gap);
  border-bottom: var(--fig-rule);
}

.fig figcaption {
  margin-top: var(--fig-pad);
  padding-top: var(--fig-gap);
  border-top: var(--fig-rule);
  font-family: 'Syne', sans-serif;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--ink-secondary);
}

.fig figcaption .fig-tag {
  display: block;
  margin-bottom: 0.35rem;
  color: var(--ink);
}

/* ── The value ladders — how a figure varies without an inline style ──── */

/* --v drives any length that is a proportion: a bar, a segment of a split
   rail, the fill of a meter. Fives are enough resolution for a diagram and
   they keep the class list readable. */
.v-5   { --v:  5%; }
.v-10  { --v: 10%; }
.v-15  { --v: 15%; }
.v-20  { --v: 20%; }
.v-25  { --v: 25%; }
.v-30  { --v: 30%; }
.v-35  { --v: 35%; }
.v-40  { --v: 40%; }
.v-45  { --v: 45%; }
.v-50  { --v: 50%; }
.v-55  { --v: 55%; }
.v-60  { --v: 60%; }
.v-65  { --v: 65%; }
.v-70  { --v: 70%; }
.v-75  { --v: 75%; }
.v-80  { --v: 80%; }
.v-85  { --v: 85%; }
.v-90  { --v: 90%; }
.v-95  { --v: 95%; }
.v-100 { --v: 100%; }

/* --x / --y place a plotted label inside a quadrant. Percentages of the plot
   box, measured from the left and from the BOTTOM (so it reads like a graph,
   not like a screen).

   THE LADDER RUNS 10 TO 90 IN FIVES AND HAS NO GAPS. It used to skip 45 and 55,
   and seven posts were written using `x-55` — a class that did not exist, so
   `--x` stayed unset and every one of those points silently took the `left:
   var(--x, 50%)` fallback and sat on the centre crosshair instead of clear of
   it. Nothing catches that: the class name looks right, the page validates, and
   5% of a plot is a nudge rather than an obvious break. It is the same failure
   as the off-scale `v-12` bar in post 20. **If you write a coordinate, check it
   is a rung.** */
.x-10 { --x: 10%; } .x-20 { --x: 20%; } .x-25 { --x: 25%; } .x-30 { --x: 30%; }
.x-35 { --x: 35%; } .x-40 { --x: 40%; } .x-45 { --x: 45%; } .x-50 { --x: 50%; }
.x-55 { --x: 55%; } .x-60 { --x: 60%; } .x-65 { --x: 65%; } .x-70 { --x: 70%; }
.x-75 { --x: 75%; } .x-80 { --x: 80%; } .x-85 { --x: 85%; } .x-90 { --x: 90%; }
.y-10 { --y: 10%; } .y-20 { --y: 20%; } .y-25 { --y: 25%; } .y-30 { --y: 30%; }
.y-35 { --y: 35%; } .y-40 { --y: 40%; } .y-45 { --y: 45%; } .y-50 { --y: 50%; }
.y-55 { --y: 55%; } .y-60 { --y: 60%; } .y-65 { --y: 65%; } .y-70 { --y: 70%; }
.y-75 { --y: 75%; } .y-80 { --y: 80%; } .y-85 { --y: 85%; } .y-90 { --y: 90%; }

/* ── 01 · FLOW — a pipeline, one stage after another ─────────────────── */

.flow-row {
  display: flex;
  flex-direction: column;
  gap: var(--fig-gap);
}

.flow-node {
  position: relative;
  min-width: 0;
  padding: 0.75rem 0.875rem;
  border: var(--fig-rule);
  background: var(--paper);
}

.flow-node .fig-key { display: block; margin-bottom: 0.2rem; }
.flow-node strong { display: block; font-weight: 600; color: var(--ink); }
.flow-node span.flow-note { display: block; color: var(--ink-secondary); }

/* The connector. Content-with-alt-text so a screen reader hears nothing. */
.flow-node:not(:last-child)::after {
  content: "\2193" / "";
  position: absolute;
  left: 50%;
  bottom: calc(var(--fig-gap) * -1);
  transform: translate(-50%, 0);
  line-height: var(--fig-gap);
  font-size: var(--text-sm);
  color: var(--ink-tertiary);
}

.flow-node.is-last { border-color: var(--ink); }

@media (min-width: 48em) {
  .flow-row { flex-direction: row; align-items: stretch; }
  .flow-node { flex: 1 1 0; padding-right: 1.5rem; }
  .flow-node:not(:last-child)::after {
    content: "\2192" / "";
    left: auto;
    right: 0.4rem;
    top: 50%;
    bottom: auto;
    transform: translate(0, -50%);
  }
}

/* ── 02 · STACK — layers, foundation at the bottom ───────────────────── */

.stack-set {
  display: flex;
  flex-direction: column-reverse;   /* markup runs bottom-up; render top-down */
  gap: 0;
  border: var(--fig-rule);
}

.stack-layer {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  align-items: baseline;
  justify-content: space-between;
  min-width: 0;
  padding: 0.7rem 0.875rem;
  background: var(--paper);
  border-top: var(--fig-rule);
}

.stack-set > .stack-layer:first-child { border-top: 0; }   /* the base row */

.stack-layer > * { min-width: 0; }
.stack-layer strong { font-weight: 600; color: var(--ink); }
.stack-layer .fig-key { flex: 0 0 auto; }

/* The base of a stack is the thing everything rests on — say so with weight. */
.stack-layer.is-base { background: var(--paper-alt); border-top: 2px solid var(--ink); }

/* ── 03 · MATRIX — a 2×2, four named positions ───────────────────────── */

.matrix-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  border: var(--fig-rule);
}

.matrix-cell {
  min-width: 0;
  padding: 0.875rem;
  border-top: var(--fig-rule);
  background: var(--paper);
}

.matrix-grid > .matrix-cell:first-child { border-top: 0; }
.matrix-cell .fig-key { display: block; margin-bottom: 0.3rem; }
.matrix-cell strong { display: block; font-weight: 600; color: var(--ink); margin-bottom: 0.2rem; }
.matrix-cell p { color: var(--ink-secondary); }
.matrix-cell.is-goal { background: var(--paper-alt); }

.matrix-axes {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.25rem;
}

@media (min-width: 48em) {
  .matrix-grid { grid-template-columns: 1fr 1fr; }
  .matrix-cell { border-top: 0; border-left: var(--fig-rule); }
  .matrix-cell:nth-child(-n+2) { border-bottom: var(--fig-rule); }
  .matrix-cell:nth-child(odd) { border-left: 0; }
}

/* ── 04 · BARS — a comparison you can read without numbers ───────────── */

.bar-set {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.6rem;
}

.bar-row { min-width: 0; }

.bar-row .bar-label {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.75rem;
  justify-content: space-between;
  margin-bottom: 0.3rem;
  color: var(--ink);
}

.bar-row .bar-label > * { min-width: 0; }

.bar-track {
  height: 0.85rem;
  background: var(--paper-alt);
  border: var(--fig-rule);
}

.bar-fill {
  height: 100%;
  width: var(--v, 50%);
  background: var(--ink);
}

/* The second series in the same chart: hatched, not a second colour. */
.bar-fill.is-alt {
  background: repeating-linear-gradient(
    135deg,
    var(--ink) 0 2px,
    var(--paper-alt) 2px 5px
  );
}

/* ── 05 · CURVE — two lines, one point ───────────────────────────────── */

.curve-plot {
  position: relative;
  border-left: var(--fig-rule-strong);
  border-bottom: var(--fig-rule-strong);
  padding: 0.25rem 0 0 0.25rem;
}

.curve-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.curve-svg .curve-grid {
  fill: none;
  stroke: var(--border);
  stroke-width: 1;
}

.curve-svg .curve-line {
  fill: none;
  stroke: var(--ink);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.curve-svg .curve-line.is-flat {
  stroke: var(--ink-tertiary);
  stroke-dasharray: 5 4;
}

.curve-svg .curve-wash {
  fill: var(--paper-alt);
  stroke: none;
}

.curve-axes {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  justify-content: space-between;
}

.curve-key {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
}

.curve-key li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  color: var(--ink-secondary);
}

.curve-key li::before {
  content: "";
  flex: 0 0 auto;
  width: 1.5rem;
  height: 0;
  border-top: 2px solid var(--ink);
}

.curve-key li.is-flat::before {
  border-top-style: dashed;
  border-top-color: var(--ink-tertiary);
}

/* ── 06 · LOOP — stations, and the path back to the start ────────────── */

.loop-set {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  border: var(--fig-rule);
  border-left: 2px solid var(--ink);
}

.loop-station {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.75rem;
  align-items: baseline;
  min-width: 0;
  padding: 0.7rem 0.875rem;
  border-top: var(--fig-rule);
  background: var(--paper);
}

.loop-set > .loop-station:first-child { border-top: 0; }
.loop-station > * { min-width: 0; }
.loop-station strong { font-weight: 600; color: var(--ink); }
.loop-station p { color: var(--ink-secondary); }

.loop-return {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.875rem;
  background: var(--paper-alt);
  border-top: var(--fig-rule-strong);
  color: var(--ink-secondary);
  overflow-wrap: anywhere;
}

.loop-return::before {
  content: "\21BA" / "";
  flex: 0 0 auto;
  font-size: var(--text-base);
  line-height: 1;
  color: var(--ink);
}

/* ── 07 · TREE — one root, its branches ──────────────────────────────── */

.tree-root {
  padding: 0.7rem 0.875rem;
  border: 2px solid var(--ink);
  background: var(--paper-alt);
}

.tree-root strong { font-weight: 600; color: var(--ink); }

.tree-branches {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--fig-gap);
  padding-left: 0.875rem;
  border-left: var(--fig-rule-strong);
}

.tree-branch {
  position: relative;
  min-width: 0;
  padding: 0.65rem 0.875rem;
  border: var(--fig-rule);
  background: var(--paper);
}

.tree-branch::before {
  content: "";
  position: absolute;
  left: -0.875rem;
  top: 1.1rem;
  width: 0.875rem;
  border-top: var(--fig-rule-strong);
}

.tree-branch strong { display: block; font-weight: 600; color: var(--ink); }
.tree-branch p { color: var(--ink-secondary); }

.tree-leaves {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.45rem;
}

.tree-leaves li {
  padding: 0.15rem 0.5rem;
  border: var(--fig-rule);
  background: var(--paper-alt);
  color: var(--ink-secondary);
  font-size: var(--text-xs);
}

@media (min-width: 48em) {
  .tree-branches { grid-template-columns: repeat(3, 1fr); }
}

/* ── 08 · FILETREE — the work as files on disk ───────────────────────── */

.filetree {
  border: var(--fig-rule);
  background: var(--paper);
}

.filetree li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.75rem;
  align-items: baseline;
  justify-content: space-between;
  min-width: 0;
  padding: 0.4rem 0.75rem;
  border-top: var(--fig-rule);
  color: var(--ink-secondary);
}

.filetree > li:first-child { border-top: 0; }
.filetree li > * { min-width: 0; }
.filetree .file-name { color: var(--ink); }
.filetree li.is-dir { background: var(--paper-alt); }
.filetree li.is-dir .file-name { font-weight: 600; }

/* Depth, drawn as a rail rather than as ASCII art, so it survives wrapping.
   The indent is capped against the viewport, not left as a flat rem: at a 32px
   reader font a flat 2.6rem is 83px of indent inside a 212px box on a 320px
   phone, and the filename had 129px left to live in. Each vw term equals its
   rem at 320px, so the default rendering is pixel-identical and only the
   raised-font case changes. Same rule as the page gutter and .btn padding. */
.filetree li.d-1 { padding-left: min(1.6rem, 8vw); }
.filetree li.d-2 { padding-left: min(2.6rem, 13vw); }
.filetree li.d-3 { padding-left: min(3.6rem, 18vw); }

.filetree-foot {
  padding: 0.45rem 0.75rem;
  border-top: var(--fig-rule-strong);
  background: var(--paper-alt);
}

/* ── 09 · SESSION — an agent working, in its own words ───────────────── */

.session {
  border: 1px solid var(--dark-border);
  background: var(--dark-bg);
  color: var(--dark-text-secondary);
}

.session li {
  display: flex;
  gap: 0.6rem;
  min-width: 0;
  padding: 0.4rem 0.75rem;
  border-top: 1px solid var(--dark-border);
  color: var(--dark-text-secondary);
}

.session > li:first-child { border-top: 0; }
.session li > * { min-width: 0; }

.session .s-mark {
  flex: 0 0 auto;
  color: var(--dark-text);
  font-weight: 600;
}

/* The transcript line itself takes the rest of the row and wraps inside it.
   Without this the row wrapped as a whole and put the marker on a line of its
   own, which read as an empty bullet above the text. */
.session li > span:last-child { flex: 1 1 auto; min-width: 0; }

.session li.is-you { background: var(--dark-surface); color: var(--dark-text); }
.session li.is-done .s-mark { color: var(--dark-text); }
.session li.is-wait { color: var(--dark-text-secondary); font-style: italic; }

/* The run readout is the last ROW of the block, not a caption under it —
   outside the <ul> it drew on paper and looked detached from the session. */
.session li.is-foot {
  display: block;
  border-top: 1px solid var(--dark-border);
  color: var(--dark-text-secondary);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
}

/* ── 10 · CANVAS — a workflow board that branches and merges ─────────── */

.canvas-board {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--fig-gap);
}

.canvas-lane {
  min-width: 0;
  border: var(--fig-rule);
  background: var(--paper);
}

.canvas-lane > .fig-key {
  display: block;
  padding: 0.4rem 0.75rem;
  border-bottom: var(--fig-rule);
  background: var(--paper-alt);
}

.canvas-node {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
  align-items: baseline;
  min-width: 0;
  padding: 0.5rem 0.75rem;
  border-top: var(--fig-rule);
  color: var(--ink-secondary);
}

.canvas-lane .canvas-node:first-of-type { border-top: 0; }
.canvas-node > * { min-width: 0; }
.canvas-node strong { font-weight: 600; color: var(--ink); }
.canvas-node.is-gate { background: var(--paper-alt); }
.canvas-node.is-gate strong::after { content: " \2014 gate" / ""; color: var(--ink-tertiary); font-weight: 400; }

.canvas-merge {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border: var(--fig-rule-strong);
  background: var(--paper-alt);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.canvas-merge::before {
  content: "\21B3" / "";
  flex: 0 0 auto;
  line-height: 1;
  color: var(--ink-tertiary);
}

@media (min-width: 48em) {
  /* Lanes size to their own content — stretched, a short branch drew a tall
     empty box beside a long one. */
  .canvas-board { grid-template-columns: 1fr 1fr; align-items: start; }
  .canvas-board > .canvas-merge { grid-column: 1 / -1; }
  .canvas-board > .canvas-head { grid-column: 1 / -1; }
}

/* ── 11 · TIMELINE — what happens when ───────────────────────────────── */

/* `.fig ol, .fig ul { padding-left: 0 }` is (0,1,2) and a bare `.timeline` is
   (0,1,0), so the rail's padding lost and the dots drew outside the frame.
   Scope it to .fig — anything here that styles a ul/ol as a list must. */
.fig .timeline {
  padding-left: 1.1rem;
  border-left: var(--fig-rule-strong);
}

.timeline li {
  position: relative;
  min-width: 0;
  padding: 0 0 0.9rem 0;
}

.timeline li:last-child { padding-bottom: 0; }

.timeline li::before {
  content: "";
  position: absolute;
  /* Centred on the rail: back out the list's padding, its border, and half
     the dot's own outer width (0.55rem box + 2px of border each side). */
  left: calc(-1.375rem - 3px);
  top: 0.42rem;
  width: 0.55rem;
  height: 0.55rem;
  background: var(--paper);
  border: 2px solid var(--ink);
}

.timeline li.is-now::before { background: var(--ink); }
.timeline .fig-key { display: block; margin-bottom: 0.15rem; }
.timeline strong { display: block; font-weight: 600; color: var(--ink); }
.timeline p { color: var(--ink-secondary); }

/* ── 12 · COMPARE — two panels, same question ────────────────────────── */

.compare-set {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--fig-gap);
}

.compare-panel {
  min-width: 0;
  padding: 0.875rem;
  border: var(--fig-rule);
  background: var(--paper);
}

.compare-panel.is-after { border-color: var(--ink); background: var(--paper-alt); }
.compare-panel > .fig-key { display: block; margin-bottom: 0.5rem; }
.compare-panel h4,
.compare-panel strong {
  display: block;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 0.4rem;
}

.compare-panel ul { display: grid; gap: 0.3rem; }
.compare-panel li { color: var(--ink-secondary); padding-left: 0.9rem; position: relative; }
.compare-panel li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 0.45rem;
  border-top: var(--fig-rule-strong);
}

@media (min-width: 48em) {
  .compare-set { grid-template-columns: 1fr 1fr; }
}

/* ── 13 · SPEC — a datasheet readout ─────────────────────────────────── */

.spec-list {
  display: grid;
  grid-template-columns: 1fr;
  border: var(--fig-rule);
  background: var(--paper);
}

.spec-list > div {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 1rem;
  align-items: baseline;
  justify-content: space-between;
  min-width: 0;
  padding: 0.55rem 0.875rem;
  border-top: var(--fig-rule);
}

.spec-list > div:first-child { border-top: 0; }
.spec-list > div > * { min-width: 0; }
.spec-list dt { color: var(--ink-tertiary); text-transform: uppercase; letter-spacing: var(--tracking-wider); font-size: var(--text-xs); }
.spec-list dd { color: var(--ink); text-align: left; }
.spec-list > div.is-total { background: var(--paper-alt); border-top: var(--fig-rule-strong); }

/* ── 14 · METER — a segmented gauge, no number needed ────────────────── */

.meter-set { display: grid; grid-template-columns: 1fr; gap: 0.6rem; }

.meter-row { min-width: 0; }

.meter-row .meter-label {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.75rem;
  justify-content: space-between;
  margin-bottom: 0.3rem;
  color: var(--ink);
}

.meter-row .meter-label > * { min-width: 0; }

.meter-cells { display: flex; gap: 0.2rem; }

.meter-cells span {
  flex: 1 1 0;
  min-width: 0;
  height: 0.7rem;
  border: var(--fig-rule-strong);
  background: var(--paper);
}

.meter-cells span.is-on { background: var(--ink); border-color: var(--ink); }

/* ── 15 · LADDER — stages that only go one way ───────────────────────── */

.ladder { display: grid; grid-template-columns: 1fr; gap: 0; }

.ladder li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.75rem;
  align-items: baseline;
  min-width: 0;
  padding: 0.7rem 0.875rem;
  border: var(--fig-rule);
  border-bottom: 0;
  background: var(--paper);
}

.ladder li:last-child { border-bottom: var(--fig-rule); }
.ladder li > * { min-width: 0; }
.ladder .fig-key { flex: 0 0 auto; }
.ladder strong { font-weight: 600; color: var(--ink); }
.ladder p { color: var(--ink-secondary); }

/* Each rung sits a little further in than the one below it. Four is the most a
   320px column can carry before the top rung has no room left to read. */
.ladder li:nth-child(2) { margin-left: 0.5rem; }
.ladder li:nth-child(3) { margin-left: 1rem; }
.ladder li:nth-child(4) { margin-left: 1.5rem; }
.ladder li:nth-child(5) { margin-left: 2rem; }
.ladder li.is-top { border: 2px solid var(--ink); background: var(--paper-alt); }

/* ── 16 · SPLIT — where a fixed thing actually goes ──────────────────── */

.split-rail {
  display: flex;
  min-width: 0;
  border: var(--fig-rule-strong);
}

.split-rail > span {
  flex: 0 0 var(--v, 25%);
  min-width: 0;
  height: 1.25rem;
  border-left: var(--fig-rule-strong);
  background: var(--paper);
}

.split-rail > span:first-child { border-left: 0; }
.split-rail > span.is-heavy { background: var(--ink); }
.split-rail > span.is-mid {
  background: repeating-linear-gradient(135deg, var(--ink) 0 2px, var(--paper) 2px 6px);
}

.split-key { display: grid; grid-template-columns: 1fr; gap: 0.3rem; }

.split-key li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.75rem;
  justify-content: space-between;
  min-width: 0;
  color: var(--ink-secondary);
}

.split-key li > * { min-width: 0; }
.split-key strong { color: var(--ink); font-weight: 600; }

@media (min-width: 48em) {
  .split-key { grid-template-columns: 1fr 1fr; gap: 0.3rem 1.5rem; }
}

/* ── 17 · HUB — one centre, everything hanging off it ────────────────── */

.hub-centre {
  padding: 0.8rem 0.875rem;
  border: 2px solid var(--ink);
  background: var(--paper-alt);
  text-align: left;
}

.hub-centre strong { display: block; font-weight: 600; color: var(--ink); }
.hub-centre p { color: var(--ink-secondary); }

.hub-spokes {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--fig-gap);
}

.hub-spoke {
  position: relative;
  min-width: 0;
  padding: 0.6rem 0.875rem 0.6rem 1.4rem;
  border: var(--fig-rule);
  background: var(--paper);
}

.hub-spoke::before {
  content: "";
  position: absolute;
  left: 0.55rem;
  top: 1rem;
  width: 0.45rem;
  border-top: var(--fig-rule-strong);
}

.hub-spoke strong { display: block; font-weight: 600; color: var(--ink); }
.hub-spoke p { color: var(--ink-secondary); }

@media (min-width: 48em) {
  .hub-spokes { grid-template-columns: 1fr 1fr; }
}

/* ── 18 · QUADRANT — two axes, things placed on them ─────────────────── */

.quad-plot {
  position: relative;
  min-height: 15rem;
  border-left: var(--fig-rule-strong);
  border-bottom: var(--fig-rule-strong);
  /* One crosshair through the middle. A tiled `50% / 50%` gradient draws the
     line twice — at the quarter and three-quarter marks — which reads as a
     four-column grid rather than as two axes. */
  background:
    linear-gradient(to right, transparent calc(50% - 0.5px), var(--border) calc(50% - 0.5px),
      var(--border) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
    linear-gradient(to bottom, transparent calc(50% - 0.5px), var(--border) calc(50% - 0.5px),
      var(--border) calc(50% + 0.5px), transparent calc(50% + 0.5px));
}

/* From 48em up, a point is placed by coordinate: --x from the left, --y from the
   BOTTOM, so it reads like a graph rather than like a screen.

   THERE IS DELIBERATELY NO `max-width` HERE. One used to say `max-width: 45%`,
   which never applied — `.fig p` sets `max-width: none` at (0,1,1) and beats a
   bare `.quad-point` at (0,1,0) — and restoring it is NOT the fix for anything.
   Measured: with 45% live the boxes wrap to three and four lines and a `y-80`
   point climbs out of the top of the 15rem plot, because a box grows upward
   from its own anchor. Wide boxes at these widths measure clean (14 posts ×
   768/1024/1280px, zero overlaps), so the desktop layout is left exactly as it
   renders. What was actually broken was the phone, and that is fixed below by
   changing the layout rather than by squeezing the boxes. */
.quad-point {
  position: absolute;
  left: var(--x, 50%);
  bottom: var(--y, 50%);
  padding: 0.2rem 0.4rem;
  border: var(--fig-rule-strong);
  background: var(--paper-card);
  color: var(--ink);
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  overflow-wrap: anywhere;
}

.quad-point.is-us { background: var(--ink); color: var(--paper); border-color: var(--ink); }

.quad-axes {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  justify-content: space-between;
}

/* ── 18a · THE QUADRANT ON A PHONE — coordinates become cells ─────────────

   WHY. Below 48em the plot is ~242px wide (320px screen) and a point's box is
   sized by its words, so a left-column label simply grows across the crosshair
   and the right-column point is painted on top of it. Measured before this
   block: four of the fourteen posts using the family overlapped at 320px, one
   of them at 390px too, and one point sat 25px above the top of its own plot.
   No gate catches it — nothing overflows the page, so it is not a finding; it
   is just two labels stacked on each other.

   THE FIX IS A LAYOUT, NOT A WIDTH. Absolute positioning has no way to know
   another box is there. Below 48em the plot becomes a 2×2 grid, which is what
   the family has always meant, and the boxes cannot overlap by construction.

   REGISTRATION IS STRUCTURAL. The crosshair is painted at 50% of the padding
   box and the column gap is centred in the same box, so the gap sits exactly on
   the crosshair at every width — the line and the cell boundary are the same
   line and cannot drift. The four boxes stretch to their cell, so all four are
   the same width whatever their words say.

   PLACEMENT COMES FROM THE COORDINATE, NEVER FROM DOCUMENT ORDER. Across the
   fourteen posts the four points are written in five different orders, so a
   naive `nth-child` grid would move points into the wrong quadrants and quietly
   change what those posts claim. The rules below read the same `x-*`/`y-*`
   class that positions the point on desktop: x below 50 is the left half, y at
   50 and above is the upper half (matching `bottom:`, which grows upward). A
   point carrying neither class auto-places, which is the old fallback centre.  */
@media (max-width: 47.999em) {
  .quad-plot {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 0.4rem;
    padding: 0.4rem;
  }

  .quad-point {
    position: static;
    align-self: center;
    max-width: 100%;
  }

  .quad-point.x-10, .quad-point.x-20, .quad-point.x-25, .quad-point.x-30,
  .quad-point.x-35, .quad-point.x-40, .quad-point.x-45 { grid-column: 1; }

  .quad-point.x-50, .quad-point.x-55, .quad-point.x-60, .quad-point.x-65,
  .quad-point.x-70, .quad-point.x-75, .quad-point.x-80, .quad-point.x-85,
  .quad-point.x-90 { grid-column: 2; }

  .quad-point.y-50, .quad-point.y-55, .quad-point.y-60, .quad-point.y-65,
  .quad-point.y-70, .quad-point.y-75, .quad-point.y-80, .quad-point.y-85,
  .quad-point.y-90 { grid-row: 1; }

  .quad-point.y-10, .quad-point.y-20, .quad-point.y-25, .quad-point.y-30,
  .quad-point.y-35, .quad-point.y-40, .quad-point.y-45 { grid-row: 2; }
}

/* ── 19 · FUNNEL — what survives each stage ──────────────────────────── */

.funnel { display: grid; grid-template-columns: 1fr; gap: 0.3rem; }

.funnel li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.75rem;
  align-items: baseline;
  justify-content: space-between;
  width: var(--v, 100%);
  /* A stage narrowed to 40% of the reading column is ~325px on a laptop, which
     is enough for a label on two lines. Below that the text towers and the
     figure reads as broken, so the narrowing stops at 16rem however small the
     --v says. `min()` keeps it honest on a phone, where 16rem is the column. */
  min-width: min(100%, 16rem);
  max-width: 100%;
  padding: 0.6rem 0.875rem;
  border: var(--fig-rule);
  background: var(--paper);
  box-sizing: border-box;
}

.funnel li > * { min-width: 0; }
.funnel strong { font-weight: 600; color: var(--ink); }
.funnel li.is-out { background: var(--paper-alt); border-color: var(--ink); }

/* A funnel narrowing to 20% of a 272px phone column is 54px and unreadable, so
   below 48em every stage is full width and the narrowing is carried by the
   labels instead. The shape is a wide-screen affordance, not the information. */
@media (max-width: 47.999em) {
  .funnel li { width: 100%; }
}

/* ── 20 · CHECKLIST — a scorecard that says yes or no ────────────────── */

.checkset {
  border: var(--fig-rule);
  background: var(--paper);
}

/* THE ROW IS A FLEX CONTAINER, SO ITS TAIL TEXT IS AN ANONYMOUS FLEX ITEM.
   `.checkset li > *` below reaches the <strong>; nothing can reach the bare text
   after it, so `min-width: 0` cannot apply to it and the row's minimum width is
   the SUM of the marker, the <strong>'s min-content and the tail's min-content.
   `overflow-wrap: break-word` does not lower a min-content width at all —
   `anywhere` is the only value that does, and on the container is the only place
   it can be written. Measured on post 17: a row 275px wide inside a 212px box at
   a 32px reader font, with no single word wider than 191px. */
.checkset li {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  min-width: 0;
  overflow-wrap: anywhere;
  padding: 0.5rem 0.75rem;
  border-top: var(--fig-rule);
  color: var(--ink-secondary);
}

.checkset > li:first-child { border-top: 0; }
.checkset li > * { min-width: 0; }

.checkset li::before {
  content: "";
  flex: 0 0 auto;
  width: 0.7rem;
  height: 0.7rem;
  margin-top: 0.32em;
  border: var(--fig-rule-strong);
  background: var(--paper);
}

.checkset li.is-yes::before { background: var(--ink); border-color: var(--ink); }
.checkset li.is-part::before {
  background: repeating-linear-gradient(135deg, var(--ink) 0 2px, var(--paper) 2px 4px);
}
.checkset li strong { color: var(--ink); font-weight: 600; }

/* ── A checklist row stacks below 48em — it does not column ───────────────

   `overflow-wrap: anywhere` above stops the row *overflowing*, but on a narrow
   column it leaves the real defect untouched: two flex items sharing ~160px
   become two towers about eight characters wide. Measured before this block, on
   a 320px phone at the DEFAULT font, `"Work less."` set as `"W / or / k / les /
   s."` — five lines, broken mid-word — and at a 32px reader font the same rows
   ran 1,000 to 2,200px tall at roughly one letter per line. **No gate sees any
   of it**, because nothing overflows the page: the row just grows downward.

   So below 48em the row stops being a flex container. The marker moves into a
   left gutter, the <strong> takes its own full-width line, and the description
   flows underneath as ordinary text — which also removes the ragged boundary
   the columns produced, where each row's description started at a different x.
   Because the breakpoint is in `em`, a reader at a 32px default font gets this
   layout at 1280px too, which is exactly when they need it.

   The gutter is a `min()` for the same reason the filetree indent is: a flat
   2rem triples at a 32px default and eats most of a 320px column. The vw term
   equals the rem term at 320px on the default font, so nothing changes for
   anyone who hasn't raised it.                                              */
@media (max-width: 47.999em) {
  .checkset li {
    display: block;
    position: relative;
    /* The gutter is COMPUTED from the marker rather than guessed, so the two can
       never drift: offset + marker + a gap, each term capped the same way. A
       flat 2rem here put the marker 2px on top of the label's first character at
       a 32px reader font, because the marker grew with the root and the gutter
       had already hit the vw cap. */
    padding-left: calc(min(0.75rem, 3.75vw) + min(0.7rem, 3.5vw) + 0.4rem);
  }

  .checkset li::before {
    position: absolute;
    left: min(0.75rem, 3.75vw);
    /* Centre the square on the first line, computed rather than eyeballed: the
       row's padding, then half of whatever the line box has left over once the
       square is in it. Written with the leading token and the same capped
       square size, so it stays centred at any reader font — a fixed em nudge
       drifted 9px low by 32px, because the line grows with the font and the
       capped square does not. */
    top: calc(0.5rem + (1em * var(--leading-normal) - min(0.7rem, 3.5vw)) / 2);
    width: min(0.7rem, 3.5vw);
    height: min(0.7rem, 3.5vw);
    margin-top: 0;
  }

  .checkset li strong { display: block; margin-bottom: 0.1rem; }
}

/* ── Narrow columns: the "label left, readout right" rows stack ───────────
   Every figure family that puts a name on the left and a small readout on the
   right is a wrapping flex row. `flex-wrap` alone is not enough on a phone at a
   raised reader font: the two items each still claim their max-content width
   before the line breaks, and on a 216px inner column that is how the stack
   figure came to be 355px wide. Below 48em they simply become two lines.
   Above it, the wide arrangement is unchanged.                              */
@media (max-width: 47.999em) {
  .stack-layer,
  .loop-station,
  .filetree li,
  .spec-list > div,
  .split-key li,
  .funnel li,
  .ladder li,
  .canvas-node,
  .bar-row .bar-label,
  .meter-row .meter-label,
  .curve-axes,
  .quad-axes,
  .matrix-axes {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
  }
}

/* ==========================================================================
   PLOT MODES — the graph vocabulary, ported from the Instagram figure library
   ==========================================================================

   WHY. `fig-curve` could draw exactly one thing: two smooth lines on a wash.
   Ten posts in, every graph on the blog was starting to look like the same
   graph — which is the identical fault the owner called out on the carousels
   in July 2026 ("if there is only one type of graph you can make, then all my
   carousels will have the same graph"). The Instagram system answered it with
   eight plot modes; this is the same answer for the website.

   WHAT CROSSES OVER AND WHAT DOESN'T. The *shapes* cross over — column, step,
   scatter, band, stacked, small multiples. The Instagram sheet's other
   apparatus does NOT: no signal ink, no third typeface, no registration
   corners, no run strip. The website stays fully monochrome warm paper, so
   emphasis here is carried by fill weight and hairlines, never by hue.

   TWO RULES BORROWED INTACT, because they are what stop a fill reading as a
   pastel cloud rather than an engineering plot:
     1. EVERY FILLED REGION HAS ITS BOUNDARY DRAWN as a hairline.
     2. NO NUMBERS ON EITHER AXIS, EVER. These show a shape, not a measurement,
        and each figure says so in its .fig-axis line.

   SYMMETRY IS STRUCTURAL, NOT EYEBALLED. Columns sit at (i+0.5)/n of the
   viewBox and their labels are flex children at `flex: 1 1 0`, so the label
   grid and the column grid are the same grid — they cannot drift apart at any
   width or any reader font size.
 * ------------------------------------------------------------------ */

/* ── Columns — a value per category, evenly spaced ────────────────────── */

.curve-svg .plot-col { fill: var(--paper-alt); stroke: var(--ink-tertiary); stroke-width: 1; vector-effect: non-scaling-stroke; }
.curve-svg .plot-col.is-lead { fill: var(--ink); stroke: var(--ink); }

/* The category strip under a column plot. Each label is an equal-width flex
   child centred on its own column, which is why it stays registered. */
.plot-cats {
  display: flex;
  margin-top: 0.35rem;
  padding-left: 0.25rem;   /* matches .curve-plot's own left padding */
}

.plot-cats span {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  overflow-wrap: anywhere;
}

.plot-cats span.is-lead { color: var(--ink); font-weight: 600; }

/* ── Scatter — a cloud, and the one that matters ──────────────────────── */

.curve-svg .plot-dot { fill: var(--paper-card); stroke: var(--ink-tertiary); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.curve-svg .plot-dot.is-lead { fill: var(--ink); stroke: var(--ink); }

/* ── Band — a range and its median ────────────────────────────────────── */

.curve-svg .plot-band { fill: var(--paper-alt); stroke: none; }
.curve-svg .plot-edge {
  fill: none;
  stroke: var(--ink-tertiary);
  stroke-width: 1;
  stroke-dasharray: 4 3;
  vector-effect: non-scaling-stroke;
}

/* ── Stacked — layers, each sitting on the one below ──────────────────── */

/* Three tones of one ink. Monochrome emphasis: the layer that carries the
   argument is the darkest, and every boundary is drawn. */
.curve-svg .plot-layer { fill: var(--ink); opacity: 0.08; }
.curve-svg .plot-layer.is-mid { opacity: 0.16; }
.curve-svg .plot-layer.is-lead { opacity: 0.30; }
.curve-svg .plot-bound { fill: none; stroke: var(--ink); stroke-width: 1; vector-effect: non-scaling-stroke; }

/* ── The mark rule — where something changed ──────────────────────────── */

/* The only annotation this system allows on a plot, and usually the most
   useful thing on it. */
.curve-svg .plot-mark {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1;
  stroke-dasharray: 3 4;
  vector-effect: non-scaling-stroke;
}

.plot-note {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.35rem;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink);
}

.plot-note::before {
  content: "";
  flex: 0 0 auto;
  width: 0;
  height: 0.85em;
  margin-top: 0.1em;
  border-left: 1px dashed var(--ink);
}

/* ── 21 · PLOTS — small multiples ─────────────────────────────────────── */

/* Three little plots that share an axis meaning. Argues: the same shape
   happens (or doesn't) across several cases. One column on a phone, three
   from 48em, and every cell is the same size — so the comparison is between
   the shapes and nothing else. */

.plots-set {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--fig-gap);
}

.plots-cell { min-width: 0; }

.plots-cell > .fig-key {
  display: block;
  margin-bottom: 0.3rem;
  padding-bottom: 0.25rem;
  border-bottom: var(--fig-rule);
}

.plots-cell .curve-plot { padding: 0.25rem 0 0 0.25rem; }

.plots-cell .plot-caption {
  margin-top: 0.3rem;
  font-family: 'Syne', sans-serif;
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--ink-secondary);
}

@media (min-width: 48em) {
  .plots-set { grid-template-columns: repeat(3, 1fr); }
}
