/* ==========================================================================
   hrithikshetty.com

   TYPE RULE: three faces, each with one job, and one body size everywhere.

     Inter          all body copy, headings, UI. One size (--fs-base) for
                    prose; headings differ by weight and a short scale.
     Jost           display only — the cover name, the loader H, the
                    oversized watermark words.
     JetBrains Mono the "system voice" — section labels, dates, numbers,
                    card indices, the loader counter. Anything that reads
                    as instrumentation rather than as writing.

   Do not introduce new families or sizes outside :root.

   COLOUR RULE: warm paper, near-black ink, and exactly one accent. The
   accent is a signal, not a decoration — it marks the live thing on screen
   (active nav item, filled meter, hovered link) and nothing else.
   ========================================================================== */

:root {
  /* ---- colour ---- */
  --paper:   #f5f3ee;     /* warm off-white — the default ground */
  --panel:   #ebe8e1;     /* alternating section ground */
  --black:   #0d0d0d;     /* cover, contact, footer, loader */
  --ink:     #14130f;
  --ink-2:   #3d3b35;
  --grey:      #6b6862;   /* passes 4.5:1 on --paper AND on --panel */
  --grey-dark: #8a8780;   /* passes 4.5:1 on --black */
  --grey-2:    #a8a49b;   /* decoration only — rules, dots. never text */
  --rule:      #ddd9d0;
  --rule-dark: #262523;
  --ghost:     #e9e6de;
  --ghost-p:   #e2ded4;
  --ghost-d:   #161614;

  /* The accent. 5.3:1 on --paper, so it is safe for text there. Only 3.3:1
     on --black — on dark grounds use it for large type and UI marks, never
     for body copy. */
  --accent:      #2b4cff;
  --accent-soft: rgba(43, 76, 255, 0.12);
  /* The same blue lifted until it clears 4.5:1 on --black (it measures
     5.5:1). Use this, not --accent, for accent-coloured TEXT on a dark
     ground — the section labels on the Contact panel, for instance. */
  --accent-on-dark: #5b78ff;

  /* ---- type ---- */
  --font:    'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --display: 'Jost', 'Century Gothic', Futura, system-ui, sans-serif;
  --mono:    'JetBrains Mono', ui-monospace, Consolas, monospace;

  --fs-xs:   0.8125rem;   /* captions, meta, tags */
  --fs-sm:   0.9375rem;   /* labels, nav, card meta */
  --fs-base: 1.0625rem;   /* ALL body copy */
  --fs-md:   1.25rem;     /* lead paragraph, card title */
  --fs-lg:   1.75rem;     /* section heading */
  --fs-xl:   2.5rem;      /* project page title */

  /* The cover name's size, hoisted out of .hero__name because the loader's
     H has to be drawn at exactly the same size to land on it. One source of
     truth — change it here and the loading sequence stays in register. */
  --hero-fs: clamp(3.2rem, 13vw, 10.5rem);

  --lh-tight: 1.3;
  --lh-body:  1.75;

  /* ---- layout ---- */
  --wrap:   1180px;
  --gutter: clamp(1.25rem, 5vw, 4rem);
  --sec-y:  clamp(4.5rem, 11vw, 8.5rem);

  /* ---- easing ---- */
  --ease-out:  cubic-bezier(0.16, 1, 0.3, 1);
  --ease-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  color-scheme: light;
}

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

html {
  scroll-behavior: smooth;
  scroll-padding-top: 5.5rem;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }
a   { color: inherit; }

h1, h2, h3 {
  font-family: var(--font);
  line-height: var(--lh-tight);
  letter-spacing: -0.011em;
  margin: 0;
}

.wrap {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.dim { color: var(--grey); }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 999;
  background: var(--black);
  color: #fff;
  padding: 0.75rem 1.25rem;
  font-size: var(--fs-sm);
}
.skip:focus { left: 0; }

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

/* ========================== LOADING SEQUENCE ============================
   The cover's opening move. A bar fills, folds itself into an H, and the H
   flies into the first letter of HRITHIK — at which point the rest of the
   name cascades in behind it.

   Three rules keep this safe:

   1. It is DISPLAY:NONE by default. It only appears when the inline script
      in <head> adds `boot-on` to <html>, which it does only when JS is on,
      motion is allowed, the page owns a cover (data-boot-page), the visitor
      landed at the top (no #hash), and it has not already played this tab
      session. Everyone else — including no-JS visitors — never sees it and
      never gets a black rectangle over the site.
   2. It shares the cover's background. The overlay and .hero are the same
      black, so the handover is a fade of *content*, not of ground.
   3. It is decorative. Every glyph in here is aria-hidden and duplicated
      nowhere — no copy lives in the loader.
   ======================================================================== */

.boot { display: none; }

/* Locking the scroll removes the scrollbar, which widens the viewport by
   ~15px on Windows and Linux. Everything centred then sits 7.5px to the
   right for the length of the sequence and SNAPS back the instant the lock
   releases — right at the handover, which is exactly where it is most
   visible. Reserving the gutter keeps the layout identical either side of
   the lock, so there is nothing to snap.

   (Browsers with overlay scrollbars — macOS, iOS — never had the shift, and
   ones without `scrollbar-gutter` are no worse off than before.) */
html.boot-on { overflow: hidden; scrollbar-gutter: stable; }
html.boot-on body { overflow: hidden; }
/* the nav is faded out at this point, so it must not be clickable either */
html.boot-on .nav { pointer-events: none; }

/* Two selectors, one block. `boot-on` paints it on the very first frame,
   before main.js has run; `is-live` is added by main.js and is what keeps it
   on screen after the sequence removes `boot-on` to release the scroll lock
   and fade the cover up. Without the second, the final fade would be cut off
   by display:none the instant the class came off. */
html.boot-on .boot,
.boot.is-live {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 200;
  transition: opacity 0.4s ease;
}
/* The black lives on a pseudo-element so the flight can fade OPACITY, which
   the compositor animates off the main thread, instead of background-color,
   which repaints the whole viewport every frame — right while the glyph's
   transform is running. Painted first, so every child sits above it. */
.boot::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--black);
  transition: opacity 0.9s ease;
}
/* the ground drops out while the H is in flight — the cover is already
   black underneath, so nothing flashes */
.boot.is-flight::before { opacity: 0; }
.boot.is-done   { opacity: 0; pointer-events: none; }

