/* Kanit 800, latin subset, self-hosted (SIL OFL 1.1 - licence shipped beside
   the file at /media/fonts/OFL.txt, which the licence requires).

   Self-hosted rather than linked: `font-src 'self'` in _headers blocks Google
   Fonts outright, and loosening the CSP for one typeface is the wrong trade.
   19.4KB for one weight of one face; the body stack stays on system fonts,
   which is why only headings carry this. */
@font-face {
  font-family: "Kanit";
  font-style: normal;
  font-weight: 800;
  font-display: swap;          /* headings render immediately in the fallback */
  src: url("/media/fonts/kanit-800-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================================
   BIGBoss Systems — design system
   One stylesheet, no framework, no external fonts.

   Direction: a precision instrument, not a brochure. The company builds systems
   that other people rely on, so the site is engineered rather than decorated —
   heavy display type at tight tracking, a strict left rail every element hangs
   from, generous air, and one accent spent only where something is clickable or
   carries status.

   Type is the system stack, deliberately. Segoe UI, SF Pro and Roboto are all
   competent corporate grotesques, they render instantly, and they need no
   font-src exception in the CSP. The character comes from how it is set — scale
   jumps, weight contrast, tracking — not from an exotic face nobody can load.
   ========================================================================= */

/* ---------------------------------------------------------------- tokens -- */
:root {
  /* spacing — 4px base, everything a multiple */
  --s1: 4px;   --s2: 8px;   --s3: 12px;  --s4: 16px;  --s5: 24px;
  --s6: 32px;  --s7: 48px;  --s8: 64px;  --s9: 96px;  --s10: 128px;
  --s11: 176px;

  --page: 1240px;
  --measure: 68ch;
  --radius: 10px;
  --radius-sm: 6px;

  --font: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto,
          "Helvetica Neue", Arial, sans-serif;
  /* Display face for headings only. The fallback is the body stack, so a
     failed font load costs weight and nothing else. */
  --display: "Kanit", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto,
             "Helvetica Neue", Arial, sans-serif;

  /* One radius for the inverted panel and the section that overlaps it, so the
     pull-up can never drift out of step with the corner it is covering. */
  --panel-radius: clamp(28px, 4vw, 60px);

  /* Hero artwork sits behind the headline, so its strength is a token: the
     cutout is dark-on-light in one theme and light-on-dark in the other, and
     the same opacity would not read the same way in both. */
  --hero-art-op: .40;
  /* The cutout is a near-white ceramic hand. On the cream ground it measured
     1.54:1 against the background - barely separable, which read as washed
     out. Opacity cannot fix that: white at any opacity over cream is still
     cream. It has to be DARKENED, so light mode grades the image down.
     Measured: this takes the artwork to 2.88:1 while the headline stays at
     4.81:1, above the 4.5 floor. Dark mode needs none of this - a light
     subject already separates from a charcoal ground. */
  --hero-art-filter: brightness(.50) contrast(1.15);

  /* Measured, not picked, and the binding constraint was not the headline -
     it was --ink-muted, the commitments and the body copy. At .25 the warm
     edge put those at 3.89:1 against this ground; the headline was fine at
     11.63:1, which is exactly how a backdrop passes a check that only looks at
     headings and still fails a reader. Highest opacity where ink, ink-soft and
     ink-muted ALL clear 4.5:1 is .13 here and .22 in dark; both take a margin
     below that. */
  --glow-op: .12;
  --mono: ui-monospace, "Cascadia Mono", SFMono-Regular, Consolas,
          "Liberation Mono", monospace;

  /* LIGHT — the default definition. Warm paper, not clinical white. */
  --ground:      #F7F6F3;
  --surface:     #FFFFFF;
  --raised:      #EDEBE5;
  --ink:         #14171A;   /* 15.8:1 on ground */
  --ink-soft:    #3D4650;   /*  9.1:1 on ground */
  --ink-muted:   #5A646F;   /*  5.9:1 on ground */
  --rule:        #DDD9D0;
  --rule-strong: #C4BEB2;
  --accent:      #7A5C00;   /* dark gold — 6.4:1 on ground, passes AA for text */
  --accent-soft: #F0E6C8;
  --on-accent:   #FFFFFF;

  --st-live:     #1B6B45;
  --st-building: #7A5300;
  --st-archived: #55606B;
  --st-concept:  #3F5578;

  --shadow-1: 0 1px 2px rgba(20, 23, 26, .06), 0 4px 12px rgba(20, 23, 26, .05);
  --shadow-2: 0 2px 4px rgba(20, 23, 26, .07), 0 14px 34px rgba(20, 23, 26, .10);

  color-scheme: light;
}

/* DARK — applies on system preference, unless the visitor chose light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground:      #0C0F14;
    --surface:     #141922;
    --raised:      #1C222D;
    --ink:         #F3F5F8;
    --ink-soft:    #C3CCD8;
    --ink-muted:   #939FAD;
    --rule:        #232B36;
    --rule-strong: #38414F;
    --accent:      #E3B84A;
    --accent-soft: #2A2415;
    --on-accent:   #0C0F14;

    --st-live:     #56D68C;
    --st-building: #F0B429;
    --st-archived: #9AA5B1;
    --st-concept:  #8FB2E8;

    --shadow-1: 0 1px 2px rgba(0, 0, 0, .5), 0 4px 14px rgba(0, 0, 0, .4);
    --shadow-2: 0 2px 6px rgba(0, 0, 0, .55), 0 18px 40px rgba(0, 0, 0, .5);

    color-scheme: dark;
  --hero-art-op: .24;
  --hero-art-filter: none;
  --glow-op: .20;
  }
}

/* DARK — explicit choice, wins in both directions. */
:root[data-theme="dark"] {
  --ground:      #0C0F14;
  --surface:     #141922;
  --raised:      #1C222D;
  --ink:         #F3F5F8;
  --ink-soft:    #C3CCD8;
  --ink-muted:   #939FAD;
  --rule:        #232B36;
  --rule-strong: #38414F;
  --accent:      #E3B84A;
  --accent-soft: #2A2415;
  --on-accent:   #0C0F14;

  --st-live:     #56D68C;
  --st-building: #F0B429;
  --st-archived: #9AA5B1;
  --st-concept:  #8FB2E8;

  --shadow-1: 0 1px 2px rgba(0, 0, 0, .5), 0 4px 14px rgba(0, 0, 0, .4);
  --shadow-2: 0 2px 6px rgba(0, 0, 0, .55), 0 18px 40px rgba(0, 0, 0, .5);

  color-scheme: dark;
  --hero-art-op: .24;
  --hero-art-filter: none;
  --glow-op: .20;
}

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

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

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 800;
  line-height: 1.04;
  letter-spacing: -0.035em;
  text-wrap: balance;
}
/* h1 and h2 only. h3 is 1.15-1.4rem and h4 is 1rem - at those sizes a display
   face reads as a font swap rather than as authority, and it would cost a
   second reflow for no gain. */
h1, h2 { font-family: var(--display); }
h1 { font-size: clamp(2.75rem, 7vw, 5.25rem); }
h2 { font-size: clamp(1.9rem, 4vw, 3rem); letter-spacing: -0.03em; }
h3 { font-size: clamp(1.15rem, 1.6vw, 1.4rem); letter-spacing: -0.02em; line-height: 1.2; }
h4 { font-size: 1rem; letter-spacing: -0.01em; }

p { margin: 0 0 var(--s4); }
p:last-child { margin-bottom: 0; }

a { color: inherit; text-underline-offset: 3px; text-decoration-thickness: 1px; }
a:hover { text-decoration-color: var(--accent); }

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

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

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

.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--accent); color: var(--on-accent);
  padding: var(--s3) var(--s5); z-index: 100; font-weight: 700;
}
.skip:focus { left: 0; }

