/* ==========================================================================
   ai-playground.css — intro + masonry experiment grid
   Sizes/weights/line-heights measured off "AI Playground.pdf" the same
   way the homepage was measured — pixel bbox scan at 150dpi, scale =
   px_width/1728, then width-calibrated against the real webfont for
   anything ambiguous between weights.
   ========================================================================== */

/* ---------- intro ---------- */
.pg-hero {
  padding-inline: var(--margin);
  padding-top: 10.25rem; /* measured: hero title cap-top sits ~164pt below the page top */
}

.pg-hero__inner {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: var(--gutter);
}

.pg-hero__title {
  grid-column: 1 / 13;
  font-size: 8.75rem; /* 140px, trying a layout experiment per direct request — was 5rem/80px */
  font-weight: 500; /* medium — reverted back from 600 semibold per direct request */

  line-height: 1.25; /* was 1.1 — bumped to fix Satoshi descender clipping (e.g. the "y" in "Playground"): Satoshi's own font-metrics box needs ~1.25× font-size to avoid clipping inside the overflow:hidden reveal mask, vs PP Neue Montreal's tighter ~1.11× */
  letter-spacing: -0.01em;
}

.pg-hero__subtitle {
  grid-column: 8 / 13; /* trying a layout experiment per direct request — was 5/12 */
  font-size: 1rem; /* 16px, per direct request — was 1.125rem/18pt, the site's new standard body text size */
  font-weight: 500; /* medium — was 400 regular */
  color: #bababa; /* per direct request — was inheriting the default (near-white) text color */
  line-height: 1.3; /* per direct request — was 1.22 (matched the homepage's work__desc, now both use the same new standard) */
  /* max-width set to match a specific reference line-break ("...exploring
     the rapid / evolution of AI...through tools, workflows, and /
     interactions."), per direct screenshot — not the exact 3-column grid
     width (24.625rem) used elsewhere, since that produced a narrower
     2-word-per-line wrap that didn't match the reference. Tested stable
     across an 1026-1728px viewport sweep with plain greedy wrapping (no
     text-wrap:balance — balance would fight the reference's deliberate
     short last line). */
  max-width: 26rem;
  margin-top: 1rem; /* 16px, trying a layout experiment per direct request — was 1.5rem/24px */
}

/* ---------- experiment grid ----------
   True staggered masonry, per direct request/reference
   (leoparpeix.com/playground) — verified live against the reference's
   own computed styles that its "grid feels like it moves differently"
   effect is NOT a second scroll-driven transform on some wrapper (only
   one scroll-linked transform exists there at all, the same single
   parallax layer this site already has on .pg-card__image-inner). It's
   a static-layout illusion: two fully independent column flows, each
   its own top-to-bottom stack, so one column's total height has zero
   effect on where the other column's cards land. .pg-grid__col--left
   (Money Routines, then Deconstructing) and .pg-grid__col--right
   (Puzzle, then Feather) are each just a grid-12 item (for their
   column-track width/position) containing a plain flex column
   underneath — the two cards inside stack via that flex column's own
   row-gap, completely decoupled from the sibling column's stack.
   Previously this was one shared CSS Grid row (row 1: Money + Puzzle)
   with Puzzle's lower position coming from a margin-top riding on top
   of whatever height Money's row happened to be — still secretly
   coupled through that shared row track, not truly independent. */
.pg-grid {
  margin-top: 5.2006rem; /* was 9.1rem — re-measured after the hero title/subtitle layout experiment (140px title, longer subtitle copy now wrapping to more lines than doodles/about's) grew this page's hero taller than the others; reduced again so the first project card still lands at the same distance from the page top as doodles.html/about.html's first image, per direct request */
  align-items: start;
}

.pg-grid__col {
  display: flex;
  flex-direction: column;
  row-gap: 7.5rem; /* 120px, per direct request — was 8rem/128px; now scoped per-column instead of the old shared grid row-gap, so each column's own internal spacing is independent of the other column's */
}

.pg-grid__col--left {
  /* Matches Deconstructing's own original span (6 of 12 cols) — this
     column's own width is what Deconstructing renders at (100% of the
     column); Money Routines sits narrower within it (see its own width
     below), same relative proportion the two cards had when both were
     direct 12-col grid items (5/12 vs 6/12 — a 5:6 ratio). */
  grid-column: 1 / 7;
  grid-row: 1;
}

