/* =========================================================================
   v2 — matched to the NK portfolio reference
   Cream ground, bold black display type, one striking coral accent and a
   mustard-gold second accent, used as fills/blocks/dots rather than text.

   This replaces two earlier palettes on this site: the original earthy
   terracotta/forest system, and a same-day chartreuse/gold/green
   experiment that mostly didn't work (too bright as a page background,
   too bold behind body copy, and low-contrast once accent hues stopped
   being dark enough to double as their own text colour). See PLAN.md's
   decision log for what was tried and why this replaced it.
   ========================================================================= */

:root {
  /* Accent colours are fills, blocks and dots — never a text colour on a
     light background. That was the recurring bug in the previous
     experiment (the nav's active tab, the "Live" badge): an accent hue
     that isn't dark enough to read as text once it's not near-black any
     more. Text stays --ink (or --cream on a dark-enough fill) throughout. */
  --coral:      #EF5A42;   /* the bold, striking colour — CTAs, hover/focus,
                              selection. Confirmed to work as a sparing
                              accent (the LinkedIn button) before this   */
  --coral-soft: #F8DCD2;   /* a pale tint of coral, for hover backgrounds  */
  --coral-dark: #9B3B2B;   /* darkened coral — the only coral dark enough
                              for white/cream text on top of it at real
                              contrast (~6.9:1). Used where a selected/
                              active state needs a coral fill with light
                              text: the nav's active tab, the LinkedIn
                              button's hover/focus.                      */
  --mustard:    #F0A93C;   /* the second accent — fills/dots only, never
                              its own text colour                        */
  --forest:     #0F3D3A;   /* deliberate holdover from the old earthy
                              palette, kept for exactly one job: the
                              "Live" badge's border and text, at
                              Bhaumik's instruction. Not part of the
                              coral/mustard system — don't reach for it
                              anywhere else.                             */
  --live:       #3FA35C;   /* the live dot only — brighter than mustard,
                              leans warm so it sits with coral          */
  --mandarin:   #F4700A;   /* WhatsNear's own brand orange, for the
                              "Near" in its name. Borrowed, not ours   */
  --rust:       #A85423;   /* darker, more muted orange — the "building"
                              chip. mandarin was too close to WhatsNear's */
  --tan:        #F6E8D3;   /* warm background wash for content-bearing
                              cards (experience boxes) — light, not a bold
                              block, so the body text inside stays readable */
  --cream:      #FBF7F0;   /* airy sections below, from NK's cream (#F5ECDD)
                              but lifted toward white across two rounds on
                              request — roughly 60% of the way there now,
                              still warm rather than plain white. Doubles as
                              the light text colour on dark fills (--ink
                              buttons, the nav's active tab), where lighter
                              only ever widens the contrast ratio, so this
                              is safe to lift further if asked            */
  --ink:        #14130F;
  --ink-70:     #14130Fb3;
  --ink-45:     #14130F73;
  --ink-14:     #14130F24;

  --gut: max(clamp(20px, 4vw, 64px), calc((100% - 1680px) / 2));

  --display: "Archivo Black", "Arial Black", sans-serif;
  --sans: "Archivo", system-ui, sans-serif;
}

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  background: var(--cream);
  scroll-behavior: smooth;
}

/* A very fine grain sits on top of the flat --cream colour so it reads as
   a papery surface rather than a flat digital fill. It's an inline SVG
   (feTurbulence, desaturated, very low opacity baked into the SVG itself)
   rather than a downloaded texture file — zero new assets, same "no
   dependencies" stack as everything else here. Purely decorative, so it's
   layered as a second background image rather than anything that could
   ever hide content. */
body {
  margin: 0;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E"),
    var(--cream);
  background-size: 160px 160px, auto;
  color: var(--ink);
  font: 400 16px/1.6 var(--sans);
  overflow-x: hidden;
}

p, h1, h2, h3 { margin: 0; }

::selection { background: var(--coral); color: var(--ink); }

:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 2px; }

html { scrollbar-color: var(--ink-45) var(--cream); scrollbar-width: thin; }
::-webkit-scrollbar { width: 12px; }
::-webkit-scrollbar-track { background: var(--cream); }
::-webkit-scrollbar-thumb { background: var(--ink-45); border: 3px solid var(--cream); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--ink); }

.arrow {
  width: 0.85em;
  height: 0.85em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
  transition: translate 180ms ease;
}

/* Announced, never seen. clip-path rather than display:none or a zero
   height, both of which take the text out of the accessibility tree and
   would silence the roll's live region. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* =========================================================================
   NAV — a single floating pill, centred, as in the reference
   ========================================================================= */

.nav {
  position: fixed;
  top: 18px;
  left: 50%;
  right: auto;
  width: auto;
  translate: -50% 0;
  z-index: 60;
}
.nav ul {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  margin: 0;
  padding: 5px;
  list-style: none;
  background: var(--cream);
  border: 1px solid var(--ink-14);
  border-radius: 999px;
  box-shadow: 0 6px 20px -8px rgba(20, 19, 15, 0.18);
}
.nav a {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 9px 18px;
  border-radius: 999px;
  color: var(--ink-70);
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  transition: color 160ms ease, background 160ms ease;
}
.nav a:hover:not(.is-here) { color: var(--ink); background: var(--coral-soft); }
/* Fallback for no-JS: the indicator below is what actually animates this,
   but if the script never runs there is still a solid fill here, sized to
   the link itself. --coral-dark, not plain --coral, because the active
   text is cream/white and needs a fill dark enough to hold ~4.5:1+. */
.nav a.is-here { background: var(--coral-dark); color: var(--cream); }

/* The green "is-here" fill lives here, not on the link itself, so it can
   glide and stretch between tabs as one continuous shape instead of one
   pill fading out while another fades in. JS sets width/height/transform
   from the active link's own measured box (see sketch.js); this only
   supplies the colour and the motion. The overshoot in the curve is the
   "liquid" part — it's a solid colour throughout, never transparent. */
