/* ===== Jan Henneberg — clean rebuild of the Framer original ===== */

@font-face { font-family: "General Sans"; font-weight: 400; src: url("../assets/fonts/GeneralSans-400.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "General Sans"; font-weight: 500; src: url("../assets/fonts/GeneralSans-500.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "General Sans"; font-weight: 600; src: url("../assets/fonts/GeneralSans-600.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "Space Mono"; font-weight: 400; src: url("../assets/fonts/SpaceMono-400.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "Space Mono"; font-weight: 700; src: url("../assets/fonts/SpaceMono-700.woff2") format("woff2"); font-display: swap; }

:root {
  --bg: #e6eaeb;
  --ink: #000;
  --grey: #3c3d3d;
  --grey-500: #868989;
  --line: #c4cccc;
  --glyph: #cfd3d4;
  --card: #e0e4e5;
  --orange: #eb4d22;
  --yellow: #e5fe4f;
  --blue: #2846dc;
  --sans: "General Sans", sans-serif;
  --mono: "Space Mono", monospace;
}

/* The dotted nav ring is a ::after, which JS can't animate directly, so its
   colour is carried in on an inherited custom property. Registering it makes
   the browser interpolate it as a colour instead of snapping. */
@property --nav-dot {
  syntax: "<color>";
  inherits: true;
  initial-value: rgba(196, 204, 204, 0);
}
/* same trick for the pill links' overlay border */
@property --pill-line {
  syntax: "<color>";
  inherits: true;
  initial-value: rgba(60, 61, 61, .18);
}

* { margin: 0; padding: 0; box-sizing: border-box; }
/* Nothing here on purpose. The original leaves html and body fully `visible`;
   `overflow-x: hidden` turns <html> into its own scroll container, which adds a
   hop to scroll chaining and can eat the first wheel delta. Anything that would
   stick out horizontally is clipped by its own container instead — see .bars,
   .stage-sticky and .device-card. Scroll behaviour is left `auto` too: smooth
   makes the wheel feel a beat behind. */
body {
  /* the original renders antialiased; on the default `auto` macOS uses
     subpixel AA and the type comes out visibly heavier */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
}
img, video { max-width: 100%; display: block; }
/* no `outline: none` here on purpose. The original leaves the browser's own
   focus ring alone (measured: outline-style `auto` on every tabbed element),
   and killing it in the reset was the one place this rebuild was quietly less
   usable than what it replaces. :focus-visible means it stays invisible to the
   mouse and only shows up for keyboard users, which is what the original does. */
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
button { background: none; border: 0; cursor: pointer; font: inherit; }

/* keyboard-only escape past the rail. Off-canvas until focused, so it changes
   nothing visually — the site's tab order starts with the logo and the rail's
   six links before any content. */
.skip-link {
  position: fixed;
  top: 8px;
  left: -200px;
  z-index: 100;
  padding: 8px 12px;
  background: var(--ink);
  color: #fff;
  font-family: var(--mono);
  font-size: 11px;
  border-radius: 4px;
  transition: left .15s;
}
.skip-link:focus-visible { left: 8px; }

.tag {
  font-family: var(--mono);
  font-size: 9px;
  line-height: 10.8px;
  color: var(--grey);
}
/* flush with the right edge of the chips row below it — the -2px nudge this
   replaces pushed it just past, at every width */
.tag-right { text-align: right; }
.tag-readme { margin-right: 24px; }

/* ===== top barcode strip ===== */
.bars {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-left: 13px;
  overflow: hidden;
  z-index: 60;
  pointer-events: none;
}
.bar-group { display: flex; gap: 13px; align-items: center; }
.pair { display: flex; }
.bars i {
  display: block;
  width: 8px;
  height: 8px;
  flex: 0 0 auto;
}
.bars .bk { background: var(--ink); }
.bars .or { background: var(--orange); }
.bars .bl { background: var(--blue); }
.bars .ye { background: var(--yellow); }
.bars .sp { width: var(--w, 8px); background: none; }
.bars-m { display: none; }
.bars-m i { position: absolute; top: 0; left: var(--x); }
@media (max-width: 809px) {
  .bars:not(.bars-m) { display: none; }
  .bars-m { display: block; }
}

/* ===== left rail ===== */
.rail {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  width: 203px;
  /* the original clips the rail: the LinkedIn note runs into it */
  overflow: hidden;
  z-index: 50;
  pointer-events: none;
}
.rail a, .rail .nav-pill { pointer-events: auto; }

.logo { position: absolute; top: 28px; left: 34px; transition: top .45s, left .45s; }
.logo-circle {
  position: relative;
  width: 141px; height: 141px;
  border-radius: 50%;
  background: var(--blue);
  display: block;
  overflow: hidden;
  transition: width .45s cubic-bezier(.6,0,.2,1), height .45s cubic-bezier(.6,0,.2,1);
}
/* grain video sits inside the circle: invisible at rest, blooms in on hover.
   Sized in % so it tracks the circle across breakpoints (141/92/36px). */
.logo-noise {
  position: absolute;
  left: -.71%; top: -.71%;
  width: 101%; height: 101%;
  max-width: none;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
  /* hover state lives in js/anim.js */
}
/* film grain over the whole logo, including the wordmark — always on, barely
   there at 2.2%. Sized 400% and inset -200% so the drift never exposes an edge;
   the 256px tile repeats at natural size. js/anim.js jitters it. */
.logo-grain {
  position: absolute;
  inset: -200%;
  width: 400%; height: 400%;
  background: url("../assets/img/grain.png");
  opacity: .022;
  pointer-events: none;
  will-change: transform;
}
/* 197x197 graph paper: 1px #c4cccc rules on a 7px grid at 50% opacity */
/* the grid is a square centred on the circle — 197 / 134 / 50px, i.e. the
   same margin on every side (28 / 21 / 7px). On tablet and phone the
   original runs its horizontal lines half a step down (3.5px), which leaves
   the grid without a top edge and with stubs at the bottom — it reads as cut
   off, so here it closes on every side like on desktop (--grid-y stays 0). */
.logo::before {
  content: "";
  position: absolute;
  inset: -28px;
  z-index: -1;
  --grid-y: 0px;
  background:
    repeating-linear-gradient(90deg, var(--line) 0 1px, transparent 1px 7px),
    repeating-linear-gradient(180deg, transparent 0 var(--grid-y), var(--line) var(--grid-y) calc(var(--grid-y) + 1px), transparent calc(var(--grid-y) + 1px) 7px);
  opacity: .5;
}
.logo-full {
  position: absolute;
  inset: 0;
  color: #fff;
  font-family: var(--mono);
  transition: opacity .3s;
}
.logo-jan, .logo-name { position: absolute; display: block; }
.logo-jan {
  left: 26px;
  top: 51px;
  font-weight: 400;
  font-size: 10px;
  line-height: 12px;
}
.logo-name {
  left: 31.938px;
  top: 61.102px;
  font-weight: 700;
  font-size: 14px;
  line-height: 17px;
}
.logo-mini {
  position: absolute;
  font-family: var(--mono);
  font-weight: 700;
  font-size: 14px;
  color: #fff;
  opacity: 0;
  transition: opacity .3s;
}
.nav-pill {
  position: absolute;
  top: 249px;
  left: 70px;
  width: 64px;
  height: 120px;
  border: 1px solid var(--line);
  border-radius: 32px;
  padding: 0;
  display: block;
}
.nav-item {
  position: absolute;
  left: 7px;
  width: 48px;
  height: 48px;
  display: grid;
  place-items: center;
  border: 1px solid transparent;
  border-radius: 32px;
  font-size: 12px;
  line-height: 14.4px;
  letter-spacing: -.36px;
  color: var(--ink);
}
.nav-item[data-nav="top"] { top: 7px; }
.nav-item[data-nav="work"] { top: 63px; }
/* the active marker is its own element so it can glide between the two items
   as you scroll, instead of the border snapping on/off (matches the original) */
.nav-ring {
  position: absolute;
  left: 7px;
  top: 7px;
  width: 48px;
  height: 48px;
  border: 1px solid var(--line);
  border-radius: 50%;
  /* the ring is painted over the active item and deliberately keeps its
     pointer events, so the active item has no hover — that's the original's
     behaviour, verified in a real browser: hovering "Work" turns it -45deg,
     hovering the active "Home" does nothing. It comes before the items in the
     DOM, so it needs the z-index to actually land on top of them. */
  z-index: 1;
}
/* the slide itself is driven by js/anim.js; this only places the ring before
   the module has loaded, so it never paints at the wrong item */
.nav-pill.at-work .nav-ring { top: 63px; }
/* hover: a dotted ring fades in and the whole item spins -45deg. The ring is a
   pseudo-element so it can be dotted independently of the solid active ring. */
.nav-item::after {
  content: "";
  position: absolute;
  inset: -1px;
  border: 1px dotted var(--nav-dot);
  border-radius: 32px;
  pointer-events: none;
}

.socials {
  position: absolute;
  bottom: 32px;
  left: 92.5px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  pointer-events: auto;
}
.socials a { position: relative; color: var(--grey); height: 24px; display: grid; place-items: center; }
.socials svg { width: 24px; height: 24px; }
/* only the X/Twitter icon has a hover in the original: the bird flies up and
   out while the 𝕏 letterform spins in from 180deg (LinkedIn has none) */
.soc-x { position: relative; width: 24px; }
.soc-x .soc-glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 24px;
  line-height: 24px;
  color: var(--ink);
  opacity: 0;
  transform: rotate(180deg);
}
/* hover-only note beside LinkedIn, from the original */
.soc-note {
  position: absolute;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 700;
  line-height: 10.8px;
  color: var(--grey-500);
  opacity: 0;
  pointer-events: none;
}
.soc-in .soc-note { left: 40px; top: -28px; width: 77px; white-space: pre-wrap; }
/* the mail link only appears in the mobile variant */
.socials .soc-mail { display: none; }

.ver {
  position: absolute;
  /* measured to the thousandth: at 5px / 3px the label rounded onto a
     different pixel row than the original's */
  bottom: 4.743px;
  left: 3.055px;
  font-size: 9px;
  font-weight: 500;
  line-height: 10.8px;
  color: var(--grey-500);
  writing-mode: vertical-rl;
  transform: rotate(180deg);
}

/* ===== hero stage: one viewport tall, not sticky (matches the original) ===== */
.stage { height: 100vh; position: relative; }
.stage-sticky {
  position: relative;
  height: 100vh;
  overflow: hidden;
  display: flex;
}

.hero-copy {
  width: 431.5px;
  flex: 0 0 auto;
  margin-left: clamp(180px, 20.4vw, 261px);
  padding-top: 92px;
  padding-bottom: 2px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.hero-headline { display: flex; flex-direction: column; gap: 32px; }
.hero-main     { display: flex; flex-direction: column; gap: 4px; }
.hero-table    { display: flex; flex-direction: column; gap: 8px; }
.hero-sub      { display: flex; flex-direction: column; gap: 12px; }
.hero-cta      { display: flex; flex-direction: column; gap: 12px; }
.hero-copy h1 {
  font-size: 28px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -.02em;
  max-width: 375.5px;
  margin: 0 0 0 12px;
}
/* the crossed-out words are real <del>s, so assistive tech and agents read
   them as deleted; the strike itself is the hand-drawn image, not CSS */
.struck, .chip-struck del { text-decoration: none; }
.struck {
  position: relative;
  white-space: nowrap;
}
/* static in the original — no draw-on animation */
.scribble {
  position: absolute;
  left: 19px;
  top: 16px;
  width: 213px;
  height: 13px;
  object-fit: contain;
}

/* a single grey frame with 1px grey rules between the labels, every label and
   rule spread with space-evenly — the original's "Table" frame, measured. The
   frame is an overlay like Framer's data-border, so it adds no layout size. */
.chips {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  height: 26px;
  margin: 0 0 0 14px;
}
.chips::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--grey-500);
  pointer-events: none;
}
.chips li {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: -.48px;
  line-height: 18px;
  text-transform: lowercase;
  white-space: nowrap;
  flex: 0 0 auto;
}
.chips .chip-rule {
  align-self: stretch;
  width: 1px;
  background: var(--grey-500);
}
/* the cross-out is placed on the whole row, not on its word: 23/33 of the
   row's width less half its own, so it drifts across "speaker" as the row
   widens — exactly as the original's marker does */
