/* =====================================================================
   brint.dev — personal site
   Dark editorial / quiet-tech. Native CSS only, one accent, CSS-only motion.

   Type system, and it is a rule rather than a habit:
     Bricolage Grotesque — everything a person reads. Prose, headings,
       navigation, buttons, values.
     JetBrains Mono     — the wordmark, and machine strings only: URLs,
       technology names, status, and measured values. Monospace is not a
       costume for "technical". If a machine would not print it, it is
       set in Bricolage.
   ===================================================================== */

:root {
  /* surfaces. There is one page surface and two hairlines; elevated surfaces
     are not global, each Work card brings its own via --brand-body. */
  --bg:        #0a0a0c;
  --line:      rgba(255, 255, 255, 0.09);
  --line-soft: rgba(255, 255, 255, 0.055);

  /* ink — three tiers, all of them AA on --bg
     (dim 7.7:1, mute 5.6:1; mute was #706f68 at 3.9:1, which failed) */
  --ink:       #eceae3;
  --ink-dim:   #a3a29a;
  --ink-mute:  #8a8981;

  /* one accent — a luminous citrus lime */
  --accent:    #cbe94f;

  --maxw: 1180px;
  --pad: clamp(1.25rem, 5vw, 4rem);

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --r-card: 20px;

  /* film grain tile, used by .grain and .nav::before (see "Atmosphere") */
  --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---- reset-ish ---- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-optical-sizing: auto;
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.12rem);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

h1, h2, h3 { margin: 0; font-weight: 700; letter-spacing: -0.02em; line-height: 1; }
p { margin: 0; }
a { color: inherit; text-decoration: none; }

/* imagery is decoration, not content to drag off the page. The HTML
   draggable="false" covers Gecko; -webkit-user-drag covers WebKit/Blink,
   including image-only links (the two .project-visual panels). */
img, svg, .project-visual, .side-logo {
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
img { user-select: none; -webkit-user-select: none; }

/* focus visibility for keyboard users */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}
/* The Work panels sit flush against their card's edge and the card is
   overflow:hidden, so an outward ring gets clipped and a keyboard user sees a
   partial or missing focus state. Draw this one inside the panel instead. */
.project-visual:focus-visible { outline-offset: -5px; }

/* Hit areas for the inline text links. WCAG 2.2 target size wants 24x24 CSS px
   and these render 17-24px tall, so each gets a transparent expanded target
   instead of padding: the visual size and the layout stay exactly as designed.
   The insets stay smaller than the smallest gap between neighbours, so no two
   targets overlap. */
.wordmark, .nav-links a:not(.nav-contact), .footer-links a { position: relative; }
.wordmark::after,
.nav-links a:not(.nav-contact)::after,
.footer-links a::after {
  content: "";
  position: absolute;
  inset: -12px -7px;
}

/* text selection — the site-wide accent with dark text.
   Work cards override this with their own brand color (see .project below). */
::selection      { background: var(--accent); color: var(--bg); }
::-moz-selection { background: var(--accent); color: var(--bg); }

.skip-link {
  position: fixed;
  top: -100px; left: var(--pad);
  z-index: 100;
  background: var(--accent);
  color: #0a0a0c;
  padding: 0.6rem 1rem;
  border-radius: 999px;
  font-size: 0.9rem;
  font-weight: 500;
  transition: top 0.25s var(--ease);
}
.skip-link:focus { top: 1rem; }

/* =====================================================================
   Atmosphere: film grain only.
   There is deliberately no aurora / radial glow. A blurred colored halo
   behind a dark page is the single most recognizable generated-UI tell,
   and at the alpha it needed to stay tasteful it was doing nothing. The
   page is flat near-black; all the color comes from content.

   The noise lives in a custom property because the header needs the identical
   tile: see .nav::before. Two hand-copied data URIs would drift.
   ===================================================================== */
.grain {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.4;
  mix-blend-mode: soft-light;
  background-image: var(--noise);
}