.pg-grid__col--right {
  /* Matches Puzzle's own original span (6 of 12 cols, starting at
     column 7 — see the old comment below for why 7 not 8). Feather
     sits narrower within it, same as Money Routines does in the left
     column. */
  grid-column: 7 / 13;
  grid-row: 1;
  margin-top: 12rem; /* 192px — the stagger start point, per direct request/reference: was a margin-top on Puzzle itself (pushing it down within a shared row's height); now it's the whole column's own starting offset, so it's a fixed, layout-independent stagger rather than one riding on the left column's height. */
}

.pg-card {
  display: flex;
  flex-direction: column;
}

.pg-grid__col--left .pg-card:nth-of-type(1) {
  /* Money Routines — vertical, 5 of the left column's 6 cols wide
     (≈83.3%, the same 5:6 ratio it had against Deconstructing as
     direct 12-col grid siblings), left-aligned to the column's own
     start. */
  width: 83.333%;
}

.pg-grid__col--left .pg-card:nth-of-type(2) {
  /* Deconstructing images — horizontal, fills the column's full width (was explicitly 6/12 as a direct grid item; the column itself is now sized to that same 6-of-12 span, so 100% here is the same rendered width). */
  width: 100%;
}

.pg-grid__col--right .pg-card:nth-of-type(1) {
  /* Puzzle platformer — horizontal, fills the column's full width (was
     explicitly 6 cols wide starting at column 7 — the grid's own last
     column line is 13, so a 6-wide card can't start any later than
     column 7 and still fit; the column itself now carries that same
     7/13 span). */
  width: 100%;
}

.pg-grid__col--right .pg-card:nth-of-type(2) {
  /* Feather — vertical, 4 of the right column's 6 cols wide (≈66.7%,
     matching its old 4:6 ratio against Puzzle as direct 12-col grid
     siblings). Originally started one column right of Puzzle's own
     start (col 8 vs col 7 — see the old comment on .pg-card:nth-of-
     type(4) before this masonry restructure), i.e. right-shifted, not
     flush to the column's left edge the way Money Routines is in the
     left column — align-self:flex-end reproduces that same right-hand
     lean now that it's a plain flex child instead of its own grid item
     with an explicit start column. Without this it defaults to
     flex-start (left-aligned), which visibly moved it per direct
     report. margin-right pulls it one column back off that flush-right edge,
     per direct request — flush-right had it sharing Puzzle's exact
     right edge, one column further right than the original design
     (Feather's own right edge, col line 12, sat one column short of
     Puzzle's, col line 13). ~16.667% approximates one of the column's
     6 internal column-widths, same rough-ratio approach as the
     83.333%/66.667% widths above. */
  width: 66.667%;
  align-self: flex-end;
  margin-right: 16.667%;
}

/* Mask only — overflow:hidden crops whatever the inner layer does.
   Matches the justgowiththeflow.com/work reference: the image itself
   starts zoomed in and scales down to rest as it scrolls into view,
   masked by this box so nothing overflows the card. */
.pg-card__image {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1403 / 1000;
  overflow: hidden;
}

/* Portrait cards (Money Routines, Feather) get their own taller,
   narrower rectangle instead of sharing the same landscape box as the
   other two — per direct request ("the vertical ones... should be in
   a rectangle container, the other two are okay"). 4:5 was picked as
   a moderate portrait ratio (not as extreme as the videos' own native
   ~9:19.5 phone-recording ratio, which would make an oddly narrow
   sliver of a card) — the .pg-card__video-frame--vertical modifier
   inside still handles the actual video's own aspect-ratio/cropping
   independently of this outer mask shape. */
.pg-card__image--portrait {
  aspect-ratio: 4 / 5;
}

/* The actual image (or, for now, its placeholder) — sized to fully
   cover the mask and absolutely positioned so scaling it never affects
   the card's own layout/aspect-ratio. Starts at scale(1.35); animated
   down to scale(1) by Common.initImageReveals() in common.js. Swap
   `background` for a real `background-image`/`<img>`/`<video>` (with
   the same width/height/position:absolute) once real project media
   replaces the placeholder — the reveal keeps working unchanged.
   Kept at an exact inset:0 match to the mask (NOT oversized) — an
   earlier version of this rule sized this to 120% height so
   initCardParallax() (js/ai-playground.js) could translate it without
   ever exposing empty space at the mask's edges, but that broke
   .pg-card__video-frame--vertical's own `calc(100% - margin*2)` sizing
   below: that formula resolves against THIS element's real height, so
   inflating it to 120% silently shrank the vertical frame's actual
   40px top/bottom clearance to something much smaller (even negative,
   i.e. clipped) once the portrait cards got their own taller mask —
   per direct report ("make sure the image container has space above,
   at least 40px"). Reverted; initCardParallax() now applies its scrub
   translate to this same exact-sized element instead, which can very
   occasionally reveal a sliver of whatever's behind the mask at the
   scroll extremes — acceptable since the whole site is black
   background/dark-mode-only, so that sliver is black-on-black. */