/* The mark. Centred by inset+margin rather than a transform, so `transform`
   stays free for the flight. Every piece inside is positioned against its
   centre.

   The five ratios below are read straight out of the Jost variable font
   instanced at wght 700 (units-per-em 1000): stems 169 units, crossbar
   151.5 (thinner than the stems — real type is like that), cap 700 tall by
   639 wide, crossbar centre 19.2 units above the cap's centre. The closer
   these sit to the real glyph, the less the sharpen step has to hide. */
.boot__mark {
  --u:    calc(var(--hero-fs) * 0.169);    /* stem width */
  --cb:   calc(var(--hero-fs) * 0.1515);   /* crossbar thickness */
  --cby:  calc(var(--hero-fs) * -0.0192);  /* crossbar lift above centre */
  --hh:   calc(var(--hero-fs) * 0.700);    /* cap height */
  --hw:   calc(var(--hero-fs) * 0.639);    /* cap width */
  --barw: min(72vw, 20rem);
  --barh: calc(var(--hero-fs) * 0.24);     /* the bar is chunkier than the H */

  position: fixed;
  inset: 0;
  margin: auto;
  width: var(--barw);
  height: max(var(--hh), var(--barh));
}

/* the unfilled track */
.boot__track {
  position: absolute;
  left: 0;
  top: 50%;
  width: 100%;
  height: var(--barh);
  margin-top: calc(var(--barh) / -2);
  background: rgba(255, 255, 255, 0.16);
  transition: opacity 0.35s ease;
}

/* The filled bar — and, after the morph, the H's crossbar. Same element:
   that is what makes the fold read as one continuous object rather than a
   swap. Width and height are what animate, so it contracts about its own
   centre. */
.boot__bar {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--barw);
  height: var(--barh);
  transform: translate(-50%, -50%);
  transition: width 0.62s var(--ease-in-out), height 0.62s var(--ease-in-out),
              transform 0.62s var(--ease-in-out);
}
.boot__fill {
  display: block;
  width: 100%;
  height: 100%;
  background: #fff;
  transform: scaleX(0);
  transform-origin: left center;
  /* no transition: main.js sets this every frame from the eased progress */
}

/* the two uprights, grown from the crossbar outwards */
.boot__stem {
  position: absolute;
  top: 50%;
  width: var(--u);
  height: var(--hh);
  margin: calc(var(--hh) / -2) 0 0 calc(var(--u) / -2);
  background: #fff;
  transform: scaleY(0);
  transition: transform 0.66s var(--ease-out) 0.06s;
}
.boot__stem--l { left: calc(50% - (var(--hw) - var(--u)) / 2); }
.boot__stem--r { left: calc(50% + (var(--hw) - var(--u)) / 2); }

.boot.is-h .boot__track { opacity: 0; }
.boot.is-h .boot__bar {
  width: var(--hw);
  height: var(--cb);
  transform: translate(-50%, calc(-50% + var(--cby)));
}
.boot.is-h .boot__stem  { transform: scaleY(1); }

/* The real letterform. Drawn at --hero-fs with the cover's exact metrics,
   so once the three bars cross-fade into it the flight is a pure
   translation and it lands on the cover's H to the pixel. */
.boot__glyph {
  position: absolute;
  left: 50%;
  top: 50%;
  width: max-content;
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--hero-fs);
  line-height: 0.84;
  /* NOT the cover's -0.02em. Spacing pads (here: shrinks) the box after the
     glyph, so with it on, centring the box sat the ink ~2px right of the
     bars. The flight is unaffected either way — it aligns top-LEFT corners,
     and spacing only moves the box's right edge. */
  letter-spacing: 0;
  text-transform: uppercase;
  color: #fff;
  opacity: 0;
  transform: translate(-50%, -50%);
  transition: opacity 0.28s ease;
  will-change: transform;   /* its own layer, so the flight never repaints */
}
/* Deliberately NOT a cross-fade. Two white shapes fading through each other
   both sit at ~0.5 alpha in the middle, which composites to 0.75 over black
   — a visible dip in brightness exactly where the eye is. So the letterform
   snaps to full opacity underneath, and the bars fade off it. Same total
   duration, no flicker, and it reads as the shape sharpening into a letter. */
.boot.is-glyph .boot__glyph { opacity: 1; transition: none; }
.boot.is-glyph .boot__bar,
.boot.is-glyph .boot__stem { opacity: 0; transition: opacity 0.24s ease; }

/* main.js appends the flight translation to the centring one. The curve
   leans late so the letter decelerates into its target rather than arriving
   at speed and stopping dead. */
.boot.is-flight .boot__glyph {
  transition: transform 0.95s cubic-bezier(0.7, 0, 0.18, 1), opacity 0.3s ease;
}

/* The counter. Oversized and deliberately cropped by the corner, the way
   Lusion's is — it reads as machine output bleeding off the frame. */
.boot__count {
  position: absolute;
  left: calc(var(--gutter) * 0.4);
  bottom: -0.24em;
  display: flex;
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(4rem, 13vw, 11rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: #fff;
  transition: opacity 0.4s ease;
}
.boot__digit { height: 1em; overflow: hidden; }
.boot__reel {
  display: block;
  transition: transform 0.5s var(--ease-soft);
}
.boot__reel span { display: block; height: 1em; }

.boot__word {
  position: absolute;
  right: var(--gutter);
  bottom: calc(var(--gutter) * 0.9);
  margin: 0;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--grey-dark);
  transition: opacity 0.4s ease;
}

.boot.is-h .boot__count,
.boot.is-h .boot__word { opacity: 0; }

/* Held back while the loader runs.

   The cover's supporting text comes up DURING the flight, not after it —
   main.js adds `boot-reveal` about half-way through. Waiting until the H
   lands made everything arrive on one frame, which is what read as a page
   swap; overlapping it means the cover assembles itself around the letter
   while the letter is still travelling, and there is no single moment where
   the screen changes. */
.hero__kicker, .hero__role, .hero__meta, .hero__cue, .nav, .progress {
  transition: opacity 0.9s ease;
}
html.boot-on .hero__kicker,
html.boot-on .hero__role,
html.boot-on .hero__meta,
html.boot-on .hero__cue,
html.boot-on .nav,
html.boot-on .progress { opacity: 0; }

html.boot-on.boot-reveal .hero__kicker,
html.boot-on.boot-reveal .hero__role,
html.boot-on.boot-reveal .hero__meta,
html.boot-on.boot-reveal .hero__cue { opacity: 1; }

/* ============================== CURSOR =================================
   A precise dot at the pointer plus a ring that lags behind it. The ring
   swells over anything clickable.

   The native cursor is only hidden once main.js has confirmed the thing is
   actually running (html.cursor-on) — so a script that fails to load leaves
   the visitor with a normal, visible pointer rather than none at all.
   ====================================================================== */

