/* ============================================================
   HOME — hero, works, graphic grid, marquee, about, footer, lightbox
   ============================================================ */

/* ================= HERO ================= */
/* A pleated curtain over the name, drawn by js/pleat.js on a plain 2D
   canvas. The folds nearest the cursor lift and slide aside; the name was
   painted underneath before the cloth went on.

   Plain 2D, not WebGL: the whole sheet is N vertical linear gradients and
   one blit of a pre-rasterised mark, which a canvas does natively. There is
   no per-pixel field here to justify a shader.

   `touch-action: auto`. Touch screens do not drive the cloth any more (they
   get one opening, see js/pleat.js), so a finger on the hero scrolls and
   pinch-zooms exactly as it does anywhere else on the page. */
#pleat {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
  touch-action: auto;
  will-change: transform;
}

#hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 0 var(--gut) clamp(1.5rem, 4vh, 2.75rem);
  overflow: hidden;
  /* Plain void, nothing else. There used to be a radial grey wash here as a
     fallback for a hero that might fail to draw. It was invisible under the
     canvas — until the canvas got a scroll parallax, which slid it up and
     uncovered a grey band across the bottom of the hero on the way out. The
     parallax is gone (js/home.js) and so is the wash: whatever shows behind
     the curtain must be the page colour, or the hero has a visible seam. */
  background: var(--void);
}

.hero-inner {
  position: relative;
  z-index: 2;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-bottom: clamp(1.25rem, 4vh, 3rem);
  will-change: transform;
}

/* The DOM headline is gone — the name is painted on the canvas now, and
   the real one is the .sr-only h1. That also retired the --ga / --gb
   per-letter lighting hooks that used to live here; there are no letter
   elements left to hang them on. */

/* Align the quiet signature with the page gutter beneath the canvas name. */
.hero-say {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.65rem;
  margin-top: 1.5rem;
  text-align: left;
}
.hero-sub {
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(1.05rem, 1.7vw, 1.5rem);
  line-height: 1.3;
  letter-spacing: -0.015em;
  color: var(--bone);
}
.hero-note {
  font-size: clamp(0.8125rem, 1vw, 0.9375rem);
  line-height: 1.6;
  color: var(--bone-dim);
}

.hero-rail {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: clamp(1rem, 2.4vh, 1.6rem);
  border-top: 1px solid var(--line);
}

.hero-scroll {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
}
.hero-scroll i {
  display: block;
  width: 46px;
  height: 1px;
  background: var(--bone-mute);
  position: relative;
  overflow: hidden;
}
.hero-scroll i::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bone);
  transform-origin: 0 50%;
  animation: scroll-cue 2.4s var(--ease-io) infinite;
}
@keyframes scroll-cue {
  0%   { transform: translateX(-100%); }
  55%  { transform: translateX(0); }
  100% { transform: translateX(100%); }
}

@media (max-width: 720px) {
  .hero-rail { font-size: var(--t-label-s); }
  .hero-rail .kicker:last-child { display: none; }
}

/* ================= INTRODUCTION ================= */
.intro {
  padding: clamp(5rem, 9vw, 9rem) var(--gut) clamp(6rem, 10vw, 10rem);
  scroll-margin-top: 5rem;
}
.intro-grid {
  --intro-gap: clamp(1rem, 2.5vw, 2.75rem);
  --intro-overhang: clamp(4.5rem, 8vw, 7rem);
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--intro-gap);
  row-gap: clamp(2.5rem, 5vw, 5rem);
}
.intro-lead,
.intro-main {
  font-family: var(--sans);
  font-weight: 400;
  font-size: clamp(3rem, 4.35vw, 4rem);
  line-height: 1.13;
  letter-spacing: -0.035em;
  color: var(--bone);
}
.intro-lead {
  grid-column: 1 / -1;
  font-size: clamp(3.25rem, 4.45vw, 4.25rem);
  max-width: 24em;
}
.intro .serif { color: inherit; font-size: 1.08em; }
.intro-main {
  grid-column: 5 / -1;
  grid-row: 2;
  position: relative;
  z-index: 2;
}
.intro-main p + p { margin-top: clamp(2rem, 4vw, 4rem); }
.intro-word { white-space: nowrap; }
.intro-portrait { grid-column: 1 / 5; grid-row: 2 / 4; align-self: start; }
/* Extend the photograph behind the first few letters, keeping the face left
   of the text edge. The fixed shade preserves contrast over the original wall. */
.intro-photo-frame {
  position: relative;
  width: calc(100% + var(--intro-overhang));
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--void);
}
.intro-photo-drift { position: absolute; inset: -16px 0; }
.intro-photo-frame img { display: block; width: 100%; height: 100%; object-fit: cover; filter: grayscale(1); }
.intro-photo-frame::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg, transparent 68%, rgba(7, 7, 6, 0.78) 100%);
}
.intro-portrait figcaption {
  max-width: 26em;
  margin-top: 1.5rem;
  font-size: clamp(0.9375rem, 1.15vw, 1.0625rem);
  line-height: 1.75;
  color: var(--bone-dim);
  text-wrap: pretty;
}
.intro-facts {
  grid-column: 5 / -1;
  align-self: start;
  /* Only the large statement overlaps the photo. Keep both facts beyond
     its visible edge, with the rule closing the group underneath. */
  margin-left: calc(var(--intro-overhang) - var(--intro-gap) + 2rem);
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: clamp(2rem, 4vw, 4rem);
  padding-bottom: 2rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.875rem;
  line-height: 1.6;
}
.intro-facts dt { color: var(--bone-dim); margin-bottom: 0.4rem; }
.intro-availability-star {
  display: inline-block;
  margin-right: 0.45em;
  color: var(--signal);
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1;
}
.intro-facts dd { margin: 0; color: var(--bone); }
@media (max-width: 899px) {
  .intro { padding-top: 4.5rem; padding-bottom: 5rem; }
  .intro-grid { grid-template-columns: 1fr; row-gap: 2rem; }
  .intro-lead, .intro-main {
    grid-column: 1;
    grid-row: auto;
    font-size: clamp(1.875rem, 4.6vw, 2.75rem);
    line-height: 1.2;
    letter-spacing: -0.03em;
  }
  .intro-main p + p { margin-top: 2rem; }
  .intro-portrait { grid-column: 1; grid-row: auto; width: min(100%, 26rem); margin-top: 1rem; }
  .intro-photo-frame { width: 100%; }
  .intro-photo-drift { inset: 0; }
  .intro-photo-frame::after { display: none; }
  .intro-facts { grid-column: 1; grid-row: auto; margin-left: 0; }
}
@media (max-width: 480px) {
  .intro-facts { grid-template-columns: 1fr; gap: 1rem; }
}

/* ================= SELECTED WORKS =================
   Two projects, presented rather than listed.

   What this replaced: a full-bleed row list — index, small thumbnail,
   oversized title, meta — that inverted to a light band on hover, plus a
   preview card that chased the cursor through it. The row read as an index
   entry when the section is supposed to be the hiring story, the thumbnail
   was too small to show anything, and the chasing card only existed for
   people who had a cursor at all.

   Now each project gets a tile: number → name → one line → a staged group
   of screens → outcome and the way in. The screens are the largest thing
   in the section, which is the point of the section. */
#work { padding-bottom: clamp(4rem, 10vh, 8rem); }

/* Six columns, but nothing spans all six. Each tile takes four and the
   second one starts two in, so the pair steps across the page instead of
   stacking. Two identical full-width blocks would read as a list again —
   the offset is what makes it a composition. */
.works {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(4rem, 11vh, 8.5rem) clamp(0.75rem, 1.2vw, 1.25rem);
  padding: 0 var(--gut);
}

.wk-item {
  display: block;
  grid-column: 1 / span 4;
}
.wk-item:nth-child(even) { grid-column: 3 / span 4; }

.wk-num {
  display: block;
  font-family: var(--mono);
  font-weight: 500;
  font-size: var(--t-label);
  letter-spacing: 0.18em;
  color: var(--bone-soft);
  font-variant-numeric: tabular-nums;
  /* THE MARGIN IS NOT THE GAP, AND ON THIS TILE THE DIFFERENCE IS 17px.
     This label is set at line-height 1.6, so about 5px of its own box is
     empty under the last glyph; the name below it is set at 0.95 on a 75px
     font, so its capitals start about 8px inside the top of ITS box. A
     14.4px margin was therefore drawing a 27px hole. 5px draws about 18,
     which is the number this rhythm is tuned to. Measured, not guessed —
     the same correction is why the three values down this block do not
     look like a sequence. */
  margin-bottom: 0.5rem;
}

.wk-name {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(2.1rem, 5.2vw, 4.5rem);
  line-height: 0.95;
  letter-spacing: -0.045em;
  text-transform: uppercase;
}