.pg-card__image-inner {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background: var(--black);
  transform: scale(1.35);
  will-change: transform;
  /* Centers .pg-card__video-frame over the full-bleed gradient — see
     that rule below. Doesn't affect .pg-card__gradient itself, which is
     absolutely positioned/inset:0 and so sits outside this flex layout's
     normal flow regardless. */
  display: flex;
  align-items: center;
  justify-content: center;
}

.pg-card__image-inner img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Full-bleed animated gradient background (see js/shader-gradient.js,
   per-card data-shadergradient-props on the markup) — sits behind
   .pg-card__video-frame, bleeding out around its edges, matching the
   gradient-behind-a-smaller-framed-preview composition on
   saimalshafi.framer.website/ai-playground. */
.pg-card__gradient {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  /* Purely decorative — never orbit/zoom-able and never allowed to eat a
     click meant for the .pg-card link underneath it. Pairs with
     enableCameraUpdate: false forced in js/shader-gradient.js (see its
     comment) so the gradient's own camera-controls library has no wheel/
     drag input to react to in the first place. */
  pointer-events: none;
}

/* Experimental solid-color background, per direct request — sits
   directly on top of .pg-card__gradient (same z-index:0 stacking
   level, but painted after it in DOM order so it wins), fully
   covering the WebGL gradient canvas without removing it: the
   gradient/js/shader-gradient.js mounting is left completely intact
   underneath, so deleting these .pg-card__solid-bg divs (and their
   inline per-card background-color) is all it takes to go back to the
   gradients later. Color itself is set inline per card in the markup
   rather than here, since each card gets a different one-off hex. */
.pg-card__solid-bg {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

/* The smaller "framed" video sitting on top of the gradient. Which
   dimension drives the size is orientation-dependent, per direct spec:
   vertical (portrait) videos get 80px off the top and bottom (per
   direct request — was var(--margin)/2.5rem/40px each side), with
   width solved from that height via aspect-ratio; horizontal
   (landscape) videos get 40px off the left and right instead (unchanged
   — this request was vertical-cards-only), with height solved from
   that width. Either way the other dimension is left to `auto` and
   computed from the frame's own `aspect-ratio` (the video's real
   encoded w:h, from assets/video/ai-playground/) — the same
   "transferred size" behavior a plain <img>/<video> gets for free from
   its own intrinsic ratio, just declared explicitly since this is a
   <div> frame instead. No cap on the free dimension: a landscape video
   wider than the card itself, or a portrait video taller than the
   card, will overflow past the card's own edge on that axis, which the
   outer .pg-card__image's overflow:hidden mask then crops — a
   deliberate tradeoff for keeping the full clearance gap on its
   driving axis on every card regardless of that video's own ratio. */
.pg-card__video-frame {
  position: relative;
  z-index: 1;
  overflow: hidden;
  flex: none; /* don't let the flex layout stretch/shrink this away from its computed size */
  /* 16px at the site's 1728px reference viewport, but expressed as a
     true viewport-relative value (same "100vw / 1728 * target" formula
     the root font-size clamp uses, see style.css) rather than rem —
     rem would hold this at a flat 16px on every viewport once the
     mobile breakpoint freezes the root font-size (see style.css's
     "mobile breakpoint" comment), which read as disproportionately
     rounded once the frame itself is much smaller on a phone. This way
     the radius keeps scaling continuously with the frame's own real
     size at any viewport, the same way a fixed corner-radius on an
     image would visually shrink/grow if you scaled the whole image —
     per direct spec. No clamp/floor, deliberately: it scales all the
     way down (and up) with true viewport width, no breakpoint step. */
  border-radius: calc(100vw / 1728 * 16);
  /* outline (not border): sits fully outside the box instead of eating
     into the height/width computed below, and — like border — is
     clipped to border-radius by every current browser, so the stroke
     traces the frame's rounded corners rather than squaring them off. */
  outline: 0.25rem solid #0B0A0B; /* 4px, black, stroke drawn outside */
  outline-offset: 0;
}

.pg-card__video-frame video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- orientation: which axis gets the clearance gap ---- */
.pg-card__video-frame--vertical {
  height: calc(100% - 10rem); /* 80px top + 80px bottom, per direct request — was var(--margin)*2/40px each side */
  width: auto;
}

.pg-card__video-frame--horizontal {
  width: calc(100% - (var(--margin) * 2)); /* unchanged — 40px each side, horizontal cards weren't part of this request */
  height: auto;
}

/* ---- per-video aspect ratio (exact encoded dimensions) ---- */

/* pixel-space: 1600x862, landscape (re-encoded from an updated,
   shorter-height source) */
.pg-card__video-frame--pixel-space {
  aspect-ratio: 1600 / 862;
}

/* money-routines: 1290x2796, portrait phone recording */
.pg-card__video-frame--money-routines {
  aspect-ratio: 1290 / 2796;
}

/* feather: 738x1600, portrait phone recording */
.pg-card__video-frame--feather {
  aspect-ratio: 738 / 1600;
}

/* puzzle-platformer: 1600x904, landscape */
.pg-card__video-frame--puzzle-platformer {
  aspect-ratio: 1600 / 904;
}

/* The curtain: a solid, page-colored panel sitting on top of the image,
   fully covering it at rest. Common.initImageReveals() (see common.js)
   slides this up and off (yPercent 0 -> -100) at the same time the image
   itself scales down to rest — same "solid panel, then it's gone"
   language as the page-transition-panel in style.css, just scoped to a
   single card instead of the whole viewport. */
.pg-card__image-cover {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--white);
  will-change: transform;
}

