/* italo — two values. See DESIGN.md.
   Greys exist only as halftone dots, never as flat fills. */

:root {
  --ink:   #000000;
  --paper: #FFFFFF;

  /* The angle. Still governs section seams, image crops and motion elsewhere;
     the wordmark itself no longer carries a slice. */
  --cut: 9deg;

  --boot-bg: #000;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: Archivo, system-ui, sans-serif;
  font-weight: 400;
  /* Width only. Naming "wght" here would override every font-weight on the
     page, because font-variation-settings beats font-weight and inherits. */
  font-variation-settings: "wdth" 100;
}

body.intro-running { overflow: hidden; }
/* iOS only honours the lock on the root. The same while a section's run is
   driving the scroll on a touch screen, so the swipe's momentum cannot
   fight it (script sets that class on touch screens only). */
html.intro-running,
html.is-auto-running { overflow: hidden; }

/* ============================================================
   BEAT 1 — loading screen
   Pure CSS so it paints instantly and costs no bytes. The bar is
   driven by the real download, paced so it reads as a 90s load
   rather than finishing before the eye catches it.
   ============================================================ */

#boot {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--boot-bg);
  color: var(--paper);
  display: grid;
  place-items: center;
  transition: opacity 220ms linear;
}
#boot.is-gone { opacity: 0; pointer-events: none; }

.boot-inner {
  display: grid;
  justify-items: center;
  gap: 18px;
  width: min(420px, 74vw);
}

.boot-ball {
  width: 54px;
  height: 54px;
  animation: roll 1.4s linear infinite;
}
.boot-8 {
  font-family: Montserrat, Archivo, system-ui, sans-serif;
  font-size: 24px;
  font-weight: 800;
  fill: #fff;
  text-anchor: middle;
  dominant-baseline: central;
}
@keyframes roll { to { transform: rotate(360deg); } }

.boot-label {
  margin: 0;
  font-family: ui-monospace, "Courier New", monospace;
  font-size: 12px;
  letter-spacing: 0.34em;
  text-indent: 0.34em;      /* absorb the trailing letter-space so it stays centred */
}

/* Chunky segmented bar — the low-fidelity tell */
.boot-bar {
  width: 100%;
  height: 14px;
  border: 2px solid var(--paper);
  padding: 2px;
}
.boot-bar-fill {
  height: 100%;
  width: 0%;
  background-image: repeating-linear-gradient(
    to right, var(--paper) 0 8px, transparent 8px 12px
  );
  /* Steps, not a glide — the bar should look like it is chewing through data. */
  transition: width 180ms steps(4, end);
}

.boot-pct {
  margin: 0;
  font-family: ui-monospace, "Courier New", monospace;
  font-size: 11px;
  letter-spacing: 0.2em;
}

.scanlines {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom, rgba(0,0,0,0) 0 2px, rgba(0,0,0,0.30) 2px 4px
  );
  mix-blend-mode: multiply;
}

/* ============================================================
   HERO
   ============================================================ */

.hero {
  position: relative;
  height: 100svh;
  min-height: 420px;
  background: var(--paper);
  overflow: hidden;
  display: grid;
  place-items: center;
}