.chip-struck img {
  position: absolute;
  left: calc(69.69697% - 27px);
  top: 5.984px;
  width: 54px;
  height: 16px;
  max-width: none;
  object-fit: contain;
}

/* both plus marks are draggable — a small easter egg from the original */
.plus {
  width: 24px;
  height: 24px;
  color: var(--grey);
  cursor: grab;
  touch-action: none;
  will-change: transform;
}
.plus.is-dragging { cursor: grabbing; }
.hero-copy .plus { flex: 0 0 24px; margin: 0 0 0 100px; }
.plus-glyph {
  position: absolute;
  left: calc(var(--a) + (100% - var(--a)) * .6974789915966389 - 12px);
  top: calc(5.992362675022053% - 12px);
  margin: 0;
  color: var(--grey);
  z-index: 1;
}

.readme {
  /* the original indents the first line with plain spaces in the copy, so they
     have to survive — hence pre-wrap rather than a text-indent */
  white-space: pre-wrap;
  font-size: 16px;
  line-height: 1.3;
  color: var(--grey);
  width: 262px;
  margin: 0 32px 0 auto;
}
.status {
  font-size: 11px;
  font-weight: 500;
  line-height: 13.2px;
  color: var(--grey-500);
  margin: 0 24px 0 auto;
  display: flex;
  align-items: flex-start;
  gap: 4px;
}
/* blinks: lit 2s, dark 0.5s, switching instantly — a clean 80/20 on a 2.5s
   cycle. Measured on the original twice: period 2500ms, dark 500ms. An earlier
   pass had 2.65s/567ms, which is 6% slow and reads as a lazier pulse.
   The original drives this from JS and drops the background to transparent
   rather than fading opacity; a step animation on opacity looks the same and
   needs no timer. */