html.dark-mode .pg-card__image-cover {
  background: #0B0A0B;
}

.pg-card__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1.5rem; /* 24px, per explicit spec */
}

.pg-card__title {
  font-size: 1rem; /* 16px, per direct spec — was 1.25rem/20px */
  font-weight: 500; /* medium — was 600 semibold (bold pass) */

  color: var(--black);
  letter-spacing: -0.01em;
}

/* var(--black) is a hardcoded #0B0A0B, not a theme-aware token — unlike
   plain body text, which inherits its color and gets flipped for free by
   the html.dark-mode body{color:#ffffff} rule in style.css, this explicit
   override needs its own dark-mode flip or it stays black-on-black once
   .page's background goes dark. .pg-card__index isn't touched here since
   var(--gray) (#a0a0a0) already has enough contrast against black. */
html.dark-mode .pg-card__title {
  color: #ffffff; /* pure white — per direct request */
}

.pg-card__index {
  font-size: 1rem; /* 16px, same size as the title */
  font-weight: 500; /* medium — was 600 semibold (bold pass) */

  color: #1e1e1e; /* the site's standard number/eyebrow color — see .case-block__eyebrow in case-study.css — per direct request, was #BABABA */
  letter-spacing: -0.01em;
  flex-shrink: 0;
}

/* ---------- project detail overlay ----------
   Full-viewport takeover, not a centered card over a dimmed backdrop —
   per direct spec/mockup. Left column: project copy, description and
   "how I built it" anchored to the top, live/GitHub links pinned to the
   bottom of the column regardless of how much copy sits above them.
   Right column: the clicked card's own .pg-card__image node, physically
   (but invisibly/instantly) moved in, then revealed in place with the
   same curtain-slide-up mechanism the main grid's own reveal-on-scroll
   uses (.pg-card__image-cover/-inner) — see js/ai-playground.js's
   initProjectOverlay for the open/close mechanics; this file only owns
   layout/appearance. z-index (900) sits above
   .site-header--nav's 500 deliberately: the overlay fully replaces the
   header while open (hence its own text "Close" control below, sitting
   exactly where the real nav row would) rather than floating over it.
   .site-header--logo is the one exception — see the
   body.pg-overlay-open override further down — it stays visible above
   the overlay per direct request. Background is a 95%-opaque scrim
   (not fully solid) and there's no scroll — content is sized to fit
   the fixed columns instead of overflowing. */
.pg-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  visibility: hidden;
  pointer-events: none;
}

.pg-overlay.is-open {
  visibility: visible;
  pointer-events: auto;
}