/* =====================================================================
   Navigation
   ===================================================================== */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: 68px;
  padding-inline: var(--pad);
  /* The page's own surface, opaque, and deliberately NOT glass. A translucent
     blurred bar over a flat near-black page is the most recognizable generated-UI
     header there is, and the saturate(120%) pass made the band measurably milkier
     than the page beneath it: a 68px strip of different black across the top.
     Opaque means content passes cleanly behind the header and the only thing
     marking it is a hairline, exactly like every other divider on the page.
     Do not reintroduce backdrop-filter here. */
  background: var(--bg);
  border-bottom: 1px solid var(--line-soft);
}
/* An opaque header hides the page's film grain, and a dead-flat 68px strip against
   a grained page is a visible seam: measured at 390px, the band sat at luminance
   10.67 with zero variance against 13.14 grained below it. The translucent nav used
   to leak the grain through, so this carries it deliberately instead.
   z-index: -1 puts it above the header's own background but below its content, so
   the noise never lands on the wordmark or the links, and .nav's own stacking
   context (sticky + z-index) keeps the soft-light blend from reaching the page
   scrolling underneath. The tile phase matches .grain for free: both boxes start at
   viewport 0,0 whenever the header is at the top, which is always. */
.nav::before {
  content: "";
  position: absolute;
  /* -1px at the bottom because inset:0 stops at the padding box, which would leave
     the border row as a flat 1px line across an otherwise grained surface. */
  inset: 0 0 -1px;
  z-index: -1;
  pointer-events: none;
  opacity: 0.4;
  mix-blend-mode: soft-light;
  background-image: var(--noise);
}
/* the wordmark is the one place mono is the mark rather than the content */
.wordmark {
  font: 500 1.05rem/1 "JetBrains Mono", monospace;
  letter-spacing: -0.02em;
  color: var(--ink);
  transition: color 0.2s var(--ease);
}
.wordmark .caret { color: var(--accent); }
.nav-links {
  display: flex;
  align-items: center;
  gap: clamp(1.1rem, 3.5vw, 2rem);
  font-size: 0.95rem;
  font-weight: 500;
}
.nav-links a { color: var(--ink-dim); transition: color 0.2s var(--ease); }
.nav-contact {
  color: var(--ink) !important;
  padding: 0.45rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease),
              transform 0.16s var(--ease);
}

/* =====================================================================
   Layout container helper
   ===================================================================== */
.hero, .about, .work, .beyond, .footer {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--pad);
}

/* =====================================================================
   Hero — the name leads. No eyebrow above it: a tracked-caps kicker over
   a headline is stock grammar, and the h1 says who this is on its own.
   ===================================================================== */
.hero {
  min-height: calc(100dvh - 68px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* bottom-weighted: optical centre sits a little above geometric centre */
  padding-block: clamp(2rem, 5vh, 4rem) clamp(3rem, 9vh, 6rem);
}
.name {
  font-weight: 800;
  /* The low bound is 56px, not 3.5rem, and those are the same number at a default
     16px root, so nothing moves for most readers. It matters at a raised reader
     font size: in rem the floor grew to 112px on a 390px screen and "Brintazzoli"
     is one unbreakable word, so it ran off the side. Display type this size is
     already scaled by the viewport (15vw); it is the body copy that needs to grow
     with the reader's preference, and that still does. */
  font-size: clamp(56px, 15vw, 14rem);
  letter-spacing: -0.045em;
  line-height: 0.86;
  color: var(--ink);
}
.name .line { display: block; }
/* the outlined surname is the site's one signature move; it is used here
   and nowhere else, so it stays a signature rather than a device. */
.name .line:last-child {
  color: transparent;
  -webkit-text-stroke: 1.3px var(--ink);
}
.lede {
  margin-top: clamp(1.8rem, 5vh, 2.8rem);
  /* em, not ch. 1ch is the advance of "0", so a ch measure silently changes width
     when the webfont replaces the fallback; em depends only on font-size, so the
     container is deterministic. Bricolage's 1ch = 0.5946em, so 26em is the measure
     the old 44ch produced (target stays 45-75 characters).
     Honest limit: this fixes the container, not the reflow, and on its own it did
     not move CLS at all (0.0509 -> 0.0507). The text's own advance still differs
     between fallback and Bricolage, so a borderline line still flips on swap;
     measured, 13 of 68 width/element combinations do. See the CLS note in
     CLAUDE.md before trying to chase that to zero. */
  max-width: 26em;
  font-size: clamp(1.15rem, 1rem + 0.9vw, 1.6rem);
  line-height: 1.4;
  color: var(--ink-dim);
  font-weight: 400;
  text-wrap: pretty;
}
.lede .hl {
  color: var(--accent);
  font-weight: 600;
}
.lede .lit {
  color: var(--ink);
  font-weight: 600;
}
.lede-link {
  color: var(--accent);
  box-shadow: inset 0 -0.08em 0 rgba(203, 233, 79, 0.35);
  transition: box-shadow 0.25s var(--ease);
}

.hero-cta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  margin-top: clamp(2.2rem, 6vh, 3.2rem);
}

