/* =============================================================================
   base.css — Studio Suratan. Foundation every other stylesheet sits on.

   1  Font + reset
   2  Tokens (and the grain, and the grainy placeholder .ph)
   3  Motion primitives
   3b The opening (every load's landing: .is-opening)
   4  Home layout (intro column + empty board container; socials, sent arrow)
   5  The mark
   6  Project shell (sticky aside + scrolling column)
   7  404 (and the sheet with one tile missing)
   8  Focus and selection

   House rules: light only. Everything in rem off the scaling root, so the
   page scales as one object with viewport width — no px except hairlines.
   svh, never vh. Weights 400 / 500 / 600. Hairlines, not borders; no shadows;
   radius ≤ 0.25rem. Every :hover lives inside the (hover: hover) and
   (pointer: fine) query, because :hover sticks after a tap.
============================================================================= */


/* -- 1  Font + reset --------------------------------------------------------- */

@font-face {
  font-family: 'Plus Jakarta Sans';
  src: url('../plus-jakarta-sans.woff2') format('woff2');
  font-weight: 200 800;
  font-display: swap;
}

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

/* The one sizing dial: 1rem tracks viewport width between two floors, so a
   fixed-frame landing holds from a laptop to a 5K display. */
html {
  font-size: clamp(11px, 1.05vw, 21px);
  -webkit-text-size-adjust: 100%;
  /* Controls answer a press themselves (:active); the grey tap box would
     be a second, cruder answer. */
  -webkit-tap-highlight-color: transparent;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

body {
  margin: 0;
  min-height: 100svh;
  background: var(--paper);
  color: var(--ink);
  font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.5;
  font-feature-settings: 'kern', 'liga';
}

h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }
h1, h2, h3, h4 { font-weight: inherit; font-size: inherit; }
img, svg, video, canvas, iframe { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.noscript { padding: var(--page-pad); max-width: 32em; }


/* -- 2  Tokens --------------------------------------------------------------- */

:root {
  color-scheme: light;

  /* Surfaces. Cream paper from the wireframe; tiles and cards a step lighter
     so they read as objects resting on it rather than holes cut into it. */
  --paper:   #F2F0EA;
  --paper-2: #FAF9F6;
  --ink:     #161616;
  --ink-2:   color-mix(in srgb, var(--ink) 80%, var(--paper));
  --ink-3:   color-mix(in srgb, var(--ink) 64%, var(--paper));   /* ≈ 5:1 on paper; keep ≥ 4.5 */
  --line:    #D8D4CB;
  /* The board's graph paper: printed lighter than any other rule, so the
     grid is felt under the composition rather than read as a spreadsheet. */
  --line-soft: color-mix(in srgb, var(--line) 60%, transparent);
  /* The edge of an object resting on the paper (a board tile), as opposed
     to a line printed on it. Stronger than --line, so a tile with a pale
     picture still reads as a thing and not as more paper. */
  --line-strong: color-mix(in srgb, var(--ink) 32%, var(--paper));
  --accent:  #1C3461;

  /* Type. Three sizes, and only three. */
  --fs-display: 2.75rem;
  --fs-body:    1.125rem;
  --fs-small:   0.875rem;

  /* Motion. Entrances decelerate hard (things arrive and settle); the flyer
     uses a symmetric-ish curve because it is a move, not an arrival. */
  --ease-enter: cubic-bezier(0.2, 0, 0, 1);
  --ease-flip:  cubic-bezier(0.4, 0, 0.1, 1);
  --t-enter: 500ms;
  --t-exit:  260ms;
  --t-flip:  550ms;
  --t-hover: 200ms;
  --t-open:  800ms;   /* the opening's long moves (§3b) */
  --t-put:   700ms;   /* one card put into place in the opening (§3b) */
  --rise: 0.5rem;

  /* Space. */
  --page-pad: 4.5rem;
  --hair: 1px;

  /*
     Grain. Fractal noise (baseFrequency 0.9, two octaves) recoloured to
     pure black whose alpha is the noise: alpha = 0.6 × (0.62 − R). Black
     with alpha composites exactly like grey noise under mix-blend-mode:
     multiply, so it needs no blend mode — which matters twice: a blend mode
     on a full-viewport layer is a compositing cost on every scroll, and
     the flyer copies only background-image (flyer.js makeFlyer), so the
     grain has to live entirely inside that one value to fly unchanged.
     Tiled at 160px with stitchTiles, so the tile has no seam. Full strength
     here is the placeholder's (≈ multiply at 0.14); the page's grain is the
     same image at a fraction of it (body::after below).
  */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0.6 0 0 0 0.37'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* On a 2× screen a 1px rule reads as a heavy stroke next to type this light. */
@media (min-resolution: 2dppx) {
  :root { --hair: 0.5px; }
}

/* 1ms rather than 0 so transitionend still fires for anything that waits on
   it. Delays are zeroed in JS (stampSequence), not here. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --t-enter: 1ms;
    --t-exit:  1ms;
    --t-flip:  1ms;
    --t-hover: 1ms;
    --t-open:  1ms;
    --t-put:   1ms;
    --rise: 0rem;
  }
}


/*
   The placeholder every null picture renders: the project's duotone as
   material rather than a diagram. A flat base of the dark tone, a broad
   blob of the light tone and a small brighter highlight, both placed and
   sized by board.js from the project's number (Suratan.placeholder:
   --ph-x1 … --ph-y2), and the grain on top. Everything — base included — is in one
   background-image value, so the flyer's copy of it is the whole picture.
   The custom properties are set on the column (project.js), the more-work
   thumb, and a tile's media box; .ph inherits them.
*/
.ph {
  background-image:
    var(--grain),
    radial-gradient(ellipse 46% 40% at var(--ph-x2, 34%) var(--ph-y2, 14%),
      color-mix(in srgb, var(--ph-b) 50%, #fff), color-mix(in srgb, var(--ph-b) 0%, transparent)),
    radial-gradient(ellipse var(--ph-w1, 92%) var(--ph-h1, 82%) at var(--ph-x1, 74%) var(--ph-y1, 24%),
      var(--ph-b), color-mix(in srgb, var(--ph-b) 0%, transparent)),
    linear-gradient(var(--ph-a, #8C8A84), var(--ph-a, #8C8A84));
}

/*
   Paper grain over the whole page: the same noise at 0.3 of the
   placeholder's strength (≈ multiply at 0.04). Fixed and static, so it
   stays under reduced motion; below the flyer (z 10), which carries its
   own grain inside its picture, and above the pinned page (z −1) and the
   column rule (z 1). The minibar (z 20) sits over it — a paper-coloured bar
   two rem tall where the grain is all but invisible anyway.
*/
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9;
  pointer-events: none;
  background-image: var(--grain);
  opacity: 0.3;
}


/* -- 3  Motion primitives ---------------------------------------------------- */
/*
   Anything that arrives carries data-seq="<group>". It starts hidden and a
   little low; stampSequence() writes its --d from the SEQ tables in core.js
   and then sets data-in="1". Timing lives in those tables — never add a
   transition-delay to a rule. data-in="0" is the exit: quicker, and in place,
   because a leaving element should not compete with what is arriving.
*/

[data-seq] {
  opacity: 0;
  translate: 0 var(--rise);
  transition:
    opacity   var(--t-enter) var(--ease-enter) var(--d, 0ms),
    translate var(--t-enter) var(--ease-enter) var(--d, 0ms);
}

[data-seq][data-in="1"] {
  opacity: 1;
  translate: 0 0;
}

[data-seq][data-in="0"] {
  opacity: 0;
  translate: 0 0;
  transition-duration: var(--t-exit);
  transition-timing-function: ease-in;
}

/* The board's paper and what is printed on it (decoration, confetti) are
   the page arriving, not things placed on it: they fade in where they lie.
   A rise would also slide a colour block across the grid line it is cut
   to. */
[data-seq="paper"] { translate: none; }

/* Revealed while scrolling up (core.js reveal): start above, so it arrives
   with the scroll. transition: none sets the start state, not animates it. */
[data-seq][data-from="above"]:not([data-in]) {
  translate: 0 calc(-1 * var(--rise));
  transition: none;
}

/* The opening's own groups (core.js SEQ) are already there outside it, and
   leave with the element they sit in rather than on their own. */
:is([data-seq="rule"], [data-seq="lines"]),
:is([data-seq="rule"], [data-seq="lines"])[data-in] {
  opacity: 1;
  translate: none;
}


/* -- 3b The opening ---------------------------------------------------------- */
/*
   Every load of the site (core.js open, SEQ.A; .is-opening on <body>
   while it runs) opens with the studio putting up its work, not a page
   fading in. Every move here runs once per load, on arrival, and is the
   answer to "when you open the site" — a refresh plays it again; a way
   back from a project (ToHome) and reduced motion get the plain primitive
   above. Each start state is keyed on
   :not([data-in]), so it only ever applies before the stamp — an exit is
   never one of these moves run backwards. Timing is the stamp's --d; the
   delays below are only that.

   Left column:
   - The cat takes no part, by the owner's wish: it is simply there from
     the first frame — no fade, no rise.
   - The welcome rises through its line, a line at a time: the line's box
     masks it (padded so no ascender or descender is cut at rest).
   - The socials' hairline draws from the left, the way every rule that
     opens something on a project page draws (project.css §3); the words
     follow it.

   Board:
   - The sheet is ruled: each grid line draws down or across from the
     sheet's top or left edge, in fronts from the top-left.
   - What is printed on it goes on in the same fronts: colour, pattern and
     type wipe on from the left like ink off a roller; confetti pops.
   - The work is put up on the grid, never brought in from off it. Every
     framed tile opens out of one of its own corner cells: it shows that
     cell first, frame and all, and grows across the rest of its cells
     until its frame closes on its own grid lines (a clip, so the tile
     never moves and never covers anything outside its cells). The corner
     and the order are its own — both random, new on every load (core.js
     aim, SEQ.A `shuffle`). A 1 × 1 tile has nothing to grow into and
     simply inks on.
   - Its number chip is stamped on as it settles: the chip's delay is its
     card's own --d plus most of the move, derived from the stamp rather
     than a table of its own, so it can never land before its card.
*/

/* The cat is there from the first frame: no entrance of any kind. */
.is-opening .mark[data-seq]:not([data-in="0"]) {
  opacity: 1;
  translate: none;
  transition: none;
}

.is-opening .welcome-line {
  overflow: hidden;
  padding-block: 0.12em 0.22em;
  margin-block: -0.12em -0.22em;
}
.is-opening .welcome-line > [data-seq] { transition-duration: var(--t-open); }
.is-opening .welcome-line > [data-seq]:not([data-in]) { translate: 0 110%; }

.is-opening .socials {
  position: relative;
  border-top-color: transparent;
}
.is-opening .socials::before {
  content: '';
  position: absolute;
  top: calc(-1 * var(--hair));
  left: 0;
  right: 0;
  border-top: var(--hair) solid var(--line);
  transform-origin: 0 50%;
  transition: scale var(--t-open) var(--ease-enter) var(--d, 0ms);
}
.is-opening .socials:not([data-in])::before { scale: 0 1; }

.is-opening .board-lines > [data-seq] {
  transition: scale var(--t-open) var(--ease-enter) var(--d, 0ms);
}
.is-opening .board-lines > .is-v { transform-origin: 50% 0; }
.is-opening .board-lines > .is-h { transform-origin: 0 50%; }
.is-opening .board-lines > .is-v:not([data-in]) { scale: 1 0; }
.is-opening .board-lines > .is-h:not([data-in]) { scale: 0 1; }

/* The ink: a deco tile's whole face, unrolled from its left edge. */
.is-opening .tile--deco > .tile-in {
  clip-path: inset(0);
  transition:
    opacity   calc(var(--t-enter) * 0.3) linear var(--d, 0ms),
    clip-path calc(var(--t-open) * 0.75) cubic-bezier(0.6, 0, 0.1, 1) var(--d, 0ms);
}
.is-opening .tile--deco > .tile-in:not([data-in]) { clip-path: inset(0 100% 0 0); }

.is-opening .board-bit {
  scale: 1;
  transition:
    opacity calc(var(--t-enter) * 0.3) linear var(--d, 0ms),
    scale   calc(var(--t-open) * 0.55) cubic-bezier(0.3, 1.8, 0.5, 1) var(--d, 0ms);
}
.is-opening .board-bit:not([data-in]) { scale: 0; }

/* Put up: grown from a corner cell (--put-x, --put-y: 0 for the left or
   top corner, 1 for the right or bottom). The seed is one cell plus the
   hairline .tile-in reaches past its cells, so the seed's frame already
   sits on grid lines; inset() interpolates, so the clip widens smoothly
   and ends at inset(0), the tile at rest. */
.is-opening .tile-in[data-seq="tiles"] {
  --put-span-x: calc(100% - var(--cell, 0px) - var(--hair));
  --put-span-y: calc(100% - var(--cell, 0px) - var(--hair));
  clip-path: inset(0);
  transition:
    opacity   calc(var(--t-put) * 0.25) linear var(--d, 0ms),
    clip-path var(--t-put) cubic-bezier(0.6, 0, 0.1, 1) calc(var(--d, 0ms) + var(--t-put) * 0.1);
}
.is-opening .tile-in[data-seq="tiles"]:not([data-in]) {
  translate: none;
  clip-path: inset(
    calc(var(--put-y, 0) * var(--put-span-y))
    calc((1 - var(--put-x, 0)) * var(--put-span-x))
    calc((1 - var(--put-y, 0)) * var(--put-span-y))
    calc(var(--put-x, 0) * var(--put-span-x)));
}

/* A stamp: down from above the paper, quick, and it stays where it lands.
   The kin colours keep their own pace in case a logo is opened mid-way. */
.is-opening .tile-no {
  transition:
    opacity          calc(var(--t-enter) * 0.2) linear calc(var(--d, 0ms) + var(--t-put) * 0.5),
    scale            calc(var(--t-open) * 0.4) cubic-bezier(0.3, 0, 0, 1) calc(var(--d, 0ms) + var(--t-put) * 0.5),
    background-color var(--t-hover) ease-out,
    border-color     var(--t-hover) ease-out,
    color            var(--t-hover) ease-out;
}
.is-opening .tile-in:not([data-in]) .tile-no { opacity: 0; scale: 1.35; }


/* -- 4  Home layout ---------------------------------------------------------- */
/*
   One screen, no scroll. Left 40% is the introduction, read top to bottom:
   who, then what, then how to reach him. Right 60% is the board, which
   board.js mounts into .board-wrap.

   The column takes its vertical extent from the board, never the other way
   round: board.js publishes --board-top and --board-h (px) on .home after
   every layout, and the intro occupies exactly that band. The mark's top
   edge sits on the board's top edge and the socials' bottom edge on its
   bottom edge, so the two halves of the page share one top and one bottom.
   The fallbacks are what the board would measure on an ordinary window, so
   the first frame (before board.js has run) is already close.
*/

.home {
  display: grid;
  grid-template-columns: 40% 60%;
  height: 100svh;
  overflow: hidden;
}

.intro {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: 0;
  margin-top: var(--board-top, var(--page-pad));
  height: var(--board-h, calc(100svh - 2 * var(--page-pad)));
  padding: 0 var(--page-pad);   /* no vertical padding: the board sets the band */
}

.intro-head {
  display: flex;
  flex-direction: column;
  gap: 3rem;
}

/* Two lines, set tight: the studio name is the page's title, and next to
   a board this full of material it has to hold its own, so it is a clear
   step above the display token (×1.35 ≈ 3.7rem, derived — not a fourth
   size). The lead-in is quieter than the name in both weight and ink. */
.welcome {
  font-size: calc(var(--fs-display) * 1.35);
  line-height: 1.02;
  letter-spacing: -0.02em;
  font-weight: 400;
  text-wrap: balance;
}
.welcome-line,
.welcome-lead,
.welcome-name { display: block; }
.welcome-lead { color: var(--ink-2); font-weight: 400; }
.welcome-name { color: var(--ink);   font-weight: 600; }

/* The paragraphs and the socials sit at the foot of the band, so the
   heading and the copy frame the empty middle the way the wireframe does.
   padding-top is only a floor for a short window. */
.intro-body {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  padding-top: 2rem;
}

/* A measure, in rem so it does not grow with the type token. */
.intro-copy {
  max-width: 30rem;
  color: var(--ink);
  text-wrap: pretty;
}

/*
   Socials are words, not buttons: one row of text links under a hairline
   that runs the full column. The arrow is a glyph in the quietest ink — it
   says "leaves the site" without being a second thing to read. line-height
   1 so the row's bottom edge is the text's, and it lands on the board's.
*/
.socials {
  --soc-gap: 1rem;   /* rule to words; the hover segment reads it */
  display: flex;
  flex-wrap: wrap;
  column-gap: 2rem;
  margin-top: 1rem;
  padding-top: var(--soc-gap);
  border-top: var(--hair) solid var(--line);
}

.socials > li { position: relative; }   /* the hover segment's box: link wide */

.social {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3em;
  font-size: var(--fs-small);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);
  transition: color var(--t-hover) ease-out;
}

.social-arrow {
  color: var(--ink-3);
  font-weight: 400;
  transition: color var(--t-hover) ease-out;
}

/* A hover inks the rule over the link, drawn from the left: the rule points
   at the word. Its bottom edge is the rule's and it is a border of the same
   width, so both snap to the same device pixels (a hairline-high box sat a
   pixel off at 2×). */
@media (hover: hover) and (pointer: fine) {
  .social::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(100% + var(--soc-gap));
    border-top: var(--hair) solid var(--ink);
    transform-origin: 0 50%;
    scale: 0 1;
    transition: scale var(--t-hover) var(--ease-enter);
  }
  .social:hover { color: var(--ink-2); }
  .social:hover::before { scale: 1 1; }
  .social:hover .social-arrow { color: var(--ink); }
}

.social:active {   /* a press: quicker in than out */
  color: var(--ink-2);
  transition-duration: calc(var(--t-hover) * 0.3);
}

/* A new-tab link's ↗ (core.js onSent) leaves up-right and a fresh one
   settles in. translate never touches layout, so nothing measured around
   it (board.js fitFace) moves. --t-sent is local: one animation's length,
   which no transition shares. */
.link-arrow { display: inline-block; }

.is-sent {
  --t-sent: 440ms;
  animation: link-sent var(--t-sent);
}

@keyframes link-sent {
  0%   { translate: 0 0;            opacity: 1; animation-timing-function: ease-in; }
  40%  { translate: 0.4em -0.4em;   opacity: 0; }
  41%  { translate: -0.4em 0.4em;   opacity: 0; animation-timing-function: var(--ease-enter); }
  100% { translate: 0 0;            opacity: 1; }
}

/* Reduced motion: no animation (every duration there is ≤ 1ms). */
@media (prefers-reduced-motion: reduce) {
  .is-sent { animation: none; }
}

/* An empty room for board.js. The board centres itself in here. */
.board-wrap {
  position: relative;
  display: grid;
  place-items: center;
  min-width: 0;
  min-height: 0;
  padding: var(--page-pad);
}


/* -- 5  The mark ------------------------------------------------------------- */
/*
   A placeholder until the real mark exists. The breathing is slow enough to
   be felt rather than watched: it says the page is alive without asking
   for attention.
*/

.mark {
  display: block;
  width: 5.5rem;
  height: 6rem;
  color: var(--ink);
}

/* Fallback when js/mark.js is missing: the navy disc the layout was built
   around, so a broken mark never breaks the page. */
.mark:not(.mark--svg) {
  width: 5rem;
  height: 5rem;
  border-radius: 50%;
  background: var(--accent);
}

.mark svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* The idle is in the four motion ticks, not the cat: they breathe in a
   staggered rhythm, so the drawing waves at you without anything on the
   page moving. The cat itself only moves when you reach for it — a small
   bob up out of the hole on hover (fine pointers only), then back. */
.mark .tick {
  transform-box: fill-box;
  transform-origin: center;
  animation: tick-breathe 3.2s ease-in-out infinite;
}
.mark .tick-2 { animation-delay: 0.4s; }
.mark .tick-3 { animation-delay: 1.6s; }
.mark .tick-4 { animation-delay: 2s; }

@keyframes tick-breathe {
  0%, 100% { opacity: 0.35; scale: 0.9; }
  30%      { opacity: 1;    scale: 1; }
  60%      { opacity: 0.55; scale: 0.95; }
}

.mark .mark-cat {
  transition: translate var(--t-hover) var(--ease-enter);
}

/* While a tile is open (board.js .is-browsing) the cat peeks at it and the
   ticks hold still. The board gates its own hover, so no media query here;
   before the hover rules, so reaching for the cat itself still wins. */
.home.is-browsing .mark-cat { translate: 0 -2.5%; }
.home.is-browsing .tick { animation-play-state: paused; }

@media (hover: hover) and (pointer: fine) {
  .mark:hover .mark-cat { translate: 0 -4%; }
  .mark:hover .tick { animation-duration: 0.9s; }
}

@media (prefers-reduced-motion: reduce) {
  .mark .tick { animation: none; opacity: 1; }
  .mark .mark-cat { transition: none; }
  .home.is-browsing .mark-cat { translate: none; }
}


/* -- 6  Project shell -------------------------------------------------------- */
/*
   Same 40 / 60 split as the landing, so arriving on a project changes what is
   in the room, not the room. The aside is sticky for the full viewport and
   the column scrolls the body natively.
*/

.project {
  display: grid;
  grid-template-columns: 40% 60%;
  align-items: start;
  min-height: 100svh;
}

/*
   The one rule between the aside and the column, on the 40% grid line.
   Fixed and drawn once, by the root: the aside is sticky and the column
   scrolls, so a border on either would travel with it or stop where its
   box stops. Both sides keep --page-pad from it, so the gutter is centred
   on the line. It lives inside the project root, so it mounts and leaves
   with the page; flyer.js fades it with the progress line (project.css
   §9). z-index 1 keeps it above the column's boxes and below the flyer
   (10) and the minibar (20). Hidden under 900px (mobile.css).
*/
.project::before {
  content: '';
  position: fixed;
  top: 0;
  bottom: 0;
  left: 40%;
  z-index: 1;
  width: var(--hair);
  background: var(--line);
  pointer-events: none;
}

.aside {
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  height: 100svh;
  padding: var(--page-pad);
  transition: color var(--t-enter) var(--ease-enter);
}

/* The story ends (project.js .is-done, More work at mid-screen): number and
   title, the only parts that inherit, recede; Back keeps the ink. */
.project.is-done .aside { color: var(--ink-3); }

/* The number is content, not wayfinding, and it is the loudest thing on the
   page. Derived from the display token rather than a fourth size, so it
   moves with the type scale. */
.aside-no {
  font-size: calc(var(--fs-display) * 5);
  font-weight: 400;
  line-height: 0.8;
  letter-spacing: -0.05em;
  font-variant-numeric: lining-nums tabular-nums;
  margin-left: -0.06em;   /* optical: the 0's side bearing */
}

.aside-title {
  margin-top: 2.5rem;
  max-width: 12em;
  font-size: var(--fs-display);
  line-height: 1.12;
  letter-spacing: -0.02em;
  font-weight: 400;
  text-wrap: balance;
}

.aside-foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  padding-top: 2rem;
}

.aside-meta {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  font-size: var(--fs-small);
  line-height: 1.5;
}

.aside-row { display: contents; }

.aside-row > dt,
.aside-row > dd {
  padding: 0.75rem 0 1.5rem;
  border-top: var(--hair) solid var(--line);
}
.aside-row > dt { color: var(--ink-3); padding-right: 1rem; }
.aside-row > dd { color: var(--ink); }
.aside-row > dd > span { display: block; }

.aside-blurb {
  margin-top: 1.5rem;
  max-width: 30em;
  font-size: var(--fs-body);
  color: var(--ink-2);
  text-wrap: pretty;
}

.aside-status {
  margin-top: 1rem;
  font-size: var(--fs-small);
  color: var(--ink-3);
}

.aside-back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 2.5rem;
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--ink);   /* stays dark when the aside recedes */
}