.pg-overlay__panel {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: 1fr; /* gives the single row a definite height (viewport minus padding) so both columns can stretch to fill it — see .pg-overlay__media's height:100% below */
  column-gap: var(--gutter);
  /* Top padding clears the Close button's own row (var(--margin) inset
     + ~1.5rem row height, matching the real header's logo/nav row —
     see .logo svg's height in style.css) plus the extra var(--margin)
     gap requested on top of that — media and body are grid siblings in
     the same row, so this single top offset is what actually aligns
     both the image and the text block to "40px below the Close
     button" at once. */
  padding: calc(var(--margin) + 1.5rem + var(--margin)) var(--margin) var(--margin);
  /* Pure black at 97% opacity, per direct spec — same regardless of
     site theme (no dark-mode override; see the now-forced light text
     below for why that's safe to read). Not a fully solid panel — the
     actual grid page underneath is meant to still just barely read
     through (that 3% is the full extent of it: this panel is no longer
     animated in via opacity — see js/ai-playground.js's openCard —
     specifically because a 0 -> 1 opacity TWEEN briefly exposed the
     grid page underneath at partial opacity while its own clicked card
     sat empty mid-move; the .pg-overlay/is-open visibility switch is
     what actually shows/hides this panel now, so it needs to be fully
     opaque (this 3% aside) from the instant that switch flips, not
     fading up from invisible. That's what the separate `opacity: 1`
     below is — the PANEL's own element opacity, not this background's
     alpha — deliberately left at 1 rather than 0.97 so nothing here
     re-introduces that old bug. */
  background: rgba(0, 0, 0, 0.97);
  opacity: 1;
}

/* Text button standing in for the real nav (hidden underneath this
   panel) — same position, size, and type treatment as .nav a, so the
   header row reads as "replaced by Close" rather than just gone. */
.pg-overlay__close {
  position: fixed;
  top: var(--margin);
  right: var(--margin);
  z-index: 2;
  font-family: inherit;
  font-size: 0.875rem; /* 14px, matches .nav a */
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  line-height: 1;
  padding-top: 0.3125rem; /* matches .site-header--nav's own baseline offset, see style.css */
  background: none;
  border: none;
  cursor: pointer;
  transition: opacity 0.35s var(--ease-in-out);
  /* Same trick the real nav uses (see .site-header in style.css): this
     button sits directly over .pg-overlay__media, whose content is a
     different project's own image/video every time — sometimes near-
     black, sometimes bright — so a fixed theme color would go invisible
     against some of them (confirmed: plain var(--black) disappeared
     entirely over Money Routines' dark video). difference blend against
     white keeps it legible over literally any color behind it, in
     either theme, with no per-project tuning needed. */
  color: #ffffff; /* pure white — per direct request */
  mix-blend-mode: difference;
}

.pg-overlay__close:hover {
  opacity: 0.55;
}

/* Enlarged invisible tap target — the visible text is only ~14px tall
   (line-height:1, 5px top padding), well under the ~44px minimum
   recommended touch target; same technique as .nav-toggle/.mobile-
   menu__close/.footer__bottom-row a in style.css. position:fixed
   already gives this button its own containing block, same as
   position:relative would, so the absolutely-positioned pseudo-element
   below sizes off it correctly without changing anything else. */
.pg-overlay__close::before {
  content: "";
  position: absolute;
  inset: -0.75rem;
}

/* Doubles as a click-to-close target — see the closeZone binding in
   ai-playground.js's initProjectOverlay(), and the matching cursor-
   follow "Close" label in initCursorLabel(). Plain pointer cursor here
   is just a fallback affordance for anyone whose OS/browser combo
   somehow doesn't get the custom label (see its own hover:hover/
   pointer:fine guard) — the label is the primary indicator. Set on the
   whole column (not just .pg-overlay__top) so the empty flex gap above
   .pg-overlay__links reads as clickable too, matching the JS binding. */
.pg-overlay__body {
  grid-column: 1 / 6; /* 5 of 12 columns */
  height: 100%;
  display: flex;
  flex-direction: column;
  cursor: pointer;
  /* No extra top offset here — .pg-overlay__panel's own padding-top
     (above) already lands both this and .pg-overlay__media on the same
     starting line, so the text and the image top-align exactly. */
  /* The panel behind this is always pure black now regardless of site
     theme (see .pg-overlay__panel above) — text needs to stay light
     regardless of theme too, rather than inheriting body's normal
     light/dark-mode-driven color (which would go black-on-black in
     light mode). Title/desc/"how I built it" paragraph all pick this up
     via inheritance since none of them set their own color; tags and
     links are the two exceptions (see their own rules below). */
  color: var(--white);
}