/* ---- buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.85rem 1.6rem;
  border-radius: 999px;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
  border: 1px solid transparent;
  transition: transform 0.16s var(--ease), background 0.2s var(--ease),
              border-color 0.2s var(--ease), color 0.2s var(--ease);
}
.btn .arrow { transition: transform 0.2s var(--ease); }
.btn-primary { background: var(--accent); color: #0a0a0c; }
.btn-ghost { border-color: var(--line); color: var(--ink); }
/* press feedback: the control has to feel like it heard the tap */
.btn:active { transform: scale(0.97); }

/* =====================================================================
   About
   ===================================================================== */
.about {
  padding-block: clamp(5rem, 14vh, 10rem);
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: start;
  border-top: 1px solid var(--line-soft);
}
.about-lead {
  /* sized so the statement lands on four full lines in its column rather
     than five ragged ones; balance then evens them out */
  font-size: clamp(1.75rem, 1.15rem + 2.1vw, 2.8rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.1;
  text-wrap: balance;
}
.about-body {
  margin-top: 1.75rem;
  max-width: 34.5em;   /* the old 58ch, in deterministic units — see .lede */
  color: var(--ink-dim);
  font-size: clamp(1.02rem, 0.98rem + 0.3vw, 1.15rem);
  text-wrap: pretty;
}

/* spec table: mono label, prose value, both left-aligned so a value that
   wraps keeps a clean left edge instead of a ragged right-aligned one. */
.facts {
  margin: 0;
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--line);
}
.fact {
  display: grid;
  grid-template-columns: 4.75rem 1fr;
  gap: 0.3rem 1.25rem;
  align-items: baseline;
  padding: 0.95rem 0;
  border-bottom: 1px solid var(--line-soft);
}
.fact dt {
  font: 500 0.75rem/1.6 "JetBrains Mono", monospace;
  color: var(--ink-mute);
}
.fact dd {
  margin: 0;
  color: var(--ink);
  font-size: 0.95rem;
  line-height: 1.55;
}

/* =====================================================================
   Work
   ===================================================================== */
.work {
  padding-block: clamp(3rem, 6vh, 5rem) clamp(4rem, 9vh, 6rem);
}
.work-head { margin-bottom: clamp(2rem, 5vh, 3rem); }
.section-title {
  font-size: clamp(2.2rem, 1.5rem + 3vw, 4rem);
  font-weight: 700;
  letter-spacing: -0.035em;
}

/* Each Work card is a scoped brand pocket of the product it links to: its own accent,
   surfaces and elevation, driven by the tokens on the .project--<name> modifier below.
   The rest of the page keeps brint's citrus lime; fonts stay brint's site-wide. */
