/* ============================================================
   Portfolio — one stylesheet for the public site.

   Derived from the Claude Design export (Portfolio.dc.html): steel-blue on a
   near-black ground, Montserrat headings over Barlow, Barlow Condensed for
   the uppercase labels, and wireframe objects with "+" registration marks at
   the corners.

   No @import — the font <link> lives in the HTML head, because @import is
   render-blocking. Everything here is one file, loaded once.
   ============================================================ */

:root {
  --bg:            #131416;
  --panel:         #17181b;
  --ink:           #e7e7ea;
  --ink-2:         #d4d4d7;
  --ink-3:         #c9c9cc;
  --ink-4:         #b7b7ba;
  --ink-dim:       #98989b;

  --accent:        #749dc4;
  --accent-light:  #94bce3;
  --accent-hover:  #b5d9fd;
  --accent-deep:   #597ea3;
  --on-accent:     #11161c;

  --rule:          rgba(231, 231, 234, .1);
  --rule-2:        rgba(231, 231, 234, .16);
  --rule-3:        rgba(231, 231, 234, .24);

  --font-head: Montserrat, system-ui, sans-serif;
  --font-body: Barlow, system-ui, -apple-system, sans-serif;
  --font-cond: "Barlow Condensed", system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  --wrap: 1180px;
  --gut: 24px;
  /* app.js measures the masthead and overwrites this at runtime; the values
     here and at the two breakpoints below are the measured heights, so the
     offset is already right on the first frame. */
  --header-h: 48px;
}

* { box-sizing: border-box; }

/* A `display` rule on a component beats the browser's default [hidden] style,
   which silently leaves accordion panels and the next-case link visible on
   load. Make hidden mean hidden, everywhere. */
[hidden] { display: none !important; }

html, body { margin: 0; padding: 0; background: var(--bg); }

/* The in-page nav (#work, #contact, ...) glides instead of teleporting, and
   anchor targets stop below the sticky masthead rather than under it. Both
   properties belong on the scrolling element, which is <html>. */
html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 16px);
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  font-family: var(--font-body);
  color: var(--ink);
  min-height: 100vh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent-light); text-decoration: none; text-underline-offset: 3px; }
