/* elba4a.com — one stylesheet, no build step, no dependencies.
   Two self-hosted variable fonts, no network request off this origin.
   Boxes use CSS logical properties throughout; the page is LTR-only today, but
   the properties cost nothing and mean a second direction would not need a
   mirrored stylesheet. */

/* ----------------------------------------------------------------- fonts */

/* Archivo replaces Schibsted Grotesk, on a capability rather than a taste. The
   display type is fitted to the measure so every line ends flush on the right
   margin; the entrance widens the letterforms until they land there. Both need
   a `wdth` axis, and a dump of the shipped Schibsted subset's `fvar` found one
   axis only: `wght` 400–900. */
@font-face {
  font-family: "Archivo";
  src: url("/assets/fonts/archivo-latin.woff2") format("woff2");
  font-weight: 100 900;
  /* Delete this line and every `font-stretch` value below silently clamps to
     100%: the fit still looks right, the entrance becomes a no-op, and nothing
     reports it. tools/check-fit.mjs asserts the axis renders. */
  font-stretch: 62% 125%;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Azeret Mono";
  src: url("/assets/fonts/azeret-mono-latin.woff2") format("woff2");
  /* The file carries 100–900. This rule used to claim 400–600, so 300 weights
     of a font already on the wire were unreachable. */
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------- tokens */

:root {
  /* Paper, not white. Pure #FFF under a warm-toned screenshot reads as a hole.
     Ratios measured against --paper:
     --ink 15.8:1 · --ink-2 6.1:1 · --accent 7.5:1 */
  --paper:   #FBFBF9;
  --paper-2: #F3F3EF;
  --paper-3: #ECECE6;
  --ink:     #16161A;
  --ink-2:   #5A5A63;
  --line:    #E4E4DE;
  --line-2:  #D2D2CA;
  /* The one ground that is not paper. A full-bleed band bounded by a rule, not
     a mid-page tint step: --paper against --paper-2 is 1.074:1, and a 7% step
     across a viewport reads as a rendering error, which is what shipped once.
     Measured against --slab, not estimated:
     --ink 13.1:1 · --ink-2 4.7:1 (AA only) · --accent 5.4:1 (AA only).
     So captions on the slab use --ink at a smaller size, and no link goes on
     it — the tab pills carry their own --ink background instead. */
  --slab:    #DCDCD4;
  /* --ink-2 is 4.95:1 on the slab, which is AA and not the AAA the rest of the
     page holds, so muted text on this band uses its own value: 7.44:1. */
  --slab-ink-2: #404048;
  --accent:  #1B44C8;
  --navy:    #06244A;
  --gold:    #BFA46B;

  --sans: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "Azeret Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --shell: 1120px;
  /* The hero runs wider than the reading column: display type needs the room,
     body copy does not. */
  --bleed: 1680px;
  --gut: clamp(1.25rem, 4.5vw, 3rem);
  --rhythm: clamp(5rem, 12vh, 9rem);
  /* Must stay above the project titles at every viewport. It used to cap at
     34px against their 40px, so "Work" read as subordinate to "Hekta" and the
     whole page hierarchy inverted. */
  --fs-h2:      clamp(2.125rem, 4.6vw, 3.125rem);
  --fs-h3:      clamp(1.1875rem, 1.8vw, 1.4375rem);
  --fs-body:    1.0625rem;
  --fs-meta:    .6875rem;

  --ease: cubic-bezier(.22, 1, .36, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
}

/* ------------------------------------------------------------------ base */

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

html {
  background: var(--paper);
  color-scheme: light;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-block-start: 5rem;
  /* `clip`, never `hidden`. `overflow-x: hidden` forces overflow-y to `auto`,
     which turns the element into a scroll container — and that container then
     becomes the nearest scrollport for every `animation-timeline: view()`
     below it. Since it never actually scrolls, those timelines go inactive and
     every scroll-revealed element stays stuck at opacity 0. */
  overflow-x: clip;
}

body {
  margin: 0;
  font-family: var(--sans);
  font-size: var(--fs-body);
  line-height: 1.65;
  color: var(--ink);
  /* Transparent, not paper. An opaque body background covers the whole
     document and would hide a canvas sitting at z-index -1 everywhere. The
     paper ground comes from `html`, which paints below the canvas. */
  background: transparent;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3 { margin: 0; font-weight: 500; text-wrap: balance; letter-spacing: -.02em; }
p { margin: 0; text-wrap: pretty; }
ul, ol { margin: 0; padding: 0; list-style: none; }
dl, dd, dt { margin: 0; }

/* height:auto is load-bearing — the width/height attributes are there to
   reserve layout space, and without this the intrinsic height wins over
   max-width and the image renders at full height, cropped. */
img { max-width: 100%; height: auto; display: block; }

a { color: inherit; text-decoration: none; }

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

code {
  font-family: var(--mono);
  font-size: .875em;
  background: var(--paper-3);
  padding: .1em .35em;
  border-radius: 4px;
}

.shell {
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gut);
}

/* Hidden by size and clip, never by an offset. The old version parked itself
   at `inset-block-start: -100%`, which resolves against the containing block's
   height — so the moment anything forced it to `position: relative`, -100% of
   an auto height became 0 and a black box appeared at the page origin. Size
   plus `clip-path` is position-independent: force `position: static` on this
   and the worst you get is a 1px sliver. */
.skip {
  position: fixed;
  inset-block-start: 0;
  inset-inline-start: 0;
  z-index: 30;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  background: var(--ink);
  color: var(--paper);
  font-size: .875rem;
}
.skip:focus {
  inline-size: auto;
  block-size: auto;
  overflow: visible;
  clip-path: none;
  margin: .75rem;
  padding: .6rem 1rem;
}

/* Arrow points along the reading direction in both scripts. */
.go::after { content: "\2197"; margin-inline-start: .35em; font-size: .85em; }

.eyebrow {
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-2);
}

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

.hdr {
  position: sticky;
  inset-block-start: 0;
  z-index: 20;
  /* Opaque, and no blur. A full-viewport backdrop filter costs more per frame
     on a mid-range phone than the entire 3D scene does, and a translucent bar
     over a ground that changes from paper to near-black mid-scroll is a seam
     generator with a timing race attached. A solid toolbar is a convention
     everyone already reads. */
  background: var(--paper);
  border-block-end: 1px solid var(--line);
}
.hdr__in {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  min-height: 60px;
}
.hdr__id {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  font-size: .9375rem;
  font-weight: 500;
  letter-spacing: -.01em;
  margin-inline-end: auto;
}
.hdr__mark {
  inline-size: 24px;
  block-size: 24px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line-2);
  border-radius: 6px;
  color: var(--accent);
  transition: border-color .25s var(--ease), background .25s var(--ease);
}
.hdr__mark svg { inline-size: 17px; block-size: 17px; }
.hdr__id:hover .hdr__mark { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, transparent); }