.stage { position: absolute; inset: 0; z-index: 1; background: #000; }
.stage.is-gone { display: none; }

/* The particle layer. Fixed to the viewport, because the same points are the
   globe in the hero and the about copy in the next section. */
.globe {
  position: fixed;
  inset: 0;
  /* Above every section: the dots cross out of one and into the next, and
     they have to be seen doing it. */
  z-index: 7;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
}
.globe.is-live { opacity: 1; }
/* Only while the copy is handing itself over to the next section's heading.
   `difference` against white gives black on paper and white on ink, so a dot
   stays correct as it crosses out of one section and into the other without
   ever being a third value. The copy itself needs no such thing: the band
   covers it rather than passing under it. */
.globe.is-inverting { mix-blend-mode: difference; }

.clip, .freeze {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}
.clip.is-live { opacity: 1; }

.freeze { image-rendering: pixelated; }
.freeze.is-live { opacity: 1; }

/* ============================================================
   BEAT 4 — the wordmark
   Montserrat ExtraBold Italic — the actual typeface the logo was set in,
   read straight out of the embedded font in wordmark-logo.eps, where the
   subset encodes exactly a/i/l/o/t. Its italic carries the single-storey `a`.
   Real text, so it stays crisp, selectable and weightless.
   The logo is a mark, not body copy — the site's type system stays Archivo.
   ============================================================ */

/* Positioned entirely by JS transform, in both the centred and docked states,
   so the move to the corner is one interpolation and never a layout change. */
.mark-wrap {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 20;
  opacity: 0;
  transform-origin: top left;
  will-change: transform;
}
.mark-wrap.is-live { opacity: 1; }
.mark-wrap.is-docked .mark { cursor: pointer; }

.mark {
  position: relative;
  display: flex;
  align-items: flex-end;
  white-space: nowrap;
  color: var(--ink);
  text-decoration: none;
  font-family: Montserrat, Archivo, system-ui, sans-serif;
  font-size: clamp(64px, 17vw, 260px);
  line-height: 0.80;
  font-style: italic;
  font-weight: 800;          /* ExtraBold, as in the source file */
  letter-spacing: -0.005em;
  /* The planet passes behind the name, so the mark carries its own clearance.
     paint-order puts the stroke behind the fill, giving a halo rather than an
     outline. The name has to win over the decoration. */
  -webkit-text-stroke: 0.07em var(--paper);
  paint-order: stroke fill;
}

.mark-word {
  display: block;
  will-change: transform, opacity;
}

.mark-ball {
  display: block;
  width: 0.72em;
  height: 0.72em;
  margin-left: 0.02em;
  margin-bottom: 0.02em;
  flex: none;
}
.mark-ball svg { width: 100%; height: 100%; display: block; }
.mark-8 {
  font-family: Montserrat, Archivo, system-ui, sans-serif;
  font-size: 24px;
  font-weight: 800;
  font-style: normal;
  fill: currentColor;
  text-anchor: middle;
  dominant-baseline: central;
}

/* ============================================================
   Controls — the intro is never a gate, and always repeatable
   ============================================================ */

.skip {
  position: absolute;
  z-index: 40;
  right: 16px;
  bottom: 16px;
  padding: 10px 14px;
  border: 2px solid var(--paper);
  background: transparent;
  color: var(--paper);
  font-family: ui-monospace, "Courier New", monospace;
  font-size: 11px;
  letter-spacing: 0.18em;
  cursor: pointer;
}
.skip:hover { background: var(--paper); color: var(--ink); }
.skip:focus-visible { outline: 3px solid var(--paper); outline-offset: 3px; }
.skip.is-gone { display: none; }

.rewatch {
  position: absolute;
  z-index: 40;
  right: 16px;
  bottom: 16px;
  transition: transform 460ms cubic-bezier(.16,.84,.26,1), opacity 260ms linear;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 14px 9px 10px;
  border: 2px solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  font-family: ui-monospace, "Courier New", monospace;
  font-size: 11px;
  letter-spacing: 0.16em;
  cursor: pointer;
}
.rewatch[hidden] { display: none; }
.rewatch-ball { width: 15px; height: 15px; display: block; }
.rewatch-ball svg { width: 100%; height: 100%; display: block; }
.rewatch:hover { background: var(--ink); color: var(--paper); }
.rewatch:hover .rewatch-ball svg circle:last-of-type { fill: var(--ink); }
.rewatch:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }

/* ---- hero chrome leaves with the hero ----------------------------------
   The replay button and the onward hint belong to the title card, not to
   the site. The moment the globe starts coming apart they go — upward,
   against the scroll, so the exit reads as a consequence of scrolling down
   rather than as something switching off. */
.rewatch.is-away,
.onward.is-away {
  opacity: 0;
  pointer-events: none;
}
.rewatch.is-away { transform: translateY(-180px); }
.onward.is-away  { transform: translateX(-50%) translateY(-180px); }

/* ---- onward hint -------------------------------------------------------
   Deliberately faint: a footnote at the bottom of the frame, not a call to
   action competing with the mark. */
.onward {
  position: absolute;
  z-index: 40;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  gap: 8px;
  padding: 6px 12px;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-family: ui-monospace, "Courier New", monospace;
  font-size: 10px;
  letter-spacing: 0.22em;
  text-indent: 0.22em;      /* absorb the trailing letter-space, stay centred */
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0.5;
  transition: transform 460ms cubic-bezier(.16,.84,.26,1), opacity 260ms linear;
}
.onward[hidden] { display: none; }
.onward:hover { opacity: 1; }
.onward:focus-visible {
  opacity: 1;
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

/* Centred, the hint runs straight into the replay button on a phone. Lift it
   clear rather than shortening it — the label is the whole point of it. */
@media (max-width: 620px) {
  .onward { bottom: 68px; }
}

/* The same hint, at the foot of the sections after the hero: the 8 ball
   points on to the projects, and the projects on to the contact page. They
   belong to their section rather than to the window, so they scroll away
   with it. */
.onward-next { bottom: 18px; }
.projects .onward-next { color: var(--paper); z-index: 30; }
.projects .onward-next:focus-visible { outline-color: var(--paper); }
/* On a phone nothing pins, so each one simply follows its section's content. */
@media (max-width: 760px) {
  .onward-next {
    position: static;
    transform: none;
    margin: 28px auto 0;
  }
  .projects .onward-next { margin-bottom: 28px; }
}

.onward-arrow {
  display: block;
  width: 12px;
  height: 24px;
  /* One hairline. Scaling the SVG would thicken it, so the box is drawn at
     the size the viewBox is authored in. */
  animation: nudge 2.6s ease-in-out infinite;
}
@keyframes nudge {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(4px); }
}