.cursor { display: none; }

html.cursor-on .cursor { display: block; }
html.cursor-on,
html.cursor-on a,
html.cursor-on button { cursor: none; }

.cursor__dot,
.cursor__ring {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 300;
  border-radius: 50%;
  pointer-events: none;
  will-change: transform;
}
.cursor__dot {
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  background: var(--accent);
  transition: opacity 0.2s ease, transform 0.2s var(--ease-out);
}
.cursor__ring {
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border: 1px solid var(--accent);
  transition: width 0.3s var(--ease-out), height 0.3s var(--ease-out),
              margin 0.3s var(--ease-out), background-color 0.3s ease,
              opacity 0.2s ease;
}

/* over a link, card or button */
.cursor.is-hot .cursor__ring {
  width: 62px;
  height: 62px;
  margin: -31px 0 0 -31px;
  background: var(--accent-soft);
}
.cursor.is-hot .cursor__dot { opacity: 0; }
.cursor.is-down .cursor__ring { width: 26px; height: 26px; margin: -13px 0 0 -13px; }
.cursor.is-out .cursor__dot,
.cursor.is-out .cursor__ring { opacity: 0; }

/* on the black sections the accent ring loses contrast — go white */
.cursor.is-dark .cursor__dot { background: #fff; }
.cursor.is-dark .cursor__ring { border-color: #fff; }
.cursor.is-dark.is-hot .cursor__ring { background: rgba(255, 255, 255, 0.14); }

/* ======================= HERO / COVER (type exception) ================== */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  padding: var(--gutter);
  padding-block: 6rem;
  background: var(--black);
  color: #fff;
  overflow: hidden;
}

.hero__inner {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
  position: relative;
  z-index: 1;
}

.hero__kicker {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  margin: 0 0 1.1rem;
  font-family: var(--mono);
  font-size: clamp(0.8rem, 0.7rem + 0.5vw, 1.05rem);
  letter-spacing: 0.14em;
  color: var(--grey-dark);
}
/* a small live mark, the one place the accent appears on the cover */
.hero__kicker::before {
  content: "";
  flex: none;
  width: 9px;
  height: 9px;
  background: var(--accent);
}

.hero__name {
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--hero-fs);
  line-height: 0.84;        /* tight, so the two lines read as one block */
  letter-spacing: -0.02em;
  text-transform: uppercase;
}
/* HRITHIK over SHETTY. The whitespace between the two spans collapses,
   because whitespace between block boxes is discarded. */
.hero__line { display: block; }
.hero__name .ch {
  display: inline-block;
  white-space: pre;
}
/* per-character reveal; --d is the per-character delay, set by main.js */
.js .hero__name .ch {
  opacity: 0;
  transform: translateY(0.35em);
  transition: opacity 0.5s ease var(--d, 0ms),
              transform 0.7s var(--ease-out) var(--d, 0ms);
}
.js .hero__name .ch.in { opacity: 1; transform: none; }

/* The landing pad. During the loading sequence the cover's first letter is
   what the glyph's flight is measured against and lands on, so it must do
   two things differently from its siblings:

   1. transform: none — its rect is its FINAL position. The others sit at
      translateY(0.35em) pre-reveal; measuring one of those sends the glyph
      to a spot 0.35em below where the letter will actually rest, and the H
      visibly hops upward at the handover.
   2. transition: none — it appears in one frame UNDER the still-opaque
      glyph. The same reasoning as the sharpen step: cross-fading two white
      glyphs over black dips to 75% brightness half-way. Snapping the letter
      on beneath, then fading the glyph off an identical letter, changes
      nothing visibly — which is the point.

   main.js removes the class once the handover is done, so replays of the
   reveal animate this character like any other. */
.js .hero__name .ch.ch--pad {
  transform: none;
  transition: none;
}

.hero__role {
  margin: 1.5rem 0 0;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1rem, 0.8rem + 1.4vw, 1.9rem);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #e9e7e2;
}
.hero__role span { color: var(--grey-dark); margin-inline: 0.35em; }

.hero__meta {
  margin: 2.2rem 0 0;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--grey-dark);
}

.hero__cue {
  position: absolute;
  left: 50%;
  bottom: 2.2rem;
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--grey-dark);
  text-decoration: none;
}
.hero__cue span { display: inline-block; margin-left: 0.4em; animation: bob 2.4s ease-in-out infinite; }
.hero__cue:hover { color: #fff; }
@keyframes bob { 0%,100% { transform: translateY(0);} 50% { transform: translateY(5px);} }

/* faint drifting grid behind the cover */
.hero__grid {
  position: absolute;
  inset: -25%;
  background-image:
    linear-gradient(to right,  #ffffff09 1px, transparent 1px),
    linear-gradient(to bottom, #ffffff09 1px, transparent 1px);
  background-size: 64px 64px;
  animation: gridDrift 40s linear infinite;
  pointer-events: none;
}
@keyframes gridDrift { to { transform: translate3d(64px, 64px, 0); } }

/* ================================ NAV =================================== */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem var(--gutter);
  background: rgba(245, 243, 238, 0.86);
  backdrop-filter: saturate(160%) blur(12px);
  border-bottom: 1px solid var(--rule);
}

.nav__brand {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  letter-spacing: 0.02em;
  text-decoration: none;
  flex: none;
}

.nav__list {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: clamp(0.7rem, 2vw, 1.8rem);
  margin: 0; padding: 0;
  list-style: none;
}

.nav__list a {
  position: relative;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  text-decoration: none;
  color: var(--grey);
  padding-block: 0.2rem;
  transition: color 0.25s;
}
/* the underline is its own layer so it can wipe in from the left rather
   than simply appearing */
.nav__list a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.3s var(--ease-out);
}
.nav__list a:hover { color: var(--ink); }
.nav__list a:hover::after,
.nav__list a.is-active::after { transform: scaleX(1); }
.nav__list a.is-active { color: var(--ink); }

/* holds the links and the motion toggle together on the right */
.nav__right {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: clamp(0.7rem, 2vw, 1.8rem);
}

/* Motion toggle. The site follows the visitor's OS "reduce motion" setting by
   default; this lets them override it in either direction and the choice is
   remembered. State is shown from the `no-motion` class rather than from
   aria-pressed, so the button is painted correctly on the very first frame. */
.motion {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  flex: none;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--grey);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s;
}
.motion:hover { color: var(--ink); border-color: var(--grey-2); }
.motion__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  background: transparent;
}
html:not(.no-motion) .motion { color: var(--ink); border-color: var(--grey-2); }
html:not(.no-motion) .motion__dot { background: var(--accent); border-color: var(--accent); }