.hdr__nav { display: flex; gap: 1.5rem; }
.hdr__nav a {
  font-size: .875rem;
  color: var(--ink-2);
  position: relative;
  transition: color .2s var(--ease);
}
.hdr__nav a::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: -3px;
  block-size: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .3s var(--ease);
}
.hdr__nav a:hover { color: var(--ink); }
.hdr__nav a:hover::after { transform: scaleX(1); }
.hdr__nav a[aria-current] { color: var(--ink); }
.hdr__nav a[aria-current]::after { transform: scaleX(1); background: var(--accent); }

.hdr__cta {
  font-size: .875rem;
  padding: .45rem .9rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  transition: border-color .25s var(--ease), background .25s var(--ease), color .25s var(--ease);
}
.hdr__cta:hover { border-color: var(--ink); background: var(--ink); color: var(--paper); }

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

/* The hero is one sentence, set as large as the measure allows, on paper.
   `100svb` with the product band as the next section means the fold does the
   cropping: at any viewport height the reader sees the sentence and the top of
   a real screen underneath it, with no magic number anywhere. */
.hero {
  position: relative;
  min-block-size: 100svb;
  display: grid;
  align-items: center;
  padding-block: clamp(3rem, 8vh, 5rem);
}

.hero__in {
  inline-size: 100%;
  max-inline-size: calc(var(--bleed) + var(--gut) * 2);
  margin-inline: auto;
  padding-inline: var(--gut);
}

/* The container the display type is measured against. Capping the CONTAINER
   rather than the font-size is the whole trick: `--gut` clamps at 1066px, so a
   clamped font-size would freeze while the measure kept widening, and past
   ~1460px every line would fall short of the margin. Sized in `cqi` against
   this box, the ratio advance/measure is invariant from 320px to 2560px —
   asserted at nine viewports by tools/check-fit.mjs. */