/* ============================================================
   Below the fold
   ============================================================ */

/* ============================================================
   ABOUT
   The copy is real type. The particles do not stand in for it — they
   deliver it: each dot flies from the globe to the glyph it is about to
   become, and the type is uncovered behind the wavefront as they land.
   Without JS the paragraph simply stays in normal flow and reads.
   ============================================================ */

/* The first half of the section. Without script it is a screen with the copy
   in it; with script the copy is fixed and layoutAbout() sizes this block to
   a full screen (more if the copy needs it), so the 8 ball follows straight
   on below the greeting without peeking in on arrival. */
.about-intro {
  min-height: 100svh;
  display: grid;
  align-items: center;
  padding: 10svh 6vw;
}
.about-intro.is-sized { min-height: 0; padding: 0; }

.about-copy {
  max-width: min(46ch, 44vw);
}

/* Pinned by JS, which also owns the line breaks — the canvas samples this
   exact layout, so the dots and the letters share one set of coordinates. */
.about-copy.is-pinned {
  position: fixed;
  z-index: 3;
  max-width: none;
  margin: 0;
  pointer-events: none;
}
/* Selectable only once it is fully written; before that it is a clipped
   overlay sitting over the hero and must not swallow the cursor. */
.about-copy.is-pinned.is-written { pointer-events: auto; }

.about-copy.is-pinned .about-body { max-width: none; }

/* One element per line: that is the unit the reveal clips against. */
.about-copy.is-pinned .ln {
  display: block;
  white-space: pre;
  clip-path: inset(0 100% 0 0);
  will-change: clip-path;
}
/* The paragraph breaks, put back. Script sets the copy line by line so the
   particles have something to land on, which flattens the paragraphs; this is
   where they go. */
.about-copy.is-pinned .ln.is-para { margin-top: 0.8em; }
/* Zero-height marker sitting on the baseline, read back by JS so the canvas
   draws where the browser actually put the type rather than where font
   metrics suggest it should be. */
.about-copy.is-pinned .bl {
  display: inline-block;
  width: 0;
  height: 0;
  overflow: hidden;
  vertical-align: baseline;
}

.about-hey {
  margin: 0 0 0.35em;
  font-family: Montserrat, Archivo, system-ui, sans-serif;
  font-style: italic;
  font-weight: 800;
  /* Sized to the line it actually holds: at the old scale
     "Hi, I'm Italo." overran the column it shares with the diorama. */
  font-size: clamp(34px, 4.4vw, 66px);
  line-height: 0.95;
}

.about-body {
  margin: 0;
  font-size: clamp(17px, 1.5vw, 22px);
  line-height: 1.55;
  max-width: 42ch;
}

/* ---- the diorama -------------------------------------------------------
   Pinned like the copy, into the empty half the copy leaves. Its left edge
   is derived from the same numbers `layoutAbout()` uses for the column, so
   the two can never collide as the window changes. No border, no card, no
   shadow: the render is already on white, so it sits straight on the paper
   and the cut-away cube reads as the frame. */
.about-scene {
  position: fixed;
  /* `left` and `width` are set by layoutAbout(), from the same measurements
     that place the copy column — the pair is centred as a unit, so neither
     block is anchored to a window edge. */
  top: 50%;
  translate: 0 -50%;
  z-index: 2;
  display: grid;
  place-items: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 600ms linear;
}
/* It arrives once the copy has finished writing, so the two do not compete
   for attention while the particles are still moving. */
.about-scene.is-lit { opacity: 1; }

/* A single unbroken take, so there is nothing to stack or dissolve here —
   the fade belongs to `.about-scene`, which brings the whole diorama in once
   the copy has finished writing. */
.scene-vid {
  grid-area: 1 / 1;
  width: 100%;
  aspect-ratio: 1 / 1;
  display: block;
  /* The diorama's own pixels are the artwork — let them stay pixels rather
     than smoothing them into a modern render on a high-DPI screen. */
  image-rendering: pixelated;
}