a:hover { color: var(--accent-hover); text-decoration: underline; }
::selection { background: rgba(116, 157, 196, .3); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

img { max-width: 100%; }

.wrap { max-width: var(--wrap); margin: 0 auto; padding-left: var(--gut); padding-right: var(--gut); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

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

   The background is a var() so an uploaded photograph can replace it:
   hydrate.js sets --band-base on the section, and the value it sets carries
   its own darkening scrim (see index.html), which is what keeps the headline
   readable over an arbitrary image. No upload, no property set, and the
   default gradient below stands. */

.band {
  --band-default: linear-gradient(162deg, #2c1e15 0%, #1a120d 55%, #261a12 100%);

  position: relative;
  /* Clips the fixed layer below to this section's box. clip-path clips every
     descendant, fixed ones included — but unlike transform, filter or contain
     it does NOT become their containing block, so the layer stays pinned to
     the viewport. That is the whole trick, and swapping this for any of those
     other properties silently turns the effect back into a scrolling one. */
  clip-path: inset(0);
  border-top: 1px solid rgba(231, 231, 234, .12);
  border-bottom: 1px solid rgba(231, 231, 234, .12);
}

/* The photograph lives on its own fixed layer rather than on
   `background-attachment: fixed`, which iOS Safari has never implemented
   properly — it was the reason this used to give up and scroll on mobile. A
   composited fixed layer is also cheaper to scroll than repainting an attached
   background every frame, so this answers the old performance worry too. */
.band::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background-image: var(--band-base, var(--band-default));
  background-size: cover;
  background-position: center;
}

.band--alt {
  --band-default: linear-gradient(162deg, #26190f 0%, #18110c 55%, #2a1c13 100%);
}

/* A flat band is a solid panel, so it wants no layer at all. */
.band--flat { background: var(--panel); }
.band--flat::before { display: none; }

/* A background pinned while content moves over it is parallax, and parallax is
   a vestibular trigger. Drop the layer back onto the section so it scrolls
   with everything else. */
@media (prefers-reduced-motion: reduce) {
  .band::before { position: absolute; }
}

/* ---------- blueprint frame ---------- */

.bp { position: relative; border: 1px solid var(--rule-2); }
.bp > .corner {
  position: absolute;
  font: 14px var(--font-mono);
  color: var(--accent-deep);
  line-height: 1;
  pointer-events: none;
}
.bp > .tl { top: -7px; left: -5px; }
.bp > .tr { top: -7px; right: -5px; }
.bp > .bl { bottom: -8px; left: -5px; }
.bp > .br { bottom: -8px; right: -5px; }
.bp--bright { border-color: rgba(231, 231, 234, .2); }
.bp--bright > .corner { color: var(--accent); }

/* ---------- page loader ----------

   The overlay dissolving IS the fade-in: content is never given opacity of its
   own, so a failed script can leave the page covered (the timeout in the head
   handles that) but can never leave it invisible with nothing to reveal it.

   Hidden unless html.is-loading is set, and only the inline script in the head
   sets it — so with JS off there is no overlay at all, just the page.
   ------------------------------------ */

.loader { display: none; }

html.is-loading .loader {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 100;                    /* over the sticky masthead (20) and nav panel (50) */
  align-items: center;
  justify-content: center;
  background: var(--bg);
  transition: opacity .45s ease;
}
html.is-loading .loader.is-done { opacity: 0; pointer-events: none; }
html.is-loading body { overflow: hidden; }

/* The logo and line are the exception, not the rule: they appear only on the
   one page per visit that the head script marks as the intro. Every other load
   — case studies, and the home page from then on — is the bare dissolve. */
.loader__inner { display: none; }
html.is-intro .loader:not(.loader--plain) .loader__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
}

/* Matches the masthead's treatment, so the mark does not change character
   between the loader and the bar it resolves into. */
.loader__logo { display: block; height: 48px; width: auto; opacity: .5; }
.loader__logo:not([src]) { visibility: hidden; }   /* until hydrate supplies the path */

/* The line: a full-width 200px rule scaled from the middle, so it opens out in
   both directions from nothing. Scale rather than width — it runs on the
   compositor instead of laying out the page on every frame. */
.loader__rule { width: 200px; height: 2px; background: var(--rule); overflow: hidden; }
.loader__rule i {
  display: block;
  height: 100%;
  background: var(--accent);
  transform: scaleX(var(--load-progress, 0));
  transform-origin: center;
  transition: transform .45s cubic-bezier(.2, .7, .3, 1);
}

@media (prefers-reduced-motion: reduce) {
  html.is-loading .loader { transition: none; }
  .loader__rule i { transition: none; }
}

@media (max-width: 640px) {
  .loader__logo { height: 36px; }   /* the line stays 200px: it fits every phone */
}

/* ---------- header ---------- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 27px;
  flex-wrap: wrap;
  padding: 14px var(--gut);
  background: rgba(19, 20, 22, .88);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--rule);
}
.masthead__brand {
  display: flex;          /* the brand is a logo OR a wordmark; centre either one */
  align-items: center;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: .04em;
  color: var(--ink);
  text-transform: uppercase;
}
.masthead__brand:hover { color: var(--ink); text-decoration: none; }

/* Height-driven: the bar sets how tall the logo is and the artwork keeps its
   own proportions, so a square mark and a wide wordmark both sit correctly. */
.masthead__logo {
  display: block;
  height: 48px;
  opacity: 0.5;
  width: auto;
  max-width: 220px;       /* a very wide wordmark cannot crowd out the nav */
  object-fit: contain;
  transition: all 0.5s;
}

.masthead__logo:hover {
  opacity: 0.8;
}
/* hydrate.js fills src, and drops the element entirely when no logo is set.
   Until then it must take up no room, or the bar is 24px taller for one frame
   on every load and the header visibly settles. */
.masthead__logo:not([src]) { display: none; }
.masthead__spacer { flex: 1; }
.masthead nav {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  font-family: var(--font-cond);
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.masthead nav a { color: var(--ink-4); }
.masthead nav a:hover { color: var(--accent-hover); text-decoration: none; }
.masthead nav a.is-accent { color: var(--accent-light); }

/* ---------- mobile nav ----------

   At 781px and below the inline nav becomes a panel that slides in from the
   left, opened by a hamburger at the top right. State lives in one place —
   data-nav on <html> — so the button, the panel and the scrim can never
   disagree about whether the menu is open. */

.nav-toggle {
  display: none;                      /* desktop: the inline nav is the nav */
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 36px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--rule-3);
  color: var(--ink);
  cursor: pointer;
}
.nav-toggle:hover { border-color: var(--accent); color: var(--accent-light); }

.nav-toggle__bars { position: relative; display: block; width: 18px; height: 12px; }
.nav-toggle__bars i {
  position: absolute;
  left: 0;
  right: 0;
  height: 1.5px;
  background: currentColor;
  transition: transform .25s cubic-bezier(.2, .7, .3, 1), opacity .15s;
}
.nav-toggle__bars i:nth-child(1) { top: 0; }
.nav-toggle__bars i:nth-child(2) { top: 5.25px; }
.nav-toggle__bars i:nth-child(3) { top: 10.5px; }

/* Hamburger folds into an X while open. */
html[data-nav="open"] .nav-toggle__bars i:nth-child(1) { transform: translateY(5.25px) rotate(45deg); }
html[data-nav="open"] .nav-toggle__bars i:nth-child(2) { opacity: 0; }
html[data-nav="open"] .nav-toggle__bars i:nth-child(3) { transform: translateY(-5.25px) rotate(-45deg); }

.nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: rgba(8, 9, 10, .62);
  backdrop-filter: blur(3px);
  opacity: 0;
  pointer-events: none;
  transition: opacity .28s ease;
}