.nav__indicator {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  background: var(--coral-dark);
  border-radius: 999px;
  z-index: 0;
  pointer-events: none;
  transition: transform 480ms cubic-bezier(0.32, 1.5, 0.4, 1),
              width 480ms cubic-bezier(0.32, 1.5, 0.4, 1),
              height 480ms cubic-bezier(0.32, 1.5, 0.4, 1),
              background 300ms ease;
}
.nav__indicator.no-transition { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .nav__indicator { transition: none; }
}

/* =========================================================================
   HERO
   The lemon wash sits behind the wordmark and fades out before the fold, so
   the page reads as one warm field rather than a coloured band.
   ========================================================================= */

.hero {
  position: relative;
  min-height: min(100svh, 1020px);
  padding: 0 var(--gut);
  overflow: hidden;
  background: var(--cream);
}

/* --- wordmark ------------------------------------------------------------
   "HI, I'M BHAUMIK" wraps as "HI," / "I'M BHAUMIK" when the measure runs
   out, never splitting "I'M" from "BHAUMIK" — a non-breaking space in the
   markup does that, not this rule. Capped at 170px so it stops growing on
   a wide monitor rather than tracking the viewport forever.
   ------------------------------------------------------------------------ */

.wordmark {
  position: absolute;
  left: var(--gut);
  right: var(--gut);
  top: clamp(104px, 15vh, 168px);
  z-index: 1;
  font-family: var(--display);
  font-weight: 400;
  font-size: min(11vw, 170px);
  line-height: 0.98;
  letter-spacing: -0.035em;
  color: var(--ink);
  text-transform: lowercase;
}

.wordmark__name {
  color: var(--coral-dark);
}

/* Blinking cursor while sketch.js types .wordmark__type out. Only animates
   under no-preference; reduced motion hides it outright below, since with
   no typing there is nothing for it to mark the end of. */
.wordmark__cursor {
  display: inline-block;
  width: 0.05em;
  height: 0.82em;
  margin-left: 0.02em;
  background: var(--ink);
  vertical-align: -0.05em;
}
@media (prefers-reduced-motion: no-preference) {
  .wordmark__cursor { animation: wordmark-blink 0.9s step-end infinite; }
  .wordmark__cursor.is-done {
    animation: none;
    opacity: 0;
    transition: opacity 300ms ease;
  }
}
@keyframes wordmark-blink { 50% { opacity: 0; } }

/* --- the sketch ---------------------------------------------------------- */

.sketch {
  position: absolute;
  right: var(--gut);
  bottom: 0;
  margin: 0;
  z-index: 3;
  width: min(48vw, 500px);
}

.sketch__svg {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
}

/* --- corner text -------------------------- */

.hero__intro {
  position: absolute;
  left: var(--gut);
  bottom: clamp(180px, 26vh, 320px);
  width: min(100ch, 42vw);
  z-index: 4;
  transition: opacity 1100ms ease, transform 1100ms ease;
}

/* Held invisible only while sketch.js is actually about to reveal it once
   the wordmark finishes typing (see sketch.js) — no JS, or reduced motion,
   means this class is never added and the text is simply there from the
   start, same "nothing hidden behind an animation that might not run"
   rule as the rest of the hero. */
.hero__intro.is-pending {
  opacity: 0;
  transform: translateY(14px);
}

.hero__lead {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(30px, 1.9vw, 35px);
  line-height: 1.3;
  letter-spacing: -0.015em;
  color: var(--ink);
}

.hero__sub {
  margin-top: 14px;
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink-70);
}

.delight-flourish {
  width: 16px;
  height: 16px;
  color: var(--coral-dark);
  margin-left: 6px;
  vertical-align: -2px;
}

/* --- scroll cue ----------------------------------------------------------
   Centred at the bottom of the hero, icon only — no "(scroll)" label any
   more. It's a real link to About, not decoration, so it does the thing it
   suggests; the accessible name now lives in the link's aria-label since
   there's no visible text to supply it. Hidden below 900px, where the hero
   is a stacked column with no fixed bottom edge to sit against and
   scrolling is self-evident anyway.
   NOTE: a centred cue can sit close to the sketch (bottom-right) at
   middling widths — this used to be bottom-left specifically to dodge
   that. Watch this zone if it starts to look cramped. */

.scroll-cue {
  position: absolute;
  left: 50%;
  translate: -50% 0;
  bottom: clamp(24px, 4.5vh, 46px);
  z-index: 4;
  display: inline-flex;
  color: var(--ink-45);
  text-decoration: none;
  transition: color 160ms ease;
}
.scroll-cue:hover, .scroll-cue:focus-visible { color: var(--ink); }
.scroll-cue .arrow { width: 22px; height: 22px; }

@media (prefers-reduced-motion: no-preference) {
  .scroll-cue .arrow { animation: scroll-bob 1.9s ease-in-out infinite; }
}

@keyframes scroll-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(5px); }
}

/* =========================================================================
   SKETCH ANIMATION
   Each of the 398 shapes is a real brushstroke, painted in on load in the
   order the piece was actually painted. --i on every <g> is its batch
   number (5 strokes per batch, ~80 batches), so the reveal reads as
   distinct dabs rather than one smooth fade, finishing in about 1.3s.
   ========================================================================= */

.stroke {
  opacity: 0;
  transform-origin: center;
}

.js .stroke {
  animation: paint-in 150ms ease-out calc(var(--i) * 14ms) both;
}

@keyframes paint-in {
  from { opacity: 0; transform: scale(0.92); }
  to   { opacity: 1; transform: scale(1); }
}

/* =========================================================================
   SCROLL REVEAL
   Content below the hero fades and rises the first time it is scrolled to.

   The hidden start state is deliberately scoped under `.reveal-ready`, a
   class sketch.js adds to <html> only when it is actually going to run the
   observer. Without JS, or under reduced motion, that class never appears,
   the rules below never apply, and every element is simply visible. Writing
   `.reveal { opacity: 0 }` unscoped would hide the entire page whenever the
   script failed to load — the same trap the sketch reveal already documents.
   ========================================================================= */