/* The claim, not a description. Display face rather than mono so it reads
   as part of the title block — this is the one line that has to land. */
.wk-line {
  /* Box 15px, visible about 14: the name's own box ends well above its
     baseline at line-height 0.95, so this margin has to pay for that. */
  margin-top: 0.95rem;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(0.95rem, 1.45vw, 1.35rem);
  /* SET AS A CLAIM, NOT AS A PARAGRAPH. This inherited the body's 1.6,
     which is a measure tuned for running prose. At 21.6px that is 34.6px
     of leading on a sentence that wraps to two lines, so "Making race
     strategy easier to understand." read as two separate lines rather than
     as one claim — and the tile beside it, whose claim happens to fit one
     line, showed nothing of the fault. Both tiles resolve this one rule,
     so the pair stays a pair. */
  line-height: 1.27;
  letter-spacing: -0.01em;
  color: var(--bone-dim);
}

/* WHAT THE PRODUCT ACTUALLY IS.
   `.wk-line` above is a claim, and a claim is not an answer: "The pit wall,
   in your pocket" does not tell a recruiter whether Race Intel is an app or
   an editorial project. This is the sentence that does, and it is set to be
   read second — mono rather than display, one step down, dimmed, on a short
   measure — so the title block still lands as a title block. */
.wk-desc {
  /* Box 6px, visible about 22. This paragraph runs at line-height 1.48 and
     the claim above it at 1.27, so between them the two boxes already hold
     about 16px of empty leading before any margin is added. */
  margin-top: 0.4rem;
  max-width: 34ch;
  font-family: var(--mono);
  font-size: var(--t-body-s);
  /* 1.6 is the measure for a long read. Three lines under a claim are not
     one, and at 1.6 the block was as tall as the title above it. */
  line-height: 1.48;
  color: var(--bone-soft);
}

/* The screens sit on a wash, not in a box.

   THE BOX THAT WAS REMOVED, AND WHY THIS IS NOT IT. There used to be a lit
   panel with a BORDER here, and the group centred inside it. Two things
   were wrong with it: the border landed 34px under the frames at 2000px
   wide against 276px above, so it read as a crop and the composition was
   bottom-heavy; and a hard fill behind an alpha cut-out simply puts back
   the background the frame was cut out of.

   The wash below has neither fault. It is a radial gradient that reaches
   zero alpha well inside its own box, so it has NO EDGE anywhere on the
   page and cannot read as a panel or a crop. It bleeds past the reserved
   box on all four sides for the same reason.

   WHY IT IS NEEDED, MEASURED. These mockups are near-black: sampled over
   opaque pixels, the Race Intel home screen has a mean luminance of 28/255
   with 86.8% of its pixels in the 0–51 band, and the Doit task screen 34.9
   with 90.0%. The page is --void at luminance 7. A black handset on a black
   page has no silhouette at all, which is what made the second tile read as
   three dark smudges. The wash peaks at luminance ~23, so the frames now
   have an outline to be a shape against. It is the ground that does this
   work, not a brighter picture — the artwork is left alone.

   No `overflow: hidden`: with nothing to clip against, the frames cannot be
   cut at any viewport. The element still reserves the row's height so the
   grid doesn't reflow when the images load. */
.wk-shots {
  position: relative;
  margin-top: clamp(1.25rem, 3.5vh, 2.25rem);
  /* 3:2 rather than 4:3 — the group is wide and short, so a taller box was
     only ever reserving dead space above the frames. */
  aspect-ratio: 3 / 2;
}

.wk-shots::before {
  content: '';
  position: absolute;
  /* NO HORIZONTAL BLEED, and that is a fix rather than a preference.

     This was `inset: -16% -11%`. The bleed was belt and braces — the
     gradient reaches zero alpha at 76% of 56% of the box, which is 301px
     from centre in a 707px box against a half-width of 353, so nothing was
     ever painted out there. But an absolutely positioned pseudo-element
     still contributes to scrollable overflow, and -11% of a 707px tile is
     77.8px each side: measured at a 768px viewport the document went to
     815px wide, held back only by `overflow-x: hidden` on <body>. It passed
     at 390 and at 1440 for different accidental reasons, which is what a
     percentage that is not checked against the gutter does.

     Vertical bleed is kept: it costs nothing (the page scrolls that way
     anyway) and it is what stops the wash ending on a horizon line above
     and below the frames. */
  inset: -16% 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(56% 62% at 50% 47%,
    rgba(236, 232, 225, 0.075) 0%,
    rgba(236, 232, 225, 0.032) 46%,
    rgba(236, 232, 225, 0) 76%);
  opacity: 0.82;
  transition: opacity 0.9s var(--ease);
}
/* The ground comes up with the pointer, the same one gesture the fan and
   the colour release already answer with. No new motion, no new idea. */
.wk-item:hover .wk-shots::before,
.wk-item:focus-visible .wk-shots::before { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .wk-shots::before { transition-duration: 0.01ms; }
}

.wk-screens {
  position: absolute;
  inset: 0;
  /* Above the wash. */
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  /* No gap: the outer two are pulled back toward the centre by transform
     instead, so the three overlap into one object rather than lining up as
     three separate pictures. */
  gap: 0;
}

/* Three phones read as one group only if the middle one is clearly the
   subject: it stands upright and forward while the outer two shrink, drop
   and tuck behind it.

   TWO ELEMENTS, TWO OWNERS. `.wk-shot` is the wrapper and the flex item;
   the entrance animation moves it. `.wk-screen` is the image and CSS owns
   its transform — the fan, and the hover. They are split because GSAP and a
   stylesheet cannot share one `transform`: GSAP parses the computed matrix
   and writes it back inline, which deletes whatever the stylesheet said and
   outranks `:hover` from then on.

   The sizing is worked out, not guessed. Each frame is ~0.507 wide for its
   height, so at 92% of the reserved height one frame lays out at 0.466H.
   The outer two scale to 0.88 and slide 9% of their own width inward, which
   makes the visible group 2.70 frame-widths — 1.26H against a 3:2 box's
   1.5H. The fan does not grow on hover.

   92% rather than 96% buys the clearance the entrance needs: the frames
   have to be able to sit 18px lower mid-animation without touching the
   footer rule 15px under the box. */
.wk-shot {
  position: relative;
  display: block;
  min-width: 0;
  height: 92%;
}
.wk-shot:nth-child(2) { z-index: 2; }

.wk-screen {
  display: block;
  height: 100%;
  width: auto;
  object-fit: contain;
  /* Grayscale for restraint, LIFTED not dimmed, and NO contrast().

     `contrast(1.14)` used to live here. On bright artwork it would be
     harmless; on these it was destroying the work. CSS contrast pivots on
     mid grey (127.5), and these mockups are near-black — mean luminance
     28/255 for the Race Intel home screen, 34.9 for the Doit task screen,
     measured over opaque pixels. A pixel at 28 came out at
     127.5 + (28 - 127.5) x 1.14 = 14. The filter was halving the luminance
     of a section whose entire job is to let you read the screens.

     brightness(1.15) instead: it is a multiply, so it lifts the dark mass
     the artwork is actually made of rather than crushing it. Only 1.2–1.3%
     of pixels sit above 204 — those are UI whites, which read as white
     either side of the clip, so nothing legible is lost at the top.

     The figure/ground separation is the wash on .wk-shots, not the picture.
     Making the artwork brighter to escape the page would have been a
     brightness war with --void; giving it a ground to be a silhouette
     against costs the artwork nothing. */
  filter: grayscale(1) brightness(1.15);
  transition: transform 1s var(--ease), filter 0.9s var(--ease);
}
/* THE FAN BELONGS TO THE THREE-FRAME STAGE, AND SAYS SO.

   These were unscoped, which was fine while every tile had three frames.
   The wide tile has one, and one frame is also `:nth-child(1)`, so it
   collected the left-hand frame's tuck. The resting state happened to
   survive on source order; the HOVER state did not —
   `.wk-item:hover .wk-shot:nth-child(1) .wk-screen` is five class-level
   selectors against `.wk-item.is-wide:hover .wk-screen`'s four, so the fan
   won and the wide tile's picture shrank to 0.92 and slid down-right on
   hover instead of stepping forward.

   Scoped with `:not(.is-wide)` rather than out-specified: a specificity
   race is a thing that has to be re-won every time either rule is edited. */
.wk-item:not(.is-wide) .wk-shot:nth-child(1) .wk-screen { transform: translate(9%, 4%) scale(0.88); }
.wk-item:not(.is-wide) .wk-shot:nth-child(3) .wk-screen { transform: translate(-9%, 4%) scale(0.88); }

/* Colour on hover, and the lift stays. Dropping back to brightness(1) at
   the same moment the wash comes up would have made the frames sink into
   the page exactly as they lit. */