@media (max-width: 760px) {
  .about-copy { max-width: none; }
  /* The copy takes the full column here, so there is nowhere for the scene
     to go. It is decoration — dropping it costs a phone nothing and saves it
     the whole download. */
  .about-scene { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* The poster still carries the whole idea; the loop is what it cannot have. */
  .about-scene { transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  .boot-ball { animation: none; }
  .onward-arrow { animation: none; }
  .rewatch, .onward { transition: opacity 160ms linear; }
  .rewatch.is-away { transform: none; }
  .onward.is-away { transform: translateX(-50%); }
}

/* ============================================================
   PROJECTS — the ink band
   The palette always said one was coming. Everything here is two
   values: the band is ink, the type is paper, and every mid-tone
   in every photograph is 1-bit dots rather than a grey fill.
   ============================================================ */

.projects {
  background: var(--ink);
  color: var(--paper);
  position: relative;
  /* Over the introduction, which is `position: fixed` and would otherwise sit
     on top of this. The band is a layer that slides across the copy and the
     diorama and covers them — a clean edge, with nothing underneath fading or
     ghosting through it. Only the particle layer stays above, because the
     dots have to be seen landing on this section's own heading. */
  z-index: 5;
}

/* On a phone this is a list and nothing more: it takes the height it needs,
   it does not pin, and above all it does not clip — a fixed-height pin with
   `overflow: hidden` around a twelve-item list shows you the first screenful
   and silently eats the rest. */
.proj-pin { position: relative; }

@media (min-width: 761px) {
  /* One screen, no more. The approach is driven automatically now, so the
     extra scroll room that used to carry it would just be dead travel at the
     foot of the page with the band already full and nothing left to change. */
  .projects { min-height: 100svh; }

  /* Sticky rather than fixed-and-measured: the browser does the pinning, so
     there is no scroll arithmetic to drift out of step with the canvas. */
  .proj-pin {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: hidden;
  }
}

.proj-title {
  position: absolute;
  top: clamp(46px, 8svh, 96px);
  left: 0;
  right: 0;
  margin: 0;
  text-align: center;
  z-index: 2;
  font-weight: 800;
  font-style: italic;
  font-variation-settings: "wdth" 118;
  font-size: clamp(38px, 7.4vw, 104px);
  letter-spacing: -0.02em;
  line-height: 1;
  pointer-events: none;
}

/* The glyphs are uncovered behind the wavefront of landing dots, the same way
   the about copy is — never faded in as a block. The clip itself is set by
   script, so a heading that script never reaches stays readable. */
.proj-title .ln {
  display: inline-block;
  will-change: clip-path;
}
/* Zero-height marker riding the baseline, read back by JS so the dots land on
   the type the browser actually drew rather than on a guess at its metrics. */
.proj-title .bl {
  display: inline-block;
  width: 0;
  height: 0;
  overflow: hidden;
  vertical-align: baseline;
}

/* The mark the ring turns around. Hidden until script takes over, because in
   the plain list there is no ring for it to be the middle of; script sizes it
   from the ring so the two stay in proportion at any window size. */
.proj-ball {
  position: absolute;
  display: none;
  aspect-ratio: 1;
  translate: -50% -50%;
  /* Above the ring, below a hovered post — nothing should reach the middle,
     but if a grown post ever does, the one being pointed at wins. */
  z-index: 25;
  pointer-events: none;
}
.projects.is-live .proj-ball { display: block; }
.proj-ball svg { width: 100%; height: 100%; display: block; }

/* ---- the list, before script touches it -------------------------------
   This is what a phone gets, and what anyone with no JS gets: images, names
   and notes, in reading order. */
.constellation {
  list-style: none;
  margin: 0;
  padding: 16svh max(22px, 6vw) 12svh;
  display: grid;
  gap: clamp(34px, 5vw, 64px);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}

.post-hit {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.post-frame {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--ink);
  overflow: hidden;
}
.post[data-ratio="4/5"] .post-frame { aspect-ratio: 4 / 5; }
.post[data-ratio="1/1"] .post-frame { aspect-ratio: 1 / 1; }

.post-img,
.post-dots {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The dithered plate is the resting state. Script draws it; until it has,
   the photograph itself is what shows, so nothing is ever a blank hole. */
.post-dots {
  opacity: 0;
  /* One canvas pixel per screen pixel, parked on whole pixels, repainted each
     frame — so it is never scaled and never resampled. Script sets its size;
     the 100% below is only for the state before script has. */
  image-rendering: pixelated;
  z-index: 1;
}

.post-label {
  display: block;
  margin-top: 12px;
  font-size: 15px;
}
.post-name { font-weight: 700; }
.post-kind { opacity: 0.62; }

.post-note {
  margin: 8px 0 0;
  max-width: 44ch;
  font-size: 15px;
  line-height: 1.5;
  opacity: 0.72;
}

/* ---- the constellation, once script owns it ---------------------------- */

@media (min-width: 761px) {
  .projects.is-live .constellation {
    position: absolute;
    inset: 0;
    display: block;
    padding: 0;
  }

  .projects.is-live .post {
    position: absolute;
    top: 0;
    left: 0;
    width: var(--w, 280px);
    /* Script writes translate and scale; the transition is only on scale and
       filter, so the orbit itself stays a per-frame value and never fights a
       CSS tween. */
    will-change: transform;
  }

  .projects.is-live .post-hit {
    transition: scale 420ms cubic-bezier(.22,.61,.36,1);
    scale: 1;
  }

  .projects.is-live .post-frame {
    background: transparent;
    /* Stated height rather than a ratio, so the plate inside is never scaled.
       See the note in placePosts. */
    height: var(--h);
    aspect-ratio: auto;
  }

  .projects.is-live .post-dots { opacity: 1; }

  /* Colour is what hovering buys you. Everything else stays 1-bit. */
  .projects.is-live .post-img {
    opacity: 0;
    transition: opacity 320ms linear;
  }

  .projects.is-live .post-label {
    position: absolute;
    left: 0;
    top: calc(100% + 10px);
    margin: 0;
    opacity: 0;
    transition: opacity 240ms linear;
    pointer-events: none;
    white-space: nowrap;
  }

  .projects.is-live .post-note { display: none; }

  /* The one you are pointing at comes forward, grows, and develops. */
  .projects.is-live .post.is-near .post-hit { scale: 1.12; }
  .projects.is-live .post.is-near .post-img,
  .projects.is-live .post.is-near .post-label { opacity: 1; }
  .projects.is-live .post.is-near .post-dots { opacity: 0; }

  /* And the rest fall back — the dots thin out rather than a grey wash
     going over them. */
  .projects.is-live .constellation.has-near .post:not(.is-near) .post-dots {
    /* Far enough back that the one you are pointing at is unmistakably in
       front, near enough that the rest are still a constellation rather than
       a black hole with one picture in it. */
    opacity: 0.46;
  }
  .projects.is-live .post-dots,
  .projects.is-live .post-hit:focus-visible { transition: opacity 320ms linear; }

  .projects.is-live .post-hit:focus-visible {
    outline: 2px solid var(--paper);
    outline-offset: 6px;
  }
}

/* ---- the opened post --------------------------------------------------- */

.sheet {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: max(20px, 4vh) max(20px, 4vw);
}
.sheet[hidden] { display: none; }

.sheet-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.88);
}

.sheet-panel {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(280px, 1fr);
  gap: clamp(20px, 3vw, 44px);
  align-items: start;
  width: min(1180px, 100%);
  max-height: 88svh;
  padding: clamp(18px, 2.4vw, 32px);
  background: var(--ink);
  border: 1px solid rgba(255, 255, 255, 0.24);
  color: var(--paper);
}

.sheet-media {
  min-width: 0;
  background: var(--ink);
}
/* ---- the deck: one piece, or several you page through -------------------
   The frame takes the shape of the first item — the way a carousel post
   does — capped to the screen, so it never jumps as you move through it.
   Anything of another shape sits inside that frame on ink. */
/* The deck is as wide as its first item is at full height, and no wider, so
   the arrows sit on the edges of the picture rather than out in the ink
   beside a portrait film. */
.deck {
  --r: 1.7778;
  --cap: 74svh;
  width: min(100%, calc(var(--cap) * var(--r)));
  margin-inline: auto;
}

.deck-stage {
  position: relative;
  aspect-ratio: var(--r);
}

.deck-track {
  display: flex;
  height: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.deck-track::-webkit-scrollbar { display: none; }

/* Flex rather than grid: a grid row sized to its content leaves a film's
   max-height: 100% with nothing definite to resolve against, and a portrait
   film then runs straight out of the bottom of the frame. */
.deck-slide {
  flex: 0 0 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  overflow: hidden;
}

.deck-slide img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  user-select: none;
}

/* A film is sized by its own shape rather than stretched to the slide.
   `width: 100%` on a portrait film leaves the element as wide as the panel
   with the picture letterboxed inside it — and the control bar then runs
   the full width of the box instead of the width of the film, which reads
   as a player that has lost its video. */
.deck-slide video {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  background: var(--ink);
}

/* Square plates on the edges of the frame, the same furniture as the rest of
   the site: ink with a hairline, inverting under the pointer. Centred on the
   height so they never sit on a film's control bar. */
.deck-arrow {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  z-index: 2;
  width: 40px;
  height: 52px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.5);
  background: var(--ink);
  color: var(--paper);
  cursor: pointer;
  transition: background-color 160ms linear, color 160ms linear;
}
.deck-arrow[hidden] { display: none; }
.deck-arrow.is-prev { left: 0; }
.deck-arrow.is-next { right: 0; }
.deck-arrow svg { width: 10px; height: 20px; }
.deck-arrow:focus-visible { outline: 2px solid var(--paper); outline-offset: 2px; }
@media (hover: hover) {
  .deck-arrow:hover { background: var(--paper); color: var(--ink); }
}

