/* ============================================================
   VISIGENCE: foundation
   Fonts, tokens, reset, display type, buttons, header.

   Loaded first and alone in defining the page's vocabulary: the four
   stylesheets after it (experience, hero, work, pipeline, editorial) are
   component sheets and declare no tokens of their own.
   ============================================================ */

/* ---------- Fonts ---------- */
@font-face {
  font-family: 'Inter';
  src: url('../assets/fonts/Inter-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('../assets/fonts/Inter-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('../assets/fonts/Inter-SemiBold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Bebas Neue';
  src: url('../assets/fonts/BebasNeue-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('../assets/fonts/JetBrainsMono-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('../assets/fonts/JetBrainsMono-Medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* ---------- Tokens ----------

   ONE palette, and it carries the argument the site is making.

   The studio does two things that used to live in two repositories: it
   reconstructs (reads a photograph and infers a volume) and it builds
   (turns that inference into a system someone can ship). The page keeps those
   two readable as two colours, and never mixes them:

     BLUE  (--signal, #3b86ff)  what the machine SEES.
           The point cloud, the reconstruction, the generated worlds, every
           dot that is inferred rather than authored. It is the colour of
           evidence being gathered, so it is allowed to be everywhere at once,
           a hundred thousand of them at a time.

     CYAN  (--cyan-2, #22E6FF)  what the studio BUILDS.
           Connectors, activation, focus, the pipeline reaching production.
           Authored, deliberate, and deliberately scarce: at most three
           full-strength cyan elements in any one viewport.

   --signal-hot (#63d8ff) is the one place they meet, which is why it reads as
   the hinge of the page: the moment inference becomes a decision.
   ---------------------------------------------------------------------- */
:root {
  /* ---------- Ground ----------
     One black for the page, arrived at from both sides: the reconstruction
     half sat on #02050c and the engineering half on #020507. Splitting the
     difference keeps the point cloud's blue from going purple against a
     neutral ground, and keeps cyan from going green against a navy one. */
  --ink: #02060b;
  --ink-2: #050d18;
  --surface: #081326;
  --surface-2: #0a1a31;
  --bg-0: var(--ink);
  --bg-1: #05090f;
  --bg-2: #071015;
  --surface-1: #08141A;

  /* ---------- Type ---------- */
  --bone: #eaf3ff;
  --bone-2: #c9dcf5;
  --text-1: var(--bone);
  --text-2: #9fb2c6;
  /* Was #5f7285: 3.5:1 over the page black, under the 4.5:1 floor for the
     small mono captions this token is used at. One step brighter clears it
     without changing the tier's role. */
  --text-3: #6d8093;
  --ice: var(--bone);
  --fog: rgba(202, 224, 255, .76);
  --mist: rgba(176, 204, 246, .62);
  --dust: rgba(186, 214, 252, .62);
  --metal-a: #e9eff2;
  --metal-b: #9fadb4;

  /* ---------- Blue: what the machine sees ---------- */
  --signal: #3b86ff;
  --signal-deep: #2a69d8;
  --signal-hot: #63d8ff;
  --blue: var(--signal);
  --blue-deep: var(--signal-deep);
  --cyan: var(--signal-hot);

  /* ---------- Cyan: what the studio builds ---------- */
  --cyan-1: #00D9F5;
  --cyan-2: #22E6FF;
  --cyan-3: #66F2FF;
  --cyan-dim: rgba(34, 230, 255, 0.14);

  /* ---------- Rules and hairlines ----------
     The reconstruction half drew its rules in blue and the engineering half
     in grey; both survive, because a rule around inferred material and a rule
     around authored material are not the same rule. */
  --line: rgba(122, 173, 255, .22);
  --line-soft: rgba(122, 173, 255, .105);
  --paper-line: rgba(122, 173, 255, .18);
  --hair: var(--line);
  --hair-soft: var(--line-soft);
  --line-1: rgba(154, 174, 190, 0.16);
  --line-cyan: rgba(34, 230, 255, 0.28);

  /* ---------- Layered blacks ----------
     Never one universal black. Depth has to be legible even where no cyan is
     present, and that only works if surfaces sit at different black levels. */
  --black-abs: #000103;   /* the void behind everything */
  --black-near: var(--ink);  /* page ground */
  --black-blue: #040d17;  /* atmospheric mid-ground */
  --black-graphite: #0a1218; /* solid machined mass */
  --black-glass: rgba(9, 20, 27, 0.55);   /* smoked panel interior */
  --black-glass-deep: rgba(4, 10, 14, 0.82);
  --black-reflect: rgba(120, 160, 176, 0.05); /* what a dark surface returns */

  /* ---------- Cyan, three levels ----------
     L1 ambient: environmental tone you should not consciously notice.
     L2 structural: connectors, labels, boundaries that carry meaning.
     L3 signal: bright, and deliberately scarce, reserved for activation,
     connection, transmission, focus, and reaching production. Budget: at most
     three L3 elements visible in any one viewport. */
  --cyan-l1: rgba(0, 150, 190, 0.07);
  --cyan-l1-strong: rgba(0, 160, 200, 0.14);
  --cyan-l2: rgba(34, 230, 255, 0.24);
  --cyan-l2-strong: rgba(34, 230, 255, 0.42);
  --cyan-l3: #2ee9ff;
  --cyan-l3-glow: rgba(34, 230, 255, 0.55);

  /* ---------- Material recipes ----------
     These replace "transparent dark rectangle + 1px cyan outline" everywhere.
     Glass reads as glass because it has an edge that catches light, an
     interior darker than its edge, and a shadow it casts into itself. */
  --edge-metal: rgba(168, 190, 200, 0.14);
  --edge-metal-lit: rgba(198, 220, 230, 0.30);

  --glass-thin:
    inset 0 1px 0 rgba(226, 240, 246, 0.07),
    inset 0 -1px 0 rgba(0, 0, 0, 0.5),
    inset 0 14px 26px -18px rgba(0, 0, 0, 0.9),
    0 10px 26px rgba(0, 0, 0, 0.45);

  --glass-thick:
    inset 0 1px 0 rgba(232, 246, 252, 0.10),
    inset 1px 0 0 rgba(198, 224, 234, 0.05),
    inset -1px 0 0 rgba(0, 0, 0, 0.45),
    inset 0 -1px 0 rgba(0, 0, 0, 0.6),
    inset 0 22px 40px -26px rgba(0, 0, 0, 0.95),
    inset 0 -30px 50px -40px rgba(0, 170, 205, 0.16),
    0 26px 60px rgba(0, 0, 0, 0.58),
    0 2px 0 rgba(0, 0, 0, 0.4);

  /* Mass sitting on a surface: contact shadow, tight, fast falloff. */
  --plinth-shadow:
    0 1px 0 rgba(226, 240, 246, 0.05),
    0 18px 34px -20px rgba(0, 0, 0, 0.9),
    0 40px 70px -40px rgba(0, 0, 0, 0.8);

  --specular-line: linear-gradient(
    90deg,
    transparent 0%,
    rgba(198, 222, 232, 0.02) 12%,
    rgba(226, 244, 250, 0.26) 42%,
    rgba(198, 222, 232, 0.04) 68%,
    transparent 100%
  );

  --reflection-fade: linear-gradient(
    180deg,
    rgba(120, 160, 176, 0.10),
    rgba(120, 160, 176, 0.03) 40%,
    transparent 78%
  );

  --font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'Bebas Neue', 'Arial Narrow', 'Roboto Condensed', sans-serif;
  --font-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;

  --ease-ui: cubic-bezier(0.2, 0.8, 0.2, 1);
  /* The scene layer's own easings: `out` for anything the reader triggers,
     `soft` for anything the scroll drives. Kept distinct from --ease-ui, which
     is an interface curve and is faster on purpose. */
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-soft: cubic-bezier(.32, .72, 0, 1);
  --t-micro: 160ms;
  --t-panel: 240ms;
  --t-section: 600ms;

  /* One gutter for the whole page.

     These were two fitted numbers: reference1 insets its header band (brand at
     x=47) less than its section bodies (headline at x=72), and the split was
     right while the hero was that composition. It is not that composition any
     more: the hero is now a full-bleed reconstruction whose copy column is set
     by --gutter, and a header on a different inset from the copy directly under
     it reads as a misalignment rather than as a hierarchy. Both now resolve to
     the scene layer's gutter, so the wordmark, the headline, every section
     eyebrow and the footer all start on one line. */
  --pad-x: var(--gutter);
  --pad-section: var(--gutter);
  /* Fitted to reference1: brand mark bbox y[32..90], nav cap y[54..70] and
     the status label y[55..67] all centre on y=62, so the header band is
     124px tall and centred, not 76. At 76 the nav rendered at y[33..46],
     21px high and visibly clipped to the top of the frame. */
  --header-h: 124px;

  /* The scene layer measures its own margin from the window rather than from
     the header band, because a full-bleed artwork is not inset the way a
     column of type is. Both are kept here so one file answers "how far in
     does anything start". */
  --gutter: clamp(24px, 4.1vw, 76px);
  --content: 1600px;
}

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

/* The hidden attribute must beat any display rule a component sets on itself,
   otherwise `display: grid` silently un-hides fallbacks and dialogs. */
[hidden] { display: none !important; }

html {
  scroll-behavior: smooth;
  /* Exactly the header band, no arbitrary margin on top of it: reference2 puts
     the first line of section content at y=125 against a 124px header, so an
     anchored section's content should come to rest immediately below it. */
  scroll-padding-top: var(--header-h);
}

body {
  margin: 0;
  background: var(--black-near);
  color: var(--text-1);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3, p, ul, ol, figure, pre { margin: 0; }
ul, ol { padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
img, svg, canvas { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }

::selection { background: rgba(0, 217, 245, 0.35); color: var(--text-1); }

:focus-visible {
  outline: 2px solid var(--cyan-2);
  outline-offset: 3px;
  border-radius: 2px;
}
/* `<main>` carries `tabindex="-1"` so the skip link actually lands focus on it,
   Safari does not move focus to a target that cannot hold it, which made the
   skip a no-op there. It is a landing, not a control, and the rule above would
   otherwise draw a 2px ring around the entire document at the moment the
   reader skips into it. The confirmation they need is that the next Tab is in
   the content, which is exactly what the landing buys. */
main[tabindex="-1"]:focus, main[tabindex="-1"]:focus-visible { outline: none; }

.skip-link {
  position: fixed;
  top: 10px; left: 10px;
  z-index: 200;
  padding: 10px 18px;
  background: var(--surface-2);
  border: 1px solid var(--line-cyan);
  border-radius: 6px;
  font-size: 14px;
  transform: translateY(-64px);
  opacity: 0;
  transition: transform var(--t-micro) var(--ease-ui), opacity var(--t-micro) var(--ease-ui);
}
.skip-link:focus-visible { transform: translateY(0); opacity: 1; }

/* ---------- Display typography ---------- */
/* Display type is lit metal, not gradient text: a hot top edge, a fast falloff
   into graphite, then a returning sheen low down where a bevel would catch the
   floor. Variants let one statement contrast a bright line against a dark one
   instead of every heading receiving the same treatment. */
.display {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 0.94;
  /* The references do NOT set all display type alike, so neither does this.
     Measured per-glyph width, normalised by cap height:
       reference1 hero headline    0.548
       reference2 section heading  0.320
     The hero statement is set open; section headings are set condensed and
     tight. A single tracking value on .display cannot serve both, one was
     fitted to the hero and applied globally, which stretched reference2's
     524px-wide heading to 1322px in the render. The base stays neutral and
     each treatment is fitted where it is used. */
  letter-spacing: 0.012em;
  text-transform: uppercase;
  background: linear-gradient(
    177deg,
    #ffffff 2%,
    #eef4f7 18%,
    #b9c6cd 44%,
    #7d8b93 63%,
    #9fb0b8 82%,
    #cdd9de 97%
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Bright: the line that carries the statement. */
.display--bright {
  background: linear-gradient(177deg, #ffffff 6%, #f3f8fa 46%, #d3dee3 100%);
  -webkit-background-clip: text;
  background-clip: text;
}

/* Metal: the line that recedes, so the pair reads as one lit object. */
.display--metal {
  background: linear-gradient(177deg, #b6c3cb 4%, #8b99a1 40%, #63717a 72%, #8fa0a9 100%);
  -webkit-background-clip: text;
  background-clip: text;
}

/* Cyan: metal with the studio's own light falling on it, for the wordmark.

   The face stays metal on purpose. Filling type this large with L3 cyan would
   spend the entire three-per-viewport signal budget on a single word and leave
   nothing bright for the things that are meant to mean something, a live
   connector, a focused control, the end of the pipeline. So the cyan arrives
   the way light does: hot along the top edge where the bevel catches it, gone
   by the middle of the letter, and returning as a cool sheen low down. What
   makes it unmistakably cyan rather than white is the halo it throws, which is
   the hero's own rule (see `.hero-wordmark`) rather than a colour in the face. */
.display--cyan {
  background: linear-gradient(
    177deg,
    #eafcff 2%,
    var(--cyan-3) 15%,
    #96c9d8 41%,
    #5c7c88 64%,
    #86a9b4 83%,
    #c6e9f2 98%
  );
  -webkit-background-clip: text;
  background-clip: text;
}

/* Fitted against reference2, which carries this site's exact heading string
   ("SYSTEMS. EXPERIENCES. IMPACT.") and so is a direct comparison:
     target   x[71..594]  w=524  cap height 56px
   Cap height sets the size (rendered cap/font ratio is 0.719, so 56px of cap
   asks for a 78px face). Section headings in the references are also tracked
   TIGHT, unlike the hero statement, hence the negative value here rather than
   the hero's positive one.

   An earlier fit put this at 114px on a bbox that had silently included the
   centre exhibit's glass rim, which shares the heading's row band and reads as
   a solid 40/40-occupancy block from x=680 to x=1080. Measuring inside x<600,
   where only the heading lives, is what separates them. */
.section-heading {
  font-size: clamp(38px, 5.2vw, 78px);
  letter-spacing: -0.012em;
  /* Synthetic condense. With cap height fitted the string still set 719px wide
     against the target's 524: the references' section-heading face is ~30%
     narrower per glyph than Bebas Neue, and no other display face can be
     fetched here. Negative tracking cannot close a gap that size without
     colliding letters, so the glyphs are condensed instead. Both metrics agree
     this is the better reconstruction: width lands on 524 exactly, MAE 21.908
     -> 21.459 and RMSE 49.362 -> 48.397. It scales the painted glyphs only,
     the layout box is unchanged, so nothing reflows. */
  display: block;
  transform-origin: 0 50%;
  transform: scaleX(0.73);
}

.section-intro { padding: 0 var(--pad-section); }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  justify-content: space-between;
  font-size: 15.5px;
  font-weight: 500;
  padding: 14px 22px;
  border-radius: 7px;
  border: 1px solid transparent;
  transition: background var(--t-micro) var(--ease-ui),
              border-color var(--t-micro) var(--ease-ui),
              box-shadow var(--t-micro) var(--ease-ui),
              transform var(--t-micro) var(--ease-ui);
}
.btn:active { transform: translateY(1px); }
.btn-arrow { transition: transform var(--t-micro) var(--ease-ui); }
.btn:hover .btn-arrow, .btn:focus-visible .btn-arrow { transform: translateX(4px); }

/* The page's one `.btn` is the build section's control, an illuminated
   switch cut into dark metal: the boundary is machined, the light comes from
   inside the lower half, and a cyan filament runs along the bottom edge as
   the activation indicator. Every other call to action is a `.cta`
   (experience.css), set in signal. */
.btn-outline {
  position: relative;
  overflow: hidden;
  border-color: rgba(180, 214, 226, 0.26);
  color: var(--text-1);
  padding: 16px 28px;
  min-width: 240px;
  border-radius: 6px;
  background:
    linear-gradient(180deg, rgba(28, 50, 60, 0.5), rgba(4, 11, 15, 0.85));
  box-shadow:
    inset 0 1px 0 rgba(226, 244, 250, 0.14),
    inset 0 -26px 34px -28px rgba(0, 200, 240, 0.55),
    var(--plinth-shadow);
}
.btn-outline::after {
  content: "";
  position: absolute;
  left: 18%;
  right: 18%;
  bottom: 0;
  height: 1px;
  background: var(--cyan-l3);
  box-shadow: 0 0 10px var(--cyan-l3-glow);
  opacity: 0.7;
  transition: opacity var(--t-panel) var(--ease-ui), left var(--t-panel) var(--ease-ui), right var(--t-panel) var(--ease-ui);
}
.btn-outline:hover, .btn-outline:focus-visible {
  border-color: rgba(198, 236, 246, 0.42);
  background: linear-gradient(180deg, rgba(38, 66, 78, 0.55), rgba(5, 14, 19, 0.88));
  box-shadow:
    inset 0 1px 0 rgba(226, 244, 250, 0.2),
    inset 0 -30px 40px -28px rgba(0, 210, 245, 0.75),
    var(--plinth-shadow);
}
.btn-outline:hover::after, .btn-outline:focus-visible::after { opacity: 1; left: 6%; right: 6%; }

/* ---------- Header ---------- */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  gap: 32px;
  padding: 0 var(--pad-x);
  /* No resting scrim. reference1 measures #000103 (luminance 0.92) at the top
     of the frame; this gradient was painting #020507 there, carrying about
     +3.1 luminance across the full width of the header band. The header sits
     over the hero's own darkness at rest and only earns a background once
     content is actually scrolling underneath it. */
  background: transparent;
  transition: background var(--t-panel) var(--ease-ui), height 360ms var(--ease-settle);
  /* The chrome steps back through the middle of the journey from the hero
     into the process section: `--chrome` is written by
     scripts/process/journey.js, rises as the hero yields and is 0 again by
     the time the composition has settled, so the navigation loses weight
     with the rest of the opening rather than standing at full strength over
     a page that is travelling. Never transitioned: the value is per frame.
     A hand on it brings it back at once, whatever the scroll says. */
  opacity: calc(1 - 0.55 * var(--chrome, 0));
}
.site-header:hover,
.site-header:focus-within { opacity: 1; }
/* Over content the band compacts. At rest it is fitted to the hero's own
   composition and has nothing under it but darkness; scrolled, it is a blurred
   strip standing on somebody's heading, and 124px of that is a seventh of a
   laptop screen. The nav keeps its size, only the band and the mark step
   down: so nothing the hand is aiming at moves. */
.site-header.is-scrolled {
  height: 84px;
  background: rgba(2, 6, 11, 0.86);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid rgba(154, 168, 174, 0.08);
}
.site-header.is-scrolled .brand-mark { height: 44px; }
.brand-mark { transition: height 360ms var(--ease-settle); }

.brand { display: inline-flex; align-items: center; gap: 12px; }
/* The header logo is one image now: the mark and the wordmark together,
   2.6 wide for every 1 tall, so it is sized by HEIGHT and keeps its own
   width: 58px tall at rest, the height the mark alone used to take
   (reference1 measures it at 50x58px, bbox x[47..97] y[32..90]), and 44px
   over content. Its black is keyed to alpha (tools/brand/crop.py), so what
   sits on the band is the light and not the card it was drawn on. */
.brand-mark { height: 58px; width: auto; flex: none; display: block; }

/* Centred on the viewport, not pushed to the right by the brand.
   Measured in reference1: the nav group runs x 525..1139, centre 832 against a
   viewport centre of 836: centred to within 4px. Rendered right-aligned it
   sat at x 939..1410, a +412px landmark error across the whole header band.
   Absolute centring is used rather than `margin: 0 auto` because the brand and
   the status label have unequal widths, so auto margins would centre the nav in
   the leftover space rather than on the frame. */
.site-nav {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}
/* Item gaps measured between the reference's nav runs: 66, 69, 72, 74px,
   mean ~70. At the previous 42px cap the group spanned 472px against the
   target's 610: but that was a five-item row. With Contact restored as the
   sixth item (the references carry it last), the reference gap would push the
   group into the brand below ~1400px, so the cap comes down to keep the row
   centred and clear across the whole range where centring is attempted.

   The row briefly ran to five items and the gap was cut to 2.6vw/46 to fit
   them beside the slogan. It is four again, so the reference gap is back: the
   compensation outlived the thing it compensated for, which is how a measured
   value quietly becomes an arbitrary one. */
.site-nav ul { display: flex; gap: clamp(26px, 3.4vw, 58px); }
/* Brand left, status label right, nav centred over the top of both. */
.site-nav a {
  position: relative;
  /* Reference nav cap-height measures 16px (y[54..70]); at 15px this rendered
     13px. Sampled colour of the type itself: #f0f1f1. */
  font-size: 18.5px;
  color: var(--text-1);
  padding: 8px 2px;
  transition: color var(--t-micro) var(--ease-ui);
}
/* Active state is a signal indicator, not a website underline: a small lit
   marker centred beneath the label, the way a channel reads as live on an
   instrument panel. */
.site-nav a::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 0;
  height: 4px;
  transform: translateX(-50%);
  border-radius: 1px;
  background: var(--cyan-l3);
  box-shadow: 0 0 8px var(--cyan-l3-glow);
  opacity: 0;
  transition: width var(--t-panel) var(--ease-ui), opacity var(--t-panel) var(--ease-ui);
}
.site-nav a:hover, .site-nav a:focus-visible { color: var(--cyan-3); }
.site-nav a:hover::after, .site-nav a:focus-visible::after { width: 4px; opacity: 0.55; }
.site-nav a.is-active { color: var(--text-1); }
.site-nav a.is-active::after { width: 4px; opacity: 1; }

/* Machine status label: recessed into the header, with a live indicator. */
.tech-signature {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  /* Auto left margin pushes the label to the right edge; the nav is absolutely
     centred over the top, so the header row is just brand + label. */
  margin: 0 0 0 auto;
  padding: 6px 12px 6px 11px;
  font-family: var(--font-mono);
  /* reference1 measures the label at x[1460..1607] (147px) with a 12px cap
     height; at 11px it rendered 110px wide with a 7px cap. */
  font-size: 14.5px;
  font-weight: 500;
  letter-spacing: 0.2em;
  /* reference1 renders this as plain cyan letter-spaced type on black, no
     panel, no border, no live dot. Sampled at #6ee2e7 across its brightest
     3% of pixels. The enclosure was a rendering of the idea "status label"
     rather than of the label actually in the target. */
  color: #6ee2e7;
  white-space: nowrap;
  padding: 6px 0;
}
.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 7px;
  width: 44px;
  height: 44px;
  align-items: center;
  border-radius: 8px;
  border: 1px solid var(--line-1);
  background: rgba(8, 20, 26, 0.6);
}
.nav-toggle-line {
  width: 20px; height: 2px;
  background: var(--text-1);
  transition: transform var(--t-panel) var(--ease-ui), opacity var(--t-panel) var(--ease-ui);
}
.nav-toggle[aria-expanded="true"] .nav-toggle-line:first-child { transform: translateY(4.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-line:last-child { transform: translateY(-4.5px) rotate(-45deg); }

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: rgba(2, 6, 11, 0.96);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  padding: calc(var(--header-h) + 24px) var(--pad-x) 40px;
  overflow-y: auto;
}
.mobile-menu ul { display: grid; gap: 4px; margin-bottom: 34px; }
/* Scoped to the list: the menu also carries the Start-a-project control, which
   is a .cta, not a .btn, the old :not(.btn) guard caught it and reset it into
   a 44px display-face slab that fell off the bottom of an 844px screen. The
   links themselves scale with viewport height so all six, the signature and
   the call to action share one screen on common phones. */
.mobile-menu ul a {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(30px, 4.6vh, 44px);
  letter-spacing: 0.03em;
  padding: 10px 0;
  color: var(--text-1);
  border-bottom: 1px solid var(--line-1);
  transition: color var(--t-micro) var(--ease-ui), padding-left var(--t-micro) var(--ease-ui);
}
.mobile-menu ul a:hover, .mobile-menu ul a:focus-visible { color: var(--cyan-3); padding-left: 8px; }
.mobile-menu-signature {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.3em;
  color: var(--cyan-2);
  margin-bottom: 26px;
}
.no-observer .reveal { opacity: 1; transform: none; }

/* ============================================================
   RESPONSIVE: global chrome
   ============================================================ */
/* The slogan is the header's right-hand label now, thirty characters of
   tracked mono, about 350px, and against a nav centred on the frame it has
   4px of clear air at 1280. It leaves forty pixels earlier than the nav
   un-centres, which is the last width the two do not touch at.

   That number moved to 1430 while the row ran to five items and came back
   when it went to four: re-measured with the reference gap restored, the two
   still clear each other by 27px at 1300, and the nav un-centres at 1280
   before they ever meet. So the original calibration holds and this is the
   number it always was. */
@media (max-width: 1320px) {
  .tech-signature { display: none; }
}

@media (max-width: 1280px) {
  /* Below this width the centred nav would sit on top of the brand, measured
     at 768px, where the brand runs to x=223 and a viewport-centred nav starts
     at x=131. Centring is a property of the reference's 1672px composition, so
     it is dropped rather than defended once the frame is too narrow to hold it,
     and the nav returns to the flow at the right of the header. The boundary
     moved up from 1024 when Contact joined the nav (the references carry it as
     the last item): six centred items reach the brand sooner than five did. */
  .site-nav {
    position: static;
    transform: none;
    margin-left: auto;
  }
}

/* In the right-aligned range the row shares the band with the brand, and six
   items at the reference's 18.5px run off the edge of an 834px screen, the
   capture showed "Contact" clipped mid-word. The type steps down before the
   hamburger takes over. */
@media (max-width: 1024px) {
  .site-nav a { font-size: 16px; }
  .site-nav ul { gap: clamp(16px, 2.4vw, 30px); }
}
@media (max-width: 880px) {
  .site-nav a { font-size: 15px; }
}

@media (max-width: 767px) {
  .site-nav { display: none; }
  .nav-toggle { display: flex; margin-left: auto; }
}

@media (max-width: 480px) {
  :root { --header-h: 64px; }
  /* Already compact at rest; there is nothing to step down to. */
  .site-header.is-scrolled { height: var(--header-h); }
  .site-header.is-scrolled .brand-mark { height: 58px; }
  .section-heading { font-size: clamp(38px, 11vw, 52px); }
}

/* ============================================================
   REDUCED MOTION
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* The universal animation/transition damping lives in styles/experience.css
     and only there. It used to be declared in both sheets, both on `*` and both
     `!important`, so the two could only ever fight on equal terms: experience
     loads later, so its `.18s` won every property and this file's `0.01ms` was
     never once applied. One rule, in the sheet that owns the motion. */
  .reveal { opacity: 1; transform: none; }
}

/* Variant spans inside a .display heading need their own clip context, since a
   background-clip:text ancestor does not pass the effect down to children. */
.display > .display--bright,
.display > .display--metal {
  display: inline-block;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ============================================================
   INTERACTION GRAMMAR
   Shared vocabulary for rest -> proximity -> hover -> press -> active -> exit.
   The attention field writes --prox / --px / --py onto any registered
   element; these rules turn that into material response.
   ============================================================ */
:root {
  --t-press: 90ms;
  --ease-press: cubic-bezier(0.3, 0, 0.2, 1);
  --ease-settle: cubic-bezier(0.16, 0.9, 0.25, 1);
}

/* Elements recede rather than disappear when something else takes focus, so
   the visitor never loses the collection they are looking at. */
.is-receded {
  opacity: 0.72;
  filter: saturate(0.82) brightness(0.86);
  transition: opacity 320ms var(--ease-ui), filter 320ms var(--ease-ui);
}
.is-focused {
  transition: opacity 260ms var(--ease-ui), filter 260ms var(--ease-ui);
}

/* Keyboard focus is an art-directed signal state, not a browser halo.
   It is drawn with `outline`, deliberately: several controls set their own
   box-shadow on :focus-visible, and a box-shadow ring would be silently
   overridden by them: leaving the most important control on the page with no
   keyboard signal at all. An outline cannot be overridden that way. */
:focus-visible {
  outline: 2px solid var(--cyan-l3);
  outline-offset: 3px;
  border-radius: 4px;
}
/* The accompanying glow composes through a filter, which likewise survives any
   component's own shadow stack. */
:focus-visible:not(body) {
  filter: drop-shadow(0 0 10px rgba(46, 233, 255, 0.5));
}
/* ---------- Controls ---------- */

/* Proximity: a control begins to warm before it is touched, so the visitor
   perceives the system noticing them. The build control's filament reads
   `--warm` (below); the attention field writes `--prox` on every control. */
.btn {
  --warm: var(--prox, 0);
  transition:
    background var(--t-micro) var(--ease-ui),
    border-color var(--t-micro) var(--ease-ui),
    box-shadow var(--t-micro) var(--ease-ui),
    transform var(--t-press) var(--ease-press);
}
.btn-outline::after { opacity: calc(0.45 + var(--warm, 0) * 0.55); }

/* Press: short, heavy, no rebound, a precision switch, not a bouncing pill. */
.btn.is-pressed {
  transform: translateY(1px) scale(0.995);
  transition-duration: var(--t-press);
}

/* Activation acknowledgement: the control confirms the action landed before
   the browser does anything about it. */
.btn.is-activating { animation: btn-activate 460ms var(--ease-settle); }
@keyframes btn-activate {
  0%   { box-shadow: inset 0 1px 0 rgba(255,255,255,0.55), 0 0 0 1px rgba(0,24,32,0.9), 0 0 0 0 rgba(46, 233, 255, 0.5); }
  35%  { box-shadow: inset 0 1px 0 rgba(255,255,255,0.9),  0 0 0 1px rgba(0,24,32,0.9), 0 0 0 8px rgba(46, 233, 255, 0.16); }
  100% { box-shadow: inset 0 1px 0 rgba(255,255,255,0.55), 0 0 0 1px rgba(0,24,32,0.9), 0 0 0 22px rgba(46, 233, 255, 0); }
}

/* ---------- Navigation indicator ---------- */
/* One marker that relocates through the nav rather than each item drawing and
   erasing its own underline. */
#nav-list { position: relative; }
.nav-indicator {
  position: absolute;
  left: 0;
  bottom: -2px;
  width: var(--nav-w, 18px);
  height: 2px;
  border-radius: 1px;
  background: var(--cyan-l3);
  box-shadow: 0 0 10px var(--cyan-l3-glow);
  transform: translateX(calc(var(--nav-x, 0px) - var(--nav-w, 18px) / 2));
  opacity: 0;
  transition:
    transform 420ms var(--ease-settle),
    width 420ms var(--ease-settle),
    opacity 220ms var(--ease-ui);
  pointer-events: none;
}
#nav-list[data-indicator="hover"] .nav-indicator { opacity: 0.6; }
#nav-list[data-indicator="active"] .nav-indicator { opacity: 1; }
/* The per-item dot is retired once the relocating indicator is live. */
#nav-list .site-nav a::after,
.site-nav #nav-list a::after { display: none; }

@media (prefers-reduced-motion: reduce) {
  .btn.is-activating { animation: none; }
  .nav-indicator { transition: opacity 120ms linear; }
}