/* --------------------------------------------------------------- layout --- */
.wrap {
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--s5);
}
@media (min-width: 900px) { .wrap { padding: 0 var(--s7); } }

.measure { max-width: var(--measure); }
.lede {
  font-size: clamp(1.06rem, 1.6vw, 1.3rem);
  line-height: 1.55;
  color: var(--ink-soft);
  max-width: 56ch;
}

section { padding: var(--s9) 0; }
@media (min-width: 900px) { section { padding: var(--s10) 0; } }
section + section { border-top: 1px solid var(--rule); }

/* A section head is a heading plus optional standfirst — no eyebrow on every
   section. One named kicker is a system; an eyebrow everywhere is grammar you
   did not choose. */
.section-head { margin-bottom: var(--s7); max-width: 60ch; }
.section-head p { margin-top: var(--s4); color: var(--ink-muted); }

.kicker {
  display: block;
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--s4);
}

/* --------------------------------------------------------------- header --- */
.site-header {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--ground) 90%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--rule);
}
.site-header .wrap {
  display: flex; align-items: center; gap: var(--s5);
  min-height: 66px;
}
.brand {
  font-weight: 800; font-size: 1.02rem; letter-spacing: -.02em;
  text-decoration: none; white-space: nowrap;
}
.brand b { color: var(--accent); font-weight: 800; }

.site-nav { margin-left: auto; display: flex; align-items: center; gap: var(--s5); }
.site-nav a {
  text-decoration: none; color: var(--ink-muted);
  font-size: .95rem; font-weight: 600;
  padding: var(--s2) 0;
  border-bottom: 2px solid transparent;
  transition: color .15s ease, border-color .15s ease;
}
.site-nav a:hover, .site-nav a[aria-current="page"] {
  color: var(--ink); border-bottom-color: var(--accent);
}

.theme-toggle {
  display: inline-grid; place-items: center;
  width: 38px; height: 38px;
  border: 1px solid var(--rule-strong); border-radius: 50%;
  background: transparent; color: var(--ink-muted);
  cursor: pointer; padding: 0;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--accent); background: var(--raised); }