@media (max-width: 781px) {
  :root { --header-h: 65px; }   /* the hamburger is taller than the nav text */

  .nav-toggle { display: inline-flex; }

  /* backdrop-filter makes an element a containing block for its fixed-position
     descendants — so with it on, the panel below sizes itself to the header
     (about 100px tall) instead of the viewport. Drop the blur here and use an
     opaque bar instead; at this width there is nothing behind it to see. */
  .masthead {
    backdrop-filter: none;
    background: #101114;
  }

  /* The panel is a child of the header, so within the header's own stacking
     context a positioned child paints over static siblings however high the
     header's z-index is. Lift the two things that must stay on top of it. */
  .masthead__brand,
  .nav-toggle {
    position: relative;
    z-index: 41;
  }

  .masthead nav {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    z-index: 40;
    width: min(78vw, 300px);
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 0;
    /* Clears the sticky header, which sits above the panel so the hamburger
       stays reachable to close it. */
    padding: 72px 0 24px;
    background: #101114;
    border-right: 1px solid var(--rule-2);
    box-shadow: 18px 0 44px rgba(0, 0, 0, .45);
    overflow-y: auto;
    overscroll-behavior: contain;
    font-size: 14px;
    transform: translateX(-100%);
    /* visibility keeps the closed panel out of the tab order and off screen
       readers; it flips back only after the slide-out finishes. */
    visibility: hidden;
    transition: transform .28s cubic-bezier(.2, .7, .3, 1), visibility 0s linear .28s;
  }
  .masthead nav a {
    padding: 15px 24px;
    border-bottom: 1px solid var(--rule);
    color: var(--ink-3);
  }
  .masthead nav a:first-child { border-top: 1px solid var(--rule); }

  html[data-nav="open"] .masthead nav {
    transform: none;
    visibility: visible;
    transition: transform .28s cubic-bezier(.2, .7, .3, 1), visibility 0s;
  }
  html[data-nav="open"] .masthead { z-index: 50; }
  html[data-nav="open"] .nav-scrim { opacity: 1; pointer-events: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .masthead nav, .nav-scrim, .nav-toggle__bars i { transition: none; }
}

/* ---------- type ---------- */

.kicker {
  font-family: var(--font-cond);
  font-size: 13px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--accent-light);
}
.kicker--mono {
  font: 11px var(--font-mono);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent-light);
}
.label {
  font-family: var(--font-cond);
  font-size: 13px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent-light);
}