/* The toggle does nothing without JavaScript, so it is not offered. */
html:not(.js) .motion { display: none; }

/* scroll progress */
.progress {
  position: fixed;
  top: 0; left: 0;
  height: 2px;
  width: 100%;
  z-index: 60;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  pointer-events: none;
}

/* ============================== SECTIONS ================================ */

.section {
  position: relative;
  overflow: hidden;
  padding-block: var(--sec-y);
}
.section--panel { background: var(--panel); }
.section--dark  { background: var(--black); color: #fff; }

/* Oversized watermark word. Decorative, so the glyphs live in CSS — they
   carry no meaning and must not be announced or judged as body text. */
.ghost::after { content: attr(data-ghost); }
.ghost {
  position: absolute;
  right: -0.055em;
  bottom: -0.12em;
  z-index: 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(4.5rem, 17vw, 15rem);
  line-height: 0.78;
  letter-spacing: -0.03em;
  color: var(--ghost);
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
  will-change: transform, opacity;
}
.section--panel .ghost { color: var(--ghost-p); }
.ghost--dark { color: var(--ghost-d); }

/* sweep-in: starts off to the right, slides home when the section arrives */
.js .ghost {
  opacity: 0;
  transform: translate3d(28vw, 0, 0);
  transition: opacity 1s ease, transform 1.25s var(--ease-out);
}
.js .ghost.in { opacity: 1; transform: translate3d(0, 0, 0); }

/* Section label — reads as a path fragment: /EXPERIENCE. The whole thing is
   the accent, which is what makes it the loudest small text on the page.

   The slash is a pseudo-element so it never lands in the text a screen reader
   or a search engine sees. `gap` is zero so the slash sits flush against the
   word; the rule that follows carries its own margin instead. */
.label {
  display: flex;
  align-items: center;
  gap: 0;
  margin: 0 0 clamp(2rem, 4vw, 3.2rem);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}
.label::before { content: "/"; }
.label::after {
  content: "";
  flex: 1;
  height: 1px;
  margin-left: 0.9rem;
  background: var(--rule);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 1.1s var(--ease-out);
}
.label.in::after { transform: scaleX(1); }
/* On black the base accent is only 3.3:1 — below the 4.5:1 minimum for text
   this size. The lifted blue clears it at 5.5:1 and reads as the same colour. */
.label--dark { color: var(--accent-on-dark); }
.label--dark::after { background: var(--rule-dark); }

.sub {
  margin: 0 0 1.3rem;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-weight: 400;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.sub--gap { margin-top: 2.8rem; }

/* =============================== ABOUT ================================== */

.about {
  display: grid;
  grid-template-columns: 190px 1fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
.about__figure { margin: 0; position: relative; }
.about__figure img {
  width: 100%;
  border-radius: 50%;
  filter: grayscale(1) contrast(1.05);
  transition: filter 0.6s ease;
}
.about__figure:hover img { filter: grayscale(0) contrast(1); }

/* One size, one colour for the whole About copy — no lead paragraph. */
.about__body p { margin: 0 0 1.35em; color: var(--ink-2); }
.about__body p:last-child { margin-bottom: 0; }

/* Decorative quotation marks wrapping the copy. They live in CSS rather than
   the HTML so screen readers don't announce loose punctuation, and so the
   text stays clean if you rewrite it. The line-height/margin pair is what
   tucks the glyphs close to the text — adjust the margins, not the height. */
.about__body::before,
.about__body::after {
  display: block;
  font-family: var(--display);
  font-size: 3.4rem;
  font-weight: 700;
  line-height: 0.5;
  color: var(--accent);
  user-select: none;
}
.about__body::before { content: "\201C"; margin-bottom: 1.05rem; }
/* Kept tight to the last line on purpose: any lower and it collides with the
   oversized ABOUT watermark sitting in the section's bottom padding. */
.about__body::after  { content: "\201D"; margin-top: 0.5rem; text-align: right; }

/* token-by-token reveal, like text streaming in */
.tok { opacity: 0; transition: opacity 0.16s linear; }
.tok.on { opacity: 1; }
.caret {
  display: inline-block;
  width: 0.5em;
  height: 1em;
  margin-left: 0.08em;
  background: var(--accent);
  vertical-align: -0.14em;
  animation: blink 1s steps(1) infinite;
}
@keyframes blink { 0%,49% { opacity: 1; } 50%,100% { opacity: 0; } }

/* ====================== EXPERIENCE / EDUCATION ========================== */

.entries { margin: 0; padding: 0; list-style: none; }

.entry {
  display: grid;
  grid-template-columns: 190px 1fr;
  gap: clamp(1rem, 4vw, 3.5rem);
  padding-block: clamp(1.5rem, 3vw, 2.2rem);
  border-top: 1px solid var(--rule);
  transition: border-color 0.3s;
}
.entry:last-child { border-bottom: 1px solid var(--rule); }
.section--panel .entry { border-color: #ddd8cd; }
.entry:hover { border-top-color: var(--accent); }

.entry__date {
  margin: 0.3rem 0 0;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  color: var(--grey);
  white-space: nowrap;
}
.entry__title { font-size: var(--fs-md); font-weight: 600; }
.entry__org { margin: 0.25rem 0 0.8rem; font-size: var(--fs-sm); color: var(--grey); }
.entry__body p:last-child { margin: 0; color: var(--ink-2); max-width: 64ch; }

/* =============================== SKILLS ================================= */

/* Capability index. Four discipline columns, each a list of tools with a
   six-dot meter. Dots rather than bars: they match the dotted connectors in
   .chain, and they don't imply a precision the numbers don't have. */

.caps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: clamp(2rem, 3.5vw, 3rem);
}

/* A column is a flex stack so the note can absorb the slack, which is what
   keeps every tool list starting on the same line even though the titles
   wrap to different depths ("BIM & Model Automation" takes two lines,
   "Robotics & Sensing" one). Without it the four lists step down the page
   raggedly. Needs no magic numbers, so it survives you rewriting a title. */
.cap { display: flex; flex-direction: column; }

.cap__idx {
  margin: 0 0 0.5rem;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.18em;
  color: var(--accent);
}
.cap__title { font-size: var(--fs-md); font-weight: 600; }
.cap__note {
  flex: 1;
  margin: 0.45rem 0 0;
  font-size: var(--fs-sm);
  color: var(--grey);
  max-width: 30ch;
}

.cap__list {
  margin: 1.3rem 0 0;
  padding: 0.6rem 0 0;
  list-style: none;
  border-top: 1px solid var(--rule);
}
.cap__list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  padding-block: 0.52rem;
}
.cap__name { font-size: var(--fs-sm); color: var(--ink-2); }

/* Six dots. The element paints the empty track; ::after paints the filled
   dots and is cut to --n of them. Both use the same repeating gradient, so
   the two layers always line up. Change the 11px step in one place and you
   must change it in all four. */
.cap__meter {
  position: relative;
  flex: none;
  width: 66px;                       /* 6 dots x 11px */
  height: 7px;
  background: radial-gradient(circle 3px at 5.5px 50%, var(--rule) 99%, transparent 100%) 0 0 / 11px 100% repeat-x;
}
.cap__meter::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(var(--n) * 11px);
  background: radial-gradient(circle 3px at 5.5px 50%, var(--accent) 99%, transparent 100%) 0 0 / 11px 100% repeat-x;
}
/* fills left to right as each row arrives; --d is the row's stagger delay */
.js [data-stagger] .cap__meter::after {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.9s var(--ease-soft) var(--d, 0ms);
}
.js [data-stagger].in .cap__meter::after { clip-path: inset(0 0 0 0); }

.capfoot {
  display: grid;
  grid-template-columns: 1.7fr 1fr;
  gap: clamp(2rem, 5vw, 4rem);
  margin-top: clamp(2.8rem, 6vw, 4.2rem);
  padding-top: clamp(2rem, 4vw, 2.8rem);
  border-top: 1px solid var(--rule);
}

.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
.chips li {
  padding: 0.45rem 0.95rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: transparent;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-2);
  line-height: 1.6;
  transition: border-color 0.25s, color 0.25s;
}
.chips li:hover { border-color: var(--accent); color: var(--accent); }