@keyframes status-blink {
  0%, 79.9%   { opacity: 1; }
  80%, 100%   { opacity: 0; }
}
.dot {
  animation: status-blink 2.5s steps(1, end) infinite;
  width: 6px; height: 6px;
  border-radius: 10px;
  background: var(--orange);
  flex: 0 0 6px;
}

/* the arrow stays pinned left while the label slides toward it on hover, and
   the whole pair shoots right on press — total width is identical in all three
   states (14+52+24 = 14+16+60 = 81+7+2 = 90px of padding+gap), so the button
   never reflows. */
.email-btn {
  display: flex;
  align-items: center;
  gap: 52px;
  padding: 14px 24px 12px 14px;
  box-sizing: border-box;
  background: var(--orange);
  color: rgb(10, 2, 0);
  font-family: var(--mono);
  font-size: 14px;
  line-height: 18px;
  letter-spacing: 3.4px;
  text-transform: lowercase;
  width: 182px;
  flex: 0 0 46px;
  margin: 0 0 0 52px;
  /* hover and press states live in js/anim.js */
}
/* the icon swaps to a smiley on hover and turns a full circle doing it */
.email-icon {
  position: relative;
  width: 20px; height: 20px;
  flex: 0 0 auto;
  display: block;
}
/* the icon is pure black while the label is the button's 10/2/0 ink */
.email-btn svg { width: 20px; height: 20px; color: #000; }
/* the swap is timed from js/anim.js, not :hover — in the original the arrow
   does all the spinning and the smiley only appears once it has settled */
.email-icon .ico-smile { position: absolute; inset: 0; opacity: 0; }
.hero-cta .tag { margin: 0 0 0 40px; }
/* shrink-wrapped in the original, so it isn't a full-width click/select target */
.counter { margin-top: 0; width: fit-content; cursor: pointer; }
/* the original's frame around it is a whole 11px, not the text's 10.797. On
   desktop/tablet the 0.2px goes into the hero's space-between and nudged every
   group above it a pixel down; on a phone it is the last thing in the hero, so
   everything below the hero sat 0.2px high. */
.counter { height: 11px; }

/* ===== device card ===== */
.device-card {
  position: absolute;
  right: 63.5px;
  top: 64px;
  bottom: 64px;
  width: 407.5px;
  z-index: 1;
  --card-r: 64px;
  border-radius: var(--card-r);
  background: var(--glyph);
  box-shadow: 0 0 16px 16px var(--glyph);
  isolation: isolate;
}
/* The card paints only itself (rounded background + halo) and clips nothing;
   its content is clipped by this layer instead. With overflow:hidden on the
   card, iOS Safari composited it for the videos and the moving track and
   painted its background as a square — four hard corners poking out of the
   rounded halo (measured off an iPhone screenshot: the corners held the card
   colour exactly, the halo next to them was already lighter). clip-path with
   a round inset holds for composited children too. The track moves, so the
   clip has to sit on this stationary wrapper, not on .slides. */
.card-clip {
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: var(--card-r);
  overflow: hidden;
  clip-path: inset(0 round var(--card-r));
}
.device-glow {
  position: absolute;
  z-index: 0;
  background: var(--glyph);
  border-radius: 200px;
  box-shadow: 0 0 40px 40px var(--glyph);
  overflow: hidden;
  will-change: transform;
}
.device-glow-a {
  left: calc(100% - 412.5px);
  right: 68px;
  top: 72px;
  bottom: 186px;
  transform: rotate(2deg);
}
.device-glow-b {
  left: calc(100% - 456.5px);
  right: 148px;
  top: 318px;
  bottom: 77px;
  transform: rotate(-2deg);
}

.slides {
  position: absolute;
  inset: 0;
  display: flex;
  /* the Spline viewer inside registers a non-passive touchmove and cancels the
     gesture to orbit its scene, which on a phone meant a swipe starting over
     the card scrolled nothing. Committing the track to horizontal-only hands
     vertical panning back to the page before any listener can claim it —
     that's what the original does on its own track. */
  touch-action: pan-y;
  /* the slide itself is a spring in js/anim.js */
  z-index: 2;
}
.slide {
  flex: 0 0 100%;
  min-width: 0;
  height: 100%;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* 16px each side — or 15/17 whenever the card's width ends in half a pixel:
     the original snaps its track to whole pixels and the scene lands a pixel
     left. main.js sets --slide-pl/--slide-pr from the card's width. */
  padding: 16px var(--slide-pr, 16px) 80px var(--slide-pl, 16px);
  /* each slide clips its own content: on a 190px-wide phone slide the 232px
     meta block overflows, and the neighbour's must not bleed into view */
  overflow: hidden;
}
/* the slide's content box reads as draggable, like the original's slideshow
   track: grab over the scene, chips, meta and the gaps between them, grabbing
   while held. The padding around it keeps the default cursor, as there — a
   ring clipped to exactly the padding (hit-testing follows clip-path) sits on
   top and hands that strip back to `auto`. */
.slide { cursor: grab; --pt: 16px; --pr: var(--slide-pr, 16px); --pb: 80px; --pl: var(--slide-pl, 16px); }
.slides.is-grabbing .slide { cursor: grabbing; }
.slide::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: auto;
  clip-path: polygon(evenodd,
    0 0, 100% 0, 100% 100%, 0 100%, 0 0,
    var(--pl) var(--pt), var(--pl) calc(100% - var(--pb)),
    calc(100% - var(--pr)) calc(100% - var(--pb)), calc(100% - var(--pr)) var(--pt), var(--pl) var(--pt));
}
/* Video stand-in for a Spline scene. The Spline camera holds a constant
   vertical FOV, so the render scales with the well's HEIGHT only and extra
   width just uncovers more background — measured: the teal button stays
   426x579 device px while the well goes 456 -> 695px wide. So the video is
   pinned to the height, centred, and allowed to spill: a narrower well crops
   the sides exactly like the live canvas would, a wider one shows the well's
   own colour. The scene's background is #CED3D4, flat to a standard deviation
   of 0.00, but limited-range YUV lands it on #CED2D4 once decoded — measured
   off the composited frame, so that is what the well is painted, not the
   nominal value. The card behind it is --glyph, one level away, exactly the
   step the live canvas already had. */