.theme-toggle svg { width: 17px; height: 17px; }
.theme-toggle .icon-dark { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-dark { display: block; }
:root[data-theme="dark"] .theme-toggle .icon-light { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-dark { display: block; }
  :root:not([data-theme="light"]) .theme-toggle .icon-light { display: none; }
}

/* --- phone: the header stacks rather than overflowing.
   Three links plus the toggle cannot share a row with the wordmark at 375px —
   measured, not guessed: the page was 431px wide in a 375px viewport. --- */
@media (max-width: 640px) {
  .site-header .wrap {
    flex-wrap: wrap;
    min-height: 0;
    padding-top: var(--s3);
    padding-bottom: var(--s2);
    row-gap: var(--s2);
  }
  .brand { font-size: .95rem; }
  .site-nav {
    margin-left: 0;
    width: 100%;
    gap: var(--s4);
    justify-content: space-between;
  }
  .site-nav a { font-size: .9rem; }
  .theme-toggle { width: 34px; height: 34px; }
}

/* ------------------------------------------------------------------ hero -- */
.hero { padding: var(--s10) 0 var(--s9); border-bottom: 1px solid var(--rule); }

/* ---- the hero artwork ---------------------------------------------------- */
/* A human hand and a machine hand reaching for each other, which is the
   argument the whole site makes: a tool should sharpen judgement rather than
   replace it.

   The photograph is a transparent cutout, NOT a rectangular background plate.
   That is what lets one 37KB file serve both themes - it composites over
   whatever --ground currently is, instead of carrying a baked-in backdrop that
   would be a light slab in dark mode. Self-hosted because img-src is 'self'.

   The glow at the fingertips is a CSS radial-gradient rather than part of the
   photo, so it is the brand gold in both themes, costs no bytes, and cannot
   go stale if the palette changes. */
.hero-home { position: relative; isolation: isolate; overflow: hidden; }

.hero-art {
  position: absolute;
  /* Not centred. Centred put the fingertip gap at roughly x=577px on a 1280
     viewport, which is behind the headline - so the picture read as "a robot
     hand on the right" and the two hands meeting, which is the whole point,
     was hidden. Pushed right and down so the touch lands in clear space
     beside the text. */
  left: 68%;
  top: 57%;
  translate: -50% -50%;
  width: min(1680px, 122vw);
  aspect-ratio: 1536 / 349;   /* the file's own ratio - no cropping, no squash */
  z-index: -1;
  pointer-events: none;
}
.hero-art img {
  width: 100%; height: 100%;
  object-fit: contain;
  opacity: var(--hero-art-op);
  filter: var(--hero-art-filter);
}

/* The fingertip gap, measured off the file at 45.9% x 21.2% rather than
   eyeballed, so the light lands on the touch and not near it. */
.hero-spark {
  position: absolute;
  left: 45.9%; top: 21.2%;
  translate: -50% -50%;
  width: 22%; aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(closest-side,
              /* 22%, not the 38% first tried. Measured: at 38% the bloom over
                 the bright ceramic hand pulled the dark theme worst case to
                 3.93:1, under the 4.5 minimum for the headline sitting on it.
                 22% with opacity .24 measures 5.63:1. */
              color-mix(in srgb, var(--accent) 22%, transparent),
              transparent 72%);
}

@media (prefers-reduced-motion: reduce) { .hero-spark { opacity: .8; } }

/* Phone: the artwork drops behind a much shorter hero, so it is pulled down
   and softened rather than sitting across the headline. */
@media (max-width: 700px) {
  .hero-art { width: 150vw; top: 62%; }
  .hero-home { --hero-art-op: .24; }
}
@media (min-width: 900px) { .hero { padding: var(--s11) 0 var(--s10); } }
.hero h1 {
  max-width: 22ch;
  /* Deliberately smaller than the global h1. The company vision is a full
     sentence, and a full sentence at 5rem becomes a wall rather than a
     headline. */
  font-size: clamp(2rem, 4.4vw, 3.25rem);
  letter-spacing: -0.03em;
  line-height: 1.1;
}
.hero .lede { margin-top: var(--s6); }
.hero-tight { padding-bottom: var(--s8); }

/* Wave on hover: each word lifts, overshoots slightly and settles, the motion
   travelling left to right. One authored moment on the page's largest element.
   Delays use nth-child because inline `style` attributes are blocked by the CSP. */
.wave span {
  display: inline-block;
  will-change: transform;
}

@keyframes word-wave {
  0%   { transform: translateY(0); }
  28%  { transform: translateY(-0.16em); }
  58%  { transform: translateY(0.045em); }
  80%  { transform: translateY(-0.012em); }
  100% { transform: translateY(0); }
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .wave:hover span,
  .wave:focus-visible span {
    animation: word-wave 620ms cubic-bezier(.22, .68, .3, 1) both;
  }
  .wave:hover span:nth-child(1), .wave:focus-visible span:nth-child(1) { animation-delay: 0ms; }
  .wave:hover span:nth-child(2), .wave:focus-visible span:nth-child(2) { animation-delay: 45ms; }
  .wave:hover span:nth-child(3), .wave:focus-visible span:nth-child(3) { animation-delay: 90ms; }
  .wave:hover span:nth-child(4), .wave:focus-visible span:nth-child(4) { animation-delay: 135ms; }
  .wave:hover span:nth-child(5), .wave:focus-visible span:nth-child(5) { animation-delay: 180ms; }
  .wave:hover span:nth-child(6), .wave:focus-visible span:nth-child(6) { animation-delay: 225ms; }
  .wave:hover span:nth-child(7), .wave:focus-visible span:nth-child(7) { animation-delay: 270ms; }
  .wave:hover span:nth-child(8), .wave:focus-visible span:nth-child(8) { animation-delay: 315ms; }
  .wave:hover span:nth-child(9), .wave:focus-visible span:nth-child(9) { animation-delay: 360ms; }
  .wave:hover span:nth-child(10), .wave:focus-visible span:nth-child(10) { animation-delay: 405ms; }
  .wave:hover span:nth-child(11), .wave:focus-visible span:nth-child(11) { animation-delay: 450ms; }
  .wave:hover span:nth-child(12), .wave:focus-visible span:nth-child(12) { animation-delay: 495ms; }
  .wave:hover span:nth-child(13), .wave:focus-visible span:nth-child(13) { animation-delay: 540ms; }
  .wave:hover span:nth-child(14), .wave:focus-visible span:nth-child(14) { animation-delay: 585ms; }
}

/* ---- cursor-reactive headings -------------------------------------------- */
/* The particle system's behaviour without the particles: ripple.js moves these
   characters as the pointer passes. They are real characters, so the type is
   crisp at any size and nothing is ever rasterised. The transform is written
   only from the animation loop, so a heading with no JS - or in a tab that is
   never painted - simply sits still and reads normally. */
.ripple .rw { display: inline-block; white-space: nowrap; }
.ripple .rc { display: inline-block; }

.slogan {
  margin-top: var(--s8);
  display: flex; flex-wrap: wrap; gap: var(--s4);
  font-family: var(--mono);
  font-size: .78rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent); font-weight: 600;
}
.slogan span { display: inline-flex; align-items: center; gap: var(--s4); }
.slogan span::after {
  content: ""; width: 28px; height: 1px; background: var(--rule-strong);
}
.slogan span:last-child::after { display: none; }