.wk-item:hover .wk-screen,
.wk-item:focus-visible .wk-screen {
  filter: grayscale(0) brightness(1.08);
}

/* The Doit landing pair has its own light studio background. Preserve its
   lighting: monochrome at rest and original colour on hover/focus, without
   the brightness lift used for dark artwork. */
.wk-item.is-lightground .wk-screen { filter: grayscale(1); }
.wk-item.is-lightground:hover .wk-screen,
.wk-item.is-lightground:focus-visible .wk-screen { filter: grayscale(0); }

/* TWO CHILDREN, ONE RULE: the credit line and the action, pushed to
   opposite ends of it. The credit is what tells a reader whether they are
   looking at client work or the designer's own before they commit to a
   click; the action is what they do about it. */
/* NO RULE OVER IT. The hairline ran the full width of the tile, directly
   under a picture that now carries its own 16px corner — so the one thing
   the rule drew attention to was the gap between a rounded edge and a
   square line. The credit and the action are a mono label row at opposite
   ends of a wide measure; that reads as a footer without a line under the
   picture to say so, and the space above it is what separates them. The
   hover no longer has a border to answer, so the row's own two hover states
   — the action turning to --signal and its arrow stepping right — carry it.

   The space that was `margin-top: 0.9rem` plus `padding-top: 0.8rem` is
   kept as one margin, so nothing under the picture moves. */
.wk-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin-top: 1.7rem;
}

/* The credit line, and now the only thing in the tile's foot. Same mono
   label role as `.wk-num` at the top of the tile: it is context for the
   click rather than the click, and the whole tile is the click. */
.wk-meta {
  font-family: var(--mono);
  font-weight: 500;
  font-size: var(--t-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--bone-soft);
}

/* `.wk-open` STOOD HERE. It was an "Open case" label and a chevron in the
   right-hand end of the tile's foot — a button drawn inside an element that
   is already one link end to end, and the only part of the tile a reader
   could mistake for the click target. The tile answers a pointer with the
   inverted disc and its arrow, and it answers a screen reader with its own
   `aria-label`; neither needed a third statement. See js/works.js. */

/* ---------------- THE WIDE TILE ----------------
   A tile whose media is ONE composed render rather than three flat frames
   (`hero` in js/data.js). Race Intel is the first.

   WHAT WAS WRONG WITH THE FOUR-COLUMN VERSION. The tile took four of six
   columns, so a third of the row was empty, and inside it the picture and
   the copy were stacked — the copy left-aligned above a group of screens
   that had its own centre. At 1440 that put the title at x=505, the phones
   between 585 and 1300, and 505px of nothing beside them. Three different
   left edges and a large hole is not a composition, it is a leftover.

   So this tile takes the whole measure and spends it: copy in a narrow
   column, picture in a wide one, both on one horizontal axis, the rule
   underneath tying them together. Nothing is left over.

   1fr / 1.62fr, measured: at 1440 that is 1324.8px of content less a 57.6px
   gutter, so 484px of copy against 784px of picture.

   IT WAS 0.78 / 2 AND THE PICTURE WAS TOO BIG. At that ratio the render
   drew 920px wide and 803px tall, which is 89% of a 900px laptop viewport
   for one tile's illustration, against 347px of copy — a column so narrow
   that a two-word claim set one word per line. The tile read as a picture
   with a caption stuck to its left. Moving the split to 1 / 1.62 takes
   about 15% off the render, gives the copy the 40% more width it needed to
   set properly, and puts the two halves close enough in weight that the
   row reads as a composition rather than as an image and a leftover.

   The picture is capped by height as well, because the ratio alone cannot
   see the viewport: see the max-height on .wk-shots below. */
.wk-item.is-wide {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.62fr);
  column-gap: var(--gut);
  align-items: center;
}
.wk-item.is-wide .wk-head { grid-column: 1; }
/* THE PICTURE AND ITS CREDIT ARE ONE GRID ITEM. The width cap lives here,
   not on the frame, so the credit line underneath is exactly as wide as the
   picture and starts on the picture's own left edge, whichever column the
   picture is in. `--ar` is the frame's ratio, written here by js/works.js. */
.wk-item.is-wide .wk-fig {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  width: 100%;
  max-width: calc(min(76vh, 44rem) * var(--ar, 1.5));
  margin-inline: auto;
}
.wk-item.is-wide .wk-shots {
  /* The box takes the render's own ratio, declared in js/data.js, so the
     picture fills it exactly and `contain` never letterboxes. */
  margin-top: 0;
  /* A width-driven ratio has no idea how tall the window is, and this row
     also carries a section head above it and another tile below. The cap
     is on the BOX; `object-fit: contain` on the image inside means capping
     the height draws the render smaller, never crops it.

     76vh, not less. At 1440x900 the new column split already puts the
     render at 784x683, so the cap is one pixel clear of it and does
     nothing — the composition is the ratio's work, not the cap's. It binds
     only where it should: on a short laptop, and on a very wide screen
     where 1.62 of the measure would otherwise draw 880px of phone.

     THE CAP IS ON BOTH AXES, AND THAT IS THE FIX FOR THE SQUARE CORNERS.
     A height cap on its own overrides the `aspect-ratio` above it, so the
     moment the cap binds the BOX stops having the render's proportion
     while the render keeps its own. `object-fit: contain` then letterboxes
     inside the box, and the rounded corner is drawn on the box's edge
     rather than on the artwork's. Measured at 1920: the Race Intel card
     laid out 1054 x 704 with the picture painted 806 x 704 inside it, so
     124px of page showed on each side and what the reader actually saw was
     a square-cornered photograph inside an invisible rounded rectangle.

     `--ar` is the frame's width/height as a decimal, written on `.wk-fig`
     by js/works.js, which carries the width cap. Capping the width at the same ratio as
     the height means the two caps can never disagree, so the box keeps the
     render's proportion at every viewport, `contain` fills it exactly, and
     the 16px corner is always on the artwork itself. */
  max-height: min(76vh, 44rem);
  width: 100%;
  /* ONE CORNER, TAKEN FROM THE PERSONA CARDS. `.sg` and `.sgwide` in
     css/case.css are 16px, and that is now the site's card radius rather
     than a case-page detail — the two previews and the graphic covers all
     read it, so a reader meets one corner on the home page and the same one
     inside a case study.

     `clip`, not `hidden`, and it has to be BOTH axes here: the picture is
     the only child and the hover grows it, so there is nothing for a
     one-axis clip to leave alone. `overflow: clip` does not create a scroll
     container, so the tile keeps its `position: static` children and the
     entrance tween is unaffected.

     NOTHING IS PAINTED BEHIND THE PICTURE. Race Intel's render is an
     opaque JPEG composited onto --void, so the rounded corner falls on
     page-coloured pixels and simply is not seen; that is correct, and a
     plate added under it to make the corner visible would be a background
     invented for a decoration. On the Doit pair, the light studio background is baked into the file. */
  /* SQUARE. The 16px corner is gone from every preview on this page — the
     two case tiles and the graphic covers — and it is gone for one reason:
     these frames are windows now, not cards. A rounded corner says "this is
     an object on the page"; a square one says "this is an opening cut in
     it", which is what a fixed frame with a picture travelling behind it
     actually is. Nothing else about the box changes.

     `clip`, not `hidden`, and it has to be BOTH axes here: the layer inside
     is taller than the frame and moves, so there is nothing for a one-axis
     clip to leave alone. `overflow: clip` does not create a scroll
     container, so the tile keeps its `position: static` children and the
     entrance tween is unaffected. */
  border-radius: 0;
  overflow: clip;
}
/* The credit sits directly under its own picture, one gap for every tile.
   The hover grows the frame about 1.4% each way, well inside this gap. */
.wk-item.is-wide .wk-foot { margin-top: 1rem; }

/* ---------- THE LAYER THAT MOVES BEHIND THE OPENING ----------
   `.wk-screens` is the flex box that holds the frames. On a wide tile it
   holds exactly one, and it becomes the travelling layer: 12% taller than
   the opening, hung 6% past each edge, so it can move 6% either way without
   ever showing the frame's own ground. Core.drift() runs it off scroll
   position — see js/home.js for the call and js/core.js for the arithmetic.

   TWO ELEMENTS, TWO OWNERS, AGAIN. GSAP writes the drift inline on
   `.wk-shot.drift-layer`; this rule only gives it somewhere to travel. The
   image's own transform stays with the stylesheet.

   `cover`, NOT `contain`. The layer is taller than its box by definition
   now, so `contain` would letterbox it back down to the box and there would
   be nothing to move. Both renders are composed for this: the Race Intel
   photograph runs off all four edges already; the Doit pair retains studio
   margins around both phones for the 6% travel. */
.wk-item.is-wide .wk-screens {
  position: absolute;
  inset: -6% 0;
  overflow: hidden;
}
.wk-item.is-wide .wk-shot.drift-layer {
  position: absolute;
  inset: 0;
  height: auto;
  will-change: transform;
}