.deck-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 14px;
}

.deck-dots {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  min-width: 0;
}
/* The hit area is the button; the visible square is its pseudo-element, so
   a 7px mark still gets a finger-sized target. */
.deck-dot {
  width: 20px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  display: grid;
  place-items: center;
}
.deck-dot::before {
  content: "";
  width: 7px;
  height: 7px;
  border: 1px solid var(--paper);
  opacity: 0.55;
}
.deck-dot[aria-current="true"]::before { background: var(--paper); opacity: 1; }
.deck-dot:focus-visible { outline: 2px solid var(--paper); outline-offset: -2px; }

.deck-count {
  flex: none;
  margin: 0;
  font-family: ui-monospace, "Courier New", monospace;
  font-size: 11px;
  letter-spacing: 0.16em;
  opacity: 0.72;
}

.sheet-copy { min-width: 0; }

.sheet-kind {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 700;
  opacity: 0.6;
}

.sheet-name {
  margin: 0 0 14px;
  font-size: clamp(24px, 2.6vw, 38px);
  font-weight: 800;
  font-style: italic;
  font-variation-settings: "wdth" 112;
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.sheet-note {
  margin: 0;
  font-size: 16px;
  line-height: 1.6;
  opacity: 0.84;
}
/* A stand-in, and it should look like one until the real words arrive. */
.sheet-note.is-placeholder {
  font-family: ui-monospace, "Courier New", monospace;
  font-size: 12px;
  letter-spacing: 0.08em;
  opacity: 0.55;
}

.sheet-links {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  display: grid;
  gap: 10px;
}
.sheet-links[hidden] { display: none; }
.sheet-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border: 1px solid var(--paper);
  color: var(--paper);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: background-color 160ms linear, color 160ms linear;
}
.sheet-link svg { width: 10px; height: 10px; }
.sheet-link:focus-visible { outline: 2px solid var(--paper); outline-offset: 3px; }
@media (hover: hover) {
  .sheet-link:hover { background: var(--paper); color: var(--ink); }
}

