/* =========================================================
   Lajos Tamás Jakab — Portfolio
   Shared styles for individual project case-study pages
   Design source: Figma "Portfolio_website" — Hachi page (node 45:81)
   ========================================================= */

.project-page {
  /* min-height:100dvh + flex column so a page shorter than the viewport
     still pushes .project-footer (position:sticky; bottom:0) down to the
     bottom of the viewport instead of leaving it sticky over the middle
     of the (too-short) content. main flex:1 absorbs the slack; on pages
     tall enough to scroll this has no effect and the footer still sticks
     to the viewport bottom as content scrolls past it. */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  padding-top: clamp(24px, 4vw, 40px);

  /* How far the footer's scrim reaches ABOVE the pills. Declared here
     rather than written into the gradient, because two rules depend on
     it and they must not drift apart: .project-footer::before draws the
     scrim this tall, and the padding below has to clear it. */
  --footer-scrim: 64px;

  /* Derived from the footer’s measured height (main.js sets
     --footer-height), not a hand-tuned clamp. .project-footer is sticky
     over the content, so this padding has to be at least as tall as the
     footer really is — and “really is” changes with the viewport, the
     webfont, and any edit to a pill’s label. The fallback covers the
     no-JS case at the tallest the footer gets.

     Note what this padding does and does not do. It sits BELOW the footer
     — footer and main are adjacent siblings, so it never separates them —
     which is what gives the sticky footer somewhere to unstick to at the
     end of the scroll. Keeping the last section clear of the footer is a
     different job, done by .project-container's own padding-bottom. */
  padding-bottom: calc(var(--footer-height, 205px) + 24px);
}

.project-page > .project-container {
  /* width:100% is load-bearing, not decorative: .project-container already
     centers itself with margin-inline:auto (below). Once .project-page is
     a flex column, a flex item with auto cross-margins and an auto cross
     size is sized to fit its content first and only THEN centered with
     the leftover space (CSS Flexbox §8.1) — stretch is skipped entirely.
     That silently discarded the 390px viewport constraint and let the
     container's widest unshrinkable descendant win, reintroducing a
     horizontal-scroll bug identical in spirit to bug 1. Giving it an
     explicit width turns the cross size back into a definite value, so
     margin-inline:auto goes back to plain centering within the leftover
     space (min(100%, max-width), same as its pre-flex behaviour). */
  width: 100%;
  flex: 1 0 auto;
}

.project-container {
  /* The gutter needs to sit OUTSIDE the 1194px design width, not eat
     into it — otherwise the grid-4 columns (Problem/Solution) only
     ever reach their true 274px/580px size at very wide viewports,
     while flat max-width:580px text elsewhere hits it immediately,
     so the two wrap at different points on any normal laptop screen.
     Padding the max-width itself by 2x the gutter means content
     reaches the full 1194px as soon as the viewport is wide enough,
     same as everything else on the page. */
  max-width: calc(var(--container-max) + 2 * var(--gutter));
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  flex-direction: column;
  gap: clamp(56px, 8vw, 96px);
  /* Clears the sticky footer's scrim at the end of the page. main and the
     footer are adjacent siblings with nothing between them, so at the
     bottom of the scroll the last section's final line ended exactly at
     the footer's top edge — and the scrim starts 64px ABOVE that edge, so
     the closing line of every project came out washed grey. (.project-
     page's padding-bottom does not help here: it lands below the footer,
     not between the two.) Reserving the scrim's own height plus a 32px
     breath is what actually lifts the last line clear of the gradient. */
  padding-bottom: calc(var(--footer-scrim, 64px) + 32px);
}

.project-section {
  /* Cancels the global `section { padding-inline: var(--gutter) }` rule
     from style.css (written for the landing page's top-level sections) —
     .project-container already supplies that gutter once; without this
     override every .project-section on this page was doubling it, over-
     narrowing the whole 1194px column and everything laid out inside it. */
  padding-inline: 0;
  display: flex;
  flex-direction: column;
  gap: 32px;
  width: 100%;
}

.project-section-title {
  margin: 0;
  font-size: 32px;
  font-weight: 500;
  color: var(--color-text);
}

/* Wraps a section's heading + its lead paragraph or text-block: every
   piece of text on this page — heading to paragraph, paragraph to
   paragraph — sits exactly 32px from its neighbour, no exceptions. */
.section-intro {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.project-section-lead {
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
  max-width: 580px;
}

/* =========================
   Hero
   ========================= */

.hero-image {
  width: 100%;
  aspect-ratio: 1194 / 670;
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.hero-image img { width: 100%; height: 100%; object-fit: cover; }

.hero-description {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
}

.hero-text {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 581px;
  max-width: 100%;
}

.hero-text h1 {
  margin: 0;
  font-size: 32px;
  font-weight: 500;
  /* Default (browser "normal") line-height adds noticeably more
     leading above a 32px line than the 16px meta text gets at its own
     line-height, so even though both boxes start at the exact same
     top (verified via getBoundingClientRect), the glyphs themselves
     read as visually offset from the year/role text beside them. */
  line-height: 1.2;
}

.hero-text p {
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
}

/* Short tagline block directly under the hero title (Project TEVE): a
   bracketed uppercase label line immediately followed by a sentence, no
   gap between them (same text block in Figma) — sits below the title
   with the hero-text's own 8px gap, same as the title-to-subtitle gap
   on every other project page. */
.hero-tagline {
  display: flex;
  flex-direction: column;
  font-size: 16px;
  line-height: 1.5;
}

.hero-tagline .bracket-label {
  text-transform: uppercase;
}

.hero-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 24px;
  /* Pushes flush to the right regardless of its own (usually much
     narrower than 580px) content width — forcing a fixed 580px box
     here was wrong: combined with hero-text's fixed 581px it left
     almost no slack before the row wrapped, which then stranded this
     block at the left edge of its own wrapped line instead of at the
     right. max-width just keeps it from ever growing past the design's
     column if a line is unexpectedly long. */
  margin-left: auto;
  max-width: 580px;
  font-size: 16px;
  text-align: right;
  white-space: nowrap;
}

/* =========================
   4-column grid system
   ---------------------------------------------------------
   The whole page is built on one shared grid: 4 tracks, 32px
   gutters, 1194px total (274px per track at full width). Problem/
   Solution each span 2 tracks, photo/method rows use 1 track per
   item. Grid (not flex-wrap) is deliberate: flex-basis + wrap
   reflows each row at its own width threshold, so a 4-item row and
   a 2-item row can (and did) wrap to a different column count at
   the same viewport width, breaking the alignment between rows.
   `repeat(4, 1fr)` never reflows on its own — every row stays in
   lockstep down to the single explicit mobile breakpoint below.
   ========================= */

.grid-4 {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 32px;
  width: 100%;
}

.span-2 {
  grid-column: span 2;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.span-1 {
  grid-column: span 1;
}

.text-block {
  display: flex;
  flex-direction: column;
  gap: 32px;
  font-size: 16px;
  line-height: 1.5;
  /* Figma caps every one of these paragraph blocks at 580px, the same
     column width as the Problem/Solution copy — without this, a section
     like "Additional features" (whose text sits in a plain, unconstrained
     wrapper) stretches its body copy across the full page instead of
     stopping at roughly half of it. */
  max-width: 580px;
}

.text-block strong { font-weight: 700; }

/* =========================
   Photo rows (4-up, 16px radius, landscape)
   ========================= */

.photo-row > div {
  position: relative;
  aspect-ratio: 274 / 265;
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.photo-row img { width: 100%; height: 100%; object-fit: cover; }

/* Problem gallery's first image (Project TEVE): a dark backing box behind
   a zoomed/offset graphic, with a translucent light wash over the top —
   matches Figma's own layer stack (bg color -> cropped image -> overlay). */
.gallery-tinted {
  background: #0c0c0e;
}

.gallery-tinted::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(237, 229, 229, 0.2);
}

/* Mercedes Aura's Problem gallery: same translucent wash, but no dark
   backing box underneath (the source photo already fills the frame). */
.gallery-wash::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0.2);
}

/* A gallery row can mix span-2 and span-1 cells in the same .grid-4 row
   (Project TEVE's Problem/Context/User-journey galleries: 581+274+274 or
   581+581) — span-1 keeps the 274/265 ratio above, span-2 needs its own
   581/265 ratio so a doubled-width cell doesn't get stretched taller. */
.photo-row .span-2 {
  aspect-ratio: 581 / 265;
}

/* Mercedes Aura's Transformation section: 2 large SQUARE videos (581/580,
   near-1:1), not the 581/265 gallery-row ratio above — a separate class
   so it doesn't disturb .photo-row's other (landscape) galleries. Videos
   autoplay/loop/muted in the markup; `poster` + the last-known static
   frame keep a sane fallback if autoplay is blocked. */