.reveal-ready .reveal {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 600ms ease, transform 600ms ease;
}
.reveal-ready .reveal.is-visible { opacity: 1; transform: none; }

/* The two project columns land one after the other rather than together. */
.reveal-ready .project__row .project:nth-child(2) { transition-delay: 130ms; }

/* Without JS, or with reduced motion, the sketch is simply there. Nothing is
   ever left hidden by an animation that did not run. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .stroke { animation: none; opacity: 1; }
  /* The stack still reorders and the badge still reads Live; only the
     movement goes. The hover fan goes too — with no transition to carry it
     it would snap open, which is worse than not fanning at all. */
  .card, .arrow, .project__link { transition: none; }
  .chip-live__dot::after, .chip-building__dot::after { animation: none; }
  .project__link:hover, .project__link:focus-visible { rotate: none; }
  /* sketch.js skips the typing effect under reduced motion, so there's no
     end for the cursor to mark — it isn't shown at all. */
  .wordmark__cursor { display: none; }
  .scroll-cue .arrow { animation: none; }
}

/* =========================================================================
   ABOUT
   ========================================================================= */

.about {
  padding: clamp(72px, 12vh, 150px) var(--gut) clamp(56px, 8vh, 100px);
  border-top: 1px solid var(--ink-14); /* scaffolding: marks where the hero ends */
}

/* The hobby roll sits left, the blurb right. The left column is much wider
   than the artefact itself (300px): the roll needs room for a neighbour
   shifted left of centre on one side, and the spotlit caption sitting to
   the right of centre on the other — see the sums in the comment on
   .roll__window below. */
.about__grid {
  display: grid;
  grid-template-columns: minmax(240px, 880px) minmax(0, 1fr);
  gap: clamp(24px, 3vw, 52px);
  align-items: center;
}

/* =========================================================================
   THE HOBBY ROLL
   -------------------------------------------------------------------------
   A vertical carousel of artefacts, upright and never tilted — like a
   dial turning: the assembly moves, but each face on it stays flat. One
   is spotlit, centred; its neighbours sit above and below, shifted left
   and faded to "slightly transparent", spaced far enough apart that none
   of the three ever overlap.

   The shape contract is the --roll-* variables below, read by sketch.js —
   the equivalent of the card deck's --fan-* set. sketch.js computes each
   item's position from a single spotlight index and writes the result as
   --x / --y / --s / --o / --z, so CSS never has to derive it, and there
   is exactly one source of truth: the spotlight index.

   Why JS owns this rather than letting CSS derive it from a single --d,
   the way the card deck does: a *signed* depth breaks that approach three
   ways. The deck's `z-index: calc(10 - var(--d))` inverts at --d:-1 (the
   item above would paint over the spotlit one); deriving |d| in CSS needs
   either abs(), which is too new to rely on, or squaring --d, whose
   falloff sends scale() negative — a mirrored flip — once a fourth hobby
   pushes |d| to 2; and a malformed --d invalidates the whole transform at
   computed-value time, where the var() fallback does not rescue it and
   the item snaps to dead centre. Writing finished numbers avoids all
   three.
   ========================================================================= */

.roll {
  /* vw-relative, not %: this gets substituted into both a WIDTH calc
     (.roll__item) and a HEIGHT calc (.roll__window) below, and a
     percentage means something different on each axis — in the height
     calc it would have resolved against the containing block's *height*,
     which is auto, collapsing the window to almost nothing. That was the
     roll's original bug: not the geometry, but the window it happened in. */
  --roll-size: min(300px, 78vw);  /* every item's width — all equal        */
  --roll-window-height: 1060px;   /* tall enough that neither neighbour's
                                      box is ever clipped — see the sums in
                                      the comment on .roll__window below   */
  --roll-edge-fade: 24px;         /* purely cosmetic: a soft edge at the
                                      window's own top/bottom, kept well
                                      short of the neighbours below so it
                                      never fades anything they contain    */
  /* --roll-x-side / --roll-y-side / --roll-scale-side / --roll-fade-side
     below are documentation, not live values: unlike --roll-size (read by
     .roll__item's width/height), nothing in this file's CSS actually
     consumes these four. sketch.js holds its own copies as the real
     source of truth and positions every item with inline styles. Found
     out the hard way — a :has() rule here once "changed" --roll-y-side
     correctly (confirmed via getComputedStyle) while the rendered items
     stayed exactly where they were, because nothing was reading it. Keep
     the numbers below matched to sketch.js's ROLL_X_SIDE / ROLL_Y_SIDE /
     ROLL_SCALE_SIDE / ROLL_FADE_SIDE by hand; changing one here changes
     nothing on screen. */
  --roll-x-side: -130px;          /* a neighbour's shift, left of centre   */
  --roll-y-side: 360px;           /* a neighbour's shift, up or down       */
  --roll-scale-side: 1;           /* all three the same size, on request   */
  --roll-fade-side: 0.82;         /* a neighbour's opacity: "slightly
                                      transparent", not faded out          */

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}

/* The globe is allowed to be bigger than the other two while it's the
   spotlit one (Bhaumik's call — ~20 travel markers need the room). On its
   own that would overlap it with its neighbours: a 460px spotlit item
   plus a 300px neighbour need 380px of clearance, more than the standard
   360px gap the other two items were tuned for. sketch.js's render()
   detects when the globe itself is the spotlit item and widens the gap
   to 400px only for that state (ROLL_Y_SIDE_GLOBE) — --roll-size is the
   only part of this that CSS can do on its own. */
.roll__item--globe.is-spotlit { --roll-size: 460px; }