.langs { margin: 0; padding: 0; list-style: none; }
.langs li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding-block: 0.5rem;
  border-bottom: 1px dotted var(--grey-2);
  font-size: var(--fs-sm);
  color: var(--ink-2);
}
.langs em {
  font-style: normal;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--grey);
}

/* =============================== ICONS ==================================
   Tool names in the project cards and in the dotted chain carry a small
   icon. There are two kinds, and one `data-ic` attribute selects either —
   which mechanism runs is decided by the properties its definition sets.

   1. A REAL LOGO, cropped from the print portfolio PDF and stored under
      assets/img/logo/. Painted as a background IMAGE, because most of these
      are a white glyph inside a dark disc and a mask would flatten them to
      a solid blob. They are greyscale, which is how they appear in the
      portfolio, so they sit quietly next to the type.

   2. A DRAWN MARK, for the seven tools the portfolio has no logo for.
      Painted through a MASK, so it takes `currentColor` and is always the
      colour of the text beside it.

   TO ADD ONE
     a logo   [data-ic="name"] { --ic-paint: transparent;
                                 --ic-img: url("../img/logo/name.webp"); }
              Paths are relative to THIS FILE, so ../img/... is correct from
              every page. Keep them ~96px on the long edge with a
              transparent background.

     a mark   [data-ic="name"] { --ic-mask: url("data:image/svg+xml,..."); }
              Draw on a 24x24 viewBox at stroke-width 2.4. Single-quote the
              attributes and write %3C %3E %23 for < > #, which otherwise
              break url() parsing. Fill small circles rather than stroking
              them, and keep to four elements — these render at 13-15px.

   Then put data-ic="name" on the <li>. An <li> without one simply gets no
   icon; nothing else about it changes.
   ====================================================================== */

/* The single box both kinds are painted into. `--ic-paint` is the fill a
   mask reveals, so a logo switches it off and supplies an image instead. */
[data-ic]::before {
  content: "";
  flex: none;
  width: 1.25em;
  height: 1.25em;
  background: var(--ic-paint, currentColor) var(--ic-img, none) center / contain no-repeat;
  -webkit-mask: var(--ic-mask, none) center / contain no-repeat;
          mask: var(--ic-mask, none) center / contain no-repeat;
}

/* Card tool lists show the ICON ONLY — see the .card__tools block further
   down for how the name is hidden without being thrown away. The chain on
   the project pages keeps its labels; only the cards are stripped. */
.card__tools li[data-ic] {
  display: inline-flex;
  align-items: center;
}

/* Chain items: two columns, so the <em> description lines up under the tool
   name rather than under the icon. The anonymous text node becomes the
   first item in column two; `em` sits below it. The dotted connector is
   position:absolute, so it never becomes a grid item. */
.chain li[data-ic] {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 0.5rem;
  align-items: start;
}
.chain li[data-ic]::before {
  width: 1.45em;
  height: 1.45em;
  margin-top: 0.02em;   /* optical: drops the glyph onto the cap height */
}
.chain li[data-ic] em { grid-column: 2; }

/* --- logos from the portfolio ---------------------------------------- */

[data-ic="rhino"] { --ic-paint: transparent; --ic-img: url("../img/logo/rhino.webp"); }   /* Rhinoceros */
[data-ic="grasshopper"] { --ic-paint: transparent; --ic-img: url("../img/logo/grasshopper.webp"); }   /* Grasshopper */
[data-ic="python"] { --ic-paint: transparent; --ic-img: url("../img/logo/python.webp"); }   /* Python */
[data-ic="rhinoscriptsyntax"] { --ic-paint: transparent; --ic-img: url("../img/logo/rhinoscriptsyntax.webp"); }   /* rhinoscriptsyntax */
[data-ic="dynamo"] { --ic-paint: transparent; --ic-img: url("../img/logo/dynamo.webp"); }   /* Dynamo */
[data-ic="kangaroo"] { --ic-paint: transparent; --ic-img: url("../img/logo/kangaroo.webp"); }   /* Kangaroo */
[data-ic="revit"] { --ic-paint: transparent; --ic-img: url("../img/logo/revit.webp"); }   /* Revit */
[data-ic="revitapi"] { --ic-paint: transparent; --ic-img: url("../img/logo/revitapi.webp"); }   /* Revit API */
[data-ic="sicksopas"] { --ic-paint: transparent; --ic-img: url("../img/logo/sicksopas.webp"); }   /* SICK SOPAS Engineering Tool */
[data-ic="fologram"] { --ic-paint: transparent; --ic-img: url("../img/logo/fologram.webp"); }   /* Fologram — desaturated to match the rest */