.transformation-row .span-2 {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.transformation-row video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* =========================
   Research methods (label + tall image)
   ========================= */

.method-item {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.method-item p {
  margin: 0;
  font-size: 16px;
}

.method-item .method-image {
  position: relative;
  width: 100%;
  aspect-ratio: 274 / 580;
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.method-image img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Insights diamond
   ========================= */

.insights-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-template-rows: auto auto auto;
  align-items: center;
  justify-items: center;
  gap: 24px;
  width: 100%;
  padding-block: 40px;
}

.insight {
  display: flex;
  align-items: center;
  gap: 32px;
  max-width: 274px;
  font-size: 16px;
  line-height: 1.5;
}

.insight-dot {
  flex-shrink: 0;
  width: 70px;
  height: 70px;
  border-radius: 50%;
  background: var(--color-icon-bg);
}

/* Directions verified against Figma metadata (node 52:260): text-then-icon
   in the DOM, laid out with plain row/column so text always leads (top
   text sits above its icon, left text sits left of its icon, etc.) —
   no flex-direction reversal, which had put a few icon/text pairs in
   the wrong order relative to the source. */
.insight--top { grid-column: 2; grid-row: 1; flex-direction: column; text-align: center; }
.insight--right { grid-column: 3; grid-row: 2; flex-direction: row; text-align: left; }
.insight--left { grid-column: 1; grid-row: 2; flex-direction: row; text-align: left; }
.insight--bottom { grid-column: 2; grid-row: 3; flex-direction: column; text-align: center; }

/* =========================
   Wireframe grid
   ---------------------------------------------------------
   Replaced the earlier rotated paper-sketch collage — the Hachi
   Wireframes section now uses 6 flat scan photos in a plain 3-col/2-row
   grid, no border-radius (sharp corners, unlike every other image on
   the site — verified against the Figma node, which has no
   rounded-corner class on these cells).

   5 of the 6 source photos (wireframe-1 through -5) were shot/scanned
   with the camera rotated 90° relative to the page — each raw file is
   a tall portrait photo of 3 phone mockups stacked vertically, every
   mockup individually drawn sideways on the paper. Matching Figma's
   render needed the actual image FILES rotated 90° left (counter-
   clockwise) so the mockups read left-to-right upright, same fix
   applied at the asset level as the EXIF-orientation photos elsewhere
   on the site (see README's Environment gotchas) — not a CSS
   transform: a plain rotate() on an already object-fit:cover'd image
   clips against the non-square frame, and there's no clean box-relative
   way to pre-rotate a cover-fit crop without duplicating the image at
   a second orientation. wireframe-6 (the matchmaking diagram) was
   already landscape and needed no rotation.
   ========================= */

.wireframe-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 31px 32px;
  width: 100%;
}

.wireframe-grid > div {
  aspect-ratio: 376 / 274;
  overflow: hidden;
}

.wireframe-grid img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Filmstrip (The Fall of Cozy Web: 9 narrow gameplay frames in a row)
   ========================= */

.filmstrip {
  display: flex;
  gap: 32px;
  width: 100%;
}

.filmstrip > div {
  flex: 1 1 0;
  aspect-ratio: 104 / 220;
  border-radius: 12px;
  overflow: hidden;
}

.filmstrip img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Bordered detail rows (user groups / matchmaking / places / features)
   ========================= */

.detail-images {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  width: 100%;
}

/* User groups: one image, then a separately-grouped pair — the big gap
   belongs between the solo image and the pair, not evenly split three
   ways, so the pair is its own flex item with its own 32px internal gap. */
.detail-image-pair {
  display: flex;
  align-items: center;
  gap: 32px;
}

.detail-image {
  /* A plain `width` (not `flex-basis`, via the `flex` shorthand) on
     purpose: flex-basis only means "width" inside a ROW-direction flex
     parent. The Fall of Cozy Web nests .detail-image inside .step-item
     (a COLUMN container, for the label-above-image layout further
     down) where flex-basis would size the main axis — i.e. height —
     instead, silently fighting the aspect-ratio below and collapsing
     it to a square. `width` is axis-independent and still gives
     .detail-images (a row container) the same 274px sizing it needs,
     since flex-basis:auto (the default) falls back to `width` anyway. */
  width: 274px;
  max-width: 274px;
  flex-shrink: 1;
  /* Defaults to Hachi's 274/608 profile-card ratio; The Fall of Cozy
     Web's game-screen rows are 274/580 instead — set inline via
     `style="--detail-aspect:274/580"` rather than a whole new class. */
  aspect-ratio: var(--detail-aspect, 274 / 608);
  border-radius: 32px;
  border: 4px solid var(--border-color, var(--color-text));
  overflow: hidden;
}

.detail-image img { width: 100%; height: 100%; object-fit: cover; }

/* Some rows (e.g. a game's plain "Starting screen", no player color
   assigned yet) have no border at all. */
.detail-image--no-border {
  border: none;
}

/* =========================
   Media row (274px text column + 887px image, 562px tall)
   Used whenever a section pairs a short text block with one large photo
   side by side (Project TEVE: Human-Robot Interaction Course, Solution,
   "Which got us thinking..."). In Figma this is built as a 4-col grid
   with the image spanning cols 2-4 and the text pinned to col 1 — but
   since the text always sits left of the image in the actual DOM order
   too, a plain flex row gives the identical visual result without the
   grid-placement indirection.
   ========================= */

.media-row {
  display: flex;
  align-items: stretch;
  gap: 32px;
  width: 100%;
}

.media-row-text {
  display: flex;
  flex-direction: column;
  gap: 32px;
  flex: 0 0 274px;
  max-width: 100%;
  font-size: 16px;
  line-height: 1.5;
}

.media-row-text h2 {
  margin: 0;
  font-size: 32px;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: -0.64px;
  /* Leading trim, so this heading starts level with the photo it sits
     beside rather than a third of a line below it. The flex row already
     aligns the two BOXES; what the eye reads is the ascender line, and
     line-height 1.5 puts 12px of leading plus font ascent above it at
     32px. Measured 12px on all five .media-row headings across four
     project pages, identical because they share this rule. Same trim and
     same reasoning as .intro-text / .grid-text h2 in about.css. */
  margin-top: -0.375em;
}

.media-row-text p {
  margin: 0;
  letter-spacing: -0.32px;
}

.media-row-image {
  flex: 1 1 auto;
  min-width: 0;
  height: 562px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  position: relative;
}

.media-row-image img,
.media-row-image video { width: 100%; height: 100%; object-fit: cover; }

/* The Fall of Cozy Web's "The concept" diagram is a white-background
   illustration that needs to stay fully visible (letterboxed), not
   cropped — object-fit:contain on a white backing instead of cover. */
.media-row-image--framed {
  background: #ffffff;
}

.media-row-image--framed img { object-fit: contain; }

/* Some media-row images are cropped/zoomed via explicit inset percentages
   from Figma rather than a plain center-crop — see .crop-img below. */

/* =========================
   Crop image (explicit Figma inset percentages)
   ---------------------------------------------------------
   A handful of source photos on Project TEVE are exported uncropped, with
   Figma's own crop expressed as left/top/width/height percentages instead
   of a simple object-fit:cover center-crop. Reproducing those exact
   percentages (rather than approximating with object-fit) is what keeps
   the framing pixel-true to the design. Percentages are set inline per
   image, same convention as the wireframe-collage on the Hachi page.
   ========================= */

.crop-img {
  position: absolute;
  max-width: none;
}

/* =========================
   Feature row (Solution's 4 captioned square images)
   ========================= */

.feature-row .feature-item {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 32px;
}

.feature-item p {
  margin: 0;
  font-size: 20px;
  line-height: 1.5;
}

.feature-item .detail-square {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-lg);
  overflow: hidden;
  position: relative;
}

.detail-square img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Step items (footer states row: tall 274x580 images, 20px label)
   ========================= */

.step-item {
  display: flex;
  flex-direction: column;
  gap: 32px;
  min-width: 0;
}

/* .detail-image's own `width: 274px` (see that rule's comment) is a
   definite size, and .step-item is a COLUMN flex container — so
   .detail-image's `flex-shrink: 1` only ever acts on its main axis
   (height) here, never on width. Cross-axis stretch (.step-item's
   default align-items) can't override a definite width either, so even
   with .step-item itself now free to shrink (min-width:0 above), the
   274px-wide card just overflows its newly-narrower parent. Falling
   back to `width: auto` only inside this nesting hands sizing to
   stretch instead: it fills .step-item's real width, capped by
   .detail-image's own max-width:274px — identical 274px result at full
   design width, and it actually shrinks below that. Scoped to this
   parent only so every other (row-nested) .detail-images usage, which
   already shrinks correctly via its own flex-basis, is untouched. */
.step-item > .detail-image {
  width: auto;
}

.step-item p {
  margin: 0;
  font-size: 20px;
  font-weight: 500;
}

/* Mercedes Aura's step labels are 16px, not TEVE's 20px. */
.step-item--sm p {
  font-size: 16px;
}