.section { padding-block: 88px; }
.section__head {
  display: flex;
  align-items: baseline;
  gap: 20px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.section__head h2 {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: clamp(26px, 3.2vw, 38px);
  letter-spacing: -.015em;
  margin: 0;
}
.section__index {
  font-family: var(--font-cond);
  font-size: 12px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.section__intro {
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink-dim);
  max-width: 62ch;
  margin: 0 0 41px;
}

/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-cond);
  font-size: 15px;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: 14px 24px;
  border: 1px solid var(--rule-3);
  color: var(--ink);
  background: transparent;
  cursor: pointer;
}
.btn:hover { border-color: var(--accent); color: var(--ink); text-decoration: none; }
.btn--primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn--primary:hover { background: var(--accent-light); border-color: var(--accent-light); color: var(--on-accent); }
.btn--small { font-size: 13px; letter-spacing: .14em; padding: 9px 17px; }
.btn--small:hover { background: rgba(116, 157, 196, .14); border-color: var(--accent-deep); }

/* ---------- hero ---------- */

.hero { padding-block: 108px 96px; }
.hero .kicker { display: block; margin-bottom: 27px; }
.hero h1 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(34px, 5.4vw, 62px);
  line-height: 1.08;
  letter-spacing: -.02em;
  margin: 0 0 27px;
  max-width: 20ch;
  text-wrap: pretty;
}
.hero p {
  font-size: 18px;
  line-height: 1.6;
  color: var(--ink-2);
  max-width: 60ch;
  margin: 0 0 34px;
}
.hero__actions { display: flex; flex-wrap: wrap; gap: 14px; }

/* ---------- grids ---------- */

.grid { display: grid; gap: 20px; }
.grid--cases   { grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.grid--tools   { grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
.grid--shots   { grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); }

.empty-note { color: var(--ink-dim); font-size: 15px; margin: 0; }

/* ---------- case card ---------- */

.case {
  background: rgba(231, 231, 234, .018);
  padding: 27px;
  display: flex;
  flex-direction: column;
}
.case__media {
  height: 160px;
  margin-bottom: 20px;
  border: 1px solid rgba(231, 231, 234, .12);
  display: grid;
  place-items: center;
  overflow: hidden;
  background-image: repeating-linear-gradient(135deg, rgba(116, 157, 196, .14) 0 1px, transparent 1px 9px);
}
.case__media img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The image is a second route to the case study. It gets a hover affordance so
   it reads as clickable; the title link is the accessible one. */
a.case__media { cursor: pointer; transition: border-color .2s; }
a.case__media:hover { border-color: var(--accent-deep); }
a.case__media img { transition: transform .45s cubic-bezier(.2, .7, .3, 1); }
a.case__media:hover img { transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) {
  a.case__media img,
  a.case__media:hover img { transition: none; transform: none; }
}
.case__kicker {
  font-family: var(--font-cond);
  font-size: 12px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent-light);
  margin-bottom: 10px;
}
.case h3 {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 22px;
  letter-spacing: -.01em;
  margin: 0 0 10px;
}
.case h3 a { color: var(--ink); }
.case h3 a:hover { color: var(--accent-light); text-decoration: none; }
.case__summary { font-size: 15px; line-height: 1.6; color: var(--ink-4); margin: 0 0 17px; }

.tags { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 17px; padding: 0; list-style: none; }
.tags li {
  font: 11px var(--font-cond);
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: 4px 10px;
  border: 1px solid rgba(116, 157, 196, .35);
  color: var(--accent-light);
}