/* Sized from two independent sums, since the spotlit item's caption pushes
   right while a neighbour pushes left — nothing here is symmetric.
   VERTICAL: a neighbour's centre sits 360px (--roll-y-side) from the
   spotlit centre, and every item's half-height is 150px (all three are
   --roll-size, unscaled), so the spotlit and a neighbour sit a genuine
   60px apart edge-to-edge, and the neighbour's own far edge reaches
   510px out — inside this box's 530px half-height, with room left for
   --roll-edge-fade to sit beyond it rather than across it.
   HORIZONTAL: a neighbour's left edge reaches 280px left of centre
   (150 half-width + 130px --roll-x-side); the spotlit item's caption
   reaches roughly 394px right of centre (150 half-width + a ~24px gap +
   up to ~220px of caption text). The window is sized to the larger of
   the two on both sides, so it never clips either. Changing --roll-size,
   --roll-x-side or --roll-y-side means re-checking these sums, the same
   way the card deck's --fan-* values were tuned by hand rather than
   derived.

   GLOBE CASE: when the globe is spotlit it's 460px (half 230) and
   --roll-y-side grows to 400px (see above), so the neighbour's far edge
   reaches 550px out — this window grows to match, the same way its width
   already has permanent headroom (880px column) for the globe's own
   wider caption reaching ~428px right of centre. */
.roll__window {
  position: relative;
  width: 100%;
  height: var(--roll-window-height);
  border-radius: 8px;
}

.roll:has(.roll__item--globe.is-spotlit) .roll__window {
  height: 1150px;
}

.roll__window:focus-visible { outline: 2px solid var(--ink); outline-offset: 8px; }

/* A soft cream fade at the very top and bottom of the window, NOT
   mask-image. A mask is a paint-time composite over the element's whole
   painted subtree, so it would fade and truncate the spotlit link's own
   focus ring (outline-offset: 6px below). The page ground is flat cream,
   so this reads identically, needs no -webkit- twin, and — sized well
   short of where the neighbours actually sit (see .roll__window above) —
   never touches them either. It does cover body's grain texture in the
   two bands, but at 0.035 opacity that is not perceptible. */
.roll__window::before,
.roll__window::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: var(--roll-edge-fade);
  pointer-events: none;
  z-index: 40;
}
.roll__window::before {
  top: 0;
  background: linear-gradient(to bottom, var(--cream) 12%, transparent);
}
.roll__window::after {
  bottom: 0;
  background: linear-gradient(to top, var(--cream) 12%, transparent);
}

.roll__items {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Every item is the same square box, centred; --x/--y move it left/right
   and up/down. Deliberately no rotate() — nothing here tilts, on request:
   the assembly moves like a dial turning, but each face on it stays flat.
   translate(-50%, -50%) first so the box's own centre is the anchor, which
   keeps scale() pivoting about the artefact rather than its corner. */
.roll__item {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--roll-size);
  /* Explicit, matching width: this box must be exactly the artefact
     square, nothing more. The bug that caused a visible overlap despite
     the numbers below adding up on paper was the caption sitting in flow
     alongside it — an invisible (opacity: 0) but still-laid-out block
     whose size fed into this element's own box, which is what
     translate(-50%, -50%) below measures itself against. Different items
     ended up with different real anchor points, so "no overlap" on paper
     wasn't "no overlap" on screen. The caption is positioned out of flow
     instead — see .roll__caption. */
  height: var(--roll-size);
  z-index: calc(var(--z, 0) + 1);
  opacity: var(--o, 1);
  transform:
    translate(-50%, -50%)
    translate3d(var(--x, 0px), var(--y, 0px), 0)
    scale(var(--s, 1));
  /* A neighbour is never a link target: the whole window is the hit zone,
     and sketch.js reads which half was pressed. Set in CSS rather than JS
     so a no-JS visitor cannot click a cropped, faded record through to
     SoundCloud. */
  pointer-events: none;
}

.roll__item.is-spotlit,
html:not(.roll-ready) .roll__item:nth-child(3) { pointer-events: auto; }

/* Reduced motion changes the spotlight instantly rather than sliding along
   the arc — same "skip the animated step, jump straight there" rule the
   card deck uses. */
@media (prefers-reduced-motion: no-preference) {
  .roll__item {
    transition: transform 480ms cubic-bezier(0.22, 0.8, 0.24, 1), opacity 420ms ease;
  }
}

/* Static fallback so the roll is a finished, correct-looking still without
   JS: the globe centred and spotlit (matching sketch.js's own starting
   spotlight), the vinyl and Climbing as neighbours, one below, one above.
   sketch.js writes these same properties inline, and inline wins. */
.roll__item:nth-child(1) { --x: -130px;  --y: 360px;  --s: 1; --o: 0.82; --z: 1; }
.roll__item:nth-child(2) { --x: -130px;  --y: -360px; --s: 1; --o: 0.82; --z: 1; }
.roll__item:nth-child(3) { --x: 0px;     --y: 0px;    --s: 1; --o: 1;    --z: 2; }

/* --- the caption, which travels with its own artefact ------------------- */

/* Positioned out of flow, to the right of the square, so its own size
   (which varies by hobby — the DJ caption is three lines, "Climbing" is
   one) can never feed back into .roll__item's box and shift that item's
   centre — the same reasoning that put it out of flow when it briefly
   lived underneath instead. */
.roll__caption {
  position: absolute;
  top: 50%;
  left: 100%;
  translate: 22px -50%;
  width: max-content;
  max-width: 22ch;
  font-size: 14px;
  line-height: 1.4;
  text-align: left;
  color: var(--ink-45);
  opacity: 0;
}

.roll__item.is-spotlit .roll__caption,
html:not(.roll-ready) .roll__item:nth-child(3) .roll__caption { opacity: 1; }

/* The pending note is scaffolding for a slot with no artefact yet — it goes
   when the real thing lands, and it must not read as finished copy. */
.roll__pending { font-style: italic; }