/* Keeps a row of captioned images on one top edge.

   These columns are [label, image]. Grid stretches every .step-item in a
   row to the tallest one, but the item packs from the top, so a column
   whose label wraps to an extra line pushes its own image down and the
   row's image edges step out of line — 20px on Mercedes Aura's four
   steps at 1090px, 24px on ULP's issue grid, both invisible at 1440px
   where those labels happen to fit on one line. Since every .step-image
   in a row is the same width and shares one aspect-ratio, they are all
   the same height, so sending the free space above the image instead of
   below it lines the images up by their tops.

   The cost is that the label-to-image gap is then 32px only in the
   tallest-label column and larger in the others. That is the right thing
   to give up: uneven whitespace reads as breathing room, whereas a 20px
   step in a row of image edges reads as broken. Aligning the labels
   instead would fix the images but leave the paragraph tops ragged,
   which is worse for columns holding five and six lines of body copy.

   No-op wherever the labels already match (no free space to distribute)
   or the item is not stretched, so it costs nothing on the rows that
   were already fine and protects the ones that only misalign at widths
   nobody happened to test. */
.step-item > .step-image {
  margin-top: auto;
}

.step-image {
  width: 100%;
  aspect-ratio: 274 / 580;
  border-radius: var(--radius-lg);
  overflow: hidden;
  position: relative;
}

.step-image img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Footer detail row (886px image + 274px caption)
   ========================= */

.footer-detail-row {
  display: flex;
  align-items: flex-start;
  gap: 32px;
  width: 100%;
}

.footer-detail-image {
  flex: 1 1 886px;
  max-width: 886px;
  aspect-ratio: 886 / 265;
  border-radius: var(--radius-lg);
  overflow: hidden;
  position: relative;
}

.footer-detail-image img { width: 100%; height: 100%; object-fit: cover; }

.footer-detail-row p {
  margin: 0;
  flex: 0 0 274px;
  max-width: 100%;
  font-size: 16px;
  line-height: 1.5;
}

/* =========================
   Feedback row (heading + 3 quote bubbles)
   ========================= */

.feedback-row {
  display: flex;
  align-items: flex-start;
  gap: 32px;
  /* nowrap + shrinkable children (flex: 0 1 <basis>, not 0 0) instead of wrap: flex-wrap
     decides line breaks using each item's unshrunk flex-basis, not its post-shrink size,
     so with flex-shrink:0 this row (269+274*3+96 gaps = 1187px) had no way to fit any
     content column even 1px narrower than that and got a whole bubble kicked onto its
     own line instead of shrinking together -- same bug/fix as .method-header and
     .ia-row, see [[portfolio-design-rules]]. */
  flex-wrap: nowrap;
  width: 100%;
}

.feedback-heading {
  margin: 0;
  flex: 0 1 269px;
  min-width: 0;
  max-width: 100%;
  /* Scales instead of holding a flat 32px, for the same reason
     .grid-text h2 does in about.css, and to the same curve. This is a
     shrinkable flex item: at the design width its column is 269px and
     32px type fits about fourteen characters to the line, but the column
     shrinks with the container, and by 1090px it is 209px — eleven
     characters — which broke the heading into five stumps,
     "Feedback / we received / from / potential / users". No wrap
     setting can fix that; the type is simply too big for the measure.
     At 26px the same heading sets as "Feedback we / received from /
     potential users". Tops out at the design's 32px by ~1334px, so the
     desktop appearance is unchanged. */
  font-size: clamp(22px, 2.4vw, 32px);
  font-weight: 500;
  line-height: normal;
}

.feedback-bubble {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  flex: 0 1 274px;
  min-width: 0;
  max-width: 100%;
}

.feedback-bubble img {
  width: 70px;
  height: 70px;
}

.feedback-bubble p {
  margin: 0;
  width: 100%;
  font-size: 16px;
  line-height: 1.5;
  /* The avatar above is centred by .feedback-bubble's align-items, so
     left-ranged text under it read as slipped to one side — most obviously
     on the one-line quote, where a short line sat hard left under a
     centred circle. Centring pairs with the text-wrap:balance these
     already carry: balanced lines centre into a tidy block rather than a
     lopsided one. */
  text-align: center;
}

/* =========================
   3-up object grid (Mercedes Aura: collected pinecone cards)
   ========================= */

.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
  width: 100%;
}

.object-card {
  aspect-ratio: 376 / 502;
  border-radius: 26px;
  overflow: hidden;
}

.object-card img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Prototyping collage (Mercedes Aura: 1 tall image + 2 stacked pairs)
   ========================= */

.tall-image {
  width: 100%;
  aspect-ratio: 274 / 562;
  border-radius: var(--radius-lg);
  overflow: hidden;
  position: relative;
}

.tall-image img { width: 100%; height: 100%; object-fit: cover; }

.stacked-pair {
  display: flex;
  flex-direction: column;
  gap: 32px;
  height: 100%;
}

.stacked-pair > div {
  position: relative;
  flex: 1 1 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.stacked-pair img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Live demo row (274px image + 887px image, both 580 tall)
   ========================= */

.livedemo-row {
  display: flex;
  gap: 32px;
  width: 100%;
}

.livedemo-row .step-image {
  flex: 0 0 274px;
  max-width: 274px;
}

.livedemo-image-lg {
  flex: 1 1 auto;
  min-width: 0;
  aspect-ratio: 887 / 580;
  border-radius: var(--radius-lg);
  overflow: hidden;
  position: relative;
}

.livedemo-image-lg img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Sticky footer — no background bar, buttons float on their own
   ========================= */

.project-footer {
  position: sticky;
  bottom: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  /* Line the pills up with the page's own left and right edges instead of
     with the raw gutter. The gutter alone is only the content edge while
     the container is still narrower than its max — past that the content
     stops at --container-max and centres, but the pills kept tracking the
     gutter and drifted outboard of everything above them (measured at
     1600px: content 195-1389, pills 96 and 1489, so about 100px adrift on
     each side). This is the full-bleed form of the shared container
     measure documented at the top of style.css: same inset the header
     already uses, so the pills, the logo and the content now share one
     edge at every width. */
  padding-inline: max(var(--gutter), calc((100% - var(--container-max)) / 2));
  padding-block: 24px;
  pointer-events: none;
  isolation: isolate;
}

.project-footer > * { pointer-events: auto; }

/* Bottom-anchored scrim behind the floating pills. Without it the light
   .footer-pill--light ("NEXT PROJECT") loses its edge over a light photo —
   chose a gradient (over a flat background) so the pills keep floating
   free of a hard bar on desktop and the scrim only earns its keep where
   content can actually scroll underneath the sticky footer. */
.project-footer::before {
  content: "";
  position: absolute;
  /* Height comes from --footer-scrim (set on .project-page) so that the
     page's bottom padding, which must clear this, is computed from the
     same number. */
  inset: calc(-1 * var(--footer-scrim, 64px)) 0 0 0;
  background: linear-gradient(to bottom, transparent, var(--color-bg) 60%);
  pointer-events: none;
  z-index: -1;
}

.footer-group {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}

.footer-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-radius: var(--radius-pill);
  border: none;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: inherit;
  font-weight: 500;
  font-size: 20px;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
  transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease;
}

.footer-pill:hover,
.footer-pill:focus-visible {
  transform: translateY(-3px);
}

.footer-pill--dark:hover,
.footer-pill--dark:focus-visible {
  background: #2c2929;
}

.footer-pill--light {
  background: var(--color-icon-bg);
  color: var(--color-bg);
}

.footer-pill--light:hover,
.footer-pill--light:focus-visible {
  background: #ffffff;
}

.footer-pill svg,
.footer-pill img { display: block; }

/* =========================
   Method header (heading + right-aligned "method: X" label)
   ---------------------------------------------------------
   Repeats throughout MOME ULP: every research-method section opens
   with a 581px heading on the left and a 581px right-aligned method
   tag on the right, same 32px gap as everywhere else on the page.
   ========================= */

.method-header {
  display: flex;
  /* baseline, not stretch. The heading is 32px on a 48px line and the
     label is 16px on `normal` — stretched, both boxes ran the full row
     height and each started its text at its own box top, which put the
     label's first baseline 20px ABOVE the heading's (measured, on all 11
     of these on mome-ulp). It read as the label floating loose above the
     line it belongs to. Aligning the first baselines is what makes them
     look like one row, and it stays right whatever either font-size or
     line-height becomes. */
  align-items: baseline;
  gap: 32px;
  width: 100%;
  /* flex-wrap:wrap looked like the obvious way to let this reflow on
     narrow screens, but CSS decides line-breaks using each item's
     unshrunk hypothetical (flex-basis) size, never the post-shrink
     size — so anywhere the content column (gutter-padded, see
     [[portfolio-design-rules]]) falls even 1px short of 581+32+581=
     1194px, the label was kicked to its own line instead of shrinking
     alongside the heading, and — worse — that second line stayed
     full-width and *left*-aligned instead of tracking the heading's
     right edge. nowrap forces real shrinking instead; the two items
     stay a single right-aligned row all the way down to the 560px
     breakpoint below, where they intentionally stack (see that rule). */
  flex-wrap: nowrap;
}

.method-header h2 {
  margin: 0;
  flex: 0 1 581px;
  min-width: 0;
  max-width: 100%;
  font-size: 32px;
  font-weight: 500;
  line-height: 1.5;
}

.method-header .method-label {
  margin: 0;
  flex: 0 1 581px;
  min-width: 0;
  max-width: 100%;
  font-size: 16px;
  text-align: right;
}