.hero__type { container-type: inline-size; }

@media (min-width: 441px) and (max-width: 700px) {
  /* Between the four-line and two-line breaks the column stops growing instead
     of the type doing it. Uncapped, four lines at a 640px measure make an
     861px block that overflows the fold on a 700x900 tablet. */
  .hero__type { max-inline-size: 440px; }
}

.hero__h {
  font-weight: 760;
  line-height: .90;          /* sentence case has descenders; .84 collides */
  margin-block: clamp(1.5rem, 4vh, 2.5rem) 0;
  font-stretch: 100%;
  /* A line that is one rounding error too wide would wrap and destroy the
     fitting, so wrapping is off — and clipped rather than scrollable, because
     a fallback face is wider than Archivo and would otherwise put a horizontal
     scrollbar on the document for the length of one font swap. */
  white-space: nowrap;
  overflow-x: clip;
}
.hero__h em { font-style: normal; color: var(--accent); }

/* Each line's size is solved so its advance lands on the measure. These are
   measurements, not taste — read off a bisection against the rendered advance
   in tools/fit-test.html, which is also what re-derives them if the copy
   changes. Uniform weight throughout: dropping the last line to 400 would
   narrow it and break the fit that is the entire premise. */
/* `letter-spacing` in `em` computes to an absolute length at the element that
   declares it, and inherits as that length. Declared on `.hero__h` it resolved
   against the inherited 17px body size — 0.5px instead of 6.9px — and every
   line overran the measure by ~6%. It has to sit where the font-size is. */
.hero__h .ln,
.hero__h .l  { letter-spacing: -.032em; }
.hero__h .ln { display: contents; }        /* below 701px the four spans ARE the lines */
.hero__h .l  { display: block; }
.hero__h .l1 { font-size: 36.92cqi; }
.hero__h .l2 { font-size: 38.89cqi; }
.hero__h .l3 { font-size: 43.76cqi; }
.hero__h .l4 { font-size: 30.07cqi; }

@media (min-width: 701px) {
  /* Two lines, and the pair wrappers become the fitted lines. Two inline spans
     between block boundaries form one anonymous block box, so "I build every"
     sets as a single line and the markup never changes across breakpoints. */
  .hero__h .ln { display: block; }
  .hero__h .l  { display: inline; font-size: inherit; }
  .hero__h .ln1 { font-size: 18.38cqi; }
  .hero__h .ln2 { font-size: 17.39cqi; }
}

/* The one gesture on the page. Every line starts condensed — short of the
   margin by nearly half — and the letterforms widen until the sentence lands
   flush. Nothing fades, translates or blurs, which is what every template
   does; this needs a real width axis, so it cannot be one. The finished widths
   are the resting state above, so reduced motion, a missing stylesheet or an
   engine without `font-stretch` all get flush type rather than an empty box.
   Line heights never change during it, so the block cannot shift the page. */
@media (prefers-reduced-motion: no-preference) {
  /* Whichever element is the LINE at this breakpoint is the one that widens.
     Animate both and the two halves of a joined line would widen out of step
     with each other. */
  @media (max-width: 700px) {
    .hero__h .l  { animation: fit .9s var(--ease) both; }
    .hero__h .l2 { animation-delay: .09s; }
    .hero__h .l3 { animation-delay: .18s; }
    .hero__h .l4 { animation-delay: .27s; }
  }
  @media (min-width: 701px) {
    .hero__h .ln  { animation: fit .9s var(--ease) both; }
    .hero__h .ln2 { animation-delay: .09s; }
  }
}

@keyframes fit { from { font-stretch: 62%; } to { font-stretch: 100%; } }

/* The four things a reader can actually open, handed over before any card. */
.open {
  margin-block-start: clamp(2.5rem, 6vh, 4rem);
  padding-block-start: 1.25rem;
  border-block-start: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .75rem 2rem;
}
.open__k {
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.open__l { display: flex; flex-wrap: wrap; gap: .5rem 1.75rem; }
.open__l a {
  font-family: var(--mono);
  font-size: .8125rem;
  color: var(--accent);
  padding-block-end: 2px;
  border-block-end: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  transition: border-color .25s var(--ease);
}
.open__l a:hover { border-block-end-color: var(--accent); }

/* ------------------------------------------------------------- prototype */

/* The product, at the top of the page, as a thing you use rather than a strip
   of thumbnails. */
.proto {
  padding-block: clamp(3rem, 8vh, 6rem);
  background: var(--slab);
  border-block: 1px solid var(--line-2);
}

.proto__in {
  max-inline-size: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gut);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}