.pg-overlay__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  margin-bottom: 1.75rem;
}

.pg-overlay__tag {
  font-size: 0.75rem; /* 12px, per direct request — was 0.8125rem/13px */
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #BABABA; /* per direct request — was var(--gray)/#a0a0a0 */
}

.pg-overlay__title {
  font-size: 2.5rem; /* 40px — was 2.25rem/36px */
  font-weight: 500;
  line-height: 1.25; /* was 1.15 — bumped to fix Satoshi descender clipping, same reasoning as .pg-hero__title above (this is the exact element clipped in the "Making Rammas..." screenshot) */
  letter-spacing: -0.01em;
  margin-bottom: 1.5rem;
  max-width: 27rem;
}

.pg-overlay__desc {
  /* Sizing matches .work__desc (homepage project paragraphs,
     css/home.css) exactly, per direct request — was 1.0625rem/17px,
     line-height 1.45. Color is white (var(--white), inherited from
     .pg-overlay__body — see that rule's own comment on why text here
     always stays light regardless of site theme), NOT #BABABA — an
     earlier pass matched .work__desc's grey too, reverted per direct
     follow-up request back to inheriting the overlay's standard white. */
  font-size: 1rem; /* 16px */
  font-weight: 500;
  line-height: 1.3;
  max-width: 26rem;
}

.pg-overlay__how {
  margin-top: 2rem;
}

.pg-overlay__how h3 {
  font-size: 0.875rem; /* 14px */
  font-weight: 500;
  letter-spacing: -0.01em;
  color: #BABABA; /* per direct request — was var(--gray)/#a0a0a0 */
  margin-bottom: 0.75rem;
}

.pg-overlay__how p {
  /* Same settings as .pg-overlay__desc, per direct request — was
     1.0625rem/17px/1.45, matching this element's own old values.
     Color inherits .pg-overlay__body's white, same as .pg-overlay__desc. */
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.3;
  max-width: 26rem;
}

/* Pinned to the bottom of the column via margin-top:auto — regardless
   of how much copy sits above (e.g. the puzzle platformer's shorter
   write-up), these always sit on the same baseline near the panel's
   bottom margin. */
.pg-overlay__links {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
}

.pg-overlay__link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  width: fit-content;
  font-size: 1rem; /* 16px */
  font-weight: 500;
  /* Always light — see the color note on .pg-overlay__body above; this
     one needs its own explicit override (rather than just inheriting)
     since it used to hard-code var(--black) for light mode specifically
     and flip via a dark-mode override, both now wrong against the
     always-black panel. */
  color: var(--white);
  transition: opacity 0.3s var(--ease-out);
}

.pg-overlay__link:hover {
  opacity: 0.6;
}

.pg-overlay__link-arrow {
  width: 0.625rem; /* 10px — matches the new diagonal icon's 10x10 viewBox
    (same icon as the cursor label's external-link glyph, per direct
    request); was 0.75rem/12px for the old rounded-linecap icon */
  height: 0.625rem;
  /* No hover transform/transition — per direct request, the icon stays
     put on hover (was translate(2px,-2px) on the old rounded-linecap
     icon; removed rather than kept for the new one). */
}

/* No live/GitHub link yet (e.g. the puzzle platformer, still in
   progress) — setLink() (see ai-playground.js) swaps the element's
   content for a plain "coming soon" label in this state, so the arrow
   icon it targets below is gone from the DOM already; the color/
   pointer-events override here is what actually visualizes "disabled". */
.pg-overlay__link.is-disabled {
  color: var(--gray);
  pointer-events: none;
}

.pg-overlay__media {
  grid-column: 6 / 13; /* 7 of 12 columns */
  height: 100%;
  position: relative;
  /* Hoverable/clickable — see initCursorLabel's "Next Project" bindZone
     and the matching click binding in initProjectOverlay
     (js/ai-playground.js). */
  cursor: pointer;
}

/* The moved-in .pg-card__image keeps its own aspect-ratio (1403/1000,
   set in the .pg-card__image rule above) by default — overridden here
   since this column instead needs it to fill the full column height
   (var(--margin) off the right/bottom, 40px below the Close button off
   the top — see .pg-overlay__panel's own padding above — flush against
   the text column's own gutter on the left) with whatever width that
   implies, matching the mockup. */