/* A small navy disc echoes the landing mark: the way home looks like home. */
.aside-back-dot {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: var(--accent);
  color: var(--paper-2);
  line-height: 1;
  transition: scale var(--t-hover) var(--ease-enter);
}
.aside-back-dot svg {
  width: 0.875rem;
  height: 0.875rem;
  transition: translate var(--t-hover) var(--ease-enter);
}

@media (hover: hover) and (pointer: fine) {
  .aside-back:hover .aside-back-dot { scale: 1.08; }
  .aside-back:hover .aside-back-dot svg { translate: -0.125rem 0; }
}

/* A press sinks the disc, quicker in than out. After the hover rule at the
   same specificity, so a press under the mouse sinks rather than swells. */
.aside-back:active .aside-back-dot,
.minibar-back:active .aside-back-dot {
  scale: 0.92;
  transition-duration: calc(var(--t-hover) * 0.3);
}

/*
   Short desktop viewports (1280×720, 1440×760): the number at display×5
   and a two-line title ran into the facts table, and the way back fell off
   the bottom of the sticky aside. The number drops to display×3.6 and the
   top padding to 3rem; nothing else moves.

   The threshold: rem inside a media query is the browser's initial font
   size (16px by default), never the html clamp above — a query cannot read
   the root it is styling. So 760px is 760 / 16 = 47.5rem, at every width,
   and it grows with a reader's larger default font, which is when the
   aside needs the room anyway. min-width keeps it off the phone layout
   (mobile.css owns the aside there, and a landscape phone is short).
*/
@media (min-width: 901px) and (max-height: 47.5rem) {
  .aside { padding-top: 3rem; }
  .aside-no { font-size: calc(var(--fs-display) * 3.6); }
}