.case__panel {
  border-top: 1px solid rgba(231, 231, 234, .14);
  padding-top: 20px;
  display: flex;
  flex-direction: column;
  gap: 17px;
  margin-bottom: 20px;
}
.case__panel h4 {
  font: 11px var(--font-mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 0 0 7px;
  font-weight: 400;
}
.case__panel p { font-size: 15px; line-height: 1.6; color: var(--ink-2); margin: 0; }
.case__actions { margin-top: auto; display: flex; flex-wrap: wrap; gap: 10px; }

/* ---------- tool card ---------- */

.tool {
  background: rgba(19, 20, 22, .42);
  padding: 27px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.tool h3 {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 21px;
  letter-spacing: -.01em;
  margin: 0;
}
.tool h3 a { color: var(--ink); }
.tool h3 a:hover { color: var(--accent-light); text-decoration: none; }
.tool p { font-size: 15px; line-height: 1.62; color: var(--ink-3); margin: 0; }
.tool__grow { flex: 1; }
.tool__points {
  display: flex;
  flex-direction: column;
  gap: 7px;
  border-top: 1px solid rgba(231, 231, 234, .14);
  padding: 14px 0 0;
  margin: 0;
  list-style: none;
}
.tool__points li {
  display: flex;
  gap: 10px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-4);
}
.tool__points li::before {
  content: "\2192";
  color: var(--accent);
  font: 13px/1.6 var(--font-mono);
  flex: none;
}

/* ---------- stack (rendered from content/stack.txt) ----------
   The doc field stores one <ul> of <li><u>Group</u> — items</li>. The <u> is
   the group label; the rest of the line is the comma-separated list. */

.stack ul {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
  gap: 0;
  border-top: 1px solid var(--rule-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
.stack li {
  padding: 24px 24px 24px 0;
  border-bottom: 1px solid var(--rule);
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink-3);
}
.stack li u {
  display: block;
  font-family: var(--font-cond);
  font-size: 13px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent-light);
  margin-bottom: 12px;
  text-decoration: none;
}

/* ---------- process (rendered from content/process.txt) ---------- */

.process ul {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: 34px;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}
.process li {
  counter-increment: step;
  font-size: 15px;
  line-height: 1.62;
  color: var(--ink-4);
}
.process li::before {
  content: counter(step, decimal-leading-zero);
  display: block;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 34px;
  color: rgba(116, 157, 196, .45);
  line-height: 1;
  margin-bottom: 14px;
}
.process li u {
  display: block;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 18px;
  color: var(--ink);
  margin-bottom: 12px;
  text-decoration: none;
}

/* ---------- resume ---------- */

.resume { display: flex; flex-direction: column; border-top: 1px solid var(--rule-2); }
.job {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 20px;
  align-items: start;
  padding: 27px 0;
  border-bottom: 1px solid var(--rule);
}
.job__years { font: 12px var(--font-mono); letter-spacing: .1em; color: var(--ink-dim); margin-bottom: 10px; }
.job h3 {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 19px;
  letter-spacing: -.01em;
  margin: 0 0 4px;
}
.job__company { font-size: 15px; color: var(--accent-light); }
.job__points {
  grid-column: span 2;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.job__points li {
  display: flex;
  gap: 12px;
  align-items: baseline;
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink-3);
}
.job__points li::before {
  content: "\2014";
  color: var(--accent-deep);
  font: 13px/1.6 var(--font-mono);
  flex: none;
}

/* ---------- notes ---------- */

.notes { display: flex; flex-direction: column; border-top: 1px solid var(--rule-2); }
.note {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 14px;
  align-items: baseline;
  padding: 24px 0;
  border-bottom: 1px solid var(--rule);
  color: var(--ink);
}
/* Only a note that actually has a link behaves like one. */
a.note[href]:hover { background: rgba(116, 157, 196, .06); text-decoration: none; color: var(--ink); }
a.note:not([href]) { cursor: default; }
.note__date { font: 12px var(--font-mono); letter-spacing: .1em; color: var(--ink-dim); }
.note__body { grid-column: span 2; }
.note h3 { font-family: var(--font-head); font-weight: 600; font-size: 19px; margin: 0 0 7px; }
.note p { font-size: 15px; line-height: 1.6; color: var(--ink-dim); margin: 0; }

/* ---------- contact ---------- */

.contact { padding-block: 96px; }
.contact h2 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(28px, 4.2vw, 46px);
  letter-spacing: -.02em;
  margin: 0 0 20px;
  max-width: 26ch;
  text-wrap: pretty;
}
.contact p { font-size: 17px; line-height: 1.6; color: var(--ink-2); max-width: 56ch; margin: 0 0 34px; }
.contact__actions { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }

/* ---------- contact disclosure ---------- */

.contact-toggle { gap: 12px; }

.contact-toggle__chev {
  display: inline-flex;
  transition: transform .35s cubic-bezier(.2, .7, .3, 1);
}
.contact-toggle[aria-expanded="true"] .contact-toggle__chev { transform: rotate(180deg); }

/* height is animated from JS, which needs a concrete number to move between;
   overflow keeps the form clipped while the section grows into it. */
.contact-panel {
  overflow: hidden;
  opacity: 1;
  transition: height .42s cubic-bezier(.2, .7, .3, 1), opacity .3s ease;
}
.contact-panel[data-animating] { opacity: .35; }

@media (prefers-reduced-motion: reduce) {
  .contact-panel { transition: none; }
  .contact-toggle__chev { transition: none; }
}

/* ---------- contact form ---------- */