/* --- an empty-but-styled slot -------------------------------------------
   Real artefacts only: until there is a genuine photo for a hobby, its slot
   ships as a frame at the real size rather than a stand-in image. Dotted,
   to match the work-history rules, so it reads as reserved rather than
   broken. The hobby's name is NOT printed in here: an off-centre item is
   faded, and --ink-45 on cream has no headroom left for a multiplier — it
   would fail 1.4.3. The name lives in the caption, which only shows at
   full opacity. */
.slot {
  aspect-ratio: 1;
  border: 2px dotted var(--ink-45);
  border-radius: 10px;
  background: var(--tan);
  /* Same weight of shadow the vinyl casts, so an empty slot still reads as
     a real object sitting on the page rather than a hairline sketch that
     nearly disappears against --cream. */
  box-shadow: 0 16px 36px -14px rgba(20, 19, 15, 0.35);
}

/* --- the travel globe ----------------------------------------------------
   Same "square box, round content" pattern .vinyl uses: .globe fills the
   roll item's box exactly (.roll__item already sets an explicit square
   size), and the circle comes from border-radius on the canvas itself.
   cobe (loaded from a CDN — see sketch.js) draws to the canvas; every
   marker is our own pin + hover card, not cobe's native dots — sketch.js
   recomputes cobe's own rotation math to know where each one belongs on
   screen (see the GLOBE comment there for why: CSS anchor positioning was
   tried first and genuinely failed in testing, not just in theory). */
.globe {
  position: relative;
  width: 100%;
  height: 100%;
}

.globe__canvas {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  cursor: grab;
  touch-action: none;
}
.globe__canvas:active { cursor: grabbing; }

/* Canvas fallback content: the browser only paints this when scripting is
   off, or canvas itself isn't supported — never alongside the real,
   working globe, so there is no visibility toggling here to get wrong. */
.globe__fallback {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

.globe__markers {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Each marker is one positioned point (sketch.js sets its transform every
   frame; display:none is its own "facing away" state, also set there) —
   everything below is offset from that single point, not from a box.
   Hidden until sketch.js explicitly shows one with a real position —
   without this, ~20 markers would flash stacked at the top-left corner
   for the brief window between cobe loading and its first frame. */
.globe__marker {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: auto;
  z-index: 10;
}
.globe__marker:hover,
.globe__marker:has(:focus-visible) { z-index: 20; }

/* The pin's own bottom-centre sits exactly on the marker's point — the
   same convention as any map pin, tip down. A real <button>, not a div,
   so it's reachable by keyboard and has a name for assistive tech; the
   photos behind it are hover-only decoration with no separate a11y path
   (see sketch.js's aria-hidden on the card). */
.globe__pin {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  translate: -50% -100%;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.globe__pin img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 2px 3px rgba(20, 19, 15, 0.35));
}

/* Hidden until hovered or focused — "pop up and disappear," and the
   point of a pin-only default state: ~20 of these on screen at once
   would be unreadable if every card were always shown. */
.globe__marker-card {
  position: absolute;
  bottom: 30px; /* pin height (22px) + a small gap above its tip */
  left: 50%;
  translate: -50% 0;
  padding: 6px 6px 22px;
  background: var(--cream);
  border-radius: 2px;
  box-shadow: 0 8px 22px -10px rgba(20, 19, 15, 0.4), 0 1px 3px rgba(20, 19, 15, 0.18);
  opacity: 0;
  transform: scale(0.85);
  transform-origin: bottom center;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .globe__marker-card { transition: opacity 180ms ease, transform 180ms ease; }
}
.globe__marker:hover .globe__marker-card,
.globe__marker:has(:focus-visible) .globe__marker-card {
  opacity: 1;
  transform: scale(1);
}

.globe__marker-photos { display: flex; gap: 4px; }
.globe__marker-photos img {
  display: block;
  width: 60px;
  height: 60px;
  object-fit: cover;
  -webkit-user-drag: none;
  user-select: none;
}
/* Berlin and Ha Giang Loop each carry two photos rather than one winning
   over the other — the pair shrinks slightly so the card doesn't have to
   grow much wider than every single-photo marker beside it. */
.globe__marker-photos:has(img + img) img { width: 46px; height: 46px; }

.globe__marker-caption {
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: 5px;
  margin: 0;
  text-align: center;
  font-size: 10px;
  letter-spacing: 0.02em;
  color: var(--ink-70);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- controls ------------------------------------------------------------
   Real buttons, not just key handlers: arrow keys on a focusable div are
   undiscoverable, and touch has no other affordance. 44px minimum, which
   is the floor an earlier audit of this page set. */
.roll__controls {
  display: flex;
  align-items: center;
  gap: 6px;
}

.roll__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--ink-14);
  border-radius: 999px;
  background: var(--cream);
  color: var(--ink-70);
  font: inherit;
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease, border-color 160ms ease;
}
.roll__btn .arrow { width: 16px; height: 16px; }
.roll__btn:hover { color: var(--ink); background: var(--coral-soft); border-color: transparent; }

.roll__count {
  min-width: 4ch;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-align: center;
  color: var(--ink-45);
  font-variant-numeric: tabular-nums;
}

/* --- the swapping story in the right column ----------------------------- */

.roll__stories { margin-top: 18px; }

.roll__story { display: none; }
.roll__story.is-shown,
html:not(.roll-ready) .roll__story:last-child { display: block; }

/* --- the record ----------------------------------------------------------
   Sleeve on top, disc behind it, both the same square box. On hover the
   disc slides out to the right and spins; the sleeve tips the other way so
   it reads as being pulled out of it rather than the two just separating.
   Percentage translates, not pixels, so this scales freely with the roll
   item's own box (--roll-size) rather than capping at a fixed pixel size. */
.vinyl {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
}

.vinyl:focus-visible {
  outline: 2px solid var(--ink-45);
  outline-offset: 6px;
}

.vinyl__record,
.vinyl__sleeve {
  position: absolute;
  inset: 0;
  transition: transform 620ms cubic-bezier(0.32, 1.15, 0.4, 1);
}