/* Scroll progress: project.js writes --progress (0 … 1) on .project. */
.aside-progress {
  position: absolute;
  left: var(--page-pad);
  right: var(--page-pad);
  bottom: 0;
  height: var(--hair);
  background: var(--line);
  overflow: hidden;
}
.aside-progress > span {
  display: block;
  height: 100%;
  background: var(--ink);
  transform-origin: 0 50%;
  scale: var(--progress, 0) 1;
}

.column {
  min-width: 0;
  min-height: 100svh;
  padding: var(--page-pad);   /* --page-pad from the rule on the left, too */
}


/* -- 7  404 ------------------------------------------------------------------ */

.notfound {
  display: grid;
  grid-template-columns: 40% 60%;
  height: 100svh;
}

.notfound-body {
  display: flex;
  flex-direction: column;
  padding: var(--page-pad);
}

.notfound-title {
  margin-top: 3rem;
  font-size: var(--fs-display);
  line-height: 1.12;
  letter-spacing: -0.02em;
}

.notfound-copy {
  margin-top: 1.5rem;
  max-width: 24em;
  color: var(--ink-2);
}

/* The way back is set like the landing's socials row (§4): a hairline the
   width of the column, then the words — weight 500, small, no underline. */
.notfound-foot {
  margin-top: auto;
  padding-top: 1rem;
  border-top: var(--hair) solid var(--line);
}