@media (max-width: 860px) {
  /* The phone first: on a phone, the product is the point. */
  .proto__in { grid-template-columns: minmax(0, 1fr); justify-items: center; }
  .proto__say { order: 2; }
  .phone { order: 1; }
}

.proto__say h2 { font-size: var(--fs-h2); font-weight: 500; margin-block: .5rem .5rem; }
.proto__lede { max-inline-size: 46ch; margin-block-start: 1rem; }
.proto__hint {
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--slab-ink-2);
  margin-block-start: clamp(1.75rem, 4vh, 2.5rem);
}

/* Every muted token on the page is measured against --paper and lands one
   grade lower on the slab, so the band re-points them rather than accepting AA
   where the rest of the page holds AAA. */
.proto .facts dt,
.proto .facts span,
.proto__meta { color: var(--slab-ink-2); }
.proto .facts { border-block-start-color: var(--line-2); }
.proto .facts > div { border-block-end-color: var(--line-2); }
.proto .chips li { border-color: var(--line-2); background: transparent; }

.proto .facts { margin-block-start: clamp(2rem, 5vh, 3rem); }
.proto .chips { margin-block-start: 1.5rem; }
.proto__meta {
  margin-block-start: 1rem;
  font-size: .875rem;
  max-inline-size: 46ch;
}
.proto__meta a { white-space: nowrap; }

/* The index. Nine named screens, always all nine — the phone shows one at a
   time but the page never hides what it holds. */