.pg-overlay__media .pg-card__image {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
}

/* Body scroll lock while the overlay is open (see initProjectOverlay,
   which also calls lenis.stop()/start() — this covers plain native
   scroll/touch-scroll that Lenis itself doesn't intercept). */
body.pg-overlay-open {
  overflow: hidden;
}

/* .pg-overlay (z-index:900) otherwise sits above literally every other
   z-indexed element on the page, .site-header--logo (300, see
   style.css) included — deliberate everywhere else (the overlay is
   meant to replace the header while open), except the logo itself,
   which stays visible above it per direct request. Scoped to
   body.pg-overlay-open rather than raising .site-header--logo's base
   z-index globally: that base value (300) is load-bearing for the page-
   transition choreography (see the z-index comments in style.css) —
   bumping it site-wide would make ordinary page transitions stop
   covering the logo during a homepage swap. This only applies while
   this specific overlay is open. */
body.pg-overlay-open .site-header--logo {
  z-index: 950;
}

/* Cursor-follow "View details" label — see initCursorLabel() in
   js/ai-playground.js, which drives visibility (scale/opacity) and
   position (x/y via GSAP quickTo) entirely from JS. No transform/scale
   set here: JS takes ownership of the element's transform from init
   (xPercent/yPercent for centering on the cursor point, plus the
   scale/x/y tweens) and CSS fighting it over the same property would
   just get overwritten anyway.

   Solid random-color chip, per direct request — replaces the earlier
   glassy koto.com-style treatment (translucent black blur, white text)
   entirely. Every zone now gets a real icon (see assets/cursor/*.svg,
   swapped in per zone by initCursorLabel in ai-playground.js) plus a
   flat fill color picked fresh from a shared 6-color palette every time
   the cursor enters a zone (see pickCursorLabelColor in common.js) —
   black text/icon throughout since it needs to read against any of the
   six. Sharp corners (no border-radius) and asymmetric padding, both
   per direct spec. */
.pg-cursor-label {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 950;
  pointer-events: none;
  display: flex;
  flex-direction: row-reverse; /* DOM is text-then-icon; this flips it to icon-first visually, matching the reference exactly */
  align-items: center;
  gap: 0.25rem; /* 4px, per direct spec */
  color: #0B0A0B; /* black — per direct request, reads against every chip color */
  font-family: "Satoshi", sans-serif;
  font-size: 0.75rem; /* 12px, per direct spec */
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0;
  line-height: 1;
  white-space: nowrap;
  padding: 0.5rem 0.375rem 0.5rem 0.25rem; /* 8px top/bottom, 6px right, 4px left — per direct spec */
  border-radius: 0; /* sharp corners, per direct spec */
  background: #ff6831; /* JS overwrites this per zone entry — see pickCursorLabelColor in common.js; this is just the pre-JS fallback */
  opacity: 0;
  will-change: transform, opacity;
}