/* One gradient, not a stack of ring elements: repeating-radial-gradient
   draws every groove at once, and the two near-black stops are close
   enough together to read as vinyl rather than as stripes. */
.vinyl__record {
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background:
    repeating-radial-gradient(circle at center,
      #1b1917 0 2px, #262220 2px 4px);
  box-shadow: 0 16px 36px -14px rgba(20, 19, 15, 0.45);
}

.vinyl__label {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34%;
  height: 34%;
  overflow: hidden;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--cream);
}
.vinyl__label-img { width: 100%; height: 100%; object-fit: cover; }

.vinyl__spindle {
  position: absolute;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--cream);
  box-shadow: inset 0 0 0 1px var(--ink-45);
}

.vinyl__sleeve {
  z-index: 2;
  overflow: hidden;
  border-radius: 4px;
  box-shadow: 0 12px 30px -12px rgba(20, 19, 15, 0.32);
}
.vinyl__sleeve-img { width: 100%; height: 100%; object-fit: cover; }

/* Hover only, only on the spotlit copy (an off-centre neighbour is not
   interactive at all — see .roll__item's pointer-events above), and only
   where motion is welcome — nothing is conveyed by the disc coming out
   (the sleeve carries the same photo), so a touch or reduced-motion
   visitor simply gets the sleeve, with nothing missing.

   No no-JS-fallback companion selector here any more: the vinyl is a
   neighbour, not the resting item, once the roll defaults to spotlighting
   the globe (see ROLL's :nth-child fallback) — pointer-events:none already
   covers it by default, so hovering it without JS running does nothing,
   correctly. */
@media (prefers-reduced-motion: no-preference) {
  .roll__item.is-spotlit .vinyl:hover .vinyl__record { transform: translateX(46%) rotate(192deg); }
  .roll__item.is-spotlit .vinyl:hover .vinyl__sleeve { transform: translateX(-6%) rotate(-4deg) scale(0.97); }
}

/* --- work history, now atop the Contact section --------------------------
   Quiet by design: no cards, just the page's existing label and list
   conventions (the uppercase tracked style already used for .project__no)
   so it reads as part of this page rather than an imported résumé
   component. Rows separated by a hairline rather than a filled box — a tan
   fill on cream read as too low-contrast to register as a boundary.
   Capped so it doesn't stretch full width on a big monitor; sits above
   Contact's own heading with room to breathe. */
.about__experience { max-width: 500px; margin-bottom: clamp(64px, 10vh, 108px); }

.about__experience-label {
  font-size: 14px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-45);
}

.jobs {
  display: flex;
  flex-direction: column;
  margin: 18px 0 0;
  padding: 0;
  list-style: none;
}

.job {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 0;
}

/* The dotted rule above each row and below the last one is its own
   pseudo-element rather than a plain border, so it can be drawn in with a
   left-to-right scaleX rather than just appearing — a dotted line growing
   from 0 width reveals its dots one after another for free, no per-dot
   markup needed. Visible by default (scaleX(1)); only .reveal-ready hides
   it to animate in, same pattern as .job below, so reduced motion and no-JS
   both just show the finished lines rather than a permanently zero-width one. */
.job::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  border-top: 2px dotted var(--ink-45);
  transform: scaleX(1);
  transform-origin: left;
}
.job:last-child::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  border-bottom: 2px dotted var(--ink-45);
  transform: scaleX(1);
  transform-origin: left;
}

/* Slides in from the left the first time it scrolls into view. The hidden
   start state is scoped under .reveal-ready — see the scroll-reveal block
   below for why that matters. */
.reveal-ready .job {
  opacity: 0;
  transform: translateX(-32px);
  transition: opacity 500ms ease, transform 500ms ease;
}
.reveal-ready .job.is-visible { opacity: 1; transform: translateX(0); }
.reveal-ready .job:nth-child(2) { transition-delay: 120ms; }

/* Lines start hidden only once .reveal-ready is armed, and each waits for
   the one before it to finish so the three draw in strictly one after
   another: row 1 in (500ms) -> its top line (450ms, starting at 500ms) ->
   row 2's top line, i.e. the divider (450ms, starting at 950ms) -> the
   closing line under the last row (450ms, starting at 1400ms). */
.reveal-ready .job::before,
.reveal-ready .job:last-child::after {
  transform: scaleX(0);
  transition: transform 450ms ease;
}
.reveal-ready .job.is-visible::before { transform: scaleX(1); transition-delay: 500ms; }
.reveal-ready .job:nth-child(2).is-visible::before { transform: scaleX(1); transition-delay: 950ms; }
.reveal-ready .job:last-child.is-visible::after { transform: scaleX(1); transition-delay: 1400ms; }

.job__icon {
  flex: none;
  width: 34px;
  height: 34px;
  object-fit: contain;
}

.job__role {
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
}
.job__company { font-weight: 400; color: var(--ink-70); }

.job__dates {
  margin-top: 4px;
  font-size: 13px;
  color: var(--ink-45);
}

/* --- the blurb, right-aligned against the section's own gutter ----------- */

.about__head {
  font-family: var(--display);
  font-size: clamp(30px, 4.2vw, 60px);
  line-height: 1.02;
  letter-spacing: -0.03em;
  text-align: right;
}

.about__body {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
  margin-left: auto;
  max-width: 62ch;
  gap: 15.2px;
}
.about__body p { font-size: 17px; line-height: 1.72; color: var(--ink-70); text-align: right; max-width: 62ch; }

/* =========================================================================
   PROJECTS
   The forest panel is gone: the section sits on paper like About, and
   typography carries the weight instead of a coloured box. One real project
   with a flickable screenshot stack, beside one in progress, at half width
   each.
   ========================================================================= */

.built {
  padding: clamp(56px, 9vh, 118px) var(--gut) clamp(64px, 10vh, 130px);
  border-top: 1px solid var(--ink-14); /* scaffolding: marks where About ends */
}