.proto__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-block-start: .875rem;
}
.proto__tabs button {
  font-family: var(--mono);
  font-size: .78125rem;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  padding: .4375rem .8125rem;
  cursor: pointer;
  transition: border-color .2s var(--ease), background-color .2s var(--ease);
}
.proto__tabs button:hover { border-color: var(--ink); }
.proto__tabs button[aria-selected="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

/* ---- the device ---------------------------------------------------------

   Not a mockup: 1320x2868 is 440x956pt at @3x, so the corner radii below are
   the device's own, expressed as a fraction of its width. No drawn notch (the
   Dynamic Island is in the pixels), no side buttons, no angle, no reflection.

   The 2px ring is what actually solves the contrast problem. The screens are
   iOS light mode on #F2F2F7; against --paper that is 1.08:1 and invisible.
   Against --ink it is 16.17:1, and that holds on any ground, under any
   ambient light, whatever the palette does later. */
.phone {
  margin: 0;
  inline-size: min(330px, 78vw);   /* 330 CSS px is exactly 660 source px at DPR 2 */
  position: sticky;
  inset-block-start: calc(60px + 1.5rem);
  /* The container has to be the PARENT. `container-type` makes an element a
     container for its descendants, not for itself, so `cqw` in the frame's own
     border-radius fell through to the viewport: 12.5% of 1280px is 160px, and
     the device rendered with a domed top. */
  container-type: inline-size;
}
@media (max-width: 860px) { .phone { position: static; } }

.phone__frame {
  position: relative;
  aspect-ratio: 660 / 1434;
  background: var(--ink);          /* no seam in the moment before an image lands */
  overflow: hidden;
  border-start-start-radius: 12.5cqw;   /* 55pt of 440pt */
  border-start-end-radius: 12.5cqw;
  /* The bottom is NOT the device radius. Probing the largest uniform corner
     region on all nine screens found tops safe at 58pt or more everywhere, but
     bottoms as tight as 15pt — so a true 55pt bottom radius would clip live
     content on four of them. 15pt of 440pt is 3.4%. */
  border-end-start-radius: 3.4cqw;
  border-end-end-radius: 3.4cqw;
  box-shadow:
    inset 0 0 0 2px var(--ink),
    0 24px 48px -28px rgb(0 0 0 / .35);
}

.screen { position: absolute; inset: 0; }
.screen img { inline-size: 100%; block-size: 100%; display: block; }

/* Only once the script has taken over does one-at-a-time become true. Hiding
   them in the stylesheet instead would leave eight screens unreachable on any
   client where the script does not run. */
.proto--live .screen[hidden] { display: none; }

@media (prefers-reduced-motion: no-preference) {
  .proto--live .screen { animation: screen-in .18s var(--ease-out) both; }
}
@keyframes screen-in { from { opacity: 0; transform: translateY(6px); } }

/* The three chevrons printed on Home, made tappable where they actually sit.
   Invisible until focused or hovered: the screenshot already draws the
   affordance, so a second one over it would be a lie about the interface. */
.hot {
  position: absolute;
  inset-block-start: var(--y);
  inset-inline-start: var(--x);
  inline-size: var(--w);
  block-size: var(--h);
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  cursor: pointer;
  /* Named for a screen reader, silent to a sighted reader. */
  color: transparent;
  font-size: 0;
}
.hot:hover, .hot:focus-visible {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.proto:not([data-screen="home"]) .hot { display: none; }

.phone__cap {
  margin-block-start: 1rem;
  font-family: var(--mono);
  font-size: var(--fs-meta);
  line-height: 1.55;
  color: var(--ink);
  max-inline-size: 34ch;
}

/* -------------------------------------------------------------- sections */

.section { padding-block: var(--rhythm) 0; }

.s-head {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem 2rem;
  padding-block-end: 1.25rem;
  margin-block-end: clamp(2.5rem, 6vh, 4rem);
}
.s-head::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 1px;
  background: var(--line);
  transform-origin: left center;
}
.s-head h2 { font-size: var(--fs-h2); font-weight: 500; }
.s-head__note {
  font-size: .875rem;
  color: var(--ink-2);
  max-inline-size: 46ch;
}

/* -------------------------------------------------------------- projects */

.proj { padding-block: clamp(2.5rem, 7vh, 4.5rem); }
.proj + .proj { border-block-start: 1px solid var(--line); }

/* Alternating tint so four stacked case studies do not read as one field. */
.proj--alt {
  background: var(--paper-2);
  border-block: 1px solid var(--line);
  max-width: none;
  padding-inline: var(--gut);
}
/* `--shell` minus both gutters, not `--shell`. `.proj--alt` drops the shell's
   max-width and applies `--gut` as padding itself, so a child capped at the
   full shell width re-centres inside an already-inset box and double-counts
   the gutter — which hung Muhrah and NOOR 48px left of every other block
   above 1216px. This caps the child at the shell's *content* width instead,
   so its edges land exactly where a plain `.shell` section's do. */
.proj--alt > * {
  max-width: calc(var(--shell) - var(--gut) * 2);
  margin-inline: auto;
}
.proj--alt + .proj { border-block-start: 0; }

.proj__head { margin-block-end: clamp(1.75rem, 4vh, 2.5rem); }

.proj__title {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .75rem 1rem;
  margin-block-end: 1rem;
}
.proj__title h3 {
  font-size: clamp(1.75rem, 3.6vw, 2.5rem);
  font-weight: 500;
  letter-spacing: -.03em;
}
.proj__ar {
  font-size: .62em;
  color: var(--ink-2);
  font-weight: 400;
  margin-inline-start: .35em;
}

.tag {
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-2);
  padding: .3rem .55rem;
  border: 1px solid var(--line-2);
  border-radius: 5px;
  white-space: nowrap;
}
.tag--live {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 5%, transparent);
}

.proj__lede {
  font-size: clamp(1.0625rem, 1.4vw, 1.1875rem);
  max-inline-size: 62ch;
  color: var(--ink);
}

.proj__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
}

.proj__side { display: flex; flex-direction: column; gap: 1.125rem; }
.proj__meta { font-size: .9375rem; color: var(--ink-2); max-inline-size: 44ch; }

.proj__link { display: flex; flex-wrap: wrap; gap: .75rem 1.5rem; }
.proj__link a {
  font-family: var(--mono);
  font-size: .8125rem;
  color: var(--accent);
  border-block-end: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  padding-block-end: 2px;
  transition: border-color .25s var(--ease);
}
.proj__link a:hover { border-block-end-color: var(--accent); }
.proj__link--big {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.125rem;
  align-self: start;
}
.proj__link--big a { font-size: 1.0625rem; }