.pg-cursor-label__arrow {
  display: flex; /* every zone gets an icon now, including View details — no more opt-in gating */
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.pg-cursor-label__arrow img {
  width: 0.75rem; /* 12px, matches text size */
  height: 0.75rem;
  display: block;
}

/* ==========================================================================
   mobile breakpoint (<=768px) — see style.css's "mobile breakpoint"
   comment for the shared root-font-freeze this all builds on, and
   home.css's own mobile block for the header/footer conventions reused
   here unchanged (nav-toggle, mobile-menu, 24px side padding, etc. — all
   scoped to shared selectors, so they already apply to this page too).
   Hero only for now — the masonry experiment grid below still needs its
   own mobile pass.
   ========================================================================== */
@media (max-width: 768px) {
  .pg-hero {
    padding-inline: 1.5rem; /* 24px, per direct spec — was var(--margin)/40px */
    padding-top: 7.5rem; /* 120px — matches the homepage's own mobile hero clearance, for consistency across pages */
  }

  .pg-hero__title {
    font-size: 2.5rem; /* 40px — was 80px, matches the homepage's mobile hero-text scale */
  }

  .pg-hero__subtitle {
    /* Was grid-column:5/12 — a deliberate desktop-only indent under the
       title. Collapses to the same 1/13 span as the title so both sit
       flush against the same left edge on mobile, per direct spec
       ("keep the title and paragraphs left aligned"). */
    grid-column: 1 / 13;
    max-width: none;
  }

  /* ---------- experiment grid ----------
     The desktop two-column masonry (see .pg-grid__col--left/--right
     above) needs a wide viewport to read as a masonry at all — on a
     390px viewport two independent columns would squeeze every card
     into illegibly narrow strips. One card per row instead, full
     width, stacked in plain DOM order (same order goToNextProject()
     cycles through) — .pg-grid__col becomes display:contents (same
     technique as .doodles-grid__col on the Doodles page) so its cards
     unwrap into direct .pg-grid children and just follow this single
     shared full-width rule, with nothing left over from the desktop
     column widths/stagger margin. */
  .pg-grid {
    display: flex;
    flex-direction: column;
    margin-top: 12.5rem; /* 200px, per direct spec — subtitle -> first project */
    row-gap: 5rem; /* 80px between stacked cards, matches the homepage's own mobile project-to-project rhythm */
  }

  .pg-grid__col {
    display: contents;
  }

  .pg-grid__col--right {
    margin-top: 0; /* desktop-only stagger offset doesn't apply to the single-column mobile stack */
  }

  /* Matches the exact specificity of the desktop
     .pg-grid__col--left/--right .pg-card:nth-of-type(N) width rules
     above (two classes + one pseudo-class each) — a plainer selector
     here (even a single-class .pg-card, or the two-class version
     without :nth-of-type) is LESS specific than those and silently
     loses the cascade despite coming later in the file, so every card
     kept its narrower desktop width on mobile until this was scoped to
     match exactly. */
  .pg-grid__col--left .pg-card:nth-of-type(1),
  .pg-grid__col--left .pg-card:nth-of-type(2),
  .pg-grid__col--right .pg-card:nth-of-type(1),
  .pg-grid__col--right .pg-card:nth-of-type(2) {
    width: 100%; /* overrides the desktop per-card 83.333%/66.667% narrower widths — every card goes full-width on mobile */
  }

  .pg-card__title,
  .pg-card__index {
    font-size: 1rem; /* 16px, per direct spec — was 1.25rem/20px */
  }

  /* ---------- project detail overlay ----------
     Two side-by-side, full-height columns (text left, image right)
     doesn't fit a narrow phone — image on top, then all the text
     stacked below, per direct spec. The panel itself scrolls if that
     stacked content runs taller than the viewport, rather than
     stretching/clipping either column to force it to fit. */
  .pg-overlay__panel {
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: 6rem 1.5rem 3rem; /* clears the close button (24px top + 24px icon + a bit of breathing room), 24px side padding matching the site's mobile inset convention */
  }

  .pg-overlay__close {
    /* Matches the mobile header's own close/hamburger position exactly
       (see .mobile-menu__close / .nav-toggle in style.css) — "the close
       should be in the header position", per direct spec. Was
       var(--margin)/40px, the desktop-only offset. */
    top: 1.5rem;
    right: 1.5rem;
    padding-top: 0; /* the desktop-only nav-text baseline offset doesn't apply to a plain mobile button */
  }

  .pg-overlay__media {
    order: 1; /* DOM order is body-then-media (text-left/image-right on desktop) — flipped visually here since flex `order` doesn't care about source order */
    grid-column: unset;
    height: auto;
    margin-bottom: 2rem;
  }

  /* Desktop stretches the moved-in .pg-card__image to fill the column's
     full viewport height (aspect-ratio:auto override, see the base rule
     above) — back to the card's own natural ratio here, since
     .pg-overlay__media isn't a fixed-height grid cell on mobile any
     more, just a normal block sized by its own content. */
  .pg-overlay__media .pg-card__image {
    height: auto;
    aspect-ratio: 1403 / 1000;
  }

  .pg-overlay__body {
    order: 2;
    grid-column: unset;
    height: auto;
  }

  .pg-overlay__title {
    font-size: 1.5rem; /* 24px, per direct spec — was 36px */
    max-width: none;
  }

  .pg-overlay__tags {
    margin-bottom: 1rem; /* 16px, per direct spec — was 1.75rem/28px */
  }

  .pg-overlay__desc,
  .pg-overlay__how p {
    max-width: none; /* body text already 16px, matches spec as-is */
  }

  .pg-overlay__links {
    margin-top: 2.5rem; /* 40px, per direct spec — was 2rem/32px */
  }
}