/* ---------- THE MIRROR ----------
   `is-flip` is the same tile facing the other way: the picture takes the
   first column and the copy the second. Nothing else changes — the 1 : 1.62
   split, the height cap, the measures, the type scale and the hover are one
   set of rules that both tiles resolve.

   The RATIO is what makes it a mirror rather than two arrangements. Written
   as `1.62fr minmax(0, 1fr)` the picture column would still be the wide one
   and the copy would still be narrow, which is the point: the reader gets
   the same two shapes in the opposite order, so the section reads as a pair
   instead of as a list of two.

   Explicit `grid-column` on both children, not `direction: rtl` or `order`:
   order would leave the DOM reading order and the visual order disagreeing
   for a keyboard user, and the copy has to stay first in the DOM because it
   carries the tile's name. */
.wk-item.is-wide.is-flip {
  grid-template-columns: minmax(0, 1.62fr) minmax(0, 1fr);
}
.wk-item.is-wide.is-flip .wk-head { grid-column: 2; }
.wk-item.is-wide.is-flip .wk-fig { grid-column: 1; grid-row: 1; }

/* The wash is off here, and this is load-bearing rather than an oversight.
   `.wk-shots::before` exists to give FLAT near-black cut-outs a silhouette
   on a black page. This render is not flat: it carries a baked shadow, lit
   metal edges and a reflection, so it already has ground. And the file is
   an opaque JPEG composited onto --void (tools/build-f1.py) because a
   transparent PNG of it is 2.6 MB — an opaque plate over a wash would
   simply cover the wash and print a dark box on top of it. */
.wk-item.is-wide .wk-shots::before { content: none; }

.wk-item.is-wide .wk-shot {
  height: 100%;
  width: 100%;
  flex: 1 1 auto;
}
.wk-item.is-wide .wk-screen {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
  /* No fan to inherit. The resting transform has to be stated, or the
     nth-child rules above would still apply their tuck to the only child. */
  transform: none;
}

/* NO HOVER GROWTH (14 September 2026). The card used to scale to 1.028 on
   hover and focus. It now holds its size; hover and focus answer with the
   colour release, the wash and the cursor. Do not scale the image inside
   either: the frame clips it, so a scale crops the work. */

/* The copy column is narrow on purpose, so the claim needs a measure or it
   sets one word per line. */
.wk-item.is-wide .wk-line { max-width: 26ch; }
.wk-item.is-wide .wk-desc { max-width: 30ch; }

/* Below the two-column grid there is not enough width for a side-by-side,
   so the wide tile stacks.

   PICTURE FIRST, AND THE SAME WAY UP FOR BOTH. Stacked in DOM order the
   tile would read name → claim → description → picture, which puts three
   text blocks between the section head and the first thing worth looking
   at, and it would be the only reading order on the page that changes
   between two adjacent tiles. `order` on a flex column moves the picture
   above the copy for BOTH tiles, so a phone gets one rhythm: image, title,
   supporting text. The DOM keeps the copy first, so a screen reader and a
   keyboard still meet the name before the picture. */
@media (max-width: 900px) {
  .wk-item.is-wide,
  .wk-item.is-wide.is-flip {
    display: flex;
    flex-direction: column;
    grid-column: 1 / -1;
    /* `align-items: center` above is the GRID's vertical centring, and it
       means the opposite thing here: on a column flex the cross axis is
       horizontal, so it was centring the copy block. Measured at 834 it
       indented the Doit tile's head 180px while Race Intel's, being full
       width, looked untouched. */
    align-items: stretch;
  }
  .wk-item.is-wide .wk-fig {
    order: -1;
    margin-bottom: clamp(1.25rem, 3.5vh, 2.25rem);
  }
  .wk-item.is-wide .wk-line { max-width: none; }
  .wk-item.is-wide .wk-desc { max-width: none; }
}

/* One column below this: four of six columns is 66% of the viewport, and a
   4:3 stage at that width leaves the phones too small to read. */
@media (max-width: 900px) {
  .works {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(3.5rem, 9vh, 6rem);
  }
  .wk-item,
  .wk-item:nth-child(even) { grid-column: 1 / -1; }
}

/* THE PHONES WERE TOO SMALL TO READ ON A PHONE.

   Measured at a 390px viewport: the reserved box was 7:5, so 350px wide by
   250px tall; frames at 88% of that laid out 220px tall and 111.7px wide
   (each frame is 0.5076 wide for its height), and the outer two then scaled
   to 0.88 — 98px. Ninety-eight pixels of a 13-screen hi-fi system. The
   section that has to sell the work was the one place the work could not be
   seen, on the device most hiring visitors arrive on.

   The width is fixed at 350px and cannot be argued with, so the group has to
   spend it differently: overlap harder and grow. The box goes square, the
   frames take 92% of it, and the outer two tuck 26% of their own width behind
   the middle instead of 9%.

   Visible group = 1 + 2 x (0.86 - 0.26) = 2.20 frame-widths, against 2.70
   before. So at 350px a frame is now 159px wide and 313px tall — 62% wider
   than it was — and the group still lands inside the gutters with room.

   The hover fan is not restored here: there is no hover on a touch screen,
   so the resting composition is the only one that exists. */
@media (max-width: 560px) {
  /* flex-shrink was the actual culprit, and it had to go first.

     `.wk-shot` was `flex: 0 1 auto`, so once three natural-width frames
     stopped fitting the row, flex squeezed all three items equally and
     `object-fit: contain` letterboxed the photograph inside the squeezed
     box. Measured at 390px: each item laid out 117 x 322, and the phone
     drawn inside it was 117 x 230 with 46px of dead space above and below.
     The composition's whole geometry — "92% of the reserved height", "2.70
     frame-widths" — silently stopped applying, because it assumes the
     frames are at their natural width. It holds on every wider breakpoint
     (measured: the row needs 819.6px of the 877.4px tile at 1440, and 482px
     of 516px at 561px) and only fails here.

     `flex: 0 0 auto` gives the frames their real width back and hands the
     overlap entirely to the transforms, where it was always supposed to
     live. The row is then wider than the reserved box and the transforms
     pull the painted group back inside it. */
  .wk-item:not(.is-wide) .wk-shot { flex: 0 0 auto; }

  /* The row is now wider than its box by design, so the page has to be told
     not to count that as somewhere to scroll — measured, it pushed
     document scrollWidth to 416 against a 390 viewport, held back only by
     `overflow-x: hidden` on <body>, which is not a thing to rely on.

     `clip` and not `hidden`: `overflow-x: hidden` forces the other axis to
     `auto`, which would have clipped the entrance animation's 14px rise
     against 12.5px of vertical clearance. `clip` lets the y axis stay
     `visible`. Measured, nothing painted is cut either way — the group
     spans 22.9 to 367.1 inside a box of 20 to 370 — so on an engine old
     enough to ignore `clip` the picture is exactly what it is today. */
  .wk-item:not(.is-wide) .wk-screens {
    overflow-x: clip;
    overflow-y: visible;
  }

  /* Group width = 2w[(1 - t) + s/2]. With s = 0.86 and t = 0.26 that is
     2.34w, so at a 350px box w = 149.6, h = 294.7 and the box wants to be
     320.3 tall: 10/9 lands at 315 and the painted group at 344px inside
     350. Each frame is 147px wide against the 117px it was, and the middle
     one is drawn at its full height instead of letterboxed. */
  .wk-item:not(.is-wide) .wk-shots { aspect-ratio: 10 / 9; }
  .wk-item:not(.is-wide) .wk-shot { height: 92%; }
  .wk-item:not(.is-wide) .wk-shot:nth-child(1) .wk-screen { transform: translate(26%, 3%) scale(0.86); }
  .wk-item:not(.is-wide) .wk-shot:nth-child(3) .wk-screen { transform: translate(-26%, 3%) scale(0.86); }

  /* The wash tightens with the group, or it would spread past a composition
     that is now much narrower than its box. */
  .wk-item:not(.is-wide) .wk-shots::before { inset: -9% 0; }

  /* The wide tile's media is ONE picture, so none of the stage geometry
     above applies to it: its box already carries the render's own ratio
     (set from js/data.js) and the picture simply fills the column. Left to
     inherit, `aspect-ratio: 10/9` plus `height: 92%` squeezed it to 301x263
     inside a 350px tile — a letterboxed thumbnail of the largest picture on
     the page. */
  .wk-item.is-wide .wk-shot { height: auto; width: 100%; }
  .wk-item.is-wide .wk-screen { height: auto; }
}

/* ================= GRAPHIC DESIGN GRID =================
   `#graphic` is the home page's section AND the archive page's, so the
   home page's own framing — the rule that separates it from Selected Work
   above, and the air that goes with it — is scoped away from the archive,
   where the section is the whole page and has a page head instead. Two
   rules that never both apply, rather than one that declares a border and
   a second that takes it back. */