/* chips */
.chips { display: flex; flex-wrap: wrap; gap: .4rem; }
.chips li {
  font-family: var(--mono);
  font-size: .6875rem;
  padding: .3rem .55rem;
  border: 1px solid var(--line-2);
  border-radius: 5px;
  color: var(--ink-2);
  background: var(--paper);
}

/* --------------------------------------------------------------- figures */

/* Numbers sit on the thing they measure, never in a decorative grid. */
.facts { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.facts > div { background: var(--paper); padding: 1.125rem 1.25rem; }
.proj--alt .facts > div { background: var(--paper-2); }
.facts dt {
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.facts dd {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: clamp(1.375rem, 2.4vw, 1.75rem);
  line-height: 1.2;
  margin-block-start: .4rem;
  letter-spacing: -.02em;
}
.facts dd span {
  display: block;
  font-family: var(--sans);
  /* `tabular-nums` is inherited from `.facts dd` and gives the comma a full
     digit cell, so "hand-written, reversible" rendered as
     "hand-written , reversible". The caption is prose, not a figure. */
  font-variant-numeric: normal;
  font-size: .8125rem;
  letter-spacing: 0;
  color: var(--ink-2);
  margin-block-start: .15rem;
}

/* brand card */
.brandcard {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 3vw, 2.5rem);
  padding: clamp(1.5rem, 3vw, 2rem);
  background: var(--navy);
  border-radius: 14px;
}
.brandcard__mark img { block-size: clamp(120px, 18vw, 168px); inline-size: auto; }
.swatches { display: grid; gap: .875rem; }
.swatches li { display: flex; align-items: center; gap: .9rem; }
/* Every chip is mounted on an ivory plate, the way a real paint chip is. A
   1px inner ring was not enough: the first swatch is #06244A on a #06244A
   card, so all that rendered was the ring and the palette read as
   empty box / gold box / white box. The mount is what makes a colour equal to
   its ground still legible as a sample. */
.swatches i {
  inline-size: 24px;
  block-size: 24px;
  border-radius: 5px;
  background: var(--c);
  box-shadow: 0 0 0 3px #F5F1E9, 0 0 0 4px rgb(0 0 0 / .22);
}
.swatches span {
  font-family: var(--mono);
  font-size: .6875rem;
  color: color-mix(in srgb, #F5F1E9 78%, transparent);
}

/* locales */
.locales { display: flex; flex-wrap: wrap; gap: .5rem; }
.locales span {
  font-size: .9375rem;
  padding: .4rem .7rem;
  border: 1px solid var(--line-2);
  border-radius: 7px;
  background: var(--paper);
}
.proj--alt .locales span { background: var(--paper-2); }

/* ----------------------------------------------------------- open source */

.oss {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: hidden;
  margin-block-start: clamp(2.5rem, 6vh, 4rem);
}
.oss__c {
  background: var(--paper);
  padding: clamp(1.5rem, 3vw, 2rem);
  display: flex;
  flex-direction: column;
  gap: .75rem;
  transition: background .25s var(--ease);
}
.oss__c:hover { background: var(--paper-2); }
.oss__c h3 { font-size: var(--fs-h3); font-family: var(--mono); font-weight: 500; letter-spacing: -.01em; }
.oss__c:hover h3 { color: var(--accent); }
.oss__c p { font-size: .9375rem; color: var(--ink-2); }
.oss__m {
  font-family: var(--mono);
  font-size: .75rem;
  color: var(--ink-2);
  margin-block-start: auto;
  padding-block-start: .875rem;
  border-block-start: 1px solid var(--line);
}

/* -------------------------------------------------------------- approach */

/* No top border: the section head already draws a rule directly above, and two
   hairlines a few pixels apart read as a mistake. */
.appr { display: grid; gap: 1px; background: var(--line); border-block-end: 1px solid var(--line); }
.appr__i {
  background: var(--paper);
  padding-block: clamp(1.75rem, 4vh, 2.5rem);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: clamp(1rem, 4vw, 3rem);
  align-items: start;
}
.appr__i h3 {
  font-size: var(--fs-h3);
  font-weight: 500;
  max-inline-size: 20ch;
  text-wrap: balance;
}
.appr__i p { color: var(--ink-2); max-inline-size: 56ch; }

/* ----------------------------------------------------------------- stack */

.stack { display: grid; gap: 1px; background: var(--line); border-block-end: 1px solid var(--line); }
.stack__r {
  background: var(--paper);
  display: grid;
  grid-template-columns: minmax(120px, 200px) 1fr;
  gap: clamp(1rem, 3vw, 2rem);
  align-items: start;
  padding-block: 1.375rem;
}
.stack__k {
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-2);
  padding-block-start: .35rem;
}

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

.contact { padding-block: var(--rhythm); }
.contact h2 { font-size: var(--fs-h2); font-weight: 500; }
.contact__l {
  margin-block-start: .75rem;
  color: var(--ink-2);
  font-size: clamp(1.0625rem, 1.5vw, 1.25rem);
}
.links {
  margin-block-start: clamp(2rem, 5vh, 3rem);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: hidden;
}
.links a {
  background: var(--paper);
  display: flex;
  flex-direction: column;
  gap: .4rem;
  padding: 1.25rem 1.375rem;
  block-size: 100%;
  transition: background .25s var(--ease);
}
.links a:hover { background: var(--paper-2); }
.links__k {
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.links__v { font-size: 1rem; letter-spacing: -.01em; }
.links a:hover .links__v { color: var(--accent); }

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

.ftr { border-block-start: 1px solid var(--line); padding-block: 2rem; }
.ftr__in {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: .8125rem;
  color: var(--ink-2);
}
.ftr__in a { border-block-end: 1px solid var(--line-2); padding-block-end: 2px; }
.ftr__in a:hover { color: var(--ink); border-block-end-color: var(--ink); }

/* ------------------------------------------------------ entrance motion */

/* Entrance motion is opt-IN, never opt-out. The default state above is fully
   visible, so anything that stops the animation from running — a reduced-motion
   preference, an extension, an old engine — still leaves a readable page
   rather than an empty box where the content should be. */
@media (prefers-reduced-motion: no-preference) {
  /* The headline's own entrance is the `fit` widening in the hero block above.
     These two are the frame around it and arrive after it has landed. */
  .hero .eyebrow { opacity: 0; animation: fade-in .7s var(--ease) .1s forwards; }
  .open { opacity: 0; animation: rise .8s var(--ease) .9s forwards; }
}

@keyframes fade-in { to { opacity: 1; } }
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* -------------------------------------------------- scroll-driven motion */

/* CSS scroll-driven animation — no JavaScript, no IntersectionObserver, no
   library. Gated twice on purpose: a browser without `animation-timeline`, or
   a reader who asked for less motion, never gets the hidden start state at
   all, so nothing can strand content at opacity 0. */

@supports (animation-timeline: view()) {
 @media (prefers-reduced-motion: no-preference) {
  /* `entry`, never `cover`. A `cover` range is `element height + viewport
     height` long, so an element near the end of the document never has that
     much scroll left and its progress parks partway — permanently. That is
     what left panels sitting at opacity 0.4987 and section rules frozen
     half-drawn, both at rest, both reading as bugs rather than motion.
     `entry` is bounded by `min(element height, viewport height)`, and ending
     at 55% leaves the rest of the range as slack so the last element before
     the footer still finishes. */
  .reveal {
    animation: reveal-up linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 55%;
  }

  /* The section rule draws itself along the reading direction. This lives on a
     pseudo-element and only touches `transform`, because `.s-head` is also a
     `.reveal` and two animations on one opacity fight each other. */
  .s-head::after {
    animation: draw linear both;
    animation-timeline: view();
    animation-range: entry 10% entry 65%;
  }

 }
}

@keyframes reveal-up {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: none; }
}
@keyframes draw {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* ------------------------------------------------------------ responsive */

@media (max-width: 900px) {
  .proj__grid { grid-template-columns: 1fr; }
  .appr__i { grid-template-columns: 1fr; gap: .625rem; }
  .stack__r { grid-template-columns: 1fr; gap: .625rem; }
  .proj__link--big { flex-direction: row; flex-wrap: wrap; gap: 1.5rem; align-self: start; }
}

@media (max-width: 620px) {
  .hdr__nav { gap: 1.125rem; }
  .hdr__nav a { font-size: .8125rem; }
  .hdr__id { margin-inline-end: auto; }
  .facts { grid-template-columns: 1fr; }
  .brandcard { flex-direction: column; align-items: flex-start; }
  .s-head { flex-direction: column; gap: .5rem; }
}

/* Two rows, never a hidden control. Below this width the three items cannot
   share a line — about 320px of inner width against roughly 124 for the name,
   150 for the nav and 100 for the button. The first fix here hid the button,
   which is the bug Islam reported: a control that disappears reads as broken,
   and trading the nav back for the CTA solved nothing. Wrapping costs ~36px of
   header height and loses nothing at all. */
@media (max-width: 520px) {
  .hdr__in {
    flex-wrap: wrap;
    row-gap: .625rem;
    padding-block: .625rem;
  }
  .hdr__nav {
    order: 3;
    flex-basis: 100%;
    gap: 1.25rem;
  }
  .hdr__nav a { font-size: .8125rem; }
}

/* --------------------------------------------------------- reduced motion */

/* Every entrance and scroll animation on this page already lives behind
   `prefers-reduced-motion: no-preference`, so there is nothing here to undo —
   only the things that are on by default. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition-duration: .001ms !important; }
}

/* ------------------------------------------------------------ the layers */

/* Five rows on paper, one screen. This was a 220svb pinned dark track with a
   machined part separating inside it, and it is gone: the part was a metaphor
   standing where evidence should be, and 2.2 viewports of near-black scroll
   for five list items was exactly the emptiness it was accused of. The counts
   were always the payload, and they read better as type than as drilled holes
   nobody can count at hero scale.
   Gone with it: the whole WebGL layer, both matcaps, and the #world canvas. */

.layers__l { display: grid; }
.layer {
  display: grid;
  grid-template-columns: minmax(0, 7rem) minmax(0, 1fr);
  gap: .35rem 2rem;
  align-items: baseline;
  padding-block: clamp(1rem, 2.4vh, 1.5rem);
  border-block-start: 1px solid var(--line);
}
.layer:last-child { border-block-end: 1px solid var(--line); }
/* The section head already draws a rule; a second one 58px below it reads as
   an empty first row. */
.layer:first-child { border-block-start: 0; }

.layer__k {
  font-size: clamp(1.5rem, 3.4vw, 2.25rem);
  font-weight: 760;
  font-stretch: 100%;
  letter-spacing: -.02em;
}
.layer__v {
  font-family: var(--mono);
  font-size: .875rem;
  line-height: 1.6;
  color: var(--ink-2);
}

@media (max-width: 560px) {
  .layer { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------- gallery */

/* Five real screens. The horizontal scroll is deliberate: it keeps the product
   at a readable size instead of shrinking five phones into thumbnails. */
.gallery {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(190px, 46vw);
  gap: clamp(.75rem, 1.6vw, 1.25rem);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  padding-block-end: 1rem;
  margin-block-end: clamp(1.5rem, 4vh, 2.5rem);
  scrollbar-width: thin;
}
.gallery figure { margin: 0; scroll-snap-align: start; }
.gallery img {
  inline-size: 100%;
  border-radius: 16px;
  border: 1px solid var(--line-2);
  background: var(--paper-3);
  box-shadow: 0 1px 2px rgb(0 0 0 / .04), 0 14px 32px -18px rgb(0 0 0 / .2);
}
.gallery figcaption {
  margin-block-start: .625rem;
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* ----------------------------------------------------------------- ledger */

/* Horus has no screenshots and no link, so what it shows is the parts list. */
.ledger {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  counter-reset: layer;
}
.ledger li {
  background: var(--paper);
  display: grid;
  grid-template-columns: minmax(84px, 110px) 1fr;
  gap: 1rem;
  align-items: baseline;
  padding: .875rem 1.125rem;
}
.proj--alt .ledger li { background: var(--paper-2); }
.ledger__k {
  font-family: var(--mono);
  font-size: var(--fs-meta);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.ledger__v { font-size: .9375rem; }

@media (max-width: 620px) {
  .ledger li { grid-template-columns: 1fr; gap: .2rem; }
}

/* The radial paper scrim that used to live here is gone. It was an admission
   that the composition had never been authored — geometry drifted into the
   reading column, so a gradient was painted over the top of it. It also
   carried `inset: -6% -10%`, which put 10% of latent horizontal overflow on
   every viewport, invisible only because `html` clips. Both problems end with
   the scissor rectangles. */