.contact-form {
  margin-top: 40px;
  padding: 28px;
  max-width: 560px;
  background: rgba(17, 18, 21, .55);
  backdrop-filter: blur(2px);
}

.contact-form__lede {
  margin: 0 0 20px;
  font: 12px var(--font-mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.contact-form .field { margin-bottom: 18px; }

.contact-form label {
  display: block;
  margin-bottom: 7px;
  font: 12px var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-4);
}

.contact-form input,
.contact-form textarea {
  width: 100%;
  padding: 11px 13px;
  font: inherit;
  color: var(--ink);
  background: rgba(0, 0, 0, .28);
  border: 1px solid var(--rule-2);
  border-radius: 0;
  /* Anything under 16px makes iOS Safari zoom the whole page on focus. */
  font-size: 16px;
}

.contact-form textarea { resize: vertical; min-height: 120px; }

.contact-form input:focus-visible,
.contact-form textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
}

.contact-form input:user-invalid,
.contact-form textarea:user-invalid {
  /* :user-invalid, not :invalid — a required field is invalid the moment the
     page loads, and nobody wants to be told off before they have typed. */
  border-color: #b4675c;
}

/* Out of sight, out of the accessibility tree, still in the DOM for a bot. */
.contact-form__trap {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.contact-form__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin-top: 4px;
}

.contact-form__note {
  margin: 0;
  font: 13px var(--font-mono);
  letter-spacing: .04em;
  color: var(--ink-3);
}
.contact-form__mailto {
  display: inline-block;
  margin-top: 18px;
  font: 12px var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.contact-form__mailto:hover { color: var(--accent-light); }

.contact-form__note[data-state="ok"]  { color: var(--accent-light); }
.contact-form__note[data-state="err"] { color: #d79187; }

.contact-form[data-sending] button { opacity: .6; pointer-events: none; }

@media (max-width: 640px) {
  .contact-form { padding: 22px 18px; margin-top: 32px; }
}

/* ---------- footer ---------- */

.site-footer {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: space-between;
  padding-block: 34px 48px;
  font: 12px var(--font-mono);
  letter-spacing: .08em;
  color: var(--ink-dim);
}

/* ---------- case detail page ---------- */

.detail-hero { padding-block: 48px 72px; }
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-cond);
  font-size: 13px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent-light);
  margin-bottom: 41px;
}
.detail-hero h1 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(30px, 4.6vw, 52px);
  line-height: 1.1;
  letter-spacing: -.02em;
  margin: 0 0 20px;
  max-width: 22ch;
  text-wrap: pretty;
}
.detail-hero .kicker--mono { display: block; margin-bottom: 17px; }
.detail-hero p { font-size: 18px; line-height: 1.6; color: var(--ink-2); max-width: 58ch; margin: 0; }

.detail-body {
  padding-block: 72px 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: 48px;
  align-items: start;
}
.detail-prose { display: flex; flex-direction: column; gap: 8px;  padding-bottom: 30px;}
.detail-prose h2 {
  font: 11px var(--font-mono);
  font-weight: 400;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent-light);
  margin: 33px 0 6px;
}
.detail-prose h2:first-child { margin-top: 0; }
.detail-prose h1 {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 24px;
  margin: 33px 0 6px;
}
.detail-prose p { font-size: 17px; line-height: 1.68; color: var(--ink-2); margin: 0; max-width: 62ch; }
.detail-prose ul, .detail-prose ol { color: var(--ink-2); font-size: 16px; line-height: 1.68; max-width: 62ch; }
.detail-prose blockquote {
  margin: 8px 0;
  padding-left: 18px;
  border-left: 2px solid var(--accent-deep);
  color: var(--ink-3);
}
.detail-prose pre {
  background: rgba(0, 0, 0, .35);
  padding: 14px 16px;
  overflow-x: auto;
  font: 13px/1.6 var(--font-mono);
}

.detail-meta { padding: 27px; }
.detail-meta .label { display: block; margin-bottom: 20px; }
.detail-meta dl { margin: 0; display: flex; flex-direction: column; }
.detail-meta .row {
  display: flex;
  justify-content: space-between;
  gap: 17px;
  padding: 10px 0;
  border-top: 1px solid rgba(231, 231, 234, .12);
  font-size: 14px;
}
.detail-meta dt {
  color: var(--ink-dim);
  font: 12px var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  padding-top: 2px;
}
.detail-meta dd { margin: 0; color: var(--ink); text-align: right; }