.notfound-link {
  display: inline-flex;
  font-size: var(--fs-small);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);
  transition: color var(--t-hover) ease-out;
}

@media (hover: hover) and (pointer: fine) {
  .notfound-link:hover { color: var(--ink-2); }
}

.notfound-link:active {
  color: var(--ink-2);
  transition-duration: calc(var(--t-hover) * 0.3);
}

/* The sheet with one tile missing (core.js render404). The cell is the
   largest whole px that fits 10 × 8 — a computed cell, like the board's, so
   every line lands on a pixel; the lines are grid mode's (board.css §7).
   The gap's frame reaches one hairline past two cells, closing on the grid
   lines as a tile's does. */
.notfound-room {
  display: grid;
  place-items: center;
  min-width: 0;
  min-height: 0;
  padding: var(--page-pad);
}

.notfound-plate {
  --cell: round(down, min((60vw - 2 * var(--page-pad)) / 10, (100svh - 2 * var(--page-pad)) / 8), 1px);
  display: grid;
  width: calc(var(--cell) * 10);
  height: calc(var(--cell) * 8);
}
@supports not (width: round(down, 1px, 1px)) {
  .notfound-plate { --cell: min((60vw - 2 * var(--page-pad)) / 10, (100svh - 2 * var(--page-pad)) / 8); }
}