#graphic:not(.gx-work) {
  padding: clamp(3.5rem, 10vh, 7rem) 0 clamp(5rem, 14vh, 11rem);
  border-top: 1px solid var(--line);
}

/* Masonry, not grid. CSS Grid sizes every row to its tallest item, so
   mixing a 2:3 poster with a 4:3 spread punches holes nothing can fill.
   Here each tile keeps its artwork's real proportions and js/home.js
   packs them into whichever column is currently shortest, so size
   variation comes from the work itself and the columns still end level. */
.gd-grid {
  display: flex;
  align-items: flex-start;
  gap: clamp(1.25rem, 2.6vw, 2.5rem);
  padding: 0 var(--gut);
}

.gd-col {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(1.75rem, 3.4vw, 3.25rem);
}

/* ---------------- THE HOME PAGE'S SIX ----------------
   A COMPOSITION, NOT A CONTACT SHEET, AND NOT A MASONRY EITHER.

   WHAT WAS HERE. Three equal columns and two level rows: six covers at one
   width, one scale and two vertical positions. Every piece weighed the
   same, which is the one thing a selection is not — and the captions were
   pushed to a shared floor to hide the ragged bottoms the equal columns
   produced, so the grid was spending a rule to fix a problem it had made.

   WHY NOT PACK IT. The archive page's masonry puts each tile in whichever
   column is currently shortest, so the running order changes with the
   window width and the tab order goes column-major. That is a fair trade
   for fifteen covers; it is not a fair trade for a shortlist whose whole
   argument is that somebody chose these six and chose their order.

   WHAT IT IS NOW. Twelve columns, three bands, two pieces in each band, and
   every piece placed by hand: a wide one against a narrow one, the narrow
   one dropped below the wide one's top edge, and the pairing turned over on
   the last band so the run does not settle into a pattern. The order is
   still exactly `DATA.SELECTED`, left to right and top to bottom, on the
   page and on the keyboard alike — `grid-row` is written out rather than
   left to auto-placement, so the reading order cannot drift when a track
   width changes.

   THE OFFSETS ARE MARGINS, NOT ROW TRACKS. A margin moves the picture and
   its own caption together, so a caption always sits under the thing it
   names. Each band is then as tall as its tallest member and no taller,
   which is what keeps an asymmetric arrangement from opening holes.

   THE SPANS ARE CHOSEN AGAINST THE ARTWORK'S OWN RATIOS. Measured at 1440,
   where a track is 76px and a gap 37px:
     01 49ers      1.79  span 7   757 x 423
     02 Wages      1.50  span 4   417 x 278, dropped 7rem
     03 Music      1.33  span 5   530 x 398, indented one track
     04 Bauhaus    1.51  span 5   530 x 352, dropped 8rem
     05 Revealed   1.42  span 5   530 x 375, dropped 7rem
     06 Cards      1.36  span 7   757 x 555
   Nothing is cropped to fit a track: every frame still carries its own
   `--ar` from js/data.js. */
.gd-grid.is-select {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  /* Each piece sits at the top of its own band and the margin below does
     the rest; `stretch` would pull a short frame down to a tall one's
     height and undo the whole arrangement. */
  align-items: start;
  column-gap: clamp(1.25rem, 2.6vw, 2.5rem);
  /* Tighter than it was: the rows no longer have to hold a 70px drop as
     well as their own separation. */
  row-gap: clamp(2rem, 3.4vw, 3.25rem);
}
/* THE BAND EACH PIECE SITS IN, WRITTEN OUT.

   WHAT THIS REPLACED, AND WHY THE OLD ONE WAS MOSTLY AIR. Six landscape
   frames in three rows of two, with three of them pushed down by 70 to 80px
   of margin to break the rows up. The drops were doing the work that
   variation in SIZE should do, and they cost what they bought: measured at
   1440 the section ran about 2,050px tall, most of the right-hand column
   under Prevailing Wages was empty, and the six frames were six of the same
   shape at four similar widths.

   TWO OF THE SIX ARE PORTRAIT NOW (see js/data.js and the two crop jobs in
   tools/build-mockups.py), so the composition has verticals to build on and
   the sizes can carry the rhythm instead of the margins. Each row now pairs
   a wide piece with a narrow one, and the pairing alternates:

     1  49ers 7 tracks   ·  Wages 5      landscape + landscape
     2  Music 7          ·  Bauhaus 4    landscape + PORTRAIT
     3  Revealed 4       ·  Cards 7      PORTRAIT + landscape

   The two halves of rows 2 and 3 land within 12px of each other in height,
   which is what makes a 1.33 landscape and a 0.75 portrait read as a pair
   rather than as an accident. Reading order is untouched: left to right,
   top to bottom, in the order js/data.js authors them, on a desktop and on
   a keyboard alike.

   THE ONE-TRACK GAPS ARE THE ASYMMETRY. Rows 2 and 3 leave a track empty
   between the two pieces and at opposite ends, so no two rows share a
   vertical. Nothing is centred and nothing lines up by accident. */
.gd-grid.is-select .gd-item:nth-child(1) { grid-column: 1 / 8;  grid-row: 1; }
.gd-grid.is-select .gd-item:nth-child(2) { grid-column: 8 / 13; grid-row: 1; }
.gd-grid.is-select .gd-item:nth-child(3) { grid-column: 1 / 8;  grid-row: 2; }
.gd-grid.is-select .gd-item:nth-child(4) { grid-column: 9 / 13; grid-row: 2; }
.gd-grid.is-select .gd-item:nth-child(5) { grid-column: 1 / 5;  grid-row: 3; }
.gd-grid.is-select .gd-item:nth-child(6) { grid-column: 6 / 13; grid-row: 3; }

/* ONE DROP, NOT THREE. The heights carry the composition now, so the only
   offset left is the one that stops the top of the section being a flat
   line across two frames — and it is a third of what the old ones were. */
.gd-grid.is-select .gd-item:nth-child(2) { margin-top: clamp(1.5rem, 2.6vw, 3.25rem); }
.gd-grid.is-select .gd-item:nth-child(6) { margin-top: clamp(1.5rem, 2.6vw, 3.25rem); }

/* THE CAPTION FOLLOWS ITS OWN PICTURE. The equal grid pushed all three
   captions in a row to one floor, because three covers of three ratios at
   one width ended at three different heights and a row of labels that will
   not sit still reads as a layout that has not been set. Nothing is level
   here by design, so a shared floor would be levelling two things that were
   never on the same line — and it would leave a gap under the shorter
   picture with nothing in it. The caption sits where the artwork ends. */