.project {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(1.5rem, 4vw, 3rem);
  background: var(--brand-body);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  overflow: hidden;
  transition: border-color 0.3s var(--ease);
}
.project + .project { margin-top: clamp(1.5rem, 4vw, 2.5rem); }

/* Kove: its lemon-green "signal" over green-tinted dark neutrals (kove/web/src/index.css). */
.project--kove {
  --brand:        #7ffa88;
  --brand-body:   #171917;
  --chip-bg:      #1e211e;
  --chip-line:    #2a2e2b;
  --chip-dim:     #9aa39b;
  --panel-accent: #7ffa88;
  --panel-base:   linear-gradient(155deg, #10130f, #0a0b0a 72%);
  --hint-bg:      #171917;
  --hint-elev:    #1e211e;
  --hint-line:    #2a2e2b;
  --hint-dim:     #9aa39b;
}

/* Pizzer: its warm dark theme, amber + gold over toasted-brown neutrals
   (pizzer/public/index.html, mirrored from the app's own design tokens). */
.project--pizzer {
  --brand:        #ffaa47;
  --brand-body:   #22180f;
  --chip-bg:      #2a2018;
  --chip-line:    #3a2e22;
  --chip-dim:     #ab9a85;
  --panel-accent: #ffaa47;
  --panel-base:   linear-gradient(155deg, #211711, #1a130c 72%);
}

/* Selecting text inside a card picks up that card's own brand color, driven by its
   --brand token above — so every product reads in its own palette, and any future
   .project--<name> gets a matching selection for free. */
.project ::selection,
.project::selection      { background: var(--brand); color: var(--bg); }
.project ::-moz-selection,
.project::-moz-selection { background: var(--brand); color: var(--bg); }

.project-body {
  /* 28px is exactly the old 1.75rem at a default root, so nothing moves for most
     readers. px on purpose: in rem this floor grew with the reader's font size and
     claimed 112px of a 390px card, squeezing the copy out. */
  padding: clamp(28px, 4vw, 3rem);
  display: flex;
  flex-direction: column;
  min-width: 0;
}
/* status is a machine-ish token, so it keeps mono, but sentence case:
   there is no reason for the page to shout it. */
.project-state {
  align-self: flex-start;
  margin-bottom: 1.6rem;
  font: 500 0.78rem/1 "JetBrains Mono", monospace;
  color: var(--brand);
  border: 1px solid color-mix(in srgb, var(--brand) 32%, transparent);
  padding: 0.4rem 0.75rem;
  border-radius: 999px;
}
.project-title {
  font-size: clamp(2.6rem, 2rem + 3vw, 4.5rem);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 0.9;
}
.project-tagline {
  margin-top: 0.6rem;
  font-size: clamp(1.1rem, 1rem + 0.6vw, 1.4rem);
  font-weight: 500;
  color: var(--ink);
}
.project-desc {
  margin-top: 1.1rem;
  color: var(--ink-dim);
  max-width: 27.4em;   /* the old 46ch, in deterministic units — see .lede */
  font-size: 1rem;
  text-wrap: pretty;
}
.tags {
  list-style: none;
  padding: 0;
  margin: 1.75rem 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* technology names are machine strings: mono earns its place here */
.tags li {
  font: 400 0.78rem/1 "JetBrains Mono", monospace;
  color: var(--chip-dim);
  padding: 0.45rem 0.75rem;
  border: 1px solid var(--chip-line);
  border-radius: 999px;
  background: var(--chip-bg);
}
.project-link {
  margin-top: auto;
  padding-top: 1.75rem;
  align-self: flex-start;
  font: 500 0.95rem/1 "JetBrains Mono", monospace;
  color: var(--brand);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  /* same reason as .side-meta: a mono host name is one unbreakable word, and at a
     200% reader font size it pushes past a 390px card on its own */
  min-width: 0;
  overflow-wrap: anywhere;
}
.project-link .arrow { transition: transform 0.2s var(--ease); }

/* ---- project visual panel ----
   Shows the product, not a monogram. The stroked K/P letters that used to sit
   here repeated the hero's outline treatment and crowded the artifact; the
   panel now gives that space to the thing being sold. */
.project-visual {
  position: relative;
  min-height: 340px;
  display: block;
  overflow: hidden;
  background:
    radial-gradient(120% 100% at 82% 6%, color-mix(in srgb, var(--panel-accent) 13%, transparent), transparent 58%),
    var(--panel-base);
  border-left: 1px solid var(--line);
}
.visual-inner {
  position: absolute;
  inset: 0;
  padding: clamp(1.5rem, 4vw, 2.5rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Kove's panel tells the product in one frame: a human sentence going in,
   structured data coming out. So the message is set in the reading face and
   the result is set in mono — the typography carries the transformation. */
.visual-stack {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.chat-sent {
  align-self: flex-end;
  max-width: min(90%, 23rem);
  padding: 0.8rem 1.05rem;
  font-size: 1rem;
  line-height: 1.45;
  text-wrap: balance;   /* stops "5k" stranding on a line of its own */
  color: var(--ink);
  background: color-mix(in srgb, var(--panel-accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--panel-accent) 22%, transparent);
  border-radius: 16px;
  border-bottom-right-radius: 5px;
  /* No backdrop-filter. It was here, and what it blurred was a smooth radial
     gradient, so it bought a compositing layer for a pixel-identical bubble
     (diffed at 3x: the only delta was gradient dither the blur smoothed). Glass
     that changes nothing is decoration. */
}
.hint-card {
  align-self: flex-start;
  max-width: min(100%, 24rem);
  border: 1px solid color-mix(in srgb, var(--panel-accent) 26%, transparent);
  background: var(--hint-bg);
  border-radius: 13px;
  padding: 1rem 1.05rem;
  /* a real cast shadow: offset, tight blur. Not a zero-offset colored halo,
     and not a hairline paired with a wide diffuse bloom. */
  box-shadow: 0 8px 14px -8px rgba(0, 0, 0, 0.85);
}
.hint-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.8rem;
  font: 500 0.85rem/1 "JetBrains Mono", monospace;
  color: var(--panel-accent);
}
.hint-glyph { flex-shrink: 0; }
.hint-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
}
/* measured values: mono, and the numbers pick up the product accent */
.hint-pill {
  font: 400 0.8rem/1 "JetBrains Mono", monospace;
  color: var(--hint-dim);
  padding: 0.45rem 0.65rem;
  border: 1px solid var(--hint-line);
  border-radius: 8px;
  background: var(--hint-elev);
  white-space: nowrap;
}
.hint-pill b { color: var(--panel-accent); font-weight: 500; }

/* Pizzer: the real renders, centred and large enough that the
   plain-base -> loaded-pizza story actually reads. */
.pizza-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.5rem, 2.5vw, 1.1rem);
}
.pizza {
  flex-shrink: 0;
  display: grid;                 /* stacks the ingredient layers in one cell */
  width: clamp(96px, 32%, 148px);
  filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.5));
}
.pizza img {
  grid-area: 1 / 1;
  display: block;
  width: 100%;
  height: auto;
}
/* curved "from this to that" arrow linking the plain pizza to the loaded one */
.pizza-arrow {
  flex-shrink: 0;
  align-self: center;
  width: clamp(50px, 17%, 74px);
}
.pizza-arrow svg { display: block; width: 100%; height: auto; }