.slide-3d-well {
  position: relative;
  overflow: hidden;
  /* #CED2D4 is what the browser paints for the video's flat background, which
     is not what ffmpeg decodes out of the same file (it reads #CED3D4, and
     #CBD1D2 for the rescaled HYPE). All four were measured off the composited
     frame instead, and all four land here - so trust the browser, not the
     encoder. --well is the per-slide escape hatch if that ever stops holding. */
  background: var(--well, #CED2D4);
  /* depth for the cursor tilt; deep enough that the lean reads as a turn of
     the object, not a fisheye */
  perspective: 1500px;

  /* Feather the well's edges away instead of butting them against the card.
     The well is inset 16px inside a 40/64px-rounded card, so its own square
     corners sit in open space, and the card around it is NOT flat: measured
     across the left edge it runs 209,214,215 -> 207,211,212 while the well
     holds 206,210,212, so a hard boundary shows as a rectangle no matter what
     single colour the well is painted. Rounding alone would only trade four
     corners for four straight edges. A soft alpha ramp removes the transition
     entirely, and the double gradient rounds the corners for free.
     14px is chosen to stay clear of the content: the phone starts ~21px in on
     the tightest breakpoint (mobile, well 245x306).
     Percentages, not pixels, so the ramp scales with the well instead of being
     generous on desktop and cramped on mobile. The limits are the content: the
     scene leaves ~19% of the width and ~7% of the height clear of the phone, so
     8% across and 5% down never touch it. */
  -webkit-mask-image:
    linear-gradient(to right, transparent, #000 8%, #000 92%, transparent),
    linear-gradient(to bottom, transparent, #000 5%, #000 95%, transparent);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to right, transparent, #000 8%, #000 92%, transparent),
    linear-gradient(to bottom, transparent, #000 5%, #000 95%, transparent);
  mask-composite: intersect;

  /* The well's square top corners sit exactly on the card's rounded ones and
     rely on the card's overflow:hidden to clip them. That clipping is not
     dependable for a composited <video> (it carries a transform), which is how
     square corners end up poking out of a rounded card on iOS. Carrying the
     radius on the well itself removes the dependency. 64px card - 16px inset
     = 48px; only the top corners, since the bottom of the well sits in the
     middle of the card. */
  border-radius: 48px 48px 0 0;
}
.slide-3d-video {
  position: absolute;
  top: 0;
  left: 50%;
  /* --tilt-x/--tilt-y lean the flat video towards the cursor (js/anim.js), the
     way the original's Spline scene turned to look at it */
  transform: translateX(-50%) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
  height: 100%;
  width: auto;
  max-width: none;
}
/* the loop's crossfade partner (cloned by main.js) waits invisible on top */
.slide-3d-video.is-twin { opacity: 0; }

/* media well fills the card above the fixed 180px caption block */
.slide-3d {
  display: block;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  margin-bottom: 8px;
}

.pill-chips {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
}
.pill-chips li {
  position: relative;
  font-size: 11px;
  line-height: 18px;
  letter-spacing: -.22px;
  color: var(--ink);
  padding: 0;
  background: none;
}
/* border as an overlay so it adds no layout size, matching the original */
.pill-chips li::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--pill-line);
  border-radius: 40px;
  pointer-events: none;
}
.pill-chips a {
  padding: 4px 10px;
  display: flex;
  align-items: center;
  gap: 4px;
  color: inherit;
}
/* all three glyphs paint #3c3d3d in the original (read off the pixels — the
   store glyphs report a black fill but render grey) */
.pill-chips svg { width: 12px; height: 12px; fill: currentColor; color: var(--grey); flex: 0 0 auto; }
/* the Apple glyph is narrower than the web/android ones */
.pill-chips a[href*="apps.apple.com"] svg { width: 9px; height: 11px; position: relative; top: -1px; }

/* flat QA mode: ?flat=1 disables sticky/reveal so the full page paints at once */
body.flat .stage { height: auto; }
body.flat .stage-sticky { position: static; height: 820px; }
body.flat .hero-copy { padding-top: 70px; }
body.flat .device-card { height: 720px; top: 40px; }
body.flat .nav-pill { top: 320px; }
body.flat .socials { bottom: auto; top: 560px; }
body.flat .work-item { opacity: 1; transform: none; }

/* the original's "Prjc" block: 60% of the slide, never under 232px (it
   overflows the slide rather than shrink, as at 320), centred under the chips,
   with 4px of inner padding on each row */
.meta {
  width: max(232px, 60%);
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 0;
}
.meta > div {
  position: relative;
  display: flex;
  align-items: center;
  height: 18px;
  padding: 0 4px;
  font-size: 11px;
  line-height: 18px;
}
.meta dt {
  order: 1;
  font-family: var(--mono);
  text-transform: lowercase;
  display: flex;
  align-items: center;
  gap: 8px;
}
.meta dd {
  order: 3;
  margin-left: 0;
  text-transform: uppercase;
}
.sq {
  width: 12px; height: 12px;
  display: inline-block;
}
.sq.or { background: var(--orange); }
.sq.ye { background: var(--yellow); }
.sq.bl { background: var(--blue); }

/* dashed leader in an 8px box with a 4px spacer either side */
.meta > div::after {
  content: "";
  order: 2;
  flex: 1;
  height: 8px;
  margin: 0 4px;
  border-bottom: 1px dashed rgb(154, 169, 172);
}

.dots {
  position: absolute;
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  border-radius: 30px;
  background: rgb(60, 61, 61);
}
/* the buttons tile the whole pill (24x24 each, the dot drawn inside), so the
   hit area is the original's, not just the 16px dot */
.dotbtn {
  width: 24px; height: 24px;
  padding: 4px;
}
.dotbtn::before {
  content: "";
  display: block;
  width: 100%; height: 100%;
  border-radius: 50%;
  background: var(--glyph);
  opacity: .08;
  transition: background .3s, opacity .3s;
}
.dotbtn.is-active::before { opacity: 1; }

/* ===== glyph wall ===== */
.glyphs {
  position: relative;
  margin: 80px 0 0 209px;
  width: 1071px;
  height: 1248px;
  overflow: hidden;
}
.glyph-rows { position: relative; }
.glyph-rows p {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 389.872px;
  line-height: 311.898px;
  letter-spacing: 38.9872px;
  color: var(--glyph);
  white-space: nowrap;
}
.glyph-rows .ink { color: var(--ink); }
.glyph-rows .ghost { color: var(--bg); }
.glyph-rows .alt { color: var(--glyph); }

/* ===== the frame below the hero =====
   The original keeps the glyph wall, photo stack, gallery and footer in one
   frame that starts at the rail anchor (--a) and runs to the end of the page.
   The stack, the vertical words and the second plus are positioned in
   percentages of that frame — straight from its CSS — so they move with the
   wall's scale AND with the gallery's height (which stops growing at ~1623). */
main { position: relative; }
/* flow-root: the wall's top margin must stay inside the frame, not collapse
   through it, or every percentage below is measured from 80px too low */
.below { position: relative; display: flow-root; overflow-x: clip; --a: 209px; }

/* The stack is a MASK, not a loose pile. The original clips all twelve photos
   to one small window — 100x197 above 810, 62x122 below — so what you see is a
   single frame with the photos flashing through it. The photos keep their own
   wildly different slots (324x243, 115x205, 148x197...) and mostly hang
   outside the window; only the part inside shows.
   Without the clip they all spill across the wall as separate pictures. */
.photo-stack {
  position: absolute;
  left: calc(var(--a) + (100% - var(--a)) * .23996265172735782 - 50px);
  top: calc(21.337150555742696% - 98.5px);
  width: 100px;
  height: 197px;
  /* On mobile the work gallery rides up far enough to overlap the mask, and
     since both are positioned with z-index auto the later one in the DOM — the
     gallery — won, slicing the bottom 17px off the frame. The original paints
     the photo on top there (elementsFromPoint on the mirror returns only stack
     nodes), so the stack has to claim the layer. */
  z-index: 1;
}
.photo-mask {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: #fff;
}
.photo-mask img {
  position: absolute;
  top: 0;
  max-width: none; /* the narrow mask would otherwise cap width via max-width:100% */
  object-fit: cover;
  opacity: 0;
}
/* several are on at once — this is a pile, not a slideshow. js/main.js takes
   them off one at a time and then restores the whole stack. */
.photo-mask img.is-shown { opacity: 1; }
/* stickers ride outside the mask, on top of it: Wave Marker (rotated 45deg),
   Love Marker, and Ureg-Ureg Marker (-11deg, pinned to the right edge) —
   boxes, fit and rotation straight from the stack component */
.sticker {
  position: absolute;
  max-width: none;
  object-fit: contain;
  opacity: 0;
  pointer-events: none;
}
.sticker.is-shown { opacity: 1; }
.sticker-wave { left: -55px; top: 61px; width: 156px; height: 23.932px; transform: rotate(45deg); }
.sticker-love { left: -26px; top: -23px; width: 68px; height: 64.557px; }
.sticker-ureg { right: -60px; top: 50px; width: 98px; height: 16.917px; transform: rotate(-11deg); }
/* left is relative to the mask now, so these are the page values minus 416 */
.photo-mask img:nth-child(1)  { left: -191px; width: 324px; height: 243px; }
.photo-mask img:nth-child(2)  { left:    0px; width: 115px; height: 205px; }
.photo-mask img:nth-child(3)  { left: -120px; width: 324px; height: 243px; }
.photo-mask img:nth-child(4)  { left:  -60px; width: 324px; height: 243px; }
.photo-mask img:nth-child(5)  { left:  -90px; width: 324px; height: 243px; }
.photo-mask img:nth-child(6)  { left:    0px; width: 115px; height: 204px; }
.photo-mask img:nth-child(7)  { left:   -2px; width: 148px; height: 197px; }
.photo-mask img:nth-child(8)  { left:  -12px; width: 148px; height: 197px; }
.photo-mask img:nth-child(9)  { left:  -11px; width: 148px; height: 197px; }
.photo-mask img:nth-child(10) { left:  -27px; width: 153px; height: 197px; }
.photo-mask img:nth-child(11) { left:  -12px; width: 148px; height: 197px; }
.photo-mask img:nth-child(12) { left: -173px; width: 295px; height: 197px; }

/* rotated strip of tiny vertical captions sitting over the third row */
.vwords {
  position: absolute;
  left: calc(var(--a) + (100% - var(--a)) * .15);
  top: 16%;
  transform: translate(-50%, -50%);
  display: flex;
  z-index: 1;
}
.vwords span {
  display: block;
  width: 10.8px;
  height: 110px;
  font-family: var(--mono);
  font-size: 9px;
  line-height: 10.8px;
  letter-spacing: .54px;
  color: var(--grey);
  white-space: nowrap;
  overflow: hidden;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
}
.vwords .vgap { width: 19.2px; }

/* ===== work gallery =====
   The item keeps a fixed height so the pitch never depends on caption length;
   the caption is taken out of flow and pinned below the image. */
.work {
  /* the original's Work frame stops 6px short when a nav link scrolls to it */
  scroll-margin-top: 6px;
  margin: 19.984375px 0 0 233px;
  width: 1023px;
  display: flex;
  flex-direction: column;
  row-gap: 6px;
}
.work-item {
  position: relative;
  height: 754.8671875px;
}
.work-item::before {
  content: "";
  position: absolute;
  inset: 0 -24px;
  background: var(--glyph);
}
.work-item:last-child { height: 754.8671875px; }
/* fixed frame, image letterboxed inside — the sources have varying ratios */
.work-item img {
  position: relative;
  margin: 100px 0 0 136.6484375px;
  width: 749.6953125px;
  height: 494.8671875px;
  object-fit: contain;
}
.work-item figcaption {
  position: absolute;
  top: 730.8671875px;
  transform: translateY(-100%);
  left: 0;
  right: 0;
  display: flex;
  align-items: flex-end;
  gap: 16px;
  margin: 0;
  padding: 0;
  border: 0;
}
.work-item figcaption > div { flex: 1; min-width: 0; }
.work-item h2 {
  font-size: 11px;
  font-weight: 600;
  line-height: 14.3px;
  text-transform: uppercase;
  color: var(--grey);
}
.work-item figcaption p {
  margin-top: 2px;
  font-size: 13px;
  line-height: 16.9px;
  color: var(--grey);
}
/* the original dims the role label and the first (Canva) shot */
.role { opacity: .73; }
.work-item:first-child img { opacity: .64; }
.role {
  font-size: 13px;
  line-height: 16.9px;
  color: var(--grey);
  white-space: nowrap;
}

/* ===== footer ===== */
/* the WIP tag sits right against the viewport edge, 20px under the gallery */
.foot {
  display: flex;
  justify-content: flex-end;
  padding-top: 20px;
}
.foot .tag { white-space: pre; }

.fix-later {
  position: absolute;
  /* 8px below the fold — .stage is exactly 100vh, so this tracks the window
     height. The fixed 728px it replaces was that same spot measured at one
     particular window height and drifted at every other one. */
  top: calc(100% + 8px);
  right: 138px;
  z-index: 2;
}


/* ===== tablet: 810–1279, same two-column layout at reduced scale ===== */
@media (min-width: 810px) and (max-width: 1279px) {
  .rail { width: 140px; }
  .logo { top: 21px; left: 27px; }
  .logo-circle { width: 92px; height: 92px; }
  .logo-jan { left: 21px; top: 27px; font-size: 8px; line-height: 9.6px; }
  .logo-name { left: 18.453px; top: 40px; font-size: 10px; line-height: 12px; }
  .logo::before { inset: -21px; }

  /* tablet keeps the desktop nav (64x120 pill, 48px items) — only moved */
  .nav-pill { top: 174px; left: 41px; }
  .socials { left: 61px; }

  .hero-copy {
    width: 437px;
    margin-left: 150px;
    padding: 68px 0 2px 12px;
  }
  .hero-copy h1 {
    font-size: 24px;
    max-width: 351px;
    margin: 0 0 0 12px;
  }
  .chips { margin: 0; }
  .chip-struck, .chip-rule:has(+ .chip-struck) { display: none; }
  .scribble { top: 13px; width: 187px; height: 11px; }
  .hero-copy .plus { margin: 0 0 0 100px; }
  .tag-right { margin-right: 0; }
  .tag-readme { margin-right: 0; }
  .readme { width: 262px; margin: 0 8px 0 auto; }
  .status { margin: 0 0 0 auto; }
  .email-btn { margin: 0 0 0 24px; }
  .hero-cta .tag { margin: 0 0 0 12px; }


  .device-card {
    right: 27px;
    top: 52px;
    bottom: 52px;
    width: 296px;
  }
  .device-glow-a {
    left: 589px;
    right: 26px;
    top: 138px;
    bottom: 96px;
  }
  .device-glow-b {
    left: 601px;
    right: 36px;
    top: 317px;
    bottom: 77px;
  }

  /* the glyph wall and the work images scale to 0.7; all copy stays at size */
  .glyphs { margin: 80px 0 0 150px; width: 750px; height: 874px; }
  .glyph-rows { width: 1071px; transform: scale(.7); transform-origin: 0 0; }
  .below { --a: 150px; }

  /* photos keep their full desktop size here — only the offsets change */
  /* mask x is 279.5 here; relative to it the slots land on the same values as
     desktop, so only the three that were measured a fraction off are restated */
  .photo-stack {
    left: calc(var(--a) + (100% - var(--a)) * .2393939393939396 - 50px);
    top: calc(21.10456351925451% - 98.5px);
  }
  .photo-mask img:nth-child(8)  { left: -11.7px; }
  .photo-mask img:nth-child(11) { left: -11.7px; }
  .photo-mask img:nth-child(12) { left: -173.1px; }

  .work {
    margin: 20px 0 0 150px;
    width: 750px;
    display: flex;
    flex-direction: column;
    row-gap: 6px;
  }
  .work-item {
    width: 750px;
    height: 606.546875px;
  }
  .work-item::before { inset: 0; }
  .work-item:last-child { height: 606.546875px; }
  .work-item img {
    margin: 100px 0 0 112.5px;
    width: 525px;
    height: 346.546875px;
  }
  .work-item figcaption {
    top: 583px;
    left: 24px;
    right: 24px;
  }
  .work-item h2 { line-height: 14.3px; }

  .foot .tag { text-transform: lowercase; font-weight: 700; color: var(--grey-500); }
  .fix-later { right: 138px; }
}

/* ===== mobile: ≤809 — card on top, copy below, rail stays a left column ===== */
@media (max-width: 809px) {
  .rail { width: 56px; }
  .logo { top: 7px; left: 13px; }
  .logo-circle { width: 36px; height: 36px; }
  .logo-full { display: none; }
  .logo-mini { opacity: 1; left: 50%; top: 50%; line-height: 16.8px; transform: translate(-50%,-50%); }
  .logo::before { inset: -7px; }

  .nav-pill {
    top: 127px;
    left: 13px;
    width: 36px;
    height: 80px;
    border: 0;
  }
  .nav-item {
    left: 0;
    width: 36px;
    height: 36px;
    font-size: 10px;
    line-height: 12px;
    letter-spacing: -.3px;
  }
  .nav-item[data-nav="top"] { top: 0; }
  .nav-item[data-nav="work"] { top: 44px; }
  .nav-ring { left: 0; top: 0; width: 36px; height: 36px; }
  .nav-pill.at-work .nav-ring { top: 44px; }

  .socials {
    top: 250px;
    bottom: auto;
    left: 21px;
    align-items: flex-start;
    gap: 20px;
  }
  .socials a, .socials svg { width: 20px; height: 20px; }
  .soc-x .soc-glyph { font-size: 24px; line-height: 24px; }
  .socials .soc-mail { display: grid; }
  /* anchored to the bottom like everywhere else — the fixed `top: 701px` this
     replaces only landed correctly at one particular window height */
  .ver { font-size: 6px; line-height: 7.2px; left: 2.164px; bottom: 4.633px; }

  /* hero: card first, then the copy */
  .stage { height: auto; }
  .stage-sticky {
    position: static;
    height: auto;
    flex-direction: column;
    overflow: visible;
  }
  .device-card {
    order: 1;
    position: relative;
    right: auto; top: auto; bottom: auto;
    width: 245px;
    height: 438px;
    margin: 86px 36px 0 auto;
    --card-r: 40px;
  }
  /* straight off the original's CSS boxes, not its rendered rects: the blobs
     are rotated AND animating, so a bounding rect measures neither. The
     original's own parent starts at x 62 and ends 12 from the right edge
     (width 100vw - 74); ours spans the viewport, so its insets are shifted
     by that much — `left: 106` there is `left: 168` here, `right: 41` is 53. */
  .device-glow-a {
    left: 168px;
    right: 53px;
    top: 82px;
    bottom: auto;
    height: 235px;
  }
  .device-glow-b {
    left: 127px;
    right: 95px;
    top: 284px;
    bottom: auto;
    height: 233px;
  }
  .slide { padding: 0 0 32px; }
  .slide { --pt: 0px; --pr: 0px; --pb: 32px; --pl: 0px; }
  .slide-3d { margin-bottom: 8px; }
  /* flush with the card here (no side padding), so it takes the card's own 40px */
  .slide-3d-well { border-radius: 40px 40px 0 0; }
  .pill-chips { gap: 6px; margin-bottom: 12px; }
  .dots {
    bottom: 8px;
    border-radius: 30px;
    background: var(--glyph);
  }
  /* 6px dots, 6px apart, 6px from the pill's ends — split into the buttons'
     own padding like the original: 12x18 inside, 15x18 at the ends */
  .dotbtn { width: 12px; height: 18px; padding: 6px 3px; }
  .dotbtn:first-child { width: 15px; padding-left: 6px; }
  .dotbtn:last-child { width: 15px; padding-right: 6px; }
  .dotbtn::before { background: var(--ink); }

  .hero-copy {
    order: 2;
    width: auto;
    margin: 56px 12px 0 62px;
    padding-top: 0;
  }
  .hero-copy { justify-content: flex-start; gap: 52px; }
  .hero-headline { gap: 16px; }
  .hero-copy h1 {
    font-size: 24px;
    line-height: 1.2;
    max-width: 304px;
    margin: 0 0 0 12px;
  }
  .chips { margin: 0 0 0 14px; }
  .chip-struck, .chip-rule:has(+ .chip-struck) { display: none; }
  .hero-copy .plus { margin: 0 0 0 100px; }

  .scribble { left: -2px; top: 14px; width: 187px; height: 11px; }
  .readme { width: min(198px, calc(100vw - 170px)); font-size: 15px; line-height: 21px; letter-spacing: -.15px; margin: 0 32px 0 auto; }
  .status { margin: 0 24px 0 auto; }
  .email-btn { margin: 0 0 0 29px; }
  .hero-cta .tag { margin: 0 0 0 17px; }


  .glyphs { margin: 26px 0 0 62px; padding: 0; width: 313px; height: 363px; }
  .glyph-rows { width: 1071px; transform: scale(.29225); transform-origin: 0 0; }
  .below { --a: 62px; }
  /* 112% across in the original — mostly past the frame's right edge, only
     the first couple of words peek in at the edge of the screen */
  .vwords { left: calc(var(--a) + (100% - var(--a)) * 1.12); top: 6%; }
  .plus-glyph {
    left: calc(var(--a) + (100% - var(--a)) * .6979865771812082 - 12px);
    top: calc(4.226783174151618% - 12px);
  }

  /* mobile has its own photo sizes — not a uniform scale of the desktop ones */
  /* mask is 62x122 here and sits at x 109.8; lefts are the page values minus it */
  .photo-stack {
    left: calc(var(--a) + (100% - var(--a)) * .25167785234899355 - 31px);
    top: calc(15.200162568583645% - 61px);
    width: 62px;
    height: 122px;
  }
  .photo-mask img:nth-child(1)  { left: -191px;  width: 286px;   height: 214.5px; }
  .photo-mask img:nth-child(2)  { left:    0px;  width:  77px;   height: 137.1px; }
  .photo-mask img:nth-child(3)  { left: -120px;  width: 286px;   height: 214.5px; }
  .photo-mask img:nth-child(4)  { left:  -60px;  width: 286px;   height: 214.5px; }
  .photo-mask img:nth-child(5)  { left:  -90px;  width: 286px;   height: 214.8px; }
  .photo-mask img:nth-child(6)  { left:    0px;  width:  77px;   height: 136.7px; }
  .photo-mask img:nth-child(7)  { left:   16.3px; width:  91.7px; height: 122px; }
  .photo-mask img:nth-child(8)  { left:    6.5px; width:  91.5px; height: 122px; }
  .photo-mask img:nth-child(9)  { left:  -11px;   width:  91.5px; height: 122px; }
  .photo-mask img:nth-child(10) { left:   -6.8px; width:  94.8px; height: 122px; }
  .photo-mask img:nth-child(11) { left:    6.5px; width:  91.5px; height: 122px; }
  .photo-mask img:nth-child(12) { left:  -98.8px; width: 182.8px; height: 122px; }

  /* the original keeps a constant 416px pitch regardless of caption length,
     so the caption is taken out of flow */
  .work {
    margin: 24px 0 0;
    width: auto;
    padding: 0 0 0 62px;
    display: flex;
    flex-direction: column;
    row-gap: 6px;
  }
  .work-item img {
    margin: 80px 0 0 28.171875px;
    width: 256.65625px;
    height: 169.4140625px;
  }
  .work-item {
    position: relative;
    width: 313px;
    height: 409.4140625px;
    margin: 0;
  }
  .work-item::before { inset: 0; }
  .work-item:last-child { height: 409.4140625px; }
  .work-item figcaption {
    position: absolute;
    top: 369px;
    left: 12px;
    right: 12px;
    transform: none;
    align-items: flex-end;
    gap: 16px;
    margin: 0;
    padding-top: 0;
    border-top: 0;
  }
  .work-item h2 { font-size: 10px; line-height: 13px; }
  .work-item figcaption p, .role { font-size: 11px; line-height: 14.3px; }

  .foot { padding-top: 24px; }
  .fix-later { display: none; }
}

/* ===== desktop is fluid, not fixed =====
   The original doesn't freeze the desktop layout at 1280 — it reserves a fixed
   203px rail (content starts at 209) and then splits everything left over into
   two equal columns, so hero copy and device card grow together as the window
   widens. Everything below is expressed against that rule, which is why the
   ratios come out round: the work image is exactly 70% of the content width,
   offset exactly 15%.

   Measured against the mirror at 1280 / 1440 / 1600:
     content width  = 100vw - 209
     column         = content / 2
     h1             = column - 160   (52+12 left, 52+44 right)
     card           = column - 128, pinned 64 from the right
   ...until the hero column reaches 960px (window ~2129px). From there the hero
   stops growing and the card's column takes everything else — measured at
   2200 / 2400 / 2560, where the card runs 871 / 1071 / 1231 wide.
   Keep this block last — it deliberately overrides the fixed values above,
   which still serve tablet and mobile. */
@media (min-width: 1280px) {
  .stage-sticky { padding-left: 209px; }
  .hero-copy {
    width: min(50%, 960px);
    margin-left: 0;
    padding: 92px 52px 2px 52px;
  }
  .hero-copy h1 { max-width: none; margin: 0 44px 0 12px; }

  .device-card {
    right: 64px;
    width: calc(100% - 209px - min((100% - 209px) / 2, 960px) - 128px);
  }

  /* the blobs are fluid too — they live in the right column, so they widen
     with it. The fixed widths they had instead only agreed at 1280, where
     fixed and fluid coincide; by 1600 they were 160px too narrow.
     In the original they are inset in that column (a: 123 left / 68 right,
     b: 79 / 148), and the column's right edge is the viewport's, so only
     `left` has to be re-based here — `right` already lands correctly. */
  .device-glow-a { left: calc(209px + min((100% - 209px) / 2, 960px) + 123px); }
  .device-glow-b { left: calc(209px + min((100% - 209px) / 2, 960px) + 79px); }

  /* the glyph wall is a 1071x1248 design that scales to fill whatever width is
     left beside the rail. --wall is set by main.js, because CSS can't divide
     one length by another to get a unitless scale factor. */
  .glyphs {
    width: auto;
    height: calc(1248px * var(--wall, 1));
  }
  .glyph-rows {
    transform: scale(var(--wall, 1));
    transform-origin: 0 0;
  }

  /* the item now spans the full content width instead of being inset 24px and
     bleeding back out through ::before, so the image can be a plain percentage */
  .work { margin-left: 209px; width: auto; }
  .work-item::before { inset: 0; }
  .work-item, .work-item:last-child { height: auto; padding-bottom: 160px; }
  .work-item img {
    /* auto margins rather than the 15% inset: below the cap the two are the
       same thing (70% wide leaves 15% each side), but once max-width bites the
       image has to stay centred instead of staying 15% from the left */
    margin: 100px auto 0;
    width: 70%;
    /* the original stops growing here. Measured: it tracks 70% up to 974px at
       1600, then holds 990x654 at 1680, 1760 and 1920 alike — the cap lands at
       about 1623px of viewport. Without it six work images each ran 137px too
       tall at 1920, which is the whole 824px the document was too long. */
    max-width: 990px;
    height: auto;
    /* the frame keeps the design ratio and letterboxes the source inside it;
       height:auto alone would follow each file's own ratio and drift */
    aspect-ratio: 1.5149; /* Framer's own rounding of 749.70/494.87 — the exact ratio comes out 1/64px shorter per item and adds up to a pixel down the gallery */
  }
  .work-item figcaption {
    top: auto;
    bottom: 24px;
    left: 24px;
    right: 24px;
    transform: none;
  }
}

/* ===== tablet is fluid too, same shape as desktop =====
   Anchor is 150 instead of 209, and the two columns aren't equal here: the
   hero always runs 80px wider than the card. Measured against the mirror at
   810 / 900 / 960 / 1024 / 1200 — exact at all five.

     h1    = (100vw - 198) / 2
     card  = max(296, (100vw - 358) / 2)      ← see note below
     work  = 100vw - 150, image 70% wide at 15% offset

   The card stops shrinking at 296px (below ~950px wide). The original lets it
   run past the right edge and clips it — that's why the phone is cut off at
   810. Positioned from the left so it overflows the same way. */
@media (min-width: 810px) and (max-width: 1279px) {
  .hero-copy { width: calc((100vw - 198px) / 2 + 24px); }
  .hero-copy h1 { max-width: none; }

  .stage-sticky {
    --card-left: calc((100vw - 198px) / 2 + 226px);
    --card-w: max(296px, calc((100vw - 358px) / 2));
  }
  .device-card {
    left: var(--card-left);
    right: auto;
    width: var(--card-w);
  }
  /* the glows are siblings of the card, so nothing clips them — with a fixed
     left AND right they stretched across the hero copy as the window grew.
     Tie them to the card instead; offsets are the measured ones. */
  /* both blobs share the same column — read straight off the original's CSS
     boxes (x 556 = card+24, width 263 = card-33 at 810); only the vertical
     extents differ. Comparing bounding rects here was a dead end: the blobs
     animate continuously, so the rect never settles. */
  .device-glow-a {
    left: calc(var(--card-left) + 24px);
    right: auto;
    width: calc(var(--card-w) - 33px);
    top: 72px;
    bottom: auto;
    height: 642px;
  }
  .device-glow-b {
    left: calc(var(--card-left) + 24px);
    right: auto;
    width: calc(var(--card-w) - 33px);
    top: 318px;
    bottom: auto;
    height: 505px;
  }

  .glyphs {
    width: auto;
    height: calc(1248px * var(--wall, .7));
  }
  .glyph-rows { transform: scale(var(--wall, .7)); }

  .work { width: auto; }
  .work-item, .work-item:last-child {
    width: auto;
    height: auto;
    padding-bottom: 160px;
  }
  .work-item img {
    margin: 100px 0 0 15%;
    width: 70%;
    height: auto;
    aspect-ratio: 749.6953125 / 494.8671875; /* exact here — unlike desktop, the original keeps the full ratio on tablet */
  }
  .work-item figcaption {
    top: auto;
    /* 24, like desktop: the 23.546875 this replaces left every caption half a
       pixel low (title 56.75px above the frame's bottom, original 57.203) */
    bottom: 24px;
    transform: none;
  }
}

/* ===== mobile is fluid too =====
   Anchor 62. Single column, so there's no pairing rule — everything just
   tracks the viewport. The work image is 82% wide at a 9% offset here (not
   70%/15% like the wider breakpoints), and the top margin is 80 rather than
   100. Measured against the mirror at 375 and 809.

     h1    = 100vw - 86      (x 74, right margin 12)
     card  = 100vw - 130     (x 94, right margin 36)
     work  = 100vw - 62
     wall  = (100vw - 62) / 1071   (on 375 that is exactly .29225) */
@media (max-width: 809px) {
  .hero-copy h1 { max-width: none; }

  .device-card { width: calc(100vw - 130px); }

  .glyphs {
    width: auto;
    /* a hair under 1248 x scale, so it rounds down to the same 1/64px as the
       original's (which sizes the wall by font size): at 600 that is 626.891,
       not 626.906, and the gallery below lands on the same pixel */
    height: calc(1248px * var(--wall, .29225) - .01px);
    /* 24, as in the original. It was 24.6 for a while: that hid the hero
       coming out 0.2px short (the counter's 11px frame, below) at one width,
       and left the wall 0.4px low everywhere else */
    margin-top: 24px;
  }
  .glyph-rows { transform: scale(var(--wall, .29225)); }

  .work-item, .work-item:last-child {
    width: auto;
    height: auto;
    padding-bottom: 160px;
  }
  .work-item img {
    margin: 80px 0 0 9%;
    width: 82%;
    height: auto;
    aspect-ratio: 1.5149;
  }
  /* anchored by its bottom, like the wider breakpoints: the caption grows
     upward as the description wraps to more lines. Pinning the top instead
     pushed longer captions down — 29px on salsa, 44px on fymo. */
  .work-item figcaption { top: auto; bottom: 12px; }
}