.gd-grid.is-select .gd-item {
  display: block;
}
.gd-grid.is-select .gd-cap {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

/* TWO EQUAL COLUMNS ON A TABLET. Below 1024 a seven-track frame is about
   440px and a four-track one about 230px, and 230px is a thumbnail rather
   than a piece of work. The composition needs width to be a composition, so
   under it the six go back to a plain pair — still in the authored order,
   still uncropped. */
@media (max-width: 1023px) {
  .gd-grid.is-select {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: clamp(2rem, 4vw, 3.5rem);
  }
  .gd-grid.is-select .gd-item:nth-child(n) {
    grid-column: auto;
    grid-row: auto;
    margin-top: 0;
  }
}

/* ONE COLUMN ON A PHONE, IN THE AUTHORED ORDER. */
@media (max-width: 600px) {
  .gd-grid.is-select { grid-template-columns: minmax(0, 1fr); }
}

/* THE WAY THROUGH TO THE REST, AND IT IS THE SIZE OF ONE.

   WHAT WAS WRONG WITH IT. It was a 13px tracked mono label pushed to the
   right-hand end of a hairline — the same treatment the case tiles' credit
   line gets inside
   a tile, where it is one of two labels in a footer and the tile itself is
   the link. Here it is the ONLY control in the section and the only route
   to nine more pieces of work, and at label size at the far edge of the
   page it read as a footnote about the selection rather than the way out of
   it.

   IT IS A CLOSING ROW OF ITS OWN NOW, and the size is the point: 28px on a
   phone and 40px on a laptop, which puts it between the covers' own titles
   and the section head above them. Large enough to be the next thing read
   after the last cover, and still a clear step under the display head that
   names the section.

   THE RULE ABOVE IT IS THE SEPARATION, and it runs the full measure: it is
   the end of the selection, so it closes the six rather than underlining
   the link. The clearance over it is generous for the same reason — this
   row belongs to the section, not to the last project in it. */
.gd-more {
  /* A block-level flex row. The words inside it sit on the left gutter, and
     the underline and the focus ring are drawn on the inner <span> so
     neither of them stretches to the measure with it. */
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 0.55rem;
  /* NO RULE ABOVE IT. The line was there to close the six covers, but the
     shortlist is a composition with a deliberately uneven bottom edge —
     three pieces ending at three heights — and a hairline ruled straight
     across it levelled the one thing the arrangement is saying. The air
     above the link is the separation; it does not need a line as well. */
  margin: clamp(3.25rem, 7vw, 5rem) var(--gut) 0;
  font-family: var(--display);
  font-weight: 600;
  /* 28 -> 40px. The vw term is what carries it between the two, and the
     floor is checked rather than guessed: at a 390px viewport the row's
     available measure is 359px and the label plus its chevron draws 292,
     so the phone has 67px of slack and cannot overflow. */
  font-size: clamp(1.75rem, 2.9vw, 2.5rem);
  line-height: 1.1;
  letter-spacing: -0.03em;
  color: var(--bone);
  transition: color 0.5s var(--ease), border-color 0.6s var(--ease);
}
/* THE CHEVRON IS ON THE SAME ROW AS THE WORDS, not hung under them. It is
   sized as a fraction of the label rather than in pixels, so it stays in
   proportion at both ends of the clamp, and it is the only part of the
   control that travels on hover. */
.gd-more .chev {
  width: 0.72em;
  height: 0.72em;
  transition: transform 0.55s var(--ease);
}
.gd-more:hover,
.gd-more:focus-visible { color: var(--signal); }
.gd-more:hover .chev,
.gd-more:focus-visible .chev { transform: translateX(9px); }
/* The underline is drawn under the WORDS, not under the row, and it is
   already there at rest: this is a link, and a link that only looks like
   one when a pointer is over it is a link a reader has to find. Hover and
   keyboard focus do not create it — they take it up to the accent and to
   full strength, which is the state the brief asks to be visible. */
.gd-more-text {
  position: relative;
  padding-bottom: 0.14em;
  border-bottom: 1px solid var(--bone-mute);
  transition: border-color 0.5s var(--ease);
}
.gd-more:hover .gd-more-text,
.gd-more:focus-visible .gd-more-text {
  border-bottom-color: var(--signal);
  border-bottom-width: 2px;
}
/* The keyboard ring goes round the words rather than round a full-width
   row with a rule in it — `:focus-visible` on the <a> would otherwise
   outline the border-box, which here is the whole measure. */
.gd-more:focus-visible { outline: none; }
.gd-more:focus-visible .gd-more-text {
  outline: 2px solid var(--signal);
  outline-offset: 6px;
  border-radius: 2px;
}

.gd-item {
  display: block;
  width: 100%;
  text-align: left;
}

.gd-frame {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--void-2);
  /* SQUARE, with the two case previews. See `.wk-item.is-wide .wk-shots`
     for why: these are openings cut in the page, not cards laid on it. */
  border-radius: 0;
  /* Set per tile from the data, so space is reserved before load. */
  aspect-ratio: var(--ar, 3 / 4);
}

/* The travelling layer, exactly as the case tiles have it: 12% taller than
   the opening, hung 6% past each edge, moved by Core.drift() on the home
   page only. On `graphic.html` the wrapper is inert — no tween is created
   for it — so the archive's covers sit still, which is right for a page
   that is a contact sheet rather than a shortlist. */
/* `--drift` IS THE OVERHANG, AND IT IS READ BACK BY THE TWEEN. Core.drift()
   measures the layer against its frame rather than being handed a number,
   so this declaration is the single source of truth and the two can never
   disagree. A frame that needs a shorter travel says so here and nothing
   else has to change. */
.gd-frame .drift-layer {
  position: absolute;
  inset: calc(var(--drift, 6%) * -1) 0;
  display: block;
  will-change: transform;
}
/* THE TWO PORTRAITS TRAVEL LESS, AND THE ARITHMETIC IS WHY. `cover` on a
   layer taller than its frame crops the picture's height by
   `1 - 1/(1 + 2 * drift)`: at 6% that is 10.7% of the file, and both poster
   crops keep only about 8% of ground above and below the artwork. At 4% the
   crop is 7.4% and the poster is whole at both ends of the run. See the
   crop boxes in tools/build-mockups.py. */
.gd-grid.is-select .gd-item:nth-child(4) .gd-frame,
.gd-grid.is-select .gd-item:nth-child(5) .gd-frame { --drift: 4%; }

.gd-frame img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  filter: grayscale(1) contrast(1.06) brightness(0.82);
  transform: scale(1.02);
  transition: transform 1.2s var(--ease), filter 0.9s var(--ease);
}
.gd-item:hover .gd-frame img {
  filter: grayscale(0) contrast(1) brightness(1);
}
/* No cover grows on hover, on the landing shortlist or on graphic.html
   (15 September 2026). */

/* NO HOVER, NO WAITING FOR COLOUR (15 September 2026). A phone or tablet has
   no gesture that releases the colour, so on a device that cannot hover every
   preview rests in the colour a pointer would reveal: both pages, case
   previews and graphic covers. Detected by hover capability, not width, so a
   narrow desktop window keeps hover-to-colour. The scroll curl copies each
   image's computed filter, so its canvas follows. */
@media (hover: none) {
  .wk-item .wk-screen { filter: grayscale(0) brightness(1.08); }
  .wk-item.is-lightground .wk-screen { filter: grayscale(0); }
  #works .wk-item .wk-shots::before { opacity: 1; }
  .gd-frame img { filter: grayscale(0) contrast(1) brightness(1); }
}

/* THE IMAGE-COUNT BADGE IS GONE, AND HAS NOT MOVED INTO THE CAPTION.
   It sat in the corner of every cover reading "5 IMAGES" over a scrim.
   It counted files, which is inventory rather than anything about the
   work, and it put a dark plate over the artwork to say so. A reader who
   opens a gallery gets a count from the lightbox's own counter, which is
   the only place a count is answering a question anyone asked. */

.gd-cap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin-top: 0.9rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
  transition: border-color 0.6s var(--ease);
}
.gd-item:hover .gd-cap { border-color: var(--bone-mute); }

.gd-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(0.9375rem, 1.25vw, 1.1875rem);
  letter-spacing: -0.02em;
  text-transform: uppercase;
}
.gd-meta { color: var(--bone-soft); }

@media (max-width: 560px) {
  .gd-title { font-size: 0.875rem; }
}

/* On a phone the section head is two labels fighting over one line: the
   heading and its trailing note were both at --t-head, 44px apart at 390px.
   The note steps down to the smallest label role so the numbered heading
   leads its own section again. Nothing moves on a wide screen. */
@media (max-width: 560px) {
  .section-head { gap: 1.25rem; }
  .section-head .kicker:last-child:not(:first-child) {
    font-size: var(--t-label-s);
    letter-spacing: 0.14em;
    color: var(--bone-soft);
  }
}

/* ================= THE ARCHIVE PAGE =================
   graphic.html: the complete collection under a page head. It borrows the
   home page's bar, tile, caption, lightbox and closing control, so nothing
   here restyles any of those — this is only the head, the section's own
   padding, and the two rules that stop the home page's `#graphic` spacing
   applying to a page whose whole body is that section. */

.gx-head {
  padding: calc(44px + 2 * var(--nav-pad) + clamp(3rem, 10vh, 7rem)) var(--gut)
           clamp(2.5rem, 7vh, 4.5rem);
  max-width: 92rem;
  margin: 0 auto;
}
.gx-title {
  margin-top: 0;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(3rem, 11vw, 9rem);
  line-height: 0.9;
  letter-spacing: -0.05em;
  text-transform: uppercase;
}
/* The serif italic carries the second word, exactly as it does in the
   home page's section head for this same collection. */
.gx-title em.serif { text-transform: none; letter-spacing: -0.03em; }
/* Instrument Serif's g descender needs room inside the entrance mask. */
.gx-title .mask:last-child {
  padding-bottom: 0.18em;
  margin-bottom: -0.18em;
}
.gx-sub {
  margin-top: clamp(1.5rem, 4vh, 2.5rem);
  max-width: 56ch;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1rem, 1.5vw, 1.35rem);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--bone-dim);
}
.gx-rail {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 2rem;
  justify-content: space-between;
  margin-top: clamp(2rem, 5vh, 3rem);
  padding-top: 1.1rem;
  border-top: 1px solid var(--line);
  color: var(--bone-soft);
}

/* The archive's grid is the whole page: the page head above it has already
   done the introducing, so the section starts flush and only pays out at
   the bottom. */
.gx-work {
  padding: 0 0 clamp(3rem, 9vh, 6rem);
}

/* THE WAY BACK. The case pages close on a full-width control and this page
   closes on the same one, so every page that is not the home page ends the
   same way. It is drawn here rather than borrowed from css/case.css,
   because this page does not load that stylesheet and should not: it would
   pull in five thousand lines of case-study theming for two rules. */