/* --- drawn marks, for the tools with no logo in the portfolio --- */
[data-ic="form"] { --ic-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='4.5' width='19' height='15' rx='2'/%3E%3Cpath d='M2.5 9h19'/%3E%3Cpath d='M6 12.6h6'/%3E%3Cpath d='M6 16.2h9'/%3E%3C/svg%3E"); }   /* Eto.Forms — a dialog with fields */
[data-ic="network"] { --ic-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 9h13.5'/%3E%3Cpath d='M13.5 6l3 3-3 3'/%3E%3Cpath d='M21 15H7.5'/%3E%3Cpath d='M10.5 12l-3 3 3 3'/%3E%3C/svg%3E"); }   /* UDP / TCP — two-way transfer */
[data-ic="threejs"] { --ic-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.5l8.5 4.6v9.8L12 21.5 3.5 16.9V7.1z'/%3E%3Cpath d='M3.5 7.1L12 11.7l8.5-4.6'/%3E%3Cpath d='M12 11.7v9.8'/%3E%3C/svg%3E"); }   /* Three.js — wireframe cube */
[data-ic="bvh"] { --ic-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='2.5' width='19' height='19' rx='1.5'/%3E%3Cpath d='M12 2.5v19'/%3E%3Cpath d='M2.5 12h9'/%3E%3C/svg%3E"); }   /* three-mesh-bvh — subdivided bounding volumes */
[data-ic="javascript"] { --ic-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 3.5h-1A2.5 2.5 0 0 0 6 6v3a2.5 2.5 0 0 1-2.5 2.5A2.5 2.5 0 0 1 6 14v3a2.5 2.5 0 0 0 2.5 2.5h1'/%3E%3Cpath d='M14.5 3.5h1A2.5 2.5 0 0 1 18 6v3a2.5 2.5 0 0 0 2.5 2.5A2.5 2.5 0 0 0 18 14v3a2.5 2.5 0 0 1-2.5 2.5h-1'/%3E%3C/svg%3E"); }   /* JavaScript — braces */
[data-ic="pyodide"] { --ic-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='4.5' width='19' height='15' rx='2'/%3E%3Cpath d='M2.5 9h19'/%3E%3Cpath d='M15 11.4v1.2a2 2 0 0 1-2 2h-2a2 2 0 0 0-2 2v.9'/%3E%3C/svg%3E"); }   /* Pyodide — python inside a browser */
[data-ic="vite"] { --ic-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.6 2.5L5 14h5.8l-1.3 7.5L19 10h-5.8z'/%3E%3C/svg%3E"); }   /* Vite — lightning bolt */

/* ========================= PROJECT CARD GRID ============================ */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 clamp(1.8rem, 4vw, 2.6rem);
  padding: 0;
  list-style: none;
}
.filters button {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--grey);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.5rem 1.1rem;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.filters button:hover { color: var(--ink); border-color: var(--grey-2); }
.filters button[aria-pressed="true"] {
  color: #fff;
  background: var(--accent);
  border-color: var(--accent);
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
  gap: clamp(1.2rem, 2.5vw, 2rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.card { display: flex; }
.card[hidden] { display: none; }

.card__link {
  display: flex;
  flex-direction: column;
  width: 100%;
  text-decoration: none;
  color: inherit;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 14px;
  /* clips the cover image and the number chip to the rounded corners */
  overflow: hidden;
  transition: border-color 0.3s, transform 0.4s var(--ease-out), box-shadow 0.4s;
}
.card__link:hover {
  border-color: var(--accent);
  transform: translateY(-5px);
  box-shadow: 0 16px 38px rgba(20, 19, 15, 0.10);
}

.card__media {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--panel);
}
.card__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease-out);
}
.card__link:hover .card__media img { transform: scale(1.045); }

.card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: 0.4rem;
  padding: 1.15rem 1.25rem 1.25rem;
}

/* The project's name is what turns accent on hover — it replaces the number
   chip that used to sit on the image. Also on :focus-visible, so the same
   signal reaches someone tabbing through rather than pointing. */
.card__title {
  font-size: var(--fs-md);
  font-weight: 600;
  transition: color 0.28s var(--ease-out);
}
.card__link:hover .card__title,
.card__link:focus-visible .card__title { color: var(--accent); }

.card__sub   { margin: 0; font-size: var(--fs-sm); color: var(--grey); }

/* The card's tool row is a line of icons and nothing else — the names are
   carried by the project page you land on when you click the card, so the
   card only has to signal WHICH tools, not spell them out. */
.card__tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.55rem 0.7rem;
  margin: auto 0 0;
  padding: 0.95rem 0 0;
  list-style: none;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--grey);
  border-top: 1px solid var(--rule);
}

/* The name is HIDDEN, not deleted. Strip it from the markup and the card
   would say nothing at all to a screen reader, and nothing to a search
   engine either — "Revit, Dynamo, Python" is exactly the vocabulary this
   site wants to be found for. So the text stays, clipped to a 1px box that
   assistive technology still reads, and `title` on the <li> hands the name
   to a mouse user on hover.

   Scoped to li[data-ic] on purpose: an item with no icon keeps its visible
   label, so adding a tool you have no glyph for degrades into readable text
   instead of vanishing entirely. */
.card__tools li[data-ic] > span {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

/* Bigger than in the chain: with no label beside it the icon is carrying the
   whole row, and sized in rem so it no longer tracks the (now invisible)
   text. */
.card__tools li[data-ic]::before {
  width: 1.5rem;
  height: 1.5rem;
}

/* The " ·" separator existed to part two words. Between icons it is noise,
   and the gap already does the job — but an item WITHOUT an icon still shows
   its name, so it keeps a separator to sit behind. */
.card__tools li:not([data-ic]):not(:last-child)::after {
  content: " ·";
  color: var(--grey-2);
}

/* ========================== PROJECT DETAIL PAGE ========================= */

.back {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--grey);
  text-decoration: none;
  transition: color 0.2s, gap 0.2s;
}
.back:hover { color: var(--accent); gap: 0.75rem; }

.phead { padding-block: clamp(2.5rem, 6vw, 4rem) clamp(1.5rem, 4vw, 2.5rem); }

.phead__title {
  margin-top: 0.9rem;
  font-family: var(--display);
  font-size: var(--fs-xl);
  font-weight: 700;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}
.phead__sub { margin: 0.5rem 0 0; font-size: var(--fs-md); color: var(--grey); }

.pgrid {
  display: grid;
  grid-template-columns: 1fr 265px;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
  padding-block: clamp(1.5rem, 4vw, 2.5rem);
}
.pgrid p { margin: 0 0 1.3em; color: var(--ink-2); }
.pgrid p:last-child { margin-bottom: 0; }

.facts {
  font-size: var(--fs-sm);
  line-height: 1.85;
  border-top: 1px solid var(--rule);
  padding-top: 0.9rem;
}
.facts dt {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey);
  margin-top: 0.9rem;
}
.facts dt:first-of-type { margin-top: 0; }
.facts dd { margin: 0.1rem 0 0; color: var(--ink-2); }
.facts a { color: var(--accent); text-underline-offset: 3px; }

/* tool chain with dotted connectors, from the portfolio workflow figures */
.chain {
  display: flex;
  flex-wrap: wrap;
  gap: 1.1rem 0;
  margin: 2rem 0 0;
  padding: 1.3rem 0 0;
  border-top: 1px solid var(--rule);
  list-style: none;
}
.chain li {
  position: relative;
  flex: 1 1 11rem;
  min-width: 9.5rem;
  padding-right: 2.6rem;
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.5;
}
.chain li:last-child { padding-right: 0; }
.chain li em {
  display: block;
  margin-top: 0.25rem;
  font-style: normal;
  font-weight: 400;
  font-size: var(--fs-xs);
  color: var(--grey);
}
.chain li:not(:last-child)::after {
  content: "";
  position: absolute;
  top: 0.62em; right: 0.6rem;
  width: 1.5rem; height: 3px;
  background-image: radial-gradient(circle at center, var(--accent) 1.4px, transparent 1.5px);
  background-size: 6px 3px;
  background-repeat: repeat-x;
}

.fig { margin: 0 0 clamp(2.5rem, 6vw, 4rem); }
.fig img { width: 100%; }
.fig--pad { background: var(--panel); padding: clamp(1.5rem, 4vw, 3rem); }
.fig--pad img { width: auto; max-height: 26rem; margin-inline: auto; }
.fig--wide img { border: 1px solid var(--rule); }

figcaption {
  margin-top: 0.8rem;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--grey);
  text-align: center;
}
figcaption b { color: var(--ink-2); font-weight: 400; }