/* ------------------------------------------------------------- divisions -- */
.grid-divisions {
  display: grid; gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
/* No accent stripe here. The accent is spent only where something is clickable
   or carries status (docs/DESIGN.md) — a gold bar across a division card is
   neither, and a 3px border under a 10px radius clips badly at the corners.
   These cards earn their distinction from type and space instead. */
.division {
  padding: var(--s6) var(--s5);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.division h3 { margin-bottom: var(--s3); }
.division p { color: var(--ink-muted); font-size: .95rem; margin: 0; }

/* ------------------------------------------------------------ flat lists -- */
.list-plain { list-style: none; padding: 0; margin: 0;
  display: grid; gap: var(--s2) var(--s5);
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.list-plain li {
  padding: var(--s3) 0; border-bottom: 1px solid var(--rule);
  font-weight: 600; font-size: .95rem;
}

/* ---- the numbered division list ----------------------------------------- */
/* Borrowed from the reference: a big numeral, the name and description beside
   it, hairline rules between. Used on /what-we-do/ where the divisions are
   meant to be read. The home page keeps .grid-divisions, because a landing
   section wants to be scanned rather than read. */
.div-list {
  list-style: none;
  counter-reset: div;
  padding: 0;
  margin: 0;
  border-top: 1px solid var(--rule);
}
.div-list li {
  counter-increment: div;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s4);
  align-items: baseline;
  padding: var(--s6) 0;
  border-bottom: 1px solid var(--rule);
}
@media (min-width: 700px) {
  .div-list li { grid-template-columns: 5.5rem 1fr; gap: var(--s7); }
}
/* The numeral is a CSS counter, NOT text in the markup. The <ol> already tells
   a screen reader this is an ordered list, so a literal "01" in the HTML would
   be announced twice; generated content is not announced at all. It also keeps
   the numbering correct on its own if a sixth division is ever added. */
.div-list li::before {
  content: counter(div, decimal-leading-zero);
  font-family: var(--display);
  font-size: clamp(2.25rem, 6vw, 4.5rem);
  font-weight: 800;
  line-height: .85;
  letter-spacing: -.05em;
  /* --ink-muted, not a faded --ink. Its contrast is already known-good in both
     themes, so the numerals do not need an exemption as decoration. */
  color: var(--ink-muted);
}
.div-list h3 { margin-bottom: var(--s3); }
.div-list p { color: var(--ink-muted); font-size: .95rem; margin: 0; }

/* ---- the inverted panel and its overlap ---------------------------------- */
/* The reference puts its services on a white panel with big rounded top
   corners and pulls the next section up over it. Here the panel is whatever
   the page is not: dark on a light page, light on a dark one.

   It works by re-declaring the palette tokens inside the panel rather than
   hardcoding a colour, so every child - rules, muted text, headings - inverts
   with it and nothing needs a special case. Hardcoding #FFF would have been a
   glaring white slab in dark mode.

   ponytail: only the ten colour tokens are inverted, not --st-* or --shadow-*.
   No status badge or raised card appears inside an inverted panel today. If one
   ever does, invert those here too rather than patching the child. */
.section-invert {
  --ground:      #0C0F14;
  --surface:     #141922;
  --raised:      #1C222D;
  --ink:         #F3F5F8;
  --ink-soft:    #C3CCD8;
  --ink-muted:   #939FAD;
  --rule:        #232B36;
  --rule-strong: #38414F;
  --accent:      #E3B84A;
  --on-accent:   #0C0F14;
  color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .section-invert { /* light panel on a dark page */
    --ground:      #F7F6F3;
    --surface:     #FFFFFF;
    --raised:      #EDEBE5;
    --ink:         #14171A;
    --ink-soft:    #3D4650;
    --ink-muted:   #5A646F;
    --rule:        #DDD9D0;
    --rule-strong: #C4BEB2;
    --accent:      #7A5C00;
    --on-accent:   #FFFFFF;
    color-scheme: light;
  }
}
:root[data-theme="dark"] .section-invert {
  --ground:      #F7F6F3;
  --surface:     #FFFFFF;
  --raised:      #EDEBE5;
  --ink:         #14171A;
  --ink-soft:    #3D4650;
  --ink-muted:   #5A646F;
  --rule:        #DDD9D0;
  --rule-strong: #C4BEB2;
  --accent:      #7A5C00;
  --on-accent:   #FFFFFF;
  color-scheme: light;
}

.section-invert {
  background: var(--ground);
  color: var(--ink);
  border-radius: var(--panel-radius) var(--panel-radius) 0 0;
}

/* The next section rides up over the panel's bottom edge, carrying its own
   rounded top. The pull-up is exactly --panel-radius, so the covered corner is
   always the corner that was drawn. */
.section-overlap {
  position: relative;
  z-index: 1;
  margin-top: calc(-1 * var(--panel-radius));
  background: var(--ground);
  border-radius: var(--panel-radius) var(--panel-radius) 0 0;
}

/* `section + section` draws a hairline rule between sections. Across a rounded
   corner that reads as a crack, so both of these opt out. */
section.section-invert,
section.section-overlap { border-top: 0; }

/* ---- vision and mission -------------------------------------------------- */
/* Both statements were already in lib/company.mjs and already on the site -
   the vision is the home page headline and the mission is the four points
   below - but neither was ever LABELLED as what it is. A company states these
   in its own words, under their own names, so here they are stated.

   The four commitments keep their place directly underneath, where they now
   read correctly: they are that mission sentence's own clauses, not a
   separate list. */
/* The backdrop from BIGBoss's reference: a soft organic form lit warm along
   one edge and cool along the other, over the page's own ground.

   Drawn in CSS rather than shipped as an image. The reference was a screenshot
   of someone else's site, and a gradient is the one thing a photograph is the
   wrong tool for: this weighs nothing, stays sharp at any resolution, and
   re-tints itself for light and dark instead of being a black rectangle in one
   of them. The rim is made by laying a ground-coloured, heavily blurred copy
   over the middle of the gradient - what is left is the lit edge. */
.vm-section { position: relative; isolation: isolate; overflow: hidden; }

.vm-glow {
  position: absolute;
  left: 50%; top: 48%;
  translate: -50% -50%;
  width: min(1120px, 104vw);
  aspect-ratio: 1 / 1.02;
  z-index: -1;
  pointer-events: none;
  border-radius: 58% 42% 47% 53% / 52% 46% 55% 48%;
  background: linear-gradient(158deg,
              #FFB020 0%, #E3B84A 17%, #6B4BD6 61%, #4560E0 88%);
  opacity: var(--glow-op);
}
.vm-glow::after {
  content: "";
  position: absolute;
  inset: 9%;
  border-radius: inherit;
  background: var(--ground);
  filter: blur(46px);
}
@media (max-width: 700px) { .vm-glow { width: 150vw; } }

.vm { display: grid; gap: var(--s7); margin-bottom: var(--s8); }
@media (min-width: 860px) { .vm { grid-template-columns: 1fr 1fr; gap: var(--s8); } }

.vm-block { border-top: 2px solid var(--accent); padding-top: var(--s5); }

/* A real <h2>, so both statements sit in the document outline, but styled as
   a label - the statement itself is the thing being read, not the word above it. */
.vm-label {
  font-family: var(--font);
  font-size: .74rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  line-height: 1.2; color: var(--accent);
  margin: 0 0 var(--s4);
}
/* SplitText, as CSS. The reference component is GSAP-driven - it declares
   gsap and @gsap/react - but the behaviour it describes is a per-character
   transition of opacity and translateY, fired once when the element scrolls
   into view. This site already has that trigger in motion.js, with a 2s
   failsafe, so the effect costs a transition rather than ~70KB of GSAP.

   from { opacity: 0; y: 40 } to { opacity: 1; y: 0 }, duration .6s, and
   power3.out as its cubic-bezier equivalent - the reference values.

   Scoped to .has-motion, which only motion.js adds. If that script never
   runs, no character is ever hidden. */
.has-motion .mission.mission-split li {
  opacity: 1; transform: none; transition: none;   /* the letters animate, not the row */
}
.has-motion .mission.mission-split li .rc {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity .6s cubic-bezier(.165, .84, .44, 1) var(--sd, 0ms),
              transform .6s cubic-bezier(.165, .84, .44, 1) var(--sd, 0ms);
}
.has-motion .mission.mission-split li.is-in .rc {
  opacity: 1;
  transform: none;
}

.vm-statement {
  font-family: var(--display);
  font-size: clamp(1.3rem, 2.3vw, 1.85rem);
  line-height: 1.3; letter-spacing: -.02em;
  color: var(--ink); margin: 0;
  text-wrap: pretty;
}

/* --------------------------------------------------------------- mission -- */
/* The filed mission is four commitments in one sentence. Set as four lines it
   is read; set as a paragraph it is skimmed past. Two columns on desktop so it
   reads as a set rather than a queue. */
.mission {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
  grid-template-columns: 1fr;
  border-top: 1px solid var(--rule);
}
.mission li {
  padding: var(--s5) 0;
  border-bottom: 1px solid var(--rule);
  /* Down from clamp(1.35rem, 2.8vw, 2rem). At the old size four commitments
     read as four headlines competing with the mission statement above them;
     they are its clauses, so they now sit below it in the hierarchy. */
  font-size: clamp(1rem, 1.7vw, 1.3rem);
  font-weight: 700;
  letter-spacing: -0.025em;
  line-height: 1.2;
  color: var(--ink-muted);
}
.mission-verb { color: var(--ink); }

@media (min-width: 860px) {
  .mission { grid-template-columns: 1fr 1fr; column-gap: var(--s8); }
  /* Keep the hairline rhythm even across two columns. */
  .mission li:nth-child(-n + 2) { border-top: 0; }
}

/* --------------------------------------------------------- project cards -- */
/* Built to the hand sketch: hovering spins the logo, clicking opens the card
   horizontally with the information on the left and the tilted logo on the
   right, and a stripe of light travels 360 degrees around the border. */

.card-grid {
  display: grid; gap: var(--s5);
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  align-items: start;
}

.pcard {
  position: relative;
  border-radius: var(--radius);
  background: var(--surface);
  transition: transform .25s cubic-bezier(.2,.7,.3,1), box-shadow .25s ease;
}
.pcard:hover { transform: translateY(-4px); box-shadow: var(--shadow-2); }
.pcard.is-open { transform: none; box-shadow: var(--shadow-2); }

/* ---- the rotating glow -------------------------------------------------- */
/* A conic gradient spun by an animated angle. @property is what makes the angle
   animatable at all; without it the gradient would jump rather than travel.
   The card body sits on top, so only a 1.5px rim of it is ever visible. */
@property --glow-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.pcard-glow {
  position: absolute;
  inset: -1.5px;
  border-radius: calc(var(--radius) + 1.5px);
  padding: 1.5px;
  background: conic-gradient(from var(--glow-angle),
              transparent 0deg, transparent 200deg,
              var(--accent) 300deg, #fff3c4 335deg, var(--accent) 350deg,
              transparent 360deg);
  opacity: 0;
  transition: opacity .3s ease;
  pointer-events: none;
  z-index: 0;
}
.pcard:hover .pcard-glow,
.pcard:focus-within .pcard-glow,
.pcard.is-open .pcard-glow {
  opacity: 1;
  animation: glow-spin 2.6s linear infinite;
}
@keyframes glow-spin { to { --glow-angle: 360deg; } }

/* ---- the clickable face ------------------------------------------------- */
.pcard-toggle {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
}
.pcard:hover .pcard-toggle { border-color: transparent; }

.pcard-plate {
  /* An explicit height, NOT aspect-ratio. The children use height:100%, and a
     percentage height inside an aspect-ratio box is a cyclic dependency: the
     browser falls back to content height, which is the square logo, and ignores
     the ratio entirely. Measured: aspect-ratio:16/9 computed correctly and the
     plate still rendered 267px tall. A fixed height has no such cycle.
     The logo is square with object-fit: contain, so it gains side margin
     rather than being cropped or stretched. */
  height: 148px;
  overflow: hidden;              /* belt and braces: nothing escapes the plate */
  display: grid; place-items: center;
  padding: var(--s5);
  background: var(--raised);
  border-bottom: 1px solid var(--rule);
  perspective: 900px;
}

/* ---- the flip ------------------------------------------------------------ */
/* The sketch's first panel: the plate turns to reveal the logo. A single
   half-turn and back, so it reads as a flip rather than a spinner. */
.pcard-flip {
  display: grid; place-items: center;
  width: 100%; height: 100%;
  min-height: 0;                 /* a grid item will not shrink without this */
  transform-style: preserve-3d;
}

/* A keyframe, NOT a transition. `transform: rotateY(360deg)` computes to the
   identity matrix - the same value as no rotation - so a transition between
   them interpolates nothing and the logo never moves. Measured: start, middle
   and end all read matrix(1,0,0,1,0,0). Keyframes pass through the intermediate
   angles explicitly, which is the only way to spin a full turn. */
@keyframes logo-spin {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(360deg); }
}
/* max-* with auto sizing, NOT width/height 100%. With height:100% the square
   logo rendered larger than the 148px plate and spilled over the card body,
   covering the project title. Constraining by max- lets a square image fit
   inside whatever box it is given, at its own ratio, without overflow and
   without distortion. */
.pcard-flip img {
  width: auto; height: auto;
  max-width: 100%;
  /* An absolute max-height, NOT 100%. A percentage max-height resolves against
     .pcard-flip, whose own height is a percentage, and that chain does not
     resolve - so max-height was ignored, height:auto took the square ratio off
     the 219px width, and every logo rendered 219px tall inside a 99px box. It
     LOOKED correct because the plate clips overflow: each logo was silently
     losing 60px off the top and bottom. Measured, not guessed.
     100px = the 148px plate less its 2 x 24px padding. */
  max-height: 100px;
  object-fit: contain;
  border-radius: var(--radius-sm);
  backface-visibility: visible;
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .pcard:hover .pcard-flip,
  .pcard:focus-within .pcard-flip {
    animation: logo-spin .85s cubic-bezier(.35,.85,.35,1) both;
  }
}

.pcard-body { padding: var(--s5); display: flex; flex-direction: column; gap: var(--s2); }
/* Two lines, always. Titles wrap to one or two lines depending on length, which
   left cards in the same row 20px apart. Reserving both lines makes every card
   identical without needing a fixed card height. */
.pcard-title {
  font-size: .98rem; font-weight: 800; letter-spacing: -.02em;
  line-height: 1.25; color: var(--ink);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  min-height: 2.5em;
}
/* Three lines then ellipsis. As well as saving height this makes every card in
   a row identical: they ranged 461px to 530px purely because summaries differ
   in length, which read as untidy rather than deliberate. The full text is
   still on the expanded panel and the detail page. */
.pcard-sum {
  font-size: .84rem; line-height: 1.45; color: var(--ink-muted);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden;
}
.pcard-meta { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }

/* ---- the horizontal expansion ------------------------------------------- */
/* Sketch panel 2: the card takes the full row, information on the left, the
   tilted logo on the right. */
.pcard.is-open { grid-column: 1 / -1; }
.pcard.is-open .pcard-toggle {
  border-radius: var(--radius) var(--radius) 0 0;
  /* Open, the card spans the whole row - so a 1:1 plate would become a
     1144px square and a logo the size of the screen. Measured: the card grew
     to 1678px tall before this. Open, the face is a compact header instead;
     the large tilted logo is already in the panel below. */
  flex-direction: row;
  align-items: center;
  gap: var(--s5);
}
.pcard.is-open .pcard-plate {
  aspect-ratio: auto;
  width: 96px; height: 96px;
  flex: 0 0 auto;
  padding: var(--s3);
  margin: var(--s4) 0 var(--s4) var(--s4);
  border-bottom: 0;
  border-radius: var(--radius-sm);
}
.pcard.is-open .pcard-sum { display: none; }
.pcard.is-open .pcard-body { padding: var(--s4) var(--s5) var(--s4) 0; }

/* An author `display` beats the browser's built-in [hidden] { display: none },
   so setting panel.hidden did nothing at all: every card rendered permanently
   expanded at ~1386px tall. The attribute flipped, the panel never hid, and
   every check that counted the attribute passed. */
.pcard-panel[hidden] { display: none; }

.pcard-panel {
  position: relative; z-index: 1;
  display: grid;
  gap: var(--s6);
  padding: var(--s6);
  border: 1px solid var(--rule);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--surface);
  grid-template-columns: 1fr;
}
.pcard-info h3 { margin-bottom: var(--s3); }
.pcard-info p { color: var(--ink-muted); }
.pcard-panel-meta { display: flex; gap: var(--s3); align-items: center; flex-wrap: wrap; }
.pcard-info .cta { margin-top: var(--s4); }

.pcard-tilt {
  display: grid; place-items: center;
  padding: var(--s6);
  background: var(--raised);
  border-radius: var(--radius);
  perspective: 1000px;
}
.pcard-tilt img {
  width: 100%; max-width: 260px; height: auto;
  object-fit: contain;
  border-radius: var(--radius-sm);
  /* Tilted, as drawn. */
  transform: rotateY(-16deg) rotateX(7deg) rotateZ(-4deg);
  box-shadow: var(--shadow-2);
}

@media (min-width: 800px) {
  /* Information left, tilted card right - the sketch's layout. */
  .pcard-panel { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); align-items: center; }
}