.notfound-plate > * { grid-area: 1 / 1; }

.notfound-paper {
  border: var(--hair) solid var(--line-strong);
  background-image:
    linear-gradient(to right,  var(--line-soft) var(--hair), transparent var(--hair)),
    linear-gradient(to bottom, var(--line-soft) var(--hair), transparent var(--hair));
  background-size: var(--cell) var(--cell);
  background-origin: border-box;
}

.notfound-gap {
  align-self: start;
  justify-self: start;
  width: calc(var(--cell) * 2 + var(--hair));
  height: calc(var(--cell) * 2 + var(--hair));
  margin: calc(var(--r) * var(--cell)) 0 0 calc(var(--c) * var(--cell));
  border: var(--hair) solid var(--ink);
}


/* -- 8  Focus and selection -------------------------------------------------- */

/* Keyboard only: :focus-visible, never :focus, so a mouse click on a tile
   or a link draws nothing. Everything focusable gets the same navy hairline,
   held a little off the edge so it never merges with a tile's own frame. */
:focus { outline: none; }

a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
  outline: var(--hair) solid var(--accent);
  outline-offset: 0.25rem;
  animation: ring-land var(--t-hover) var(--ease-enter);
}

/* The ring lands, closing in as it inks: a keyframe, so it never replaces
   an element's own transition list. */
@keyframes ring-land {
  from { outline-offset: 0.5rem; outline-color: transparent; }
}

/* A page's name, focused on arrival (core.js settle), is not a control. */
h1[tabindex="-1"]:focus-visible,
.aside-title:focus-visible { outline: none; animation: none; }

::selection {
  background: var(--accent);
  color: var(--paper-2);
}