/* Source credit for a figure that is not the author's own work. Its own
   line under the caption, set on the same grey — NOT lighter. Attribution
   that is hard to read is not attribution, and --grey-2 fails contrast.
   Separation comes from the line break and the margin instead. */
.credit {
  display: block;
  max-width: 76ch;      /* two comfortable lines for a typical credit */
  margin: 0.5rem auto 0;
  color: var(--grey);
}
/* Mono has no italic in this project, so a <cite> would be synthesised into
   a slanted approximation. The title reads as a title from its context. */
.credit cite { font-style: normal; }

/* ---- live app embed ----------------------------------------------------
   A facade in front of a whole application hosted elsewhere. Same bargain as
   the video below it: show a still, load the real thing only when asked.

   That is not politeness, it is arithmetic. The UR10e simulator is 2.8 MB
   before it draws anything — a 742 KB bundle and a 2.1 MB robot mesh — and
   pulls Pyodide, another ~10 MB, if the code lab is opened. Auto-loading
   that at the top of a project page would make the largest element on screen
   an iframe that is still downloading, which is precisely what Core Web
   Vitals measures. Clicking is one gesture; the alternative is every visitor
   paying for a simulator most of them will not touch.

   Taller than the video facade because the thing inside is an interface, not
   a picture: a 3D viewport, a catalogue and a teach pendant all need room.
   -------------------------------------------------------------------- */
/* `max-width: 100%` is load-bearing, not belt-and-braces. With an
   aspect-ratio AND a min-height, the min-height wins on a narrow screen and
   the ratio then derives the WIDTH from that height — 544px x 1.6 = 870px —
   so the box grows sideways out of its container instead of getting taller.
   `overflow-x: hidden` on the body hides the damage rather than preventing
   it, which is worse: the embed is simply clipped off the side of a phone.
   Capping the width forces the ratio to give way instead. */
.embed {
  position: relative;
  aspect-ratio: 16 / 10;
  max-width: 100%;
  min-height: min(72vh, 34rem);
  overflow: hidden;
  background: var(--panel);
  border: 1px solid var(--rule);
}
.embed > img { width: 100%; height: 100%; object-fit: cover; object-position: top center; }
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* The note under the label — what clicking will actually cost. It needs its
   own backing plate for the same reason the label has one: the poster
   underneath is a screenshot, so any colour of text will be illegible over
   some part of it. The negative margin pulls it back against the label so
   the two read as one stacked unit rather than two floating chips. */
.embed__note {
  display: block;
  margin-top: -0.4rem;
  background: rgba(245, 243, 238, 0.92);
  padding: 0.3rem 0.7rem;
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--grey);
}

/* video facade */
.video { position: relative; aspect-ratio: 16 / 9; overflow: hidden; background: var(--panel); }
.video > img { width: 100%; height: 100%; object-fit: cover; }
.video__btn,
.embed__btn {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  border: 0;
  cursor: pointer;
  background: rgba(245, 243, 238, 0.28);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  text-decoration: none;
  color: var(--ink);
  transition: background 0.3s;
}
.video__btn:hover,
.embed__btn:hover { background: rgba(245, 243, 238, 0.5); }
.video__icon,
.embed__icon {
  position: relative;
  width: clamp(3.4rem, 8vw, 4.4rem);
  aspect-ratio: 1;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: rgba(245, 243, 238, 0.92);
  transition: transform 0.3s var(--ease-soft), border-color 0.3s, background-color 0.3s;
}
.video__icon::after {
  content: "";
  position: absolute;
  top: 50%; left: 54%;
  transform: translate(-50%, -50%);
  border: solid transparent;
  border-width: 0.52rem 0 0.52rem 0.88rem;
  border-left-color: var(--ink);
  transition: border-left-color 0.3s;
}
.video__btn:hover .video__icon,
.embed__btn:hover .embed__icon { transform: scale(1.09); border-color: var(--accent); }
.video__btn:hover .video__icon::after { border-left-color: var(--accent); }
.video__label,
.embed__label { background: rgba(245, 243, 238, 0.92); padding: 0.4rem 0.85rem; }

/* The video's mark is a play triangle; the embed's is a launch chevron —
   one starts a recording, the other opens a program. */