.gx-out {
  display: flex;
  align-items: center;
  gap: 0.5rem 1.5rem;
  min-height: 56px;
  padding: 1rem var(--gut);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-weight: 500;
  font-size: var(--t-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--bone);
  transition: background-color 0.5s var(--ease), color 0.5s var(--ease);
}
.gx-out:hover,
.gx-out:focus-visible { background: var(--void-2); color: var(--signal); }
.gx-out-label { display: inline-flex; align-items: center; gap: 0.9rem; }
/* Square now, because the glyph is: the shaft arrow this carried was
   34 x 12 and the chevron that replaced it is a 24-square. See `.chev` in
   css/base.css. */
.gx-out-arrow {
  display: block;
  width: 18px;
  height: 18px;
  transition: transform 0.55s var(--ease);
}
.gx-out-arrow .chev { width: 100%; height: 100%; }
.gx-out:hover .gx-out-arrow { transform: translateX(-6px); }

/* The foot of graphic.html: the address, then the same `.ft-rail` the
   landing page closes on, with the landing footer's bottom padding. */
.gx-foot {
  padding: clamp(1.75rem, 5vh, 3rem) var(--gut) clamp(1.5rem, 4vh, 2.5rem);
}
.gx-mail {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  transition: color 0.5s var(--ease);
}
.gx-mail:hover,
.gx-mail:focus-visible { color: var(--signal); }
.gx-foot .ft-rail { margin-top: clamp(1.5rem, 5vh, 3rem); }

/* ================= EXPERTISE MARQUEE ================= */
#expertise {
  padding: clamp(3rem, 9vh, 7rem) 0 clamp(5rem, 14vh, 11rem);
  border-top: 1px solid var(--line);
  padding-top: clamp(3rem, 9vh, 7rem);
}

.mq {
  overflow: hidden;
  white-space: nowrap;
}
.mq + .mq { margin-top: clamp(0.1rem, 0.8vh, 0.5rem); }

.mq-track {
  display: inline-flex;
  width: max-content;
  will-change: transform;
}

/* Two identical halves so an xPercent -50 loop is seamless. */
.mq-half {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}

.mq-item {
  display: inline-flex;
  align-items: center;
  gap: clamp(1.25rem, 3vw, 3rem);
  padding-right: clamp(1.25rem, 3vw, 3rem);
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(2.25rem, 7.6vw, 7rem);
  line-height: 1.05;
  letter-spacing: -0.045em;
  text-transform: uppercase;
}
.mq-item i {
  font-family: var(--serif);
  font-style: normal;
  font-size: 0.42em;
  color: var(--bone-soft);
  transform: translateY(-0.12em);
}

.mq-serif .mq-item {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.75rem, 5.6vw, 5.25rem);
  letter-spacing: -0.015em;
  text-transform: none;
  color: var(--bone-dim);
}
.mq-serif .mq-item i { color: var(--bone-ghost); }

/* ================= FOOTER ================= */
#contact {
  position: relative;
  padding: clamp(5rem, 14vh, 11rem) var(--gut) clamp(1.5rem, 4vh, 2.5rem);
  border-top: 1px solid var(--line);
  overflow: hidden;
}

/* "Let's Talk" measures 4.6em; 92vw / 4.6 ≈ 20vw, held back to 19vw
   so the scroll scale-up still lands inside the viewport. */
.ft-title {
  font-size: clamp(2rem, 19vw, 26rem);
  line-height: 0.86;
  letter-spacing: -0.05em;
  text-align: center;
  transform-origin: 50% 100%;
  will-change: transform;
}
.ft-title .ft-line { display: inline; }

/* Hugs the text so the fitter measures the words, not the column. */
.ft-fit {
  display: inline-block;
  white-space: nowrap;
}

/* The heading is a mail link. inline-block so it hugs .ft-fit exactly and
   fitToWidth()'s offsetWidth measurement is unchanged; the <h2> keeps the
   centring and the scroll-driven scale. */
.ft-talk {
  display: inline-block;
  color: inherit;
}

/* The rule is drawn under the WORDS, not under .ft-fit, so it stops before
   the raised star instead of running out past it.

   Sized in em: the heading's font-size is set by the fitter and then
   scaled on scroll, so an em rule keeps its weight against the letters at
   every viewport. `max()` stops it vanishing at the 2rem floor.

   It wipes left to right on the site's standard --ease, the same gesture
   and curve as .nav-link::after and .nav-mail — one motion, three
   places. */
.ft-words {
  position: relative;
  display: inline-block;
}
.ft-words::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.015em;
  height: max(2px, 0.022em);
  background: var(--signal);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.85s var(--ease);
}
.ft-talk:hover .ft-words::after,
.ft-talk:focus-visible .ft-words::after { transform: scaleX(1); }

/* Only TALK answers. "Let's" stays bone, so the colour marks the verb
   rather than repainting the whole line. */
.ft-title em { transition: color 0.6s var(--ease); }
.ft-talk:hover .ft-line em,
.ft-talk:focus-visible .ft-line em { color: var(--signal); }

@media (prefers-reduced-motion: reduce) {
  .ft-words::after { transition-duration: 0.01ms; }
  .ft-title em { transition-duration: 0.01ms; }
}
.ft-title em {
  font-size: 1.12em;
  letter-spacing: -0.02em;
}

/* The signature: a footnote mark after "Talk", pointing at the
   availability note directly below — an asterisk doing an asterisk's
   job. Same glyph as the marquee separator, so the site keeps one star.
   It lives INSIDE .ft-fit so fitToWidth measures it and the fitted
   title still lands inside the viewport, and it inherits the scroll
   scale with the rest of the line. Decorative, so aria-hidden. */
.ft-star {
  font-family: var(--serif);
  font-style: normal;
  font-size: 0.32em;
  color: var(--signal);
  display: inline-block;
  vertical-align: 1.35em;
  margin-left: 0.08em;
}

/* The note the mark points to. */
.ft-note-star {
  font-family: var(--serif);
  color: var(--signal);
  margin-right: 0.4em;
}

.ft-mail {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin: clamp(2.5rem, 7vh, 5rem) auto 0;
  width: max-content;
  max-width: 100%;
  font-family: var(--display);
  font-weight: 400;
  /* THE HIERARCHY WAS UPSIDE DOWN ON A PHONE. The floor was 0.95rem, so at
     390px the email address — the single thing this whole footer exists to
     hand over — rendered SMALLER than the availability sentence underneath
     it, which is set at --t-body. The one line a hiring visitor is meant to
     copy was the quietest text in the section.

     The floor is now 1.25rem, which clears --t-body at every width, and the
     preferred value rises to 4.2vw so it grows on its own between the floor
     and the 2.1rem ceiling instead of sitting flat at the floor until 800px.
     Measured: 24 characters of the sans at 400 / 1.25rem is ~232px, inside the
     350px column with room. The ceiling is unchanged, so nothing about the
     desktop footer moves. */
  font-size: clamp(1.25rem, 4.2vw, 2.1rem);
  letter-spacing: -0.02em;
  overflow: hidden;
}
/* No `will-change`. There are 24 of these, one per character of the email
   address, and they only move while the pointer is actually over the link —
   promoting all 24 for the whole visit was 24 composited layers waiting on a
   hover that mostly never comes. GSAP promotes them while it animates them. */
.ft-mail .ch {
  display: inline-block;
}

/* The availability statement. "Open to work" was a pulsing dot in the nav
   and nothing else — it never said what role, when, or where. */
.ft-avail {
  margin: clamp(1.5rem, 4vh, 2.5rem) auto 0;
  /* wide enough that the sentence keeps its own line break instead of
     wrapping "available now" onto a third orphan line */
  max-width: 64ch;
  text-align: center;
  font-size: var(--t-body);
  line-height: 1.68;
  color: var(--bone-dim);
}
.ft-avail strong { color: var(--bone); font-weight: 500; }

.ft-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(1.5rem, 5vw, 4rem);
  margin-top: clamp(2.5rem, 7vh, 4.5rem);
}

.ft-link {
  font-family: var(--mono);
  font-weight: 500;
  font-size: var(--t-nav);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  /* 44px hit area without inflating the label */
  display: inline-flex;
  align-items: flex-end;
  min-height: 44px;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--line);
  transition: border-color 0.5s var(--ease);
}
.ft-link:hover { border-color: var(--bone); }


/* Two stacked copies of the label; hover slides one out, one in. */
/* The box and the two copies inside it must be exactly the same height, or
   the swap lands crooked. `top: 100%` on the second copy resolves against
   the BOX, while `translateY(-100%)` resolves against the SPAN — so with the
   body's inherited line-height of 1.6 the spans were 19.2px inside a 13.8px
   box and the incoming label arrived 5.4px high, clipped at the bottom.
   Pinning line-height to the same 1.15 makes both percentages agree. */