.sheet-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--paper);
  cursor: pointer;
}
.sheet-close svg { width: 18px; height: 18px; }
.sheet-close:focus-visible { outline: 2px solid var(--paper); outline-offset: 2px; }

@media (max-width: 860px) {
  .sheet-panel {
    grid-template-columns: 1fr;
    overflow-y: auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The constellation holds still. It is still readable, still hoverable,
     still clickable — it simply stops orbiting. */
  .projects.is-live .post-hit { transition: none; }
}


/* ============================================================
   CONNECT — the page's foot
   Back onto paper. It is the quietest thing on the site on
   purpose: one line, two links, and nothing else competing
   with the only two words that are an instruction.
   ============================================================ */

.connect {
  position: relative;
  /* The field is laid out in whole cells, so it overhangs the section by up
     to one of them. */
  overflow: hidden;
  /* Over the ink band, under the particle layer — the same arrangement the
     band has over the introduction. It slides across and covers it with a
     clean edge, and nothing underneath has to fade to make room. */
  z-index: 6;
  min-height: 100svh;
  background: var(--paper);
  color: var(--ink);
  display: grid;
  place-items: center;
  padding: 12svh max(22px, 6vw);
}

/* Drawn at one canvas pixel per dot and scaled up in whole numbers, so the
   dots stay dots rather than being resampled into grey — the same reason the
   post plates are 1:1. */
.connect-field {
  position: absolute;
  top: 0;
  left: 0;
  image-rendering: pixelated;
  pointer-events: none;
}

.connect-inner {
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: center;
  gap: clamp(30px, 4.6svh, 56px);
  text-align: center;
}

/* Half a step under the line above it, not a second heading. */
.connect-sub {
  margin: -0.35em 0 0;
  font-size: clamp(16px, 1.6vw, 24px);
  font-weight: 600;
  font-style: italic;
  font-variation-settings: "wdth" 108;
}


.connect-tagline {
  margin: 0;
  font-weight: 800;
  font-style: italic;
  font-variation-settings: "wdth" 118;
  font-size: clamp(30px, 5.2vw, 76px);
  letter-spacing: -0.02em;
  line-height: 1;
}
.connect-tagline .ln {
  display: inline-block;
  will-change: clip-path;
}
/* Zero-height marker riding the baseline, read back by JS so the dots land on
   the type the browser actually drew rather than on a guess at its metrics. */
.connect-tagline .bl {
  display: inline-block;
  width: 0;
  height: 0;
  overflow: hidden;
  vertical-align: baseline;
}

.connect-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(22px, 4vw, 56px);
}