.embed__icon::after {
  content: "";
  position: absolute;
  top: 52%; left: 47%;
  width: 0.8rem; height: 0.8rem;
  transform: translate(-50%, -50%) rotate(45deg);
  border-top: 2px solid var(--ink);
  border-right: 2px solid var(--ink);
  transition: border-color 0.3s;
}
.embed__btn:hover .embed__icon::after { border-color: var(--accent); }
.video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* next/prev project */
.pnav {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding-block: clamp(2rem, 5vw, 3rem);
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.pnav a { color: var(--grey); text-decoration: none; transition: color 0.2s; }
.pnav a:hover { color: var(--accent); }

/* ============================== CONTACT ================================= */

.contact__lead {
  margin: 0 0 clamp(2rem, 4vw, 3rem);
  font-size: clamp(1.35rem, 1rem + 1.6vw, 2.1rem);
  font-weight: 300;
  line-height: 1.35;
  max-width: 22ch;
  color: #fff;
}
.contact__list { margin: 0; padding: 0; list-style: none; max-width: 42rem; }
.contact__list li {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: 1rem;
  align-items: baseline;
  padding-block: 1rem;
  border-top: 1px solid var(--rule-dark);
}
.contact__list li:last-child { border-bottom: 1px solid var(--rule-dark); }
.contact__key {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey-dark);
}
.contact__list a,
.contact__list span:not(.contact__key) {
  font-size: var(--fs-base);
  color: #fff;
  text-decoration: none;
  border-bottom: 1px solid #3a3a3a;
  padding-bottom: 2px;
  transition: border-color 0.25s, color 0.25s;
  overflow-wrap: anywhere;
}
.contact__list span:not(.contact__key) { border-bottom-color: transparent; color: #b8b5ae; }
.contact__list a:hover { border-color: #fff; }

/* =============================== FOOTER ================================= */

.foot {
  background: var(--black);
  color: var(--grey-dark);
  padding-block: 2.4rem;
  border-top: 1px solid #1c1c1a;
}
.foot .dim { color: var(--grey-dark); }
.foot__inner { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.6rem; }
.foot p { margin: 0; font-family: var(--mono); font-size: var(--fs-xs); letter-spacing: 0.06em; }

/* ============================= ANIMATION ================================ */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.7s ease, transform 0.7s var(--ease-soft);
}
.js [data-reveal].in { opacity: 1; transform: none; }

/* stagger children of a revealed group (cards, entries) */
.js [data-stagger] > * {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.6s ease, transform 0.7s var(--ease-soft);
  transition-delay: var(--d, 0ms);
}
.js [data-stagger].in > * { opacity: 1; transform: none; }

/* ============================ RESPONSIVE ================================ */

@media (max-width: 860px) {
  .about, .entry, .capfoot, .pgrid { grid-template-columns: 1fr; }
  .about__figure { width: 150px; }
  .entry__date { margin-bottom: 0.3rem; }
  .facts { border-top: 1px solid var(--rule); padding-top: 1rem; }
  .nav { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
  .nav__right { width: 100%; }
  .nav__list { justify-content: flex-start; }
  /* Give the links the whole row so the Motion button always starts on the
     next one. Left to wrap on its own it sits beside them until the web font
     loads, widens the links, and drops the button onto a second row — which
     grows the nav and shoves the entire page down as it lands. */
  .nav__list { flex: 1 0 100%; }
}

@media (max-width: 520px) {
  :root { --fs-xl: 2rem; }
  .nav__brand { display: none; }
  .contact__list li { grid-template-columns: 1fr; gap: 0.2rem; }
  .cards { grid-template-columns: 1fr; }
  .boot__word { display: none; }
}

/* Held on an element for one frame while main.js rewinds an animation so it
   can be replayed. Without it the rewind transitions too, the element never
   reaches its start value, and the replay is invisible. Needs !important to
   outrank the compound selectors above. */
.is-resetting, .is-resetting * { transition: none !important; }

/* ============================ REDUCED MOTION ============================
   `no-motion` goes on <html> by the inline script in every page's <head>:
   it follows the visitor's OS setting unless they have used the Motion
   toggle in the nav, in which case their choice wins and is remembered.

   Every rule below must leave content VISIBLE. This suppresses movement,
   never information — that is what makes it safe to turn on by default.
   Keep this block AFTER the animation rules above; the two have equal
   specificity, so it wins on source order.

   The loading sequence and the custom cursor need no rules here: neither is
   ever switched on when motion is off, because the classes that reveal them
   (`boot-on`, `cursor-on`) are never added.
   ======================================================================== */

.no-motion { scroll-behavior: auto; }
.no-motion [data-reveal],
.no-motion [data-stagger] > * { opacity: 1; transform: none; transition: none; }
.no-motion .ghost { opacity: 1; transform: none; transition: none; }
.no-motion .hero__name .ch { opacity: 1; transform: none; transition: none; }
/* Must repeat [data-stagger] to match the specificity of the rule that clips
   this — `.no-motion .cap__meter::after` alone loses and the dots stay empty. */
.no-motion [data-stagger] .cap__meter::after { clip-path: inset(0 0 0 0); transition: none; }
.no-motion .label::after { transform: scaleX(1); transition: none; }
.no-motion .nav__list a.is-active::after { transform: scaleX(1); transition: none; }
.no-motion .tok { opacity: 1; transition: none; }
.no-motion .caret,
.no-motion .hero__cue span,
.no-motion .hero__grid { animation: none; }
.no-motion .progress { display: none; }
.no-motion .boot { display: none; }
.no-motion .cursor { display: none; }

/* Same intent for visitors with JavaScript off. They get no toggle, so the
   OS setting is simply obeyed. Only the purely CSS-driven animations can run
   in that case — everything else is started by main.js and never begins. */
@media (prefers-reduced-motion: reduce) {
  html:not(.js) { scroll-behavior: auto; }
  html:not(.js) .label::after { transform: scaleX(1); transition: none; }
  html:not(.js) .hero__cue span,
  html:not(.js) .hero__grid { animation: none; }
  html:not(.js) .progress { display: none; }
}

/* =============================== PRINT ================================== */

@media print {
  .nav, .hero__cue, .hero__grid, .video__btn, .ghost, .progress, .filters, .pnav,
  .boot, .cursor { display: none !important; }
  .hero, .section--dark, .foot { background: #fff !important; color: #000 !important; }
  .hero { min-height: auto; }
  .section { padding-block: 1.5rem; break-inside: avoid; }
  .contact__list a, .contact__lead, .foot p { color: #000 !important; }
  .tok { opacity: 1 !important; }
}