@media (prefers-reduced-motion: reduce) {
  .pcard-glow { animation: none !important; }
  .pcard-flip { transition: none; }
  .pcard-tilt img { transform: none; }
}

/* ---------------------------------------------------------------- badge --- */
/* badge-live, badge-building, badge-archived and badge-concept are generated
   from STATUS in lib/schema.mjs. Only badge-live appears in the built site
   today, because every catalogued project is live - the other three are not
   dead, they are waiting on content. Do not remove them in a sweep. */
.badge {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-size: .68rem; font-weight: 800;
  letter-spacing: .1em; text-transform: uppercase;
  padding: var(--s1) var(--s3);
  border: 1px solid currentColor; border-radius: 999px;
  white-space: nowrap;
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.badge-live { color: var(--st-live); }
.badge-building { color: var(--st-building); }
.badge-archived { color: var(--st-archived); }
.badge-concept { color: var(--st-concept); }

.kind { font-family: var(--mono); font-size: .74rem; color: var(--ink-muted); }

/* ---------------------------------------------------------------- index --- */
.filters { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; margin-bottom: var(--s6); }
.filters .filters-label { font-size: .85rem; color: var(--ink-muted); margin-right: var(--s2); font-weight: 600; }
.chip {
  font: inherit; font-size: .86rem; font-weight: 600;
  padding: var(--s2) var(--s4);
  border: 1px solid var(--rule-strong); border-radius: 999px;
  background: transparent; color: var(--ink-muted);
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.chip:hover { color: var(--ink); border-color: var(--ink-muted); }
.chip[aria-pressed="true"] { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }

.index-summary { color: var(--ink-muted); font-size: .9rem; margin-top: var(--s1); }
.col-year { font-family: var(--mono); color: var(--ink-muted); font-size: .84rem; }

/* --------------------------------------------------------------- detail --- */
.detail-header { padding: var(--s9) 0 var(--s7); border-bottom: 1px solid var(--rule); }
.detail-header h1 { max-width: 16ch; font-size: clamp(2rem, 4vw, 3.1rem); }
.detail-header .lede { margin-top: var(--s5); }
.detail-meta { display: flex; gap: var(--s3); align-items: center; flex-wrap: wrap; margin-top: var(--s6); }

/* The detail page opens the way an expanded card does: information on the
   left, the tilted logo on the right. A small flat square above an oversized
   title was neither the card's language nor what was asked for. */
.detail-head {
  display: grid;
  gap: var(--s7);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .detail-head { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: var(--s8); }
}

.detail-tilt {
  display: grid; place-items: center;
  padding: var(--s6);
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  perspective: 1000px;
}
.detail-tilt img {
  width: 100%; max-width: 300px; height: auto;
  object-fit: contain;                 /* square source, square box: no crop */
  border-radius: var(--radius-sm);
  transform: rotateY(-16deg) rotateX(7deg) rotateZ(-4deg);
  box-shadow: var(--shadow-2);
}
@media (prefers-reduced-motion: reduce) {
  .detail-tilt img { transform: none; }
}

.prose { max-width: var(--measure); font-size: 1.03rem; }
.prose h2 { font-size: clamp(1.3rem, 2vw, 1.65rem); margin: var(--s8) 0 var(--s4); }
.prose blockquote {
  margin: var(--s7) 0;
  padding: var(--s5) var(--s6);
  background: var(--raised);
  border-radius: var(--radius);
  color: var(--ink-soft);
  font-size: 1.05rem;
}
.prose blockquote p:last-child { margin-bottom: 0; }
.prose strong { font-weight: 700; color: var(--ink); }

.backlink { display: inline-block; font-size: .95rem; color: var(--ink-muted); text-decoration: none; font-weight: 600; }
.backlink:hover { color: var(--accent); }
.mt-7 { margin-top: var(--s7); }
.mt-8 { margin-top: var(--s8); }
.mr-5 { margin-right: var(--s5); }
.pb-9 { padding-bottom: var(--s9); }

/* ----------------------------------------------------------------- misc --- */
.cta {
  display: inline-flex; align-items: center; gap: var(--s3);
  margin-top: var(--s6);
  padding: var(--s4) var(--s6);
  background: var(--accent); color: var(--on-accent);
  text-decoration: none; font-weight: 700; font-size: .98rem;
  border-radius: var(--radius-sm);
  transition: transform .18s ease, box-shadow .18s ease;
}
.cta:hover { transform: translateY(-2px); box-shadow: var(--shadow-2); }

.contact-list { display: grid; gap: var(--s6); margin: 0; }
.contact-list dt {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .14em;
  color: var(--ink-muted); font-weight: 700;
}
.contact-list dd { margin: var(--s2) 0 0; font-size: 1.25rem; font-weight: 600; }

.notice {
  border: 1px solid var(--rule);
  background: var(--surface);
  padding: var(--s5) var(--s6);
  border-radius: var(--radius);
  color: var(--ink-muted); font-size: .94rem;
}

.site-footer { border-top: 1px solid var(--rule); padding: var(--s8) 0; color: var(--ink-muted); font-size: .9rem; }
.site-footer .wrap { display: flex; gap: var(--s5); flex-wrap: wrap; align-items: baseline; }
.site-footer a { color: var(--ink-muted); font-weight: 600; }
.site-footer a:hover { color: var(--accent); }
.site-footer .spacer { margin-left: auto; }

/* ---- reveal on scroll ------------------------------------------------------
   Scoped to .has-motion, a class only motion.js adds. If that script fails to
   load or the browser lacks IntersectionObserver, nothing is ever hidden. That
   ordering matters: a reveal that defaults to invisible is a blank page waiting
   for a bug. */
.has-motion .section-head,
.has-motion .division,
.has-motion .pcard,
.has-motion .mission li,
.has-motion .contact-list > div,
.has-motion .div-list li,
.has-motion .vm-block,
.has-motion .prose > * {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .62s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms),
              transform .62s cubic-bezier(.16, 1, .3, 1) var(--reveal-delay, 0ms);
}
/* Each reveal selector mirrors its hiding selector exactly, plus .is-in.
   A single `.has-motion .is-in` rule was NOT enough: `.has-motion .contact-list
   > div` and `.has-motion .mission li` each carry an element selector, giving
   them specificity (0,2,1) against the reveal rule's (0,2,0). Those two stayed
   invisible forever while every pure-class target revealed correctly - which is
   exactly why testing only the cards missed it. */
.has-motion .section-head.is-in,
.has-motion .division.is-in,
.has-motion .pcard.is-in,
.has-motion .mission li.is-in,
.has-motion .contact-list > div.is-in,
.has-motion .div-list li.is-in,
.has-motion .vm-block.is-in,
.has-motion .prose > .is-in {
  opacity: 1;
  transform: none;
}

/* The card keeps its own hover transform; drift overrides it inline. */
.pcard { transform-style: preserve-3d; }

@media (prefers-reduced-motion: reduce) {
  .has-motion .section-head,
  .has-motion .division,
  .has-motion .pcard,
  .has-motion .mission li,
  .has-motion .contact-list > div,
  .has-motion .div-list li,
  .has-motion .vm-block,
  .has-motion .prose > * { opacity: 1; transform: none; transition: none; }
}

/* One authored moment: content rises once on load. Not an entrance on every
   section, and it starts from an already-visible default so a failed animation
   never hides content. */
@keyframes rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
.hero > .wrap > * { animation: rise .6s cubic-bezier(.16, 1, .3, 1) backwards; }
.hero > .wrap > *:nth-child(2) { animation-delay: .06s; }
.hero > .wrap > *:nth-child(3) { animation-delay: .12s; }
.hero > .wrap > *:nth-child(4) { animation-delay: .18s; }

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