.shots { padding-block: 72px; }
.shots .label { display: block; margin-bottom: 20px; }
.shots__hint { font-size: 15px; line-height: 1.6; color: var(--ink-dim); max-width: 58ch; margin: 0 0 27px; }

.shot {
  display: block;
  width: 100%;
  padding: 0;
  background: transparent;
  cursor: pointer;
  text-align: left;
  color: inherit;
  font: inherit;
  border: 1px solid rgba(231, 231, 234, .16);
}
.shot:hover { border-color: var(--accent-deep); }
.shot img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; }
.shot__cap {
  display: flex;
  gap: 10px;
  align-items: baseline;
  padding: 12px 14px;
  border-top: 1px solid rgba(231, 231, 234, .12);
}
.shot__n { font: 11px var(--font-mono); color: var(--accent-light); flex: none; }
.shot__text { font-size: 14px; line-height: 1.45; color: var(--ink-3); }

.detail-nav {
  padding-block: 34px 88px;
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: space-between;
  align-items: center;
  border-top: 1px solid rgba(231, 231, 234, .14);
}

/* ---------- lightbox ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 24px;
  background: rgba(10, 11, 12, .94);
  backdrop-filter: blur(6px);
}
.lightbox[hidden] { display: none; }
.lightbox__bar {
  display: flex;
  width: 100%;
  max-width: var(--wrap);
  align-items: center;
  justify-content: space-between;
  gap: 17px;
}
.lightbox__counter {
  font: 11px var(--font-mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.lightbox__frame {
  position: relative;
  width: min(100%, calc(62vh * 16 / 9));
  margin: 0 auto;
  border: 1px solid rgba(231, 231, 234, .22);
  background: #0b0c0d;
}
.lightbox__frame img { display: block; width: 100%; max-height: 70vh; object-fit: contain; }
.lightbox__foot {
  width: 100%;
  max-width: var(--wrap);
  display: flex;
  flex-wrap: wrap;
  gap: 17px;
  align-items: baseline;
  justify-content: space-between;
}
.lightbox__foot p { font-size: 16px; line-height: 1.55; color: var(--ink-2); margin: 0; max-width: 62ch; }
.lightbox__nav { display: flex; gap: 10px; }
.lightbox__nav button { font: 13px var(--font-mono); padding: 9px 15px; }

/* ---------- edit pill ---------- */

.edit-pill {
  position: fixed;
  top: 68px;
  right: 18px;
  z-index: 50;
  font-family: var(--font-cond);
  font-size: 13px;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: 8px 15px;
  border-radius: 999px;
  border: 1px solid var(--rule-3);
  background: rgba(19, 20, 22, .82);
  backdrop-filter: blur(8px);
  color: var(--ink);
}
.edit-pill:hover { border-color: var(--accent); color: var(--accent-light); text-decoration: none; }

/* ---------- reveal ---------- */

@keyframes riseIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* A [data-reveal] element is hidden ONLY once the inline head script has added
   .reveal-armed. That script runs before first paint, which is what stops an
   above-the-fold card being painted, then hidden, then animated back in. Tying
   it to a class rather than styling [data-reveal] directly means that with JS
   off — or if that script never runs — nothing is ever stranded invisible. */
.reveal-armed [data-reveal]:not(.rise) { opacity: 0; }

.rise {
  animation: riseIn .7s cubic-bezier(.2, .7, .3, 1) both;
  /* Siblings that enter together cascade instead of popping at once. app.js
     sets --reveal-i per element and caps it, so a long grid never leaves the
     last card waiting. */
  animation-delay: calc(var(--reveal-i, 0) * 70ms);
}

@media (prefers-reduced-motion: reduce) {
  /* Belt and braces: app.js also short-circuits, but if it never runs the
     content must not stay hidden. */
  .reveal-armed [data-reveal]:not(.rise) { opacity: 1; }
  .rise { animation: none; }
}

/* ---------- narrow screens ---------- */