.built__head {
  max-width: 26ch;
  font-family: var(--display);
  font-size: clamp(30px, 5vw, 68px);
  line-height: 1;
  letter-spacing: -0.03em;
}

/* --- one project, real or reserved -------------------------------------- */

.project__no {
  font-size: 14px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-45);
}

.project__name {
  margin-top: 10px;
  font-family: var(--display);
  font-size: clamp(28px, 3.4vw, 44px);
  line-height: 1.04;
  letter-spacing: -0.025em;
}

.project__blurb {
  max-width: 46ch;
  margin-top: 12px;
  font-size: 17px;
  line-height: 1.68;
  /* Two lines' worth of room regardless of actual length. This is breathing
     room only now — the shared grid rows on .project__row are what align
     the two columns, so shortening this tightens both rather than knocking
     anything out of line. */
  min-height: 3.36em;
  color: var(--ink-70);
}

.brand-mandarin { color: var(--mandarin); }

/* The featured blurb runs to the same width as the card deck below it, so
   the two share an edge instead of stopping at unrelated points. */
.project--featured .project__blurb { max-width: var(--card-width); }

.project--featured, .project--building {
  --card-width: 620px;
}

/* --- the card stack ------------------------------------------------------
   Every card sits in the same 16:10 box. --d is a card's depth from the
   front (0 = front) and drives its offset, rotation, scale and stacking
   order, so the whole deck's shape is one number per card. --dx / --drot
   are set by sketch.js only while a card is being dragged.
   ------------------------------------------------------------------------ */

.stack {
  /* The deck's shape, as three numbers per step of depth, plus --fan-y0: a
     flat offset applied to every card regardless of depth, so the front
     card can drop slightly too instead of only the back ones lifting. */
  --fan-x: 0px;
  --fan-y: -11px;
  --fan-y0: 0px;
  --fan-rot: -1.05deg;
  --fan-scale: 0.042;

  position: relative;
  width: min(100%, var(--card-width));
  aspect-ratio: 16 / 10;
  margin-top: clamp(40px, 5.5vh, 62px);
  touch-action: pan-y; /* keep vertical page scrolling on touch */
  cursor: grab;
}
.stack:active { cursor: grabbing; }
.stack:focus-visible { outline-offset: 12px; }

/* Fan the deck out on hover or focus. This is the only cue that the stack is
   interactive, now that the text hint beside the counter is gone. Gated on
   motion being welcome: with no transition to carry it, a fan would snap
   open, which is worse than not fanning at all. */
@media (prefers-reduced-motion: no-preference) {
  .stack:hover, .stack:focus-visible {
    --fan-x: 11px;
    --fan-y: -14px;
    --fan-y0: 5px;
    --fan-rot: -2.4deg;
    --fan-scale: 0.03;
  }
}

.card {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: 20px;
  background: var(--cream);
  border: 1px solid var(--ink-14);
  box-shadow: 0 24px 48px -30px rgba(20, 19, 15, 0.5);
  z-index: calc(10 - var(--d, 0));
  transform:
    translate3d(
      calc(var(--dx, 0px) + var(--d, 0) * var(--fan-x)),
      calc(var(--fan-y0, 0px) + var(--d, 0) * var(--fan-y)),
      0)
    rotate(calc(var(--d, 0) * var(--fan-rot) + var(--drot, 0deg)))
    scale(calc(1 - var(--d, 0) * var(--fan-scale)));
  transition: transform 420ms cubic-bezier(0.22, 0.8, 0.24, 1), opacity 300ms ease;
}

/* Static depths so the deck is a deck before, and without, JS. sketch.js
   sets --d inline once it runs, and inline styles win over these. */
.card:nth-child(1) { --d: 0; }
.card:nth-child(2) { --d: 1; }
.card:nth-child(3) { --d: 2; }
.card:nth-child(4) { --d: 3; }

/* Tracking a finger or cursor 1:1 means no easing in the way. */
.stack.is-dragging .card { transition: none; }

/* The flick: the front card leaves in the direction it was thrown. */
.card--out {
  transform:
    translate3d(calc(var(--fling, 1) * 128%), -6%, 0)
    rotate(calc(var(--fling, 1) * 16deg))
    scale(0.96);
  opacity: 0;
  transition: transform 380ms cubic-bezier(0.4, 0, 0.7, 1), opacity 380ms ease;
}

/* Applied for a single frame while a spent card is moved to the back, so it
   snaps into the deck instead of flying back in from off-screen. */
.card--reseat { transition: none; }

/* The captures are already 16:10, so cover is a no-op today — it is here so a
   future screenshot at another ratio fills the card instead of distorting. */
.card__art {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Without this, dragging a card triggers the browser's own "drag this
     image" instead of sketch.js's pointer-based drag — draggable="false" on
     the <img> covers most browsers, -webkit-user-drag covers Safari. */
  -webkit-user-drag: none;
  user-select: none;
}

.stack__note {
  width: min(100%, var(--card-width));
  margin-top: 16px;
}
.stack__count {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--ink-45);
  font-variant-numeric: tabular-nums;
}

/* --- live badge and link ------------------------------------------------- */

.project__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  margin-top: clamp(10px, 1.4vh, 16px);
}

.chip-live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 12px;
  border-radius: 999px;
  /* Outline badge, border and text back to the original dark forest
     green (--forest) at Bhaumik's instruction — not the new palette's
     mustard, and not touching this component further. --forest is
     near-black, so this also happens to have strong contrast on cream. */
  background: var(--cream);
  border: 1px solid var(--forest);
  color: var(--forest);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.chip-live__dot {
  position: relative;
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--live);
}
/* The pulse is a second dot expanding out from under the first. */
.chip-live__dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--live);
  animation: live-pulse 2s ease-out infinite;
}

@keyframes live-pulse {
  from { transform: scale(1);   opacity: 0.5; }
  to   { transform: scale(3.4); opacity: 0; }
}