.ft-swap {
  display: block;
  position: relative;
  height: 1.15em;
  line-height: 1.15;
  overflow: hidden;
}
.ft-swap > span {
  display: block;
  line-height: 1.15;
  transition: transform 0.6s var(--ease);
}
/* The second copy has to WAIT one line below, not sit on top of the first.
   With `inset: 0` both copies stacked at the same spot, so hovering slid
   both of them out of the box together and the link vanished. `top: 100%`
   parks it just under the visible line; the shared -100% then carries the
   first out and the second exactly into its place. */
.ft-swap > span:last-child {
  position: absolute;
  left: 0;
  top: 100%;
  width: 100%;
  color: var(--bone-dim);
}
.ft-link:hover .ft-swap > span { transform: translateY(-100%); }

.ft-rail {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: clamp(4rem, 12vh, 9rem);
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
/* THE WHOLE RAIL IS THE SIGNATURE, SO THE WHOLE RAIL IS THE SECOND INK.
   The three items are one line about the author of the page — where he
   is, whose it is, who made it — and they were set in three different
   weights of the same grey, which made the last one look like a leftover.
   Vermilion is already the site's signature colour; measured at 5.06:1 on
   --void, it passes AA at this size. */
.ft-rail .kicker { color: var(--signal); }
#clock { font-variant-numeric: tabular-nums; }

/* TWO ROWS ON A PHONE (15 September 2026), on the landing page and
   graphic.html. First row: place and clock at the left gutter, copyright at
   the right one. Second row: the credit, centred across the full width.
   Nothing is shrunk to fit: the labels keep their size and tracking, and on
   the narrowest phones the place-and-clock label wraps inside its column
   rather than pushing the copyright onto a row of its own. */
@media (max-width: 720px) {
  .ft-rail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    gap: 1.1rem 1.25rem;
  }
  .ft-rail .kicker:nth-child(2) { text-align: right; }
  .ft-rail .kicker:last-child {
    grid-column: 1 / -1;
    text-align: center;
  }
  #clock { white-space: nowrap; }
}

/* ================= LIGHTBOX ================= */
#lightbox {
  position: fixed;
  inset: 0;
  z-index: 9700;
  background: rgba(7, 7, 6, 0.965);
  opacity: 0;
  visibility: hidden;
  padding: clamp(3.5rem, 8vh, 6rem) var(--gut) clamp(1.5rem, 4vh, 3rem);
}

/* `align-items: center` used to size the row to its content, so the
   image's max-height had nothing definite to resolve against and tall
   posters ran off the bottom. Stretch the row instead and let the stage
   be a definite box the image fits inside. */
.lb-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 0.85fr);
  grid-template-rows: minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 4rem);
  height: 100%;
  align-items: stretch;
}

.lb-stage {
  position: relative;
  min-height: 0;
  min-width: 0;
  margin: 0;
}

/* A quiet pulse while the full-resolution slide decodes. */
.lb-stage.is-loading::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  border: 1px solid var(--bone-mute);
  border-top-color: var(--bone);
  border-radius: 999px;
  animation: lb-spin 0.9s linear infinite;
}
@keyframes lb-spin { to { transform: rotate(360deg); } }
.lb-stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* contain, never cover — the whole artwork has to be visible */
  object-fit: contain;
  object-position: center;
  /* No `will-change`: this lives in a dialog that is closed for nearly the
     whole visit, and a promoted full-viewport image costs GPU memory the
     entire time it is doing nothing. */
}

.lb-panel {
  align-self: center;
  max-height: 100%;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: none;
}
.lb-panel::-webkit-scrollbar { display: none; }

.lb-title {
  font-size: clamp(1.4rem, 3vw, 2.6rem);
  line-height: 0.98;
  margin: 0.85rem 0 1.25rem;
}
.lb-desc {
  font-size: var(--t-body-s);
  line-height: 1.68;
  color: var(--bone-dim);
  max-width: 46ch;
}
.lb-cap {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
  text-transform: none;
  letter-spacing: 0.08em;
}

/* Optional context for pieces that carry it (DrinkUp): a placement status
   and a short list of what Rodion did. Set at the description's size and
   measure, so the artwork stays the largest thing in the preview. */
.lb-status {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  margin: -0.35rem 0 1.25rem;
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-label-s);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bone);
}
.lb-status::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px var(--bone-soft);
}
.lb-role { margin-top: 1.35rem; max-width: 46ch; }
.lb-role-h {
  margin-bottom: 0.65rem;
  font-family: var(--mono);
  font-size: var(--t-label-s);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bone-soft);
}
.lb-role-list { display: grid; gap: 0.6rem; }
.lb-role-list li {
  position: relative;
  padding-left: 1.1rem;
  font-size: var(--t-body-s);
  line-height: 1.55;
  color: var(--bone-dim);
}
.lb-role-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.78em;
  width: 6px;
  height: 1px;
  background: var(--bone-soft);
}
.lb-status[hidden],
.lb-role[hidden] { display: none; }

.lb-nav {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  margin-top: 1.75rem;
}
.lb-nav button {
  width: 44px;
  height: 44px;
  border: 1px solid var(--line);
  border-radius: 999px;
  display: grid;
  place-items: center;
  font-size: 0.9rem;
  transition: background-color 0.4s var(--ease), color 0.4s var(--ease), border-color 0.4s var(--ease);
}
.lb-nav button:hover {
  background: var(--bone);
  border-color: var(--bone);
  color: var(--void);
}
.lb-nav button[disabled] { opacity: 0.25; pointer-events: none; }
/* The two characters that used to sit in these buttons are now the site's
   one drawn chevron, sized to the 44px control rather than to a font. */
.lb-nav button .chev { width: 17px; height: 17px; }

.lb-close {
  position: absolute;
  top: clamp(1rem, 3vh, 2rem);
  right: var(--gut);
  width: 46px;
  height: 46px;
  border: 1px solid var(--line);
  border-radius: 999px;
  display: grid;
  place-items: center;
  transition: background-color 0.4s var(--ease), border-color 0.4s var(--ease);
}
.lb-close span {
  position: absolute;
  width: 15px;
  height: 1px;
  background: var(--bone);
  transition: background-color 0.4s var(--ease);
}
.lb-close span:first-child { transform: rotate(45deg); }
.lb-close span:last-child { transform: rotate(-45deg); }
.lb-close:hover { background: var(--bone); border-color: var(--bone); }
.lb-close:hover span { background: var(--void); }

body.lb-open { overflow: hidden; }
/* The blur is attached only while the lightbox is open. Declared on the
   element itself it created a full-viewport backdrop-filter layer that the
   compositor carried for the whole visit, for a dialog that is shut. */
body.lb-open #lightbox {
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

@media (max-width: 900px) {
  /* The artwork row keeps at least 46vh. With a 0 minimum, a long panel
     (DrinkUp's context) squeezed the row and the stage spilled over the
     text; now the dialog scrolls instead. */
  .lb-grid {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(46vh, 1.15fr) auto;
    gap: 1.5rem;
    align-items: stretch;
    overflow-y: auto;
  }
  .lb-stage { min-height: 46vh; }
  /* THE ARROWS SIT UNDER THE PICTURE ON A PHONE (16 September 2026).
     Beside the artwork on a desktop, the controls close the panel. Stacked
     under the artwork they were the last thing after a long description:
     stepping through a gallery meant scrolling past the whole text to reach
     the next arrow, then scrolling back up to see what it had loaded.
     `order` puts them at the top of the panel, directly under the picture,
     where the thumb already is. The DOM is untouched, so a screen reader and
     the keyboard still meet the text before the buttons. */
  .lb-panel { overflow: visible; max-height: none; align-self: start; display: flex; flex-direction: column; }
  .lb-nav { order: -1; margin-top: 0; margin-bottom: 1.5rem; }
  .lb-desc { max-width: none; }
  .lb-role { max-width: none; }
}

/* ================= SCROLL CURL (js/curl.js) =================
   A canvas sits just before each landing image's clipping frame and draws
   that image, curled by scroll speed. The host becomes the canvas's
   containing block; the frame paints over the canvas, so its ground has to
   be transparent once the canvas is live (the image itself is made
   transparent inline). Without the script, WebGL or motion, none of these
   classes is ever added and the page is exactly as before. */
.curl-host { position: relative; }
.curl-canvas {
  position: absolute;
  display: block;
  /* The base reset caps media at their container's width. This canvas is
     deliberately wider than its frame (room for the bowed sides), and a cap
     squeezed the drawn picture narrower than the image it stands for. */
  max-width: none;
  max-height: none;
  pointer-events: none;
}
.curl-live { background: transparent; }

/* Blur belongs to the text, not the old sliding mask. */
.is-home .wk-head .mask { overflow: visible; }
.content-word { display: inline-block; }