@media (max-width: 640px) {
  :root { --gut: 18px; }
  .section { padding-block: 64px; }
  .hero { padding-block: 72px 64px; }
  :root { --header-h: 61px; }   /* same toggle, tighter padding */
  .masthead__logo { width: 36px; }
  .masthead { gap: 14px; padding: 12px var(--gut); }
  /* No .masthead nav rules here — below 781px it is the slide-in panel, and a
     later gap/font-size would override the panel's own. */
  .job__points, .note__body { grid-column: auto; }
  .detail-body { padding-block: 48px 0; gap: 34px; }
}

/* ---------- doc video: poster button + fullscreen player ----------

   No spacer images. The thumbnail's box is held open by aspect-ratio, so the
   poster can be any size and the layout never shifts while it loads. The
   fullscreen stage takes the video's own ratio at runtime (set by app.js from
   videoWidth/videoHeight), so nothing is cropped or letterboxed by a guess.
   ------------------------------------------------------------------ */

.vid-thumb {
  display: block;
  position: relative;
  width: 100%;
  /* The prose column is a flex stack with an 8px gap, which leaves a video
     sitting flush against the paragraphs on either side. Margin adds to that
     gap rather than padding the black box out. */
  margin-block: 24px 28px;
  aspect-ratio: 16 / 9;          /* the box, not a background image */
  padding: 0;
  border: 1px solid var(--rule-2);
  border-radius: 2px;
  background: #0c0d0f;
  overflow: hidden;
  cursor: pointer;
  color: var(--ink);
}
.vid-thumb__poster {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;             /* fill the 16:9 box; the player shows it whole */
  opacity: .78;
  transition: opacity .3s ease, transform .5s cubic-bezier(.2, .7, .3, 1);
}
.vid-thumb__poster--empty { background: linear-gradient(140deg, #16171a, #22262c); }
.vid-thumb:hover .vid-thumb__poster { opacity: 1; transform: scale(1.02); }

.vid-thumb__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--ink);
  filter: drop-shadow(0 2px 10px rgba(0, 0, 0, .55));
  transition: color .3s ease, transform .3s ease;
}
.vid-thumb:hover .vid-thumb__play { color: var(--accent-hover); transform: scale(1.06); }
.vid-thumb:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 3px; }

/* ---- fullscreen ---- */

.vid-modal {
  position: fixed;
  inset: 0;
  z-index: 200;                  /* over the loader (100) and the nav panel (50) */
  display: grid;
  place-items: center;
  padding: clamp(12px, 4vw, 48px);
  background: rgba(8, 9, 10, .93);
  backdrop-filter: blur(6px);
}
.vid-modal[hidden] { display: none; }

/* The stage is only a centring box now. Sizing the wrapper by aspect-ratio and
   a definite width meant the derived height could exceed the viewport — a 4:3
   video in a short window computed 888px tall inside 800px and hung its own
   controls off the bottom of the screen. A replaced element given auto
   dimensions and max-width/max-height already scales to fit while keeping its
   intrinsic ratio, so the browser does it, for any ratio, with no JS. */
.vid-modal__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  min-height: 0;
}
.vid-modal__video {
  display: block;
  /* Fill the available box and let object-fit letterbox inside it. The box is
     the viewport minus padding, so nothing can overflow, and a small source
     like a 640x480 transfer scales up to fill the screen instead of sitting in
     the middle at native size. No background or shadow: the painted picture is
     smaller than this box, so either would outline the wrong rectangle. */
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: transparent;
}

.vid-modal__close {
  position: absolute;
  top: clamp(10px, 2vw, 22px);
  right: clamp(10px, 2vw, 22px);
  width: 44px;                   /* a real touch target, not a 16px glyph */
  height: 44px;
  display: grid;
  place-items: center;
  font-size: 26px;
  line-height: 1;
  color: var(--ink);
  background: rgba(23, 24, 27, .8);
  border: 1px solid var(--rule-3);
  border-radius: 50%;
  cursor: pointer;
}
.vid-modal__close:hover { color: var(--accent-hover); border-color: var(--accent); }
.vid-modal__close:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .vid-thumb__poster, .vid-thumb__play { transition: none; }
  .vid-thumb:hover .vid-thumb__poster { transform: none; }
}

/* Images placed in a write-up. Same rhythm as the video block, and never wider
   than the prose column no matter what was uploaded. */
.detail-prose img,
[data-doc] img {
  display: block;
  width: 100%;
  height: auto;
  margin-block: 24px 28px;
  border: 1px solid var(--rule-2);
  border-radius: 2px;
  background: #0c0d0f;
}