/* =====================================================================
   Beyond code — index-style project list (distinct from the feature card)
   ===================================================================== */
.beyond {
  border-top: 1px solid var(--line-soft);
  padding-block: clamp(4rem, 9vh, 6rem) clamp(6rem, 16vh, 10rem);
}
.side-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}
.side-item { border-bottom: 1px solid var(--line-soft); }
.side-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.75rem 0;
  /* transform, not padding: nudging padding on hover thrashes layout */
  transition: transform 0.3s var(--ease), opacity 0.16s var(--ease);
}
.side-lead {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 3vw, 1.25rem);
  min-width: 0;
}
.side-logo {
  flex-shrink: 0;
  width: clamp(48px, 13vw, 62px);
  height: auto;
  filter: drop-shadow(0 5px 12px rgba(0, 0, 0, 0.45));
}
.side-main { min-width: 0; }
.side-title {
  font-size: clamp(1.4rem, 1rem + 1.6vw, 2rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.05;
}
.side-desc {
  margin-top: 0.5rem;
  color: var(--ink-dim);
  font-size: 0.98rem;
  max-width: 31em;     /* the old 52ch, in deterministic units — see .lede */
  text-wrap: pretty;
}
/* the destination, same as the Work cards' link: a URL, so mono */
.side-meta {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font: 500 0.85rem/1 "JetBrains Mono", monospace;
  color: var(--ink-mute);
  transition: color 0.2s var(--ease);
  /* an unbreakable mono host name overflows 390px once a reader turns their
     default font size up, and flex-shrink alone cannot help a single long word */
  min-width: 0;
  overflow-wrap: anywhere;
}
.side-meta .arrow { transition: transform 0.2s var(--ease); }

/* =====================================================================
   Footer
   ===================================================================== */
.footer {
  padding-block: clamp(2.5rem, 6vh, 4rem);
  border-top: 1px solid var(--line-soft);
}
.footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.footer-name {
  font-weight: 600;
  letter-spacing: -0.02em;
  font-size: 1.05rem;
}
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  font-size: 0.95rem;
  font-weight: 500;
}
.footer-links a { color: var(--ink-dim); transition: color 0.2s var(--ease); }
.footer-fine {
  margin-top: 1.25rem;
  font-size: 0.85rem;
  color: var(--ink-mute);
}
.footer-fine .year { margin-left: 0.5rem; }