/* No underline: the tilt is the affordance. Pivoting on the left edge lifts
   the far end and the arrow with it, so the whole thing leans off the page in
   the direction it is about to take you. */
.project__link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  font-family: var(--display);
  font-size: clamp(19px, 1.7vw, 24px);
  letter-spacing: -0.015em;
  text-decoration: none;
  transform-origin: left center;
  transition: rotate 200ms cubic-bezier(0.22, 0.8, 0.24, 1);
}
.project__link:hover, .project__link:focus-visible { rotate: -2.6deg; }

/* --- project 1 and project 2, side by side ------------------------------- */

/* Seven shared rows, one per stacked part of a project: label, name, blurb,
   description, box, counter, meta. Each .project is a subgrid spanning all
   seven, so corresponding parts sit in the *same* row and therefore at the
   same height — the box lines up with the box, the pill with the pill,
   whatever the copy above them does.

   This replaced a chain of matched magic numbers (a reserved blurb height,
   hand-matched margin-tops, a hidden ghost spacer). That chain held only
   while both columns had identical structure, and broke the moment
   WhatsNear gained a description paragraph that project 2 has no
   counterpart for. Don't go back to spacers: add a row here instead.

   No `align-items` — the articles must keep the default `stretch` to span
   their rows. Setting `start` collapses each one to its content height and
   the shared rows stop being shared. */
.project__row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(7, auto);
  /* Column gutter only. A plain `gap` here also becomes a *row* gap, which
     was harmless while this was a single-row grid but adds its full value
     between every one of the seven rows now — i.e. between the label, the
     name, the blurb and everything below them. Vertical rhythm stays where
     it always was: each element's own margin-top. */
  column-gap: clamp(24px, 3.5vw, 56px);
  row-gap: 0;
  margin-top: clamp(42px, 6vh, 80px);
}

.project {
  display: grid;
  grid-row: span 7;
  grid-template-rows: subgrid;
}

/* Project 2 has no description paragraph — nothing invented goes in that
   slot — so it skips row 4 and places the rest by hand. Row 6 (WhatsNear's
   card counter) is empty here too, which is what keeps the two meta rows
   level. */
.project--building .building-slot { grid-row: 5; }
.project--building .project__meta { grid-row: 7; }

/* --- project 2: in progress, same format as project 1 -------------------- */

/* Same capped width and aspect ratio as .stack, so the two boxes are
   identical in size; the shared grid row above puts them at the same
   height. The margin-top matches .stack's so the row's own height is the
   same either way. */
.building-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: min(100%, var(--card-width));
  margin-top: clamp(40px, 5.5vh, 62px);
  aspect-ratio: 16 / 10;
  border-radius: 18px;
  border: 1px dashed var(--ink-14);
}

.googly { display: flex; gap: 28px; }

.googly__eye {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--cream);
  border: 1px solid var(--ink-14);
  box-shadow: inset 0 3px 8px rgba(20, 19, 15, 0.18);
}

.googly__pupil {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--ink);
  transform: translate(var(--eye-x, 0px), var(--eye-y, 0px));
}

/* Cursor-tracking (set inline by sketch.js on a fine pointer) turns this
   off via inline `animation: none`, so the two never fight over the
   pupil's position. */
@media (prefers-reduced-motion: no-preference) {
  .googly__pupil { animation: googly-idle 3.6s ease-in-out infinite; }
  .googly__eye:nth-child(2) .googly__pupil { animation-delay: -1.8s; }
}

@keyframes googly-idle {
  0%, 100% { transform: translate(-6px, 2px); }
  50%      { transform: translate(6px, -2px); }
}

.chip-building {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--cream);
  border: 1px solid var(--rust);
  color: var(--rust);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.chip-building__dot {
  position: relative;
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--rust);
}
/* Same pulse as the live dot, just the rust colour. */
.chip-building__dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--rust);
  animation: live-pulse 2s ease-out infinite;
}

/* =========================================================================
   CONTACT
   ========================================================================= */

.contact {
  padding: clamp(48px, 8vh, 108px) var(--gut) clamp(44px, 7vh, 84px);
  border-top: 1px solid var(--ink-14); /* scaffolding: marks where Built ends */
}

.contact__head {
  max-width: 30ch;
  font-family: var(--display);
  font-size: clamp(28px, 4.6vw, 66px);
  line-height: 1.04;
  letter-spacing: -0.032em;
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: clamp(32px, 5vh, 52px) 0 0;
  padding: 0;
  list-style: none;
}
.actions li[hidden] { display: none; }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 15px 28px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--cream);
  font-size: 15px;
  font-weight: 500;
  text-decoration: none;
  transition: background 170ms ease, color 170ms ease;
}
/* --coral-dark, not plain --coral, so the hover/focus text can be
   cream/white and still hold real contrast (~6.9:1). */
.btn:hover, .btn:focus-visible { background: var(--coral-dark); color: var(--cream); }

/* =========================================================================
   NARROWER
   Below the fold-over the hero stops being a stacked composition and simply
   reads top to bottom.
   ========================================================================= */

@media (max-width: 900px) {
  .hero {
    min-height: auto;
    padding-top: 96px;
    padding-bottom: 32px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
  }
  .wordmark {
    position: static;
    font-size: min(17vw, 150px);
    margin-bottom: 12px;
  }
  .sketch { position: static; width: min(300px, 72vw); align-self: center; }
  .hero__intro { position: static; width: auto; max-width: 40ch; margin-top: 28px; }
  .about__grid, .project__row { grid-template-columns: minmax(0, 1fr); }
  /* Stacked, there is no second column to line anything up with, so the
     subgrid comes off and each project is just a block in normal flow.
     `row-gap` is 0 above (see .project__row), so the separation the two
     columns used to get from the gutter is restored as a margin here. */
  .project { display: block; grid-row: auto; }
  .project--building { margin-top: clamp(24px, 3.5vw, 56px); }
  .nav a { padding: 8px 13px; font-size: 13px; }
  .scroll-cue { display: none; }
}