.connect-link {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  color: inherit;
  text-decoration: none;
  font-size: clamp(14px, 1.15vw, 17px);
  font-weight: 600;
  padding: 6px 2px;
}

/* Drawn, not filled: at this weight the two marks sit at the same visual
   density as the type beside them, which a solid glyph would not. */
.connect-icon svg {
  display: block;
  width: clamp(20px, 1.7vw, 25px);
  height: auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: square;
  stroke-linejoin: miter;
  overflow: visible;
}
.connect-icon .connect-dot { fill: currentColor; stroke: none; }

/* The rule draws itself in from the leading edge on hover — the same move the
   copy uses, rather than an underline switching on. */
.connect-label {
  position: relative;
  padding-bottom: 3px;
}
.connect-label::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 260ms cubic-bezier(.22,.61,.36,1);
}
.connect-link:hover .connect-label::after,
.connect-link:focus-visible .connect-label::after { transform: scaleX(1); }
.connect-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* The links arrive after the line that introduces them has finished being
   written, never alongside it. Script sets `is-open` from the same hand-over
   the tagline is drawn by; with no script they are simply here. */
.connect.is-live .connect-links {
  opacity: 0;
  translate: 0 14px;
  transition: opacity 420ms linear, translate 420ms cubic-bezier(.22,.61,.36,1);
}
.connect.is-live.is-open .connect-links {
  opacity: 1;
  translate: 0 0;
}

/* ---- back to top -------------------------------------------------------
   The hero's replay button, at the other end of the page and in the other
   corner: same plate, same border, same mono label, so the two controls read
   as one pair bracketing the site rather than as two unrelated widgets. It
   sits above the field and is opaque, and the field is held clear of it as
   well — a dot landing against that border is the one thing that would make
   it look pasted on. */
.totop {
  position: absolute;
  z-index: 2;
  right: max(16px, 2.2vw);
  bottom: max(16px, 2.2vw);
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 14px 9px 11px;
  border: 2px solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  font-family: ui-monospace, "Courier New", monospace;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-indent: 0.16em;        /* absorb the trailing letter-space */
  text-transform: uppercase;
  cursor: pointer;
  transition: transform 620ms cubic-bezier(.16,.84,.26,1),
              background-color 160ms linear, color 160ms linear;
}
.totop:hover { background: var(--ink); color: var(--paper); }
.totop:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }

/* Armed by script: parked below the edge of the section — which is the bottom
   of the screen by the time this section is the one you are standing in — and
   brought up once you are actually here. The section clips, so nothing of it
   shows and nothing of it can be scrolled to while it waits. */
.totop.is-armed { transform: translateY(calc(100% + max(16px, 2.2vw))); }
.totop.is-armed.is-up { transform: translateY(0); }

.totop-arrow {
  display: block;
  width: 12px;
  height: 18px;
  flex: none;
}

@media (prefers-reduced-motion: reduce) {
  .connect.is-live .connect-links { transition: none; }
  .connect-label::after { transition: none; }
  /* No slide: it is either waiting or it is here. */
  .totop { transition: background-color 160ms linear, color 160ms linear; }
}


/* ============================================================
   THE 8 BALL — the second half of the introduction
   Part of the about section, directly under the copy: the picture
   sits under the copy column and the aside under the diorama, so
   the pair reads as one piece rather than as two screens.
   ============================================================ */

.eight {
  position: relative;
  color: var(--ink);
  /* Room under the last line for the hint to the projects, close enough that
     it is in view while you finish reading. Leaving the section for the ink
     band is an automatic run, cued by scrolling past the end of this block,
     so this is also the room to finish reading without setting it off. */
  padding: 2svh max(22px, 4vw) max(130px, 17svh);
}

.eight-inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(30px, 4vw, 76px);
  align-items: center;
  width: min(1180px, 100%);
  margin: 0 auto;
}
/* On a desktop, lined up to the same outer edges as the copy and diorama
   above it — layoutAbout() hands over the numbers it placed them with. */
@media (min-width: 761px) {
  .about.is-sized .eight { padding-inline: 0; }
  .about.is-sized .eight-inner {
    width: var(--intro-w);
    margin-left: var(--intro-x);
  }
}
/* And the same gap between picture and words as between copy and diorama,
   where there are two columns to hold one. */