/* =========================
   MOME ULP: findings clusters (Communication / Student approach /
   App issues / Student ideas — 4 uneven-width columns)
   ========================= */

/* Proportional columns, not the four fixed Figma widths (348/174/156/
   160px) these categories used to carry inline. Those widths were drawn
   around 8px quote text; at a readable 14px the 156px and 160px columns
   became unreadable ribbons about two words wide. 2fr for Communication
   (it holds a 2-up mini-grid of six items, so it needs roughly double)
   and 1fr for the three single-column lists keeps the same visual
   hierarchy — widest first, narrower after — while letting every column
   scale with the container instead of fighting it. */
.findings-grid {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1fr;
  align-items: start;
  gap: 32px;
  width: 100%;
}

.finding-category {
  display: flex;
  flex-direction: column;
  gap: 32px;
  /* Grid items default to min-width:auto; without this a long unbroken
     quote sets the column's floor and blows the 2fr/1fr ratio apart. */
  min-width: 0;
}

.finding-category h3 {
  margin: 0;
  font-size: 20px;
  font-weight: 500;
  line-height: 1.5;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.finding-items {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Communication is wide enough to run its findings two-up.

   columns, not a 2-track grid. A grid aligns rows, so every row grew to
   its tallest item and the shorter one left dead space underneath —
   measured 42-63px of slack, which turned the gap between one finding and
   the next into 16px here and 61px there down the same column. These are
   six independent quotes, not a table: nothing needs to line up across the
   columns, so letting each column pack its own items gives one even rhythm
   throughout. DOM order is unchanged, so reading order and the tab order
   are unaffected. */
.finding-items--grid {
  /* Overrides .finding-items display:flex, which is also on this element:
     columns is ignored on a flex container, so without this the modifier
     silently collapsed back to one flex column and the flex gap stacked on
     top of the item margin below (32px instead of 16px). */
  display: block;
  columns: 2;
  column-gap: 16px;
}

.finding-items--grid .finding-item {
  /* Without this a finding can be split across the column break, leaving
     its term at the foot of one column and its quote at the head of the
     next. */
  break-inside: avoid;
  /* columns lay children out in normal flow, so the vertical rhythm comes
     from margin rather than a parent gap. Same 16px .finding-items uses. */
  margin-bottom: 16px;
}

/* The term and its quote sat flush — 0px between them, against 16px
   between one finding and the next — so a term read as a trailing line of
   the quote above rather than the label for the quote below. 8px binds the
   pair while staying half the gap that separates findings. */
.finding-item {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.finding-item p {
  margin: 0;
}

.finding-item .finding-term {
  font-size: 16px;
  letter-spacing: -0.176px;
  line-height: 1.5;
}

/* 14px, not the 8px this used to carry, and at full opacity. 8px at 0.85
   opacity is not "small print", it is unreadable — and it was 26 quotes
   on this page at every viewport. The Figma column widths (348/174/156/
   160px, previously set inline in mome-ulp.html) do not survive readable
   type, so .findings-grid re-flows to proportional columns instead; the
   type wins. */
.finding-item .finding-quote {
  font-size: 14px;
  letter-spacing: -0.088px;
  line-height: 1.5;
}

/* =========================
   MOME ULP: main-issues grid (4 square photo cards, text above image)
   ========================= */

.issue-grid .step-item .step-image {
  aspect-ratio: 1 / 1;
}

.issue-grid .step-item p {
  font-size: 16px;
  font-weight: 500;
  line-height: 1.5;
}

/* =========================
   MOME ULP: "How might we" question grid (2x2, icon + text)
   ========================= */

.hmw-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 32px;
  width: 100%;
}

.hmw-item {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* height:auto (not a fixed 44px): 3 of the 4 icon-hmw-*.svg source files
   are square, but icon-hmw-4 is 35.17x40.67 — a fixed 44x44 box stretched
   it into a square it was never drawn as, same bug as .rite-legend/
   .priority-grid below. auto is a no-op for the already-square icons. */
.hmw-item img {
  flex-shrink: 0;
  width: 44px;
  height: auto;
}

.hmw-item p {
  margin: 0;
  font-size: 20px;
  line-height: 1.5;
  letter-spacing: -0.2px;
}

/* =========================
   MOME ULP: navigation-priority matrix
   ---------------------------------------------------------
   A 6-column grid where every content cell keeps the exact column/row
   Figma placed it in (frequency of use left-to-right, alphabetical
   topic top-to-bottom isn't uniform — some cells span multiple rows
   of empty space) plus 4 axis labels and 2 legend icons pinned to
   their own explicit cells.
   ========================= */

.priority-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-auto-rows: minmax(0, auto);
  gap: 32px;
  width: 100%;
}

.priority-grid p { margin: 0; font-size: 16px; line-height: 1.5; }

.priority-grid .axis-label {
  color: #f1f11d;
  font-size: 20px;
  letter-spacing: -0.22px;
}

.priority-grid .axis-label--bottom {
  align-self: end;
}

/* height:auto (not a fixed 44px) lets the icon keep its own native
   40.67x35.17 aspect ratio — the source SVG has preserveAspectRatio="none",
   so forcing it into a 44x44 square stretched/squished it vertically. */
.priority-grid img {
  width: 44px;
  height: auto;
}

/* =========================
   MOME ULP: information-architecture boxes (Home / Resources / Contacts)
   ========================= */

.ia-row {
  display: flex;
  /* flex-start (not stretch): each box now hugs its own content with
     equal padding on every side, per explicit request — Home/Contacts
     have far less content than Resources, so stretching them to match
     Resources' height would just pile the extra space up at the bottom
     of the shorter boxes and undo that symmetry. */
  align-items: flex-start;
  gap: 32px;
  /* nowrap on purpose: Home/Resources/Contacts are sized to their exact
     Figma widths below and sum to the full 1194px row with zero slack —
     flex-wrap:wrap here made Contacts drop to its own line the moment
     the row was even a fraction of a pixel tight, which also stranded
     it away from the empty gap it left behind in the row above. Wraps
     to a column only at the mobile breakpoint. */
  flex-wrap: nowrap;
  width: 100%;
}

.ia-box {
  display: flex;
  flex-direction: column;
  /* stretch (not flex-start): lets .ia-links below fill the box's own
     width, which itself needs to shrink below the ~1442px viewport
     where the row is narrower than the boxes' combined 1194px. With
     flex-start, .ia-links sized to its own preferred (unshrunk)
     content width regardless of what its parent box was doing, and
     that preferred width became the box's own content-driven shrink
     floor — nothing below ever actually got smaller. h3 opts back out
     with its own align-self below. */
  align-items: stretch;
  /* Same shrink-floor trap one level up: a flex item's default
     min-width is `auto` (its content's min-content size), not 0 — so
     without this the 3 boxes in .ia-row still refuse to shrink past
     their own content's natural width even with flex-shrink:1 set. */
  min-width: 0;
  gap: 32px;
  /* Equal on all four sides, so the border sits the same distance from
     the text whichever edge you measure from (was 8px top/bottom vs.
     20px left/right, visibly tighter above/below than at the sides). */
  padding: 24px;
  border: 4px solid var(--color-text);
  border-radius: var(--radius-lg);
}

/* Exact Figma widths (node 167:571) — Home/Resources/Contacts don't
   share a common column unit like the rest of the page, so they're
   sized explicitly rather than left to hug their own content, which
   summed a few px wider than the 1194px row actually has room for. */
.ia-box--home { width: 340px; }
.ia-box--resources { width: 637px; }
/* 160px was the exact Figma width, sized for the original 20px side
   padding. The switch to a uniform 24px padding (+4px border) needs
   28px clear on every side, and 160px is 16px too narrow for that
   around the .ia-link-col's own fixed 120px column — bumped up just
   enough to keep the same 28px gap on the right that the other two
   sides already get, instead of the column overflowing into it. */
.ia-box--contacts { width: 176px; }

.ia-box h3 {
  margin: 0;
  align-self: center;
  font-size: 20px;
  font-weight: 500;
  white-space: nowrap;
}

.ia-links {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  /* Resources' 5 columns sum to exactly the box's content width at the
     fixed .ia-box--resources size (same off-by-a-hair wrap risk as
     .ia-row above) — nowrap here too, switched back to wrap only once
     .ia-box goes full-width at the mobile breakpoint. */
  flex-wrap: nowrap;
  /* Scales with the boxes. The columns bottom out at min-content (their
     longest word) and the <p> siblings are nowrap, so once the row is
     shrunk far enough nothing can give and .ia-links overflows its box —
     measured 4px of spill at a 1280px viewport rising to 32px by 1170
     with a flat 16px. Shrinking the label type shrinks those floors
     along with everything else, which is what carries the three-across
     diagram down through the whole laptop range instead of stacking it
     at the first sign of pressure. Tops out at the design's 16px by
     ~1392px, so the full-width appearance is unchanged. */
  font-size: clamp(13px, 1.15vw, 16px);
  text-align: center;
}

.ia-links > p { margin: 0; white-space: nowrap; }

.ia-link-col {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 120px;
  /* A real floor, not 0. These columns are the only shrinkable things in
     the row: their <p> siblings (Now+Next, Canteen, Library) are single
     words with white-space:nowrap and cannot give up a pixel, so every
     bit of deficit lands here. With min-width:0 they shrank without
     limit — measured 53px wide at a 1200px viewport, 28px at 1100, 0px
     by 950, with the text spilling sideways out of collapsed columns.
     min-content rather than a flat number: the floor that matters is the
     longest unbreakable WORD in each column, and that differs per column
     (78px for "Academic", up to 99px for "Scholarships"). A single flat
     value would either still clip the widest column or waste ~20px on
     every narrow one, and it would silently go stale the moment a label
     is edited. This is what lets the row keep its three-across shape all
     the way down to the 1160px breakpoint below instead of stacking at
     1360. */
  min-width: min-content;
}

.ia-link-col p { margin: 0; text-align: center; }

/* =========================
   MOME ULP: R.I.T.E. testing flow (repeat icon + Test -> Identify -> Fix)
   ========================= */

.rite-flow {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  width: 100%;
}

.rite-legend {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

/* height:auto — repeat-icon.svg's native box is 35.17x42.13, not
   square, so a fixed 44x44 was stretching it into a circle-that's-
   actually-an-oval. Same fix as .priority-grid/.hmw-item img above. */
.rite-legend img { width: 44px; height: auto; }

.rite-legend p {
  margin: 0;
  color: #f1f11d;
  font-size: 20px;
}

.rite-steps {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 32px;
  width: 100%;
  max-width: 862px;
}

.rite-steps p {
  margin: 0;
  font-size: 20px;
  text-align: center;
  white-space: nowrap;
}

.rite-arrow {
  flex: 1 1 auto;
  min-width: 40px;
  height: 12px;
}

/* =========================
   MOME ULP: usability-test task cards (plain text, no image)
   ========================= */

.task-item h3 {
  margin: 0;
  font-size: 20px;
  font-weight: 500;
  line-height: 1.5;
}

.task-item p {
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
}

/* =========================
   MOME ULP: absolute-positioned collage canvas
   ---------------------------------------------------------
   Used for the interview sticky-note board and the 3 usability-test
   participant sections — every element's left/top/width/height is a
   percentage of the canvas (converted 1:1 from Figma's own px
   offsets), so the whole hand-arranged composition scales together
   instead of the elements drifting apart at other viewport widths.
   ========================= */

.collage-canvas {
  position: relative;
  width: 100%;
  /* Every note/doodle inside is positioned as a % of this canvas, sized
     off Figma's real px frame — that keeps the box geometry perfectly
     proportional at any width, but text doesn't scale with a box the
     same way an image does. A fixed 20px note font fit its box exactly
     at the design's native ~1194px canvas width; at any narrower width
     the (also-shrunk) box no longer has room for fixed-size text and
     it overflows top and bottom. Establishing a query container here
     lets the note text below size itself in cqw units instead — that
     scales down in lockstep with the box, so the same number of lines
     always fits, right down to a readable floor via clamp(). */
  container-type: inline-size;
}

.collage-item {
  position: absolute;
}

.collage-item.collage-image {
  border-radius: 32px;
  border: 4px solid var(--collage-color, var(--color-text));
  overflow: hidden;
}

.collage-item.collage-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.collage-item.collage-note {
  display: flex;
  align-items: center;
  justify-content: center;
  /* visible, not hidden: the font-size clamp's readable floor (see
     collage-item.collage-note p below) means a handful of the longer
     notes still run a few px taller than their box on very narrow
     screens — better to let those spill slightly than crop mid-glyph
     top and bottom. */
  overflow: visible;
  /* 8px at the canvas's native ~1193px width, scaling down with it
     (see .collage-canvas) so it never eats a disproportionate share
     of a shrunk note box; clamp keeps it from vanishing entirely. */
  padding: clamp(2px, 0.67cqw, 8px);
  background: var(--collage-color, #1915f1);
  color: var(--collage-note-text, var(--color-text));
}

.collage-item.collage-note p {
  margin: 0;
  /* 20px at native width, same reasoning as the padding above — this
     is what actually fixes the sticky notes overflowing their boxes:
     font size now shrinks in lockstep with the box instead of staying
     a fixed px size the box can no longer fit at narrower viewports. */
  /* 12px floor, not 8px. Below 900px this whole canvas becomes a plain
     list (see the breakpoint below), so the floor now only has to hold
     for the narrow end of the canvas layout rather than propping up
     illegible 8px text on a phone.

     1.6cqw, down from 1.68, with line-height 1.4 rather than 1.5: the
     old pair did not actually fit its boxes — measured 8px of text
     spilling below the coloured note at 1360px and 4.6px at 1024px,
     because line breaks land on whole lines and the box height does not.
     1.6/1.4 is the largest of the combinations tried that gives zero
     overflow at every width from 901 to 1920, so it keeps the notes as
     large as they can be while staying inside their boxes. At the
     design's own 1194px canvas that resolves to 19.1px against the
     original 20px. */
  font-size: clamp(12px, 1.6cqw, 20px);
  line-height: 1.4;
  text-align: center;
}

.collage-item.collage-doodle img {
  display: block;
  width: 100%;
  height: 100%;
}

.collage-item.collage-arrow img {
  display: block;
  width: 100%;
  height: auto;
}

/* Interview board: a single flattened image supplied directly by the user, not a
   reconstruction -- see [[portfolio-figma-lessons]]. The sticky notes + red connector
   lines aren't real nodes in get_metadata/get_design_context's traversal for this frame,
   and a first attempt at cropping them into individual PNGs came out visibly low-quality
   (re-encoding artifacts) and duplicated the connector lines. One full-quality image is
   simpler and looks correct. */
.collage-flat-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--radius-lg);
}

/* Interview board: dark rounded backing behind the photo + sticky notes */
.collage-canvas--interview {
  aspect-ratio: 1194 / 580;
  background: var(--color-bg);
  border-radius: var(--radius-lg);
}

.collage-canvas--interview .collage-note {
  padding: clamp(2px, 0.67cqw, 8px);
  text-align: left;
}

.collage-canvas--interview .collage-note p {
  text-align: left;
  font-size: clamp(7px, 1.34cqw, 16px);
}

/* Participant sections: taller canvases, sized per section */
.collage-canvas--participant1 { aspect-ratio: 1193 / 1937; }
.collage-canvas--participant2 { aspect-ratio: 1192 / 1248; }
.collage-canvas--participant3 { aspect-ratio: 1192 / 769; }

/* =========================
   MOME ULP: usability-test results (colour-coded score cards)
   ========================= */

.results-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 32px;
  width: 100%;
}

.results-grid .task-label {
  margin: 0;
  font-size: 16px;
}

.results-score {
  padding: 16px;
  color: #1a1818;
  font-size: 16px;
  line-height: 1.5;
}

.results-score p { margin: 0; }

.results-score--p1 { background: #1915f1; color: #ede5e5; }
/* #ff3333, not pure #ff0000: against this cell's #1a1818 text pure red
   measures 4.42:1 — under the 4.5:1 AA floor for 16px text, i.e. failing
   while looking like it passes. Note the fix is to LIGHTEN, not darken:
   the text here is dark, so a darker red drops it further (#d20000 is
   3.15:1), and flipping to cream on pure red is worse still at 3.23:1.
   #ff3333 keeps the red colour-coding unmistakable and keeps the dark
   text consistent with its yellow and green siblings.
   Measured: 4.86:1. (p1 blue/cream 7.00:1, p3 yellow/dark 14.60:1,
   win green/dark 6.45:1 — all already comfortable, left alone.) */
.results-score--p2 { background: #ff3333; }
.results-score--p3 { background: #f1f11d; }
.results-score--win { background: #16b364; }

.results-compare-label {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 16px;
}

/* Per-card "Task I."-style tag and the "Participant N" group heading only
   exist to keep a score readable once the grid collapses to one column on
   phones -- desktop already conveys both via the grid's own row/column
   position, so both are hidden by default and switched on inside the
   mobile results-grid rules below. */
.task-tag,
.results-participant-label {
  display: none;
}

/* =========================
   Responsive
   ========================= */

/* ---------- MOME ULP: R.I.T.E. flow shrinks, doesn't stack, on phones ----------
   Test → Identify → Fix is a nowrap flex row whose three labels are
   themselves white-space:nowrap and whose two arrows hold a 40px floor, so
   the row has a hard minimum around 350px of content. Below roughly 414px
   it overruns its own box (12px at 390, 47px at 320) and by 360px that
   became real horizontal scroll on the document.

   A vertical stack (rotating the arrows 90deg) was tried first and reverted
   on request — the desktop version keeps the arrows horizontal, and the
   fix that matches it is shrinking the row's own slack, not changing its
   direction. The three labels (140px combined at their fixed 20px size)
   and the container's own gutter padding are the only hard floors here;
   the row's real give is the gap and the arrows' min-width, both generous
   on desktop (32px / 40px) because there was 862px to spend. Tightened to
   8px / 24px, the row still clears its own available width with room to
   spare at 320px (measured: 272px available, 216px needed) — same
   labels, same horizontal arrows, just shorter, exactly like the desktop
   row already reads at its own narrower measures. */
@media (max-width: 600px) {
  .rite-steps {
    gap: 8px;
  }

  .rite-arrow {
    min-width: 24px;
  }
}
/* ---------- Project pages: header stops sticking below 768px ----------
   Documented component-specific exception to the shared 900/600 set (see
   BREAKPOINTS at the top of style.css), and the one place the site needs
   a third number.

   Project pages are the only pages carrying BOTH a sticky header and a
   sticky footer, so they are the only pages where the two can close in on
   the content together. Measured against an 844px phone viewport:

     <=430px   header 72 + footer 205 = 33% of the viewport
     ~480-700  header 72 + footer 147 = 26%
     >=767px   header 72-89 + footer 90 = 19%

   The step is the footer wrapping its three pills onto extra lines, which
   stops just under 768px — so that is where the crowding actually ends,
   not at 900 or 600. Below it the header scrolls away normally instead of
   holding a third of a phone screen hostage; the sticky footer keeps BACK
   TO WORK / CONTACT / NEXT PROJECT reachable throughout, so nothing
   becomes unreachable. The header is not removed — it is still the first
   thing on the page, with the logo and full nav.
   ---------------------------------------------------------------- */
@media (max-width: 767px) {
  .project-page .site-header {
    position: static;
  }
}
/* MOME ULP: Home/Resources/Contacts row (.ia-row) stacks only once it
   genuinely cannot hold three boxes side by side.

   This used to stack at 1360px — the width at which .project-container
   stops delivering the full un-shrunk 1194px the boxes were drawn at.
   That was far too early: the design reads perfectly well shrunk, with
   the labels simply wrapping onto more lines, and stacking it turned
   every normal laptop into three full-width boxes instead of the
   three-across diagram the section exists to show.

   The reason it stacked that early was a shrink imbalance, not a real
   space limit. The row's flex-shrink prorates any deficit across the
   three boxes and, one level down, across .ia-links' children — but
   .ia-link-col had min-width:0 while its sibling <p> labels (Now+Next,
   Canteen, Library) are single words with white-space:nowrap and cannot
   shrink at all, so the columns absorbed 100% of it and collapsed
   (measured: 53px wide at a 1200px viewport, 28px at 1100, 0px by 950,
   with text spilling sideways). Giving .ia-link-col a 76px floor instead
   fixes the cause, so the row now holds its shape all the way down to
   ~1100px, where the floors finally sum past the available width and
   stacking is the honest answer.

   Still a documented component-specific exception to the shared 900/600
   set (see BREAKPOINTS at the top of style.css), just a much smaller one
   than before. */
@media (max-width: 1160px) {
  .ia-row {
    flex-direction: column;
  }

  .ia-box,
  .ia-box--home,
  .ia-box--resources,
  .ia-box--contacts {
    width: 100%;
  }

  /* Centred, not left-hugged. In the row layout each box is sized to its
     own content, so the links fill it and read as a balanced block. Once
     the boxes go full-width the same links kept their default
     flex-start, leaving Home's three items stranded in the left third of
     a ~1400px box with a lake of empty space beside them — the stacked
     state looked broken even though the row layout it falls back from is
     fine. Centring restores the composition at any box width. */
  .ia-links {
    flex-wrap: wrap;
    justify-content: center;
  }
}

/* ---------- tablet: <= 900px ---------- */
@media (max-width: 900px) {
  /* .method-header and .feedback-row stack here, not at their old 560px.
     Both use flex-wrap:nowrap with shrinkable children on desktop — that
     is deliberate and stays (see the long comment on .method-header for
     why wrap misbehaves there) — but nowrap means they keep shrinking
     instead of breaking, and .feedback-row has four items: a 32px
     heading plus three quote bubbles. At ~600px that was ~114px each.
     Stacking at the same width the rest of the page reflows keeps the
     desktop behaviour intact and stops the shrink well before it gets
     that far. */
  .method-header {
    flex-wrap: wrap;
  }

  .method-header h2,
  .method-header .method-label {
    flex-basis: 100%;
  }

  .method-header .method-label {
    text-align: left;
  }

  .feedback-row {
    flex-wrap: wrap;
  }

  .feedback-heading,
  .feedback-bubble {
    flex-basis: 100%;
  }

  .hero-meta {
    align-items: flex-start;
    text-align: left;
    white-space: normal;
    max-width: 100%;
    /* On desktop this margin pushes the block to the right edge; once
       the row wraps to a single mobile column that same auto margin
       keeps shoving it inward from the left instead, so the now
       left-aligned text sits indented under the title instead of
       flush with it. */
    margin-left: 0;
  }

  /* 4 tracks -> 2: a span-2 block still fills a full row on its own,
     a span-1 item now pairs up two-to-a-row instead of four. */
  .grid-4 {
    grid-template-columns: repeat(2, 1fr);
  }

  .insights-grid {
    grid-template-columns: 1fr;
    justify-items: stretch;
  }

  .insight,
  .insight--top,
  .insight--right,
  .insight--left,
  .insight--bottom {
    grid-column: 1;
    grid-row: auto;
    flex-direction: row;
    text-align: left;
    max-width: none;
  }

  .wireframe-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  /* Mercedes Aura: four of the five prototyping photos were invisible from
     here down — not missing from the page, rendered at height 0.

     .stacked-pair is `height: 100%` holding `flex: 1 1 0` children, which
     only resolves while the pair shares a grid row with the tall photo that
     gives the row its height. Once the grid drops below four tracks the two
     pairs end up in a row of their own, the percentage has nothing to
     resolve against, and the whole pair collapses to its 32px gap. Four
     photos gone, no error, nothing in the console.

     So from here the section becomes a compact two-up grid instead: the
     pairs stop being grid items (display: contents lifts their children up
     into the same grid) and every photo takes an explicit aspect-ratio
     rather than a share of a height that does not exist. The rig photo,
     which is the establishing shot, keeps the full width above the rest. */
  /* .grid-4.<name>, not .<name>: the mobile block below sets .grid-4 to a
     single column, and at equal specificity that later rule would win. */
  .grid-4.prototyping-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
  }

  .prototyping-grid > .media-row-text {
    grid-column: 1 / -1;
  }

  .prototyping-grid > .span-1:not(.media-row-text) {
    display: contents;
  }

  .prototyping-grid .tall-image {
    grid-column: 1 / -1;
    aspect-ratio: 4 / 3;
  }

  .prototyping-grid .stacked-pair > div {
    flex: none;
    aspect-ratio: 1 / 1;
  }

  /* Hand the six children back to auto-placement so they stack in source
     order — Problem, its photos, Solution, its photos.
     !important because the desktop placement is set per position
     (.problem-solution > :nth-child(n), specificity 0,2,0) and would
     otherwise outrank this reset from inside the media query — the same
     reason .about-grid and .priority-grid reset theirs this way. */
  .problem-solution > * {
    grid-column: auto !important;
    grid-row: auto !important;
  }

  .detail-images {
    flex-wrap: wrap;
    /* Centred, not space-between: once these wrap, a row holding fewer
       items than the last would otherwise push them to the outer edges
       with a hole in the middle. */
    justify-content: center;
  }

  /* Every app screen the same size, whatever it is nested in.
     They were coming out at two different widths: a solo screen took
     flex-basis and grew to fill its line, while the two inside
     .detail-image-pair sit in a COLUMN flex parent, where flex-basis sizes
     height and the width quietly fell back to the 274px below — 320px
     against 274px, side by side down the page. Sizing by `width` instead
     is axis-independent, so it holds in either parent (the same reasoning
     as the note on .detail-image itself). 274px is the design's own screen
     width, which is what the pair was already landing on. */
  .detail-image {
    flex-basis: auto;
    flex-grow: 0;
    width: 274px;
    max-width: 100%;
  }

  .detail-image-pair {
    flex-basis: 100%;
    justify-content: center;
  }

  /* Text-beside-image rows stack: text on top, full-width image below,
     with the image's fixed 562px height swapped for an aspect-ratio so
     it scales down with the narrower viewport instead of staying 562px
     tall on a much narrower box. */
  .media-row {
    flex-direction: column;
  }

  .media-row-text {
    flex-basis: auto;
    width: 100%;
  }

  /* Drop the leading trim with the side-by-side layout that justified
     it: stacked, this heading has the previous section above it rather
     than a photo edge to meet, so the negative margin would only shave
     12px off that gap. */
  .media-row-text h2 {
    margin-top: 0;
  }

  .media-row-image {
    height: auto;
    aspect-ratio: 887 / 562;
  }

  .footer-detail-row {
    flex-direction: column;
  }

  .footer-detail-image {
    max-width: 100%;
    flex-basis: auto;
    width: 100%;
  }

  /* The caption needs the same release the image just got, and for the same
     reason. `flex: 0 0 274px` means 274px along the MAIN axis, and the row
     above has just become a column — so the paragraph was reserving 274px of
     HEIGHT for three lines of text and leaving ~200px of blank page under
     it. (Third time this trap has bitten in this file: see .detail-image and
     .stacked-pair. In a container whose direction changes with the
     breakpoint, size with `width`, never `flex-basis`.) */
  .footer-detail-row p {
    flex-basis: auto;
  }

  /* Mercedes Aura: 3-up pinecone grid -> 2-up, live-demo row stacks
     with the small image swapping its fixed 274px width for full-width
     (its aspect-ratio keeps the shape correct at any width). */
  .grid-3 {
    grid-template-columns: repeat(2, 1fr);
  }

  .livedemo-row {
    flex-direction: column;
  }

  .livedemo-row .step-image {
    flex-basis: auto;
    max-width: 100%;
  }

  /* The Fall of Cozy Web: 9-frame filmstrip wraps to 3 rows of 3 instead
     of squeezing every frame to a sliver at narrower widths. */
  .filmstrip {
    flex-wrap: wrap;
  }

  .filmstrip > div {
    flex: 1 1 calc(33.333% - 22px);
  }

  /* MOME ULP: four proportional columns cannot hold 14px quotes at this
     width — the 1fr columns land around 130px, roughly two words a line.
     Two columns keeps the quotes readable; they go to one at 600px. */
  .findings-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  /* ---------- MOME ULP participant collages: canvas -> grouped list ----------
     Above this width the notes are hand-placed on an absolutely
     positioned canvas, sized in cqw so they scale with it. That stops
     working on a phone: the boxes get too small for their text at any
     legible size, which is why the note font used to bottom out at 8px
     and still overflowed its coloured box by 4-6px. Rather than trade
     legibility against overflow, the canvas is abandoned here.

     A flat "every screenshot, then every quote" list was tried first and
     rejected: the DOM only groups quotes by participant, not by which
     specific screen they were pinned next to on the desktop board, so a
     reader had no way to tell "hide my saved events" apart from "this is
     a lot of words" once both were loose at the bottom of the same list.
     Each collage-item still carries its original left/top position (kept
     for the desktop canvas), so every note here has an explicit inline
     `order` matching it to whichever screenshot it sat closest to on that
     canvas (nearest by column, computed from those same coordinates) —
     the grid runs single-column so a note in reading position always
     falls directly under its screen instead of beside an unrelated one.

     A named caption per screenshot was tried next and reverted: it
     duplicated what proximity alone can say, and the names it had to
     invent for screens with no on-page title read as guesses rather than
     fact. What actually reads as "these quotes go with that screen" is
     Gestalt proximity — closer to its own screen than to the next one —
     so the uniform 8px gap is replaced with two distances doing two
     different jobs: 12px within a group (screenshot to its first note,
     note to note) stays close enough to read as one unit, and every new
     screenshot after the first adds an extra 20px on top of that gap
     (12+20=32px) so the jump to an unrelated screen is unmistakably
     bigger than the jump between two notes about the same one. Doodles
     and arrows are still hidden — they're connectors between positions
     that no longer exist now that order, not position, carries the
     association. */
  .collage-canvas--participant1,
  .collage-canvas--participant2,
  .collage-canvas--participant3 {
    /* container-type:inline-size stays harmless, but the cqw font sizes
       below are overridden outright so it no longer drives anything. */
    aspect-ratio: auto;
    display: grid;
    grid-template-columns: 1fr;
    gap: 12px;
  }

  .collage-canvas--participant1 .collage-image,
  .collage-canvas--participant2 .collage-image,
  .collage-canvas--participant3 .collage-image {
    width: 100%;
    max-width: 220px;
    margin: 20px auto 0;
  }

  .collage-canvas--participant1 .collage-image:first-child,
  .collage-canvas--participant2 .collage-image:first-child,
  .collage-canvas--participant3 .collage-image:first-child {
    margin-top: 0;
  }

  .collage-canvas--participant1 .collage-item,
  .collage-canvas--participant2 .collage-item,
  .collage-canvas--participant3 .collage-item {
    /* Cancels the inline left/top/width/height percentages, which are
       meaningless once the items are back in normal flow. !important is
       required, not decorative: the placement really is inline on every
       item in mome-ulp.html, so a plain rule loses to it and the notes
       stay pinned at their canvas percentages (measured: a note still
       94x24px in list mode, with its text 264px tall spilling out). */
    position: static !important;
    left: auto !important;
    top: auto !important;
    width: auto !important;
    height: auto !important;
  }

  /* Every collage screenshot is the same 274x608 crop of its canvas,
     whichever participant it belongs to — so one ratio covers all three. */
  .collage-canvas--participant1 .collage-image,
  .collage-canvas--participant2 .collage-image,
  .collage-canvas--participant3 .collage-image {
    aspect-ratio: 274 / 608;
  }

  .collage-canvas--participant1 .collage-note,
  .collage-canvas--participant2 .collage-note,
  .collage-canvas--participant3 .collage-note {
    grid-column: 1 / -1;
    justify-content: flex-start;
    padding: 12px 16px;
    border-radius: var(--radius-lg);
  }

  .collage-canvas--participant1 .collage-note p,
  .collage-canvas--participant2 .collage-note p,
  .collage-canvas--participant3 .collage-note p {
    font-size: 16px;
    line-height: 1.5;
    text-align: left;
  }

  .collage-canvas--participant1 .collage-doodle,
  .collage-canvas--participant2 .collage-doodle,
  .collage-canvas--participant3 .collage-doodle,
  .collage-canvas--participant1 .collage-arrow,
  .collage-canvas--participant2 .collage-arrow,
  .collage-canvas--participant3 .collage-arrow {
    display: none;
  }

  .finding-items--grid {
    columns: 1;
  }

  .hmw-grid {
    grid-template-columns: 1fr;
  }

  /* ---------- MOME ULP: Resources box reads as one row of categories ----------
     Resources is the only one of the three IA boxes with sub-groups (7
     short lists under one heading). .ia-links wraps its 5 columns two-to-a-
     line once the box goes full width, but nothing distinguished a group's
     own first item ("Wellness and sport") from the items under it, so a
     wrapped pair like "Wellness and sport / Freshmen info" read as two
     items from the same list rather than two separate group headings.
     Below 900px the columns stack one under another instead of wrapping
     into pairs, and each group's first item becomes a real, underlined
     heading -- so the box reads top-to-bottom as a small table of contents
     (Wellness and sport: Booking, Counseling... / Freshmen info: ...)
     instead of one flat, ambiguously-grouped list. Scoped to mobile only:
     the desktop row conveys the same grouping for free via column
     position, and doesn't need the extra emphasis. */
  .ia-box--resources .ia-links {
    flex-direction: column;
    align-items: center;
    gap: 24px;
  }

  .ia-category {
    font-weight: 500;
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  /* Below 900px the 2-axis matrix no longer has room for both axes. A
     single-column list fallback (grouped by the frequency axis) was tried
     first, same pattern as every other MOME ULP grid on this page — but
     unlike those, this one didn't hold up: 24 short, similarly-weighted
     phrases with no image, colour, or other visual anchor between them
     just read as a wall of text once the matrix's two axes were gone, and
     the "All of the time" / "None of the time" group headings weren't
     enough on their own to make 24 items skimmable. Removed rather than
     patched further, per explicit request — the matrix is real
     information but only reads as a matrix, so it's kept for desktop only
     (see .priority-section below) rather than trying a third mobile
     treatment. */
  .priority-section {
    display: none;
  }

  /* ---------- MOME ULP results grids: read as a chart, not a wall ----------
     Desktop tells a score apart by its grid position (row = participant,
     column = task). Collapsing that same grid to one column threw both
     axes away at once -- every card became "Success: X / Time: Ys" with no
     task and no participant attached to it, which is why the mobile view
     read as an unlabelled stack of coloured blocks rather than a table.
     Below 900px the DOM's own grouping (task-label row, then every card
     already in participant order) is put back on screen: a "Participant N"
     heading (colour-matched to the collage boards above) opens each group,
     and every card grows a small "Task I." tag of its own. The original
     4-across task-label row is redundant once every card carries its own
     tag, so it's hidden rather than left floating with nothing to point
     at. */
  .results-grid {
    grid-template-columns: 1fr;
    gap: 12px;
  }

  .results-grid .task-label {
    display: none;
  }

  .task-tag {
    display: block;
    margin-bottom: 4px;
    font-size: 13px;
    font-weight: 500;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    opacity: 0.75;
  }

  .results-score {
    display: flex;
    flex-direction: column;
    gap: 2px;
    border-radius: var(--radius-md, 12px);
  }

  .results-participant-label {
    display: block;
    grid-column: 1 / -1;
    margin: 20px 0 0;
    font-size: 18px;
    font-weight: 500;
  }

  .results-participant-label:first-child {
    margin-top: 0;
  }

  .results-compare-label {
    margin-top: 20px;
    font-size: 18px;
    font-weight: 500;
  }

  .results-compare-label:first-child {
    margin-top: 0;
  }

  /* The plain (non-winning) cells in the compare grid carry the same
     task-tag but no coloured box -- keep their tag left-aligned with the
     Success/Time line under it rather than inline, so it reads as a label
     over a value like every other card here instead of a fragment glued
     to the front of a sentence. */
  .results-grid > p .task-tag {
    display: block;
  }
}

/* ---------- mobile: <= 600px ---------- */
@media (max-width: 600px) {
  .findings-grid {
    grid-template-columns: 1fr;
  }

  .grid-4 {
    grid-template-columns: 1fr;
  }

  .span-2 {
    grid-column: span 1;
  }

  .grid-3 {
    grid-template-columns: 1fr;
  }

  .wireframe-grid {
    grid-template-columns: 1fr;
  }

  .filmstrip > div {
    flex: 1 1 calc(50% - 16px);
  }

  /* Inherits the 274px width and centring from the 900px block above; only
     the cap changes, so a screen still shrinks with a viewport narrower
     than the design width instead of overflowing it. */
  .detail-image {
    max-width: 100%;
  }

  .detail-image-pair {
    flex-direction: column;
    align-items: center;
  }

  /* Stays four-across when everything else collapses to one column. These
     are near-square thumbnails read as a set — a closing strip — so a
     stack of four full-width photos says something the row does not, and
     costs four screens of scrolling to say it. The gap drops to 8px
     because four 32px gutters would eat a third of a phone's width. */
  .grid-4.photo-row--keep-row {
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
  }

  .photo-row--keep-row > .span-1 {
    grid-column: span 1;
  }

  .project-footer {
    justify-content: center;
  }

  .footer-group {
    justify-content: center;
    width: 100%;
  }

  /* NEXT PROJECT above CLOSE once the pills stack. Side by side the
     left-to-right order reads fine, but stacked it put the way out of the
     page above the way onward — the primary action should be the one under
     the thumb and the one met first. Selected structurally (the pill that
     is a direct child, rather than the one wrapped in .footer-group) so it
     stays correct if the pills are ever restyled.
     `order` moves the box, not the DOM, so a keyboard user still reaches
     CLOSE first here. Left as is: correcting it would mean flipping the
     markup on all six pages and pushing the same mismatch onto the desktop
     row, and with two adjacent controls this is the smaller cost. */
  .project-footer > .footer-pill {
    order: -1;
  }


  .results-grid {
    grid-template-columns: 1fr;
  }
}

/* =========================
   Line breaking in the narrow columns
   -------------------------
   style.css sets `pretty` on running copy and `balance` on headings. These
   selectors are the exceptions: every one of them is a short block sitting
   in a 274px-or-narrower column (a step caption, a sticky note, an IA link,
   a quote bubble, an insight), and at that measure `pretty` does almost
   nothing — it only rescues a lone last word, and with five or six words to
   a line there is rarely one to rescue. What actually reads as broken there
   is the ragged first line, which only `balance` addresses.

   Measured on fall-of-cozy-web at 1090px, the concept paragraph in a 274px
   column:

     pretty  "It presents the challenge to the / players whether they can /
              differentiate between their partner / and Ai."
     balance "It presents the challenge to / the players whether they /
              can differentiate between / their partner and Ai."

   Same four lines either way, but the second has an even rag and a last
   line that carries its weight.

   One caveat learned the hard way, so it does not get re-broken: only put
   a block here if it reliably stays under about six lines. Past that the
   browser silently stops balancing, and because this declaration also
   overrides the `pretty` inherited from style.css, the block ends up on
   the plain greedy wrap with no orphan protection at all — worse than if
   it had been left alone. .finding-quote was in this list and runs to
   seven lines on the longest quote, which is exactly how it ended up
   stranding "that.”" on a line by itself. It is deliberately not here now;
   the global `pretty` sets it as "...students aren't really / like
   that.”" instead. */
.media-row-text p,
.step-item p,
.method-label,
.insight p,
.ia-box p,
.ia-link-col p,
.collage-note p,
.object-card p,
.hmw-item p,
.feedback-bubble p,
.priority-grid p,
.task-label {
  text-wrap: balance;
}

/* =========================
   Hachi: Problem / Solution, copy and its photos in one grid
   -------------------------
   The two halves used to be a text grid followed by a separate photo grid.
   That reads correctly on desktop, where each pair of photos happens to sit
   under the column it belongs to — but the moment the grid stacks, source
   order takes over and you get both arguments and then all four photos, so
   the shelter photos land under the Solution copy and read as if they
   illustrate it.

   Merged into one grid whose source order IS the reading order — Problem,
   its two photos, Solution, its two photos — which is what stacks correctly
   and what a screen reader announces. Desktop then puts the six children
   back into the design's two rows by explicit placement, so the wide layout
   is byte-for-byte what it was.

   Placement is by position rather than by class because it is the position
   in the pair that decides the column; :nth-child beats .span-2's own
   grid-column on specificity, which is what lets the copy blocks keep that
   class for its flex-column-with-32px-gap. */
.problem-solution--quad > :nth-child(1) { grid-column: 1 / 3; grid-row: 1; }
.problem-solution--quad > :nth-child(2) { grid-column: 1 / 2; grid-row: 2; }
.problem-solution--quad > :nth-child(3) { grid-column: 2 / 3; grid-row: 2; }
.problem-solution--quad > :nth-child(4) { grid-column: 3 / 5; grid-row: 1; }
.problem-solution--quad > :nth-child(5) { grid-column: 3 / 4; grid-row: 2; }
.problem-solution--quad > :nth-child(6) { grid-column: 4 / 5; grid-row: 2; }

/* Mercedes Aura's variant: one wide photo under each half rather than two
   narrow ones, so four children instead of six. Source order is the same
   idea — copy, its photo, copy, its photo. */
.problem-solution--duo > :nth-child(1) { grid-column: 1 / 3; grid-row: 1; }
.problem-solution--duo > :nth-child(2) { grid-column: 1 / 3; grid-row: 2; }
.problem-solution--duo > :nth-child(3) { grid-column: 3 / 5; grid-row: 1; }
.problem-solution--duo > :nth-child(4) { grid-column: 3 / 5; grid-row: 2; }

/* A photo spanning two tracks is the wide 581x265 crop, not the square-ish
   274x265 one — mirrors .photo-row .span-2. */
.ps-photo.span-2 {
  aspect-ratio: 581 / 265;
}

/* Same treatment .photo-row gives its children. Applied by its own class
   rather than by reusing .photo-row on the grid, because that selector is
   `.photo-row > div` — on this grid it would also catch the two copy
   blocks and force them into a photo's aspect-ratio. */
.ps-photo {
  position: relative;
  aspect-ratio: 274 / 265;
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.ps-photo img { width: 100%; height: 100%; object-fit: cover; }

/* =========================
   Video play control
   -------------------------
   Autoplay of these muted clips is blocked by Chrome's power-saving rule for
   video-only media (see the note in main.js), so every clip carries a real
   press-to-play control. Injected by JS, so with JS off the poster still
   stands on its own rather than a dead button sitting on it.

   Styled from the same palette as the CONTACT tiles and the NEXT PROJECT
   pill — cream plate, near-black glyph, fully rounded — so it reads as part
   of the site rather than a browser default.
   ========================= */

.video-holder {
  position: relative;
}

/* The button is a bare box: the supplied artwork draws its own dark rounded
   plate, so there is no background, radius or shadow here to fight with it,
   and no optical nudge — the glyph is already positioned inside its own
   frame.

   Drawn at 32px, rendered at 44px. That is the minimum comfortable touch
   target, and these pages are mostly read on a phone, so the whole control
   is scaled to it rather than padding an invisible hit area around a 32px
   graphic — the press target and the thing you aim at are then the same
   object. It is vector on a 32x32 viewBox, so it scales exactly; the plate's
   6px corner radius scales with it, to 8.25px. */
.video-play {
  position: absolute;
  /* inset + auto margins centres it without a transform, which keeps it out
     of the way of the containing-block rules that transforms impose. */
  inset: 0;
  margin: auto;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  transition: opacity 0.25s ease, transform 0.2s ease;
}

.video-play svg {
  width: 100%;
  height: 100%;
  display: block;
}


.video-play:hover,
.video-play:focus-visible {
  transform: scale(1.06);
}

/* Visibility is driven by .controls-visible, which main.js puts on and takes
   off — NOT by .is-playing. The two are deliberately separate: the icon
   reflects playback, while whether the control is on screen follows the
   user's last interaction (see the note in main.js).

   Hidden means faded and click-through, never display:none and never
   removed — the button must stay in the tab order so a keyboard user can
   reach it while it is invisible, at which point :focus-visible below brings
   it back into view. */
.video-play {
  opacity: 0;
  pointer-events: none;
}

/* Deliberately NO :hover reveal. There used to be one, and it silently beat
   every tap-to-hide: the pointer is by definition over the frame right after
   you tap it, so hover pinned the control back to opacity 1 the instant the
   class came off. Permanent with a mouse, and sticky-hover reproduces it on
   touch until you tap elsewhere. Visibility is the interaction state's job
   alone now — :focus-visible is the one exception, so a keyboard user can
   still find a control they cannot see. */
.video-holder.controls-visible .video-play,
.video-play:focus-visible {
  opacity: 1;
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
  .video-play {
    transition: none;
  }

  .video-play:hover,
  .video-play:focus-visible {
    transform: none;
  }
}