/* =====================================================================
   Hover — gated so a tap on a touch device does not stick in a hover state
   ===================================================================== */
@media (hover: hover) and (pointer: fine) {
  .wordmark:hover { color: var(--accent); }
  .nav-links a:hover { color: var(--ink); }
  .nav-contact:hover { border-color: var(--accent); background: rgba(203, 233, 79, 0.08); }

  .lede-link:hover { box-shadow: inset 0 -0.6em 0 rgba(203, 233, 79, 0.22); }

  .btn-primary:hover { background: #d8f36a; transform: translateY(-2px); }
  .btn-primary:hover .arrow { transform: translate(2px, -2px); }
  .btn-ghost:hover { border-color: var(--ink-dim); background: rgba(255, 255, 255, 0.03); transform: translateY(-2px); }
  /* keep the press read on top of the hover lift */
  .btn:active { transform: scale(0.97); }

  .project:hover { border-color: color-mix(in srgb, var(--brand) 30%, transparent); }
  .project:hover .project-link .arrow,
  .project-link:hover .arrow { transform: translate(2px, -2px); }

  .side-link:hover { transform: translateX(8px); }
  .side-link:hover .side-meta { color: var(--accent); }
  .side-link:hover .side-meta .arrow { transform: translate(2px, -2px); }

  .footer-links a:hover { color: var(--accent); }
}

/* Press feedback on the things that read as pressable surfaces rather than as
   words in a sentence. Outside the hover block on purpose: a touch user gets no
   hover, so this is the only confirmation their tap landed.
   The row and the panel dip opacity rather than scaling, because .side-link
   already owns transform for its hover nudge and a second transform on the same
   element would replace that nudge mid-press instead of composing with it. */
.nav-contact:active   { transform: scale(0.97); }
.side-link:active     { opacity: 0.7; }
.project-visual:active { opacity: 0.92; }

/* =====================================================================
   Motion — CSS only, gated on reduced-motion + feature support.

   Two behaviours, deliberately different. The hero gets the one authored
   entrance on the page: a staggered rise. Everything below it only fades,
   with no movement, so scrolling the page does not become a slideshow of
   identical slide-ups.
   ===================================================================== */
@media (prefers-reduced-motion: no-preference) {
  .hero-anim { animation: rise 0.7s var(--ease) backwards; }
  .d1 { animation-delay: 0.03s; }
  .d2 { animation-delay: 0.09s; }
  .d3 { animation-delay: 0.20s; }
  .d4 { animation-delay: 0.28s; }
}

/* scroll-reveal via native scroll-driven animation (no JS).
   Default = fully visible, so unsupported browsers + reduced-motion show everything. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .reveal {
      animation: fade-in 1s var(--ease) both;
      animation-timeline: view();
      animation-range: entry 10% cover 24%;
    }
  }
}

/* The header's hairline is a state, not decoration: it exists only once there is
   content passing behind the header. At the top of the page the header IS the top
   of the page, so a rule across it separates nothing. This is not a third motion
   behaviour — nothing moves, a border colour is tied to scroll position — and the
   default (line always present) is what unsupported browsers and reduced-motion
   get, which is honest either way. rgba(255,255,255,0) rather than `transparent`
   so the interpolation never passes through a dark tint. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .nav {
      animation: nav-edge 1s linear both;
      animation-timeline: scroll(root block);
      animation-range: 0px 72px;
    }
  }
}

/* The duration is the LCP lever here, not the shape of the fade. The outlined
   surname is the largest text on the page, so Chrome keeps it as the LCP
   candidate until the entrance settles: LCP lands at roughly delay + duration.
   Measured on a warm load, 820ms with this entrance against 44ms with none.
   Front-loading the opacity to a 35% stop was tried and measured as a no-op
   (820ms either way), so it is not here. Lengthening the entrance is a direct
   LCP regression, which is what the render budget in tools/verify.mjs guards. */
@keyframes rise {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes nav-edge {
  from { border-bottom-color: rgba(255, 255, 255, 0); }
  to   { border-bottom-color: var(--line-soft); }
}

/* =====================================================================
   Responsive — mobile-first collapse (design verified at 390px)

   Breakpoints are in em, not px, and the values are the same as the old pixel
   ones at a default 16px root (53.75em = 860px, 33.75em = 540px, 23.75em =
   380px). em buys one thing px cannot: a media query in em keys off the
   reader's own default font size, so someone browsing at 24px gets the narrow
   layout while there is still room for it. In px, the nav overflowed 390px
   once the default size went up.
   ===================================================================== */
@media (max-width: 53.75em) {
  .about {
    grid-template-columns: 1fr;
    gap: 2.5rem;
  }
  .project {
    grid-template-columns: 1fr;
  }
  .project-visual {
    grid-row: 1;
    min-height: auto;
    border-left: none;
    border-bottom: 1px solid var(--line);
  }
  .visual-inner {
    position: relative;
    padding-block: clamp(2rem, 7vw, 2.75rem);
  }
  .pizza { width: clamp(104px, 30vw, 150px); }
  /* the panel is wide and short in this band, which pushed the message to one
     edge and the result card to the other and stopped the pair reading as one
     exchange. Cap the stack and centre it so the conversation holds. */
  .visual-stack {
    align-self: center;
    width: 100%;
    max-width: 30rem;
  }
}

@media (max-width: 33.75em) {
  .nav-links { gap: 1.1rem; font-size: 0.88rem; }
  .nav-contact { padding: 0.4rem 0.75rem; }
  .hero-cta { gap: 0.7rem; }
  .btn { padding: 0.8rem 1.3rem; font-size: 0.95rem; }
  /* label above value: a 4.75rem label column leaves too little for the value */
  .fact {
    grid-template-columns: 1fr;
    gap: 0.35rem;
  }
  .hint-card { max-width: 100%; }
  .side-link { flex-direction: column; align-items: flex-start; gap: 0.8rem; }
  .side-link:hover { transform: none; }
}

/* very small phones, or a reader with a large default font size */
@media (max-width: 23.75em) {
  .nav-links a:not(.nav-contact) { display: none; }
}