@media (min-width: 861px) {
  .about.is-sized .eight-inner { column-gap: var(--intro-gap); }
}

/* Written before the picture and placed after it: the heading is the thing to
   read first whatever the width, but on a desktop it belongs on the right,
   under the diorama it answers. */
.eight-copy { grid-column: 2; grid-row: 1; }
.eight-figure { grid-column: 1; grid-row: 1; margin: 0; }

/* Set exactly as the first half is — the same heading and the same body —
   so the section reads as one voice rather than a headline and a footnote. */
.eight-title {
  margin: 0 0 0.35em;
  font-family: Montserrat, Archivo, system-ui, sans-serif;
  font-style: italic;
  font-weight: 800;
  font-size: clamp(34px, 4.4vw, 66px);
  line-height: 0.95;
}

.eight-copy p {
  margin: 0 0 0.8em;
  font-size: clamp(17px, 1.5vw, 22px);
  line-height: 1.55;
  max-width: 42ch;
}
.eight-copy p:last-child { margin-bottom: 0; }

/* Set line by line by script on a desktop, as the greeting is, so the dots
   that carry this copy up into the band have one line at a time to take
   apart. The breaks are the ones the browser would have made. */
.eight-copy.is-lined .ln {
  display: block;
  white-space: pre;
  will-change: clip-path;
}
.eight-copy.is-lined .bl {
  display: inline-block;
  width: 0;
  height: 0;
  overflow: hidden;
  vertical-align: baseline;
}

.eight-figure { position: relative; }
/* The one photograph left in colour: this is the picture of you. */
.eight-src {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}

@media (max-width: 860px) {
  .eight-inner { grid-template-columns: 1fr; }
  .eight-copy, .eight-figure { grid-column: 1; }
  .eight-copy { grid-row: 1; }
  .eight-figure { grid-row: 2; max-width: 420px; }
}

/* On a phone the deck and the words share one scrolling column; keep the
   frame short enough that the title is still in view beneath it. */
@media (max-width: 860px) {
  .deck { --cap: 60svh; }
  /* The close button gets its own row rather than sitting on the picture. */
  .sheet-panel { padding-top: 56px; }
}

/* A long deck on a narrow screen: the count says it, a row of seventeen
   squares wrapping onto a second line does not. */
@media (max-width: 600px) {
  .deck.is-long .deck-dots { display: none; }
  .deck.is-long .deck-foot { justify-content: flex-end; }
}


/* ============================================================
   PHONES
   Everything here is phone-only: the desktop layout above is left
   exactly as it is. A phone gets the same site at a phone's scale —
   type a step down, the picture held to a size that leaves the words
   in view, and the projects as a grid of thumbnails you tap to open
   rather than a column of full-width posters and essays.
   ============================================================ */
@media (max-width: 760px) {
  /* The introduction and the 8 ball, set as one voice at phone size. */
  .about-hey,
  .eight-title { font-size: 30px; }
  .about-body,
  .eight-copy p { font-size: 15px; }

  .eight-figure {
    max-width: 280px;
    margin-inline: auto;
  }

  /* ---- projects: a grid of thumbnails ------------------------------- */
  /* The heading holds the top of the screen while the grid scrolls under
     it, on a strip of the band's own black. It is what the dust writes on
     the way in and what it leaves from on the way to the contact page, so
     it has to be on screen at both ends of a long list — not scrolled away
     with its first row. */
  .proj-title {
    position: sticky;
    top: 0;
    padding: 54px 0 14px;
    background: var(--ink);
    font-size: 34px;
    z-index: 3;
  }

  .constellation {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 24px 14px;
    padding: 18px 18px 8px;
  }
  /* One cell shape for every post, so the grid reads as rows. The picture
     keeps its own shape inside it: a slide sits letterboxed and a portrait
     pillarboxed, on ink the same black as the band, so the bars disappear
     into it and nothing is cropped. */
  .post-frame,
  .post[data-ratio] .post-frame { aspect-ratio: 1 / 1; }
  .post-img { object-fit: contain; }

  .post-label {
    margin-top: 8px;
    font-size: 13px;
    line-height: 1.3;
  }
  .post-name,
  .post-kind { display: block; }
  .post-kind { margin-top: 2px; font-size: 12px; }
  /* The write-up is in the sheet a tap opens; in the grid it only made
     every post a screen long. */
  .post-note { display: none; }

  /* ---- the rest, a step down ----------------------------------------- */
  .connect-tagline { font-size: 30px; }
  .sheet { padding: 14px; }
  .sheet-panel { max-height: 92svh; padding: 48px 16px 18px; }
  .sheet-name { font-size: 22px; }
  .sheet-note { font-size: 15px; }
}
