/* ===================================================================
   Alexandra Aranda — Portfolio
   Design tokens + shared layout, rebuilt from the Figma file
   (file PFJBOLsEJEXdleIVyeIJBf: Home, Project 1-4, About, Playground).
   =================================================================== */

/* Two typefaces, site-wide: Crimson Text for every serif use, Roboto
   for every sans-serif use. (Inter has been dropped entirely — it was
   the one inconsistent substitution; --font-ui and --font-nav now both
   resolve to the same Roboto stack, kept as two variables only because
   many rules already reference --font-ui by name.) A third font,
   IBM Plex Mono, was tried for the New Entity collaborators row's
   business-name line (standing in for Figma's Degular Mono) and then
   explicitly reverted — the brief for that component now calls for
   staying inside the existing Roboto/Crimson Text system rather than
   introducing a third typeface, so that row's "mono-style" label reads
   via letter-spacing on Roboto instead. */
@import url('https://fonts.googleapis.com/css2?family=Crimson+Text:wght@400;600&family=Roboto:wght@400;500&display=swap');

:root{
  --black:#000000;
  --white:#ffffff;
  --grey:#7d7d7d;
  --grey-line:#e4e4e4;
  --cyan:#00aeef;
  --green:#00a85a;
  --active-yellow:#ffff00;

  --font-serif:'Crimson Text', Georgia, serif;
  --font-ui:'Roboto', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-nav:'Roboto', -apple-system, BlinkMacSystemFont, sans-serif;

  /* Figma's grid: a 1440px desktop frame, 12 columns, 60px outer
     margins, 20px gutters. --max is that frame width minus nothing
     (the 60px margins are added separately via --side below), so
     the two together reproduce the frame exactly at desktop widths.
     Column math: colWidth ≈ (1440 - 2*60 - 11*20) / 12 ≈ 91.7px;
     an N-column measurement ≈ N*91.7 + (N-1)*20 — e.g. 5 columns
     ≈ 538px, which is where the home hero image width comes from.
     Work new sizes out this way rather than eyeballing pixels. */
  --max:1440px;
  --side:60px;

  /* Project pages, desktop only: the gap Figma specifies between the
     fixed nav and the start of project content is 64px. The nav itself
     is fixed at top:30px (see the desktop nav rules below); this adds
     that 64px on top of its rendered height, so the title block and the
     first hero image both start exactly 64px below it. The sticky
     project-info block (see .project-info) reuses this same value as
     its scroll-stuck position, so there's no jump the moment it engages
     — it just stays put. */
  --project-top:110px;
}

@media (max-width: 720px){
  :root{ --side:20px; }
}

*{ box-sizing:border-box; }
html{ -webkit-text-size-adjust:100%; }
body{
  margin:0;
  background:var(--white);
  color:var(--black);
  font-family:var(--font-ui);
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden; /* guards against any full-viewport-width breakout elements (e.g. the home caption) ever introducing a horizontal scrollbar */
}
img{ max-width:100%; display:block; }
a{ color:inherit; }
.wrap{
  max-width:var(--max);
  margin:0 auto;
  padding-left:var(--side);
  padding-right:var(--side);
}

/* -------------------------------------------------------------
   Nav
   ------------------------------------------------------------- */
header.site{ padding:20px 0 0; }
.nav-row{
  display:flex;
  align-items:center;
  justify-content:space-between;
  height:36px;
}
.wordmark{
  font-family:var(--font-nav);
  font-size:14px;
  letter-spacing:0.7px;
  text-decoration:none;
  color:var(--black);
  text-transform:uppercase;
}
.nav-pills{ display:flex; gap:8px; align-items:center; }
.pill{
  font-family:var(--font-nav);
  font-size:14px;
  letter-spacing:0.7px;
  text-transform:uppercase;
  text-decoration:none;
  color:var(--black);
  padding:8px 14px;
  border-radius:29px;
  line-height:1;
  transition:background-color .15s ease, color .15s ease;
}
.pill:hover{ background:var(--grey-line); }
.pill.is-active{ background:var(--black); color:var(--white); }
.pill.is-active:hover{ background:var(--black); }
/* Playground's active pill is green, per the Figma nav — kept distinct from About's black pill by design. */
body.page-playground .pill.is-active{ background:var(--green); color:var(--white); }
body.page-playground .pill.is-active:hover{ background:var(--green); }

.visually-hidden{
  position:absolute;
  width:1px; height:1px;
  padding:0; margin:-1px;
  overflow:hidden;
  clip:rect(0,0,0,0);
  white-space:nowrap;
  border:0;
}

/* -------------------------------------------------------------
   Mobile nav: a reusable header + toggle + overlay menu, used the
   same way on every page (see build.py's nav() and the 4 hand-authored
   work pages). Hidden entirely on desktop by default; the min-width
   901 rule below switches it off explicitly, and the max-width 900
   block further down switches it on and does the actual layout.
   The top-right "+" here is the menu toggle, a completely separate
   element from the yellow "+" inside the project caption — this one
   is never yellow, so the two are never visually confused.
   ------------------------------------------------------------- */
.mobile-nav-toggle{
  display:none;
  width:32px; height:32px;
  align-items:center;
  justify-content:center;
  background:none;
  border:none;
  padding:0;
  margin:0;
  cursor:pointer;
  position:relative;
  z-index:201;
}
.mobile-nav-toggle-mark{ position:relative; width:14px; height:14px; display:block; }
.mobile-nav-toggle-mark::before,
.mobile-nav-toggle-mark::after{
  content:"";
  position:absolute;
  background:var(--green); /* #00a85a — the menu toggle only; the yellow + inside the project caption is unaffected (separate .cap-cross element) */
  transition:transform .25s ease;
}
.mobile-nav-toggle-mark::before{ top:50%; left:0; width:100%; height:1.5px; transform:translateY(-50%); }
.mobile-nav-toggle-mark::after{ left:50%; top:0; height:100%; width:1.5px; transform:translateX(-50%); }
/* Open state: the + rotates into an × — a pure CSS response to the
   aria-expanded state main.js sets, no extra class needed. */
.mobile-nav-toggle[aria-expanded="true"] .mobile-nav-toggle-mark::before{ transform:translateY(-50%) rotate(45deg); }
.mobile-nav-toggle[aria-expanded="true"] .mobile-nav-toggle-mark::after{ transform:translateX(-50%) rotate(45deg); }

.mobile-nav-panel{
  display:none;
  flex-direction:column;
}
.mobile-nav-link{
  font-family:var(--font-nav);
  font-size:20px;
  letter-spacing:0.3px;
  text-transform:uppercase;
  text-decoration:none;
  color:var(--black);
  padding:14px 0;
  border-bottom:1px solid var(--grey-line);
}
.mobile-nav-link:last-child{ border-bottom:none; }

/* -------------------------------------------------------------
   Desktop nav: fixed 30px corner positions + hide-on-scroll-down.
   Mobile keeps the original in-flow header (unchanged) — this is
   scoped to desktop per Alexandra's request.
   ------------------------------------------------------------- */
@media (min-width: 901px){
  header.site{ padding:0; height:0; }
  /* Desktop Home only: the fixed wordmark stays hidden since the
     intro block already shows the name — unchanged desktop behavior.
     Mobile shows the wordmark on every page, Home included. */
  body.page-home .wordmark{ display:none; }
  .wordmark{
    position:fixed;
    top:30px; left:30px;
    z-index:100;
    transition:transform .3s ease, opacity .3s ease;
  }
  .nav-pills{
    position:fixed;
    top:30px; right:30px;
    z-index:100;
    transition:transform .3s ease, opacity .3s ease;
  }
  body.nav-hidden .wordmark,
  body.nav-hidden .nav-pills{
    transform:translateY(-24px);
    opacity:0;
    pointer-events:none;
  }
  main.home,
  main.project,
  main.about,
  main.playground{ padding-top:88px; }
}

/* -------------------------------------------------------------
   Mobile nav: the About/Playground pills are replaced by a single
   "+" toggle that opens a full-screen overlay menu (About, Playground,
   project links) — reusable and identical on every page. 16px margins
   here are the mobile nav's own, independent of --side (--side stays
   20px for the rest of each page's mobile content, unchanged).
   ------------------------------------------------------------- */
@media (max-width: 900px){
  header.site .nav-row{ padding-left:16px; padding-right:16px; }
  .nav-pills{ display:none; }
  .mobile-nav-toggle{ display:inline-flex; }
  .mobile-nav-panel{
    position:fixed;
    inset:0;
    z-index:200;
    background:var(--white);
    padding:96px 16px 24px;
    overflow-y:auto;
  }
  .mobile-nav-panel.is-open{ display:flex; }
  body.mobile-nav-open{ overflow:hidden; }
}

/* -------------------------------------------------------------
   Type
   ------------------------------------------------------------- */
h1{
  font-family:var(--font-serif);
  font-weight:400;
  font-size:32px;
  line-height:38px;
  margin:0;
}
h3{
  font-family:var(--font-serif);
  font-weight:400;
  font-size:20px;
  line-height:24px;
  margin:0;
}
.eyebrow{
  font-family:var(--font-ui);
  font-size:14px;
  letter-spacing:0.42px;
  text-transform:uppercase;
  color:var(--black);
  display:block;
}
.eyebrow.cyan{ color:var(--cyan); }
p{ font-family:var(--font-serif); font-size:16px; line-height:20px; margin:0 0 12px; }
p:last-child{ margin-bottom:0; }

/* -------------------------------------------------------------
   Home — mobile: a normal scrolling stack of static images (the
   ".track-set--dup" copy that exists for the desktop loop is
   hidden here, so it doesn't just repeat the same 5 images twice).
   Desktop turns this into a fixed, non-scrolling screen with a
   slow auto-scrolling image loop — see the min-width:901 block
   below and home.js.
   ------------------------------------------------------------- */
main.home{ padding-top:56px; }
/* .home-grid intentionally does NOT use .wrap: .wrap caps out at
   --max and then centers with auto margins, which grows the outer
   margin past 60px on wide screens. This grid gets its own fixed
   60px (var(--side)) margins at every width instead — no max-width,
   no margin:auto — so the 12 columns simply get wider or narrower
   between two margins that never move. */
.home-grid{
  display:grid;
  grid-template-columns:repeat(12, 1fr);
  gap:20px;
  align-items:start;
  width:100%;
  padding-left:var(--side);
  padding-right:var(--side);
  box-sizing:border-box;
}
/* Hero images: 5 of the 12 columns, including the gutters between
   them — a grid area, not a fixed pixel width, so it's fluid at
   every viewport size and flush with column 1 (60px from the
   viewport edge via .home-grid's own padding above). */
.home-track-viewport{ grid-column:1 / span 5; overflow:visible; }
.home-info-col{ grid-column:6 / -1; }
.home-track{ display:flex; flex-direction:column; gap:24px; }
.track-set{ display:flex; flex-direction:column; gap:24px; }
.home-track figure{ margin:0; flex:none; }
.home-track img{
  width:100%;
  height:auto;
  aspect-ratio:538/430; /* the Figma hero ratio — scales fluidly, crop stays consistent */
  object-fit:cover;
  background:#c4c4c4;
}
.home-track .placeholder-block{ width:100%; height:auto; aspect-ratio:538/430; }
.home-intro .role-label{
  font-family:var(--font-ui);
  font-size:22px; /* was 24px — reduced 2pt per request; hidden entirely on
                      mobile (see display:none override below), so this
                      only ever affects the desktop size */
  letter-spacing:0.24px;
  display:block;
  margin-bottom:14px;
}
.home-intro h1{ max-width:46ch; margin-bottom:14px; }
.home-intro .read-more{
  font-family:var(--font-ui);
  font-size:14px;
  text-decoration:underline;
  text-decoration-skip-ink:none;
}
.hero-caption{
  margin-top:40px;
  display:flex;
  flex-direction:column;
  gap:8px;
  opacity:0;
  transform:translateY(6px);
  transition:opacity .35s ease, transform .35s ease;
  /* Now an <a> (was a plain div) so the title/role/desc text links to
     the active project's page, same as the image above it already did
     — text-decoration reset keeps it looking exactly as before, no
     underline. Slots with no project page (the forthcoming-book
     teaser) get no href at all (see home.js), so the browser's default
     "not a link" styling/cursor applies there with no extra CSS. */
  text-decoration:none;
}
.hero-caption.is-visible{ opacity:1; transform:none; }
/* Mobile-safe default: title and role simply stack, each left-aligned —
   this is the same layout they had before the desktop-only row treatment
   below was introduced, so mobile is unaffected by this change. */
.hero-caption .cap-row{
  display:flex;
  flex-direction:column;
  gap:8px;
  width:100%;
}
.hero-caption .cap-title{
  font-family:var(--font-ui);
  font-weight:500;
  font-size:16px;
  letter-spacing:0.48px;
  text-transform:uppercase;
  text-align:left;
}
.hero-caption .cap-role{
  font-family:var(--font-ui);
  font-size:16px;
  color:var(--grey);
  text-align:left;
}
.hero-caption .cap-cross{
  width:14px;
  height:14px;
  color:var(--active-yellow);
  margin:2px 0;
  align-self:flex-start;
}
.hero-caption .cap-desc{
  font-family:var(--font-serif);
  font-size:16px;
  line-height:20px;
  width:100%;
  text-align:left;
}

/* Mobile home: two independent regions, matching the Figma mobile
   reference exactly rather than reusing the desktop layout.
     Top    — header + intro + active project info: stationary.
     Bottom — the image track: scrolls on its own, inside its own
              viewport, while the top region never moves.
   The page itself never scrolls here (same non-scrolling principle as
   desktop, just built with a two-row grid instead of absolute
   positioning, since mobile content stacks full-width anyway). The
   title/role row sits directly above the image viewport on purpose —
   see home.js for the matching "image top reaches viewport top"
   trigger, which is the mobile equivalent of desktop's center trigger. */
@media (max-width: 900px){
  /* body becomes the 100%-viewport flex column: header gets its natural
     height, main.home gets exactly what's left. (A plain height:100vh
     on main.home alone would ignore the header's own in-flow height
     and run that far past the bottom of the screen — the header here
     isn't fixed-position the way desktop's is.) */
  body.page-home{
    overflow:hidden;
    height:100vh;
    height:100dvh;
    display:flex;
    flex-direction:column;
  }
  body.page-home header.site{ flex:0 0 auto; }
  main.home{
    flex:1 1 auto;
    min-height:0;
    height:auto;
    padding-top:0;
    overflow:hidden;
  }
  /* The mobile 5-column grid: 16px margins, 12px gutters — its own
     system, independent of the desktop 12-column grid above (60px
     margins, 20px gutters). Everything today spans the full 5 columns,
     but the tracks are real rather than a single 1fr column, so any
     future content can sit inside a specific span. */
  .home-grid{
    display:grid;
    grid-template-columns:repeat(5, 1fr);
    grid-template-rows:auto 1fr;
    align-items:stretch; /* overrides the base rule's align-items:start — without this the image viewport grows to its full content height instead of being clipped to its row */
    gap:0;
    column-gap:12px;
    height:100%;
    width:100%;
    padding-left:16px;
    padding-right:16px;
  }
  .home-info-col{
    grid-row:1;
    grid-column:1 / -1;
    margin-bottom:12px; /* the image viewport begins immediately below this */
  }
  /* The duplicate figure set (".track-set--dup") used to be hidden here
     — mobile just real-scrolled through one set. Now that mobile also
     auto-scrolls in a seamless loop (see home.js), it needs the same
     back-to-back duplicate set desktop uses, so there is no rule hiding
     it at this breakpoint any more. */
  .home-track-viewport{
    grid-row:2;
    grid-column:1 / -1;
    min-height:0; /* without this a grid row item won't actually shrink to fit — it'll overflow instead of clipping */
    overflow:hidden; /* the track is moved by home.js's own transform loop now, not native scrolling */
    touch-action:none; /* hands touch input entirely to home.js (swipe = carousel speed), instead of letting the browser try to pan/scroll this element natively */
  }
  /* Mobile-only image spacing: exactly 6px between hero images, no
     other margin/padding adding to it (figures already have margin:0
     in the base rule above) — overrides the base rule's 24px gap just
     at this breakpoint; desktop's own 6px (set inside its own
     min-width:901 block) is untouched. */
  .home-track,
  .track-set{ gap:6px; }

  /* Only the small header wordmark says "Alexandra Aranda" on mobile —
     the larger duplicate above the intro statement is removed here. */
  .home-intro .role-label{ display:none; }
  /* The intro statement is still Crimson Text, just smaller — this
     leaves more of the active project and image viewport visible on a
     phone screen without changing the 5-column grid it sits in. */
  .home-intro h1{
    font-size:22px;
    line-height:26px;
    margin-bottom:10px;
  }

  /* Figma's mobile order is intentionally reversed from desktop:
     description, then the yellow cross, then the title/role row —
     not desktop's row/cross/desc. */
  .hero-caption .cap-row{ order:3; }
  .hero-caption .cap-cross{ order:2; }
  .hero-caption .cap-desc{ order:1; }

  /* Title left / role right on one row, like desktop — but the role
     truncates with an ellipsis instead of wrapping to a second line;
     there's much less width to share on a phone. */
  .hero-caption .cap-row{
    flex-direction:row;
    flex-wrap:nowrap;
    justify-content:space-between;
    align-items:baseline;
    gap:12px;
  }
  .hero-caption .cap-title{ flex:0 0 auto; white-space:nowrap; }
  .hero-caption .cap-role{
    flex:0 1 auto;
    min-width:0;
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
    text-align:right;
  }
}

/* Desktop: the page itself doesn't scroll — main.home is pinned to
   one viewport and the footer is left off this page entirely (see
   build.py) — but the image track is NOT constrained to fit inside
   that viewport. It spans the full height and simply gets clipped
   by it, so images travel through and off the true top/bottom edges
   like a continuous, uncentered scroll rather than a boxed-in
   carousel. The hero track loops on its own via home.js; the mouse
   wheel doesn't move the page, it just speeds that loop up (see
   home.js) — nothing about the caption changing ever repositions it.
   The static intro and the dynamic caption are two independently
   positioned elements within the same column, not one grouped/
   centered block: the intro is pinned a fixed distance below the
   nav, and only the caption responds to which image is centered. */
@media (min-width: 901px){
  body.page-home{ overflow:hidden; }
  main.home{
    height:100vh;
    padding-top:0;
    overflow:hidden;
  }
  .home-grid{ height:100%; align-items:stretch; }
  .home-track-viewport{
    height:100%;
    overflow:hidden;
  }
  .home-track,
  .track-set{ gap:6px; will-change:transform; }
  .home-info-col{ position:relative; height:100%; }
  .home-intro{
    position:absolute;
    top:84px; /* nav's own bottom edge (60px, see .nav-pills) + a 24px gap */
    left:0;
  }
  .hero-caption{
    /* top:50% on an absolutely-positioned box places its TOP edge at
       50% of .home-info-col's height (100vh) — i.e. exactly 50vh —
       with no centering transform, so the project title's top edge
       sits at the viewport's vertical center and everything else
       flows downward from there. (The base rule's translateY(6px)
       fade-in slide still applies and resolves to none once visible
       — that's just the reveal micro-animation, not positioning.) */
    position:absolute;
    top:50vh;
    left:0;
    right:0; /* spans the full 7-column width so the role can anchor to its far right edge */
    margin-top:0;
  }
  /* Desktop-only: title + role share one row spanning the full caption
     width — title pinned to the left edge, role pinned to the right
     edge, with whatever space is between them simply flexing — the
     same responsive behavior as the rest of the grid, no fixed widths.
     (Matches the Figma "Hero Image Caption_v2" component; mobile keeps
     the simple stacked layout defined above.) */
  .hero-caption .cap-row{
    flex-direction:row;
    flex-wrap:wrap;
    justify-content:space-between;
    align-items:baseline;
    gap:8px 24px;
  }
  .hero-caption .cap-role{ text-align:right; }
  /* Desktop-only: the description is fixed to exactly 3 of the page's
     12 grid columns, using the same column math as .home-grid itself
     (column = (100% - 11*gutters)/12), rather than a pixel width or
     content-based size — so it resizes fluidly with the viewport right
     alongside the rest of the grid.
     This element's own 100% is the 7-column caption area's width (call
     it IC). Since that area is itself 7 of the same page columns plus
     6 of the same 20px gutters, one column here = (IC - 6*20px)/7 —
     identical in size to one column of the outer 12-column grid.
     3 columns + 2 of those same gutters between them therefore equals:
       3 * (IC - 6*20px)/7 + 2*20px = (3*IC - 80px) / 7
     expressed below directly in terms of this element's own 100%. */
  .hero-caption .cap-desc{ width:calc((300% - 80px) / 7); }
}

/* -------------------------------------------------------------
   Project pages
   ------------------------------------------------------------- */
/* padding-bottom is true page-bottom breathing room — after the end of
   the footer/project index (or the imagery, whichever is longer), not
   extra space between the images and the Project Index. */
main.project{ padding-top:56px; padding-bottom:40px; }
/* .project-layout intentionally does NOT use .wrap, same reasoning as
   .home-grid: .wrap caps at --max and centers with auto margins, which
   grows the outer margin past 60px on wide screens. This gets its own
   fixed 60px (var(--side)) margins at every width instead, and splits
   the Figma 12-column grid the same way the rest of the site does:
   sidebar = columns 1-3 (315px at 1440), content = columns 5-12 (873px
   at 1440), column 4 left empty as the gutter between them — a ~132px
   gap at 1440 (1 column + 2 gutters) that scales fluidly, rather than
   a flat 40px. One grid, referenced by every element on the page —
   there is no separate fixed-width sidebar grid. */
.project-layout{
  display:grid;
  grid-template-columns:repeat(12, 1fr);
  gap:20px;
  width:100%;
  padding-left:var(--side);
  padding-right:var(--side);
  box-sizing:border-box;
  /* align-items is left at its default (stretch) on purpose: both
     columns need to span the full row height so .project-info (inside
     .project-sidebar) can stick for the full scroll of the imagery and
     then yield to .sidebar-bottom — see below. */
}
.project-sidebar{
  grid-column:1 / span 3;
  display:flex;
  flex-direction:column;
  /* No sticky/scroll/overflow here — this column is just a full-height
     (stretch-aligned) flex container. The page itself is the only
     scrolling container; see .project-info-zone and .sidebar-bottom. */
}
/* .project-info-zone is the sticky CONTAINING block for .project-info,
   and it's deliberately a separate element from .sidebar-bottom's flex
   sibling rather than relying on margin-top:auto directly on
   .sidebar-bottom. Giving the zone flex:1 (so it fills exactly
   "everything except .sidebar-bottom's own height") means its bottom
   edge is, by construction, always precisely where .sidebar-bottom
   starts — at any viewport height. Position:sticky never lets an
   element travel past its containing block's bottom edge, so
   .project-info is mathematically guaranteed to finish yielding right
   as .sidebar-bottom's top edge arrives, with no overlap, instead of
   being bounded by the whole column's height (which would let it
   overlap .sidebar-bottom on shorter viewports — the two aren't the
   same thing whenever .sidebar-bottom has any height of its own). */
.project-info-zone{ flex:1 1 auto; min-height:0; position:relative; }
/* The primary project info (title, description, Role/Scope/Credits) is
   the thing that stays put while the imagery scrolls — not the whole
   left column (the Project Index + footer below need normal, non-sticky
   behavior, see .sidebar-bottom). --project-top is one shared value for
   both "how far the sticky block sits below the fixed nav" and "where
   the project content starts on first paint", so there's no jump the
   moment stickiness engages. */
.project-info{ position:sticky; top:var(--project-top); }
.project-info h2{
  font-family:var(--font-ui);
  font-weight:400;
  font-size:24px;
  letter-spacing:0.24px;
  margin:0 0 16px;
  text-transform:uppercase;
}
.project-info .field{ margin-top:24px; }
.project-info .field .eyebrow{ margin-bottom:6px; }
/* A plain flex sibling of .project-info-zone (flex:none, natural
   height) — normal, non-sticky, non-fixed document flow. It's what
   "naturally scrolls up into view" at the bottom of the page. */
.sidebar-bottom{ flex:none; padding-top:48px; }
.project-index{ margin-bottom:24px; }
.project-index .eyebrow{ margin-bottom:12px; }
.project-index a{
  display:block;
  width:fit-content; /* highlight hugs the text, not the full 3-column width */
  text-decoration:none;
  color:var(--black);
  font-family:var(--font-ui);
  font-size:16px;
  padding:2px 4px;
  margin-left:-4px;
  border-radius:2px;
}
.project-index a.is-active{ background:var(--active-yellow); }
.project-footer .copyright{
  font-family:var(--font-ui);
  font-size:14px;
  color:var(--grey);
  margin:0;
}
/* margin-left:-7px shifts the icon so the center of its vertical
   stroke (at x=7 within its own 14px box) lands exactly on the column-1
   boundary (x=0) — it straddles the edge rather than sitting flush
   inside it. */
.cross-mark{ width:14px; height:14px; display:block; margin-left:-7px; margin-bottom:24px; }

/* Editorial grid for the right-hand imagery — an 8-column sub-grid
   matching the same column math as the rest of the page (this content
   area IS columns 5-12 of .project-layout), not a single-file vertical
   stack. Each image/group gets an explicit .col-N span per the Figma
   layout (full-width, half-and-half, thirds, or the uneven 3/5 store
   marquee + press row) rather than being forced to width:100%.
   Mobile-first default below is a plain stacked column; the grid only
   applies at the desktop breakpoint, where the spans actually mean
   something (see the min-width:901 rules further down). */
.project-media{ grid-column:5 / span 8; display:flex; flex-direction:column; gap:20px; }
.project-media img{ width:100%; display:block; object-fit:cover; }

/* Generic multi-image row, for pages that group a few images side by side
   without Majo's bespoke editorial components: .media-row splits its
   children evenly across the row (two detail shots, two posters), and the
   .thirds modifier lays out a longer strip three-up (Instagram posts, app
   screens), wrapping onto additional rows as needed. Both stretch every
   image in a row to a shared height, cropping via object-fit:cover above,
   so an uneven mix of portrait/landscape source images still reads as one
   tidy strip. */
.media-row{ display:flex; gap:20px; }
.media-row > *{ flex:1 1 0; min-width:0; }
.media-row.thirds{ display:grid; grid-template-columns:repeat(3, 1fr); gap:20px; }
.media-row.thirds > *{ width:100%; }

.placeholder-block{
  background:#d9d9d9;
  display:flex;
  align-items:center;
  justify-content:center;
  color:var(--grey);
  font-family:var(--font-ui);
  font-size:13px;
  letter-spacing:0.4px;
  text-transform:uppercase;
  text-align:center;
  min-height:260px;
}
.video-placeholder{ aspect-ratio:873/430; position:relative; }
.video-placeholder::before{
  content:"";
  width:0; height:0;
  border-style:solid;
  border-width:14px 0 14px 22px;
  border-color:transparent transparent transparent var(--grey);
}

@media (max-width: 900px){
  /* minmax(0,1fr), not plain 1fr: a 1fr track's automatic minimum width
     is content-based (effectively max-content), not 0. With .ne-track's
     wide, unshrinkable collaborator-figure row inside .project-media
     (now a direct child of this single-column grid, via the
     display:contents unwrap below), that content-based minimum was
     stretching the whole track — and every other item sharing it,
     sidebar text included — out to the carousel's full unrolled width,
     blowing out the page horizontally on mobile. minmax(0,1fr) gives
     the track an explicit 0 floor so it sizes to the viewport instead,
     same as it would if this were a flex container needing min-width:0. */
  .project-layout{ grid-template-columns:minmax(0,1fr); }
  .project-sidebar{ grid-column:1; }
  .project-info{ position:static; }
  .project-media{ grid-column:1; }
  .sidebar-bottom{ padding-top:32px; }

  /* Reorder, don't restructure: .sidebar-bottom (Project Index + the
     grey "+" + copyright) stays nested inside .project-sidebar so the
     desktop sticky math above is untouched, but on mobile the sidebar
     is unwrapped (display:contents) so its two pieces and .project-media
     become plain sibling flow items that `order` can rearrange — info
     text first, then all the imagery, with the index/footer pushed to
     the very end of the page instead of sitting above the images. */
  .project-sidebar{ display:contents; }
  .project-info-zone{ order:1; }
  .project-media{ order:2; }
  .sidebar-bottom{ order:3; }
}

/* Desktop-only: the project-media editorial grid (see the comment above
   .project-media) and the project-layout's own placement within the
   page's fixed nav. Mobile keeps the plain stacked column above —
   project pages on mobile haven't been speced yet, so this just leaves
   that default alone rather than risk breaking it. */
@media (min-width: 901px){
  main.project{ padding-top:var(--project-top); }
  .project-media{
    display:grid;
    grid-template-columns:repeat(8, 1fr);
    gap:20px;
  }
  /* Default: any media-area child without an explicit .col-N (i.e. pages
     that haven't been broken into an editorial grid yet, like HDC, New
     Entity and RIA today) spans the full width rather than being squeezed
     into a single implicit 1-of-8 column. Every child on the Majo page
     already carries its own .col-N, so the more specific rules below
     always win there and this default never touches it. */
  .project-media > *{ grid-column:1 / -1; }
  .project-media .col-3{ grid-column:span 3; }
  .project-media .col-4{ grid-column:span 4; }
  .project-media .col-5{ grid-column:span 5; }
  .project-media .col-8{ grid-column:span 8; }
}

/* -------------------------------------------------------------
   Majo Ideas: flip cards, auto carousel, vertical marquee
   ------------------------------------------------------------- */
.artist-cards{ display:grid; grid-template-columns:repeat(3, 1fr); gap:16px; }
.flip-card{
  perspective:1200px;
  aspect-ratio:278/389;
  cursor:pointer;
}
.flip-card .flip-inner{
  position:relative;
  width:100%; height:100%;
  transition:transform .6s cubic-bezier(.4,.2,.2,1);
  transform-style:preserve-3d;
}
.flip-card.is-flipped .flip-inner{ transform:rotateY(180deg); }
.flip-card .flip-face{
  position:absolute; inset:0;
  backface-visibility:hidden;
  overflow:hidden;
}
.flip-card .flip-face img{ width:100%; height:100%; object-fit:cover; }
.flip-card .flip-back{
  transform:rotateY(180deg);
}
.flip-card .flip-back img{ width:100%; height:100%; object-fit:cover; }

.auto-carousel{ position:relative; aspect-ratio:426/528; overflow:hidden; background:#f4f4f4; }
.auto-carousel img{
  position:absolute; inset:0;
  width:100%; height:100%; object-fit:contain;
  opacity:0; transition:opacity 0.25s ease;
}
.auto-carousel img.is-active{ opacity:1; }

/* Pack-cover flip: a continuous, constant-speed turn around the
   vertical axis (rotateY) — like a slowly rotating display stand —
   showing a pack's front, then that same pack's own back, then the
   next pack's front, and so on. Same two-sided 3D-flip technique as
   .flip-card above, just spinning non-stop via a CSS animation
   instead of pausing between hover-triggered flips. */
.pack-flip{ position:relative; aspect-ratio:426/528; overflow:hidden; background:#f4f4f4; perspective:1600px; }
.pack-flip-inner{
  position:relative; width:100%; height:100%;
  transform-style:preserve-3d;
}
.pack-flip-inner.is-spinning{ animation:pack-flip-spin 8s linear infinite; }
@keyframes pack-flip-spin{
  from{ transform:rotateY(0deg); }
  to{ transform:rotateY(360deg); }
}
.pack-flip-face{
  position:absolute; inset:0;
  backface-visibility:hidden;
  background:#f4f4f4;
}
.pack-flip-face img{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain; }
.pack-flip-face-b{ transform:rotateY(180deg); }

/* Desktop shows the odd/even pair side by side (1-6 left, 7-12 right);
   mobile swaps in a single full-width spinner that cycles straight
   through all 12 packs instead, since two half-width spinners don't
   read well stacked on a narrow screen. display:contents keeps the
   pair's two .col-4 children as .project-media's direct grid items at
   desktop (no change there); max-width:900px flips which one is shown. */
.pack-flip-desktop-pair{ display:contents; }
.pack-flip-mobile{ display:none; }
@media (max-width: 900px){
  .pack-flip-desktop-pair{ display:none; }
  .pack-flip-mobile{ display:block; }
}

/* Figma-authored manual zoom+pan crops (not a plain object-fit:cover) —
   an overflow:hidden wrapper holding an inner <img> sized/positioned
   larger than 100%/0,0, matching Figma's own crop exactly. Specificity
   is bumped (two classes on the wrapper) so these beat the general
   ".project-media img{width:100%;object-fit:cover}" rule above. */
.project-media .crop-wrap{ position:relative; overflow:hidden; width:100%; }
.project-media .crop-wrap img{ position:absolute; max-width:none; }
.crop-wrap.crop-lifestyle-3{ aspect-ratio:873/570; }
.crop-wrap.crop-lifestyle-3 img{ width:100%; height:122.53%; left:0; top:-22.49%; }
.crop-wrap.crop-activity-1{ aspect-ratio:427/532; }
.crop-wrap.crop-activity-1 img{ width:156.46%; height:100.38%; left:-22.92%; top:0; }

/* The final row (store marquee + press) is a fixed Figma proportion —
   873:538 at the content area's full 8-column width — rather than a
   row whose height is simply whatever its two children happen to need.
   Both children fill this shared, fluidly-scaling height exactly, so
   they always share the same top and bottom edge at any viewport
   width. See .final-row below for the wrapper that establishes this. */
.final-row{
  display:grid;
  grid-template-columns:repeat(8, 1fr);
  gap:20px;
  aspect-ratio:873/538;
}
.final-row > *{ height:100%; min-height:0; }
/* The Strategist (press) image keeps its own appearance — contain, not
   cover, so the fixed-aspect row never crops or distorts it; any
   letterboxing inside its cell is expected and correct. Specificity is
   bumped (two classes on the row) to beat the general
   ".project-media img{object-fit:cover}" rule above. */
.project-media .final-row img{ width:100%; object-fit:contain; }

/* Mobile: the 873:538 side-by-side proportion only makes sense at the
   full 8-column desktop width — shrunk to a phone's width it squashes
   the marquee into an unreadable sliver. Below 900px, drop the shared
   aspect box and just stack the two full width, marquee on top of the
   press image, each sized on its own terms. */
@media (max-width: 900px){
  .final-row{
    display:flex;
    flex-direction:column;
    aspect-ratio:auto;
  }
  .final-row > *{ height:auto; }
  .store-marquee{ width:100%; aspect-ratio:315/538; }
}

.store-marquee{
  position:relative;
  overflow:hidden; /* hard clip — logos simply disappear at this edge, no fade */
}
.store-marquee .track{
  display:flex;
  flex-direction:column;
  gap:60px;
  animation:marquee-vertical 26s linear infinite; /* never paused — see below, intentionally no :hover/:focus-within rule */
}
/* Real store/museum logos, each its own aspect ratio reproducing the
   individually-authored height Figma gives it (e.g. Portland ~32px
   tall, Blanton ~263px tall at this 315px width), with 60px of space
   between each — so no fixed heights are set here. */
.store-marquee .track img{
  width:100%;
  height:auto;
  display:block;
  object-fit:contain;
}
@keyframes marquee-vertical{
  from{ transform:translateY(0); }
  to{ transform:translateY(-50%); }
}

/* -------------------------------------------------------------
   New Entity: fog hero + phrase loop, horizontal scroll
   ------------------------------------------------------------- */
/* Figma's hero is light, not dark: pale sky-blue/foggy image with dark
   (#242424) text directly on top — no dark backing panel or cream text,
   which was a mistaken earlier assumption. */
.ne-embed{ background:#fff; color:#242424; }
.ne-hero{
  position:relative;
  aspect-ratio:873/558;
  overflow:hidden;
  display:flex;
  align-items:center;
}
/* Full opacity, not the .8 this used to carry: that was tuned for a
   plain photo background with the fog/text/chrome layered on top of it
   separately. Both breakpoints now use a flattened composite with its
   own final color treatment (including the logo/nav/button pixels)
   already baked in, so dimming it here would wash that out. */
.ne-hero img.bg, .ne-hero video.bg{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.ne-hero .fog{
  position:absolute; inset:0;
  background:radial-gradient(ellipse at 50% 60%, rgba(244,242,234,.9), rgba(244,242,234,0) 65%);
  opacity:1;
  transition:opacity 1.6s ease;
  pointer-events:none;
}
.ne-hero.is-revealed .fog{ opacity:0; }
.ne-hero-nav{
  position:absolute;
  top:0; left:0; right:0;
  z-index:2;
  display:flex;
  align-items:center;
  justify-content:space-between;
  padding:26px 36px 0;
  pointer-events:none; /* decorative chrome recreating New Entity's own
    site header within this hero embed — not real navigation for this
    portfolio, so clicks pass straight through. */
}
.ne-hero-logo{ display:block; color:#242424; }
.ne-hero-logo svg{ display:block; height:16px; width:auto; }
.ne-hero-links{ display:flex; gap:22px; }
.ne-hero-links a{
  font-family:var(--font-ui);
  font-size:11px;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:#242424;
  text-decoration:none;
}
/* Figma specs this box at 650px, sized for Trade Gothic Next LT Pro;
   Roboto (our substitute for that font) runs wider, and box-sizing:
   border-box means the 48px side padding eats into that max-width too
   — so the box is widened enough that the longest explicit line
   ("New Entity backs great people who", ~656px at 42px Roboto) still
   fits on one line inside the padding, keeping the three-line break
   below intact instead of wrapping a fourth line. */
.ne-hero .copy{ position:relative; padding:0 48px; max-width:770px; }
/* Desktop: align the headline's left edge to the project's own 8-column
   editorial grid (the same repeat(8,1fr)/20px-gap grid .project-media uses
   for the imagery below — see that rule's comment) instead of an arbitrary
   fixed px inset, per the user's column-grid reference screenshot showing
   the text should start further in than the old 48px padding placed it.
   The hero becomes the grid itself (it already spans the full width as a
   single .project-media item), and .copy starts at column 2's line —
   at this frame's ~873px native width that lands at ~112px, which is
   almost exactly Figma's own literal spec (left:111px) for this box, a
   reassuring cross-check that this is the intended column. It then runs
   through to the hero's right edge (matching Figma's box ending at ~761px
   of the 873px frame too), with a right-side padding standing in for a
   column's worth of right margin so it doesn't run flush to the hero's
   edge. Scoped to the same min-width:901px breakpoint .project-media's
   own grid uses — mobile keeps the simple fixed-padding version above
   untouched. */
@media (min-width:901px){
  .ne-hero{
    display:grid;
    grid-template-columns:repeat(8, 1fr);
    gap:20px;
    /* start, not center: hero-desktop-flat.png's buttons are baked in at
       a fixed spot (measured ~289-319px of the 558px-tall frame) that
       sits higher than where centering the headline in the full frame
       would land it — the flattened image was composed with a tighter
       vertical rhythm than this centered-in-the-whole-frame math
       produces, so centering the live h3 the old way runs it straight
       into the buttons. align-items:start + the margin-top below instead
       centers just the headline within the actual open band (below the
       nav, above the buttons), which is where it needs to sit against
       this image. */
    align-items:start;
  }
  .ne-hero .copy{
    grid-column:2 / -1;
    padding:0 48px 0 0;
    max-width:none;
    /* ~140px of vertical offset at the frame's native 873px width (95px
       to center the headline in the ~46-289px open band, plus as much of
       the requested 60px-further-down move as still clears the buttons
       baked into hero-desktop-flat.png at y:289-319 — the full 60px ran
       the last line straight into them, see the conversation). Expressed
       as a %-of-width margin since that's what CSS resolves margin
       percentages against — keeps it correctly proportioned at any
       rendered width, the same way the aspect-ratio-locked frame itself
       scales. */
    margin-top:16%;
  }
}
/* Both desktop (hero-desktop-flat.png) and mobile (hero-mobile-flat.png)
   backgrounds are now flattened composites with the logo, nav,
   fog/atmosphere and both buttons already baked in by Alexandra — two
   separate, purpose-built compositions, not one image stretched to fit
   both. The live versions of that same chrome stay in the markup (kept
   rather than deleted, in case either image is swapped out later) but
   are redundant, double-rendered elements now, so they're hidden at
   every width. Only the headline stays live text, positioned over each
   image by the rules above/below. (This has to come after the
   .ne-hero-nav/.fog/.ctas base rules further down the file — same
   specificity, so source order decides, and a copy placed before them
   would just get overridden back to visible.) */
/* Mobile now mirrors desktop's approach: hero-mobile-flat.png is its own
   flattened composite (background, logo, nav, both buttons baked in,
   purpose-built at a taller 368/510 portrait ratio rather than the
   desktop frame just shrunk) — not a live nav/ctas layout fighting for
   room inside the wide 873/558 desktop frame, which is what the old
   version of this rule (visible in prior history) was built to patch
   around. With the chrome baked in and hidden (see the unconditional
   .ne-hero-nav/.fog/.ctas rule below), .ne-hero only needs to lock to
   the new image's own aspect ratio and let the headline sit on top. */
@media (max-width:900px){
  .ne-hero{
    aspect-ratio:368/510;
    display:block;
  }
  /* The headline is the only live element left at mobile. Positioned
     absolutely within .ne-hero (already position:relative) using
     percentages of the flattened image's own frame, so it scales and
     stays anchored to the same open sky area — below the hamburger/logo
     row, above the two stacked buttons — at any viewport width, the
     same way object-fit:cover keeps the background itself anchored.
     left:5.2% matches hero-mobile-flat.png's own measured left edge for
     both the hamburger icon (x:19-38px) and the buttons (x:19px),
     19/368 ≈ 5.2%, so the headline lines up with that shared column
     rather than an arbitrary inset. top:30.9% centers it vertically
     within the actual open band (hamburger bottom ~36px to the button
     block's top ~279px, midpoint ~157.5px of the 510px-tall frame) —
     not the whole frame, which would run the text into the buttons the
     same way it once ran into the desktop image's. Left/right insets
     (not a fixed max-width) let the text reflow to more, shorter lines
     than desktop's three rather than overflowing or needing a second
     set of manually-authored line breaks. */
  .ne-hero .copy{
    position:absolute;
    top:30.9%;
    left:5.2%;
    right:10%;
    transform:translateY(-50%);
    padding:0;
    max-width:none;
  }
  /* Let the sentence reflow on its own at mobile's narrower width/smaller
     type, instead of soft-wrapping on top of desktop's three manual
     breaks (which produced uneven, badly-broken lines — a lone "people
     who" orphan, "biotech." stranded alone). */
  .ne-hero .copy h3 .hl-br{ display:none; }
}
/* Figma: Trade Gothic Next LT Pro at 42px — this project substitutes
   Roboto (--font-ui) everywhere Trade Gothic appears in the source,
   and a serif was a mistaken earlier substitution for this headline
   specifically. clamp() keeps 42px as the ceiling (matching Figma's
   873px-wide frame) while scaling down on narrower project-media
   columns rather than overflowing the 650px box. */
.ne-hero .copy h3{
  font-family:var(--font-ui);
  color:#242424;
  font-size:clamp(26px, 3.6vw, 42px);
  line-height:1.15;
  min-height:3.6em;
}
/* This has to come AFTER the unconditional rule above, not inside the
   earlier max-width:900px block — same specificity, so source order
   decides, and a copy placed before it was silently losing (the mobile
   headline has been rendering at the desktop rule's 26-42px this whole
   time, not the 17-26px it looked like in style alone — which is the
   real reason the longest phrase ("restaurants to renewables.") was
   wrapping to a 5th line and shifting the vertical centering). Sized so
   all three cycling phrases — the longest included — hold to 4 lines at
   any width in the mobile range, keeping the box height (and so its
   centered position) stable as the phrase swaps. */
@media (max-width:900px){
  .ne-hero .copy h3{
    font-size:clamp(16px, 5.8vw, 25px);
    line-height:1.24;
    min-height:0;
  }
}
/* The lead-in ("...exceptional things, from") is fixed; only the sector
   pairing after it (hero-phrase-text) cycles between the three confirmed
   variants (new-entity.js). Reveal is a clip-path wipe (left edge
   unclipped, right edge sweeping from 100%-clipped to 0%) paired with an
   opacity fade, giving a "fade in from left to right" feel. Works
   identically at both breakpoints — nothing here is scoped to a
   min/max-width — since the headline is the one piece of this hero that
   stays live (and animated) over either flattened background. */
.hero-phrase-text{
  display:inline-block;
  clip-path:inset(0 100% 0 0);
  opacity:0;
  transition:clip-path 1.4s cubic-bezier(.22,.61,.36,1), opacity 1s ease;
}
.hero-phrase-text.is-revealed{
  clip-path:inset(0 0 0 0);
  opacity:1;
}
.ne-hero .ctas{ display:flex; gap:12px; margin-top:20px; }
.ne-hero .ctas a{
  font-family:var(--font-ui); font-size:12px; letter-spacing:.5px; text-transform:uppercase;
  padding:8px 16px; text-decoration:none; border-radius:2px;
}
.ne-hero .ctas a.primary{ background:#fff84a; color:#1a1c1c; }
/* Figma spec: rgba(225,233,230,.8) with dark text — a light solid-ish
   chip matching the primary button's treatment, not the glassy, barely
   -there chip (.25 opacity + light text) this used to be. */
.ne-hero .ctas a.secondary{ background:rgba(225,233,230,.8); color:#242424; }
/* Both desktop (hero-desktop-flat.png) and mobile (hero-mobile-flat.png)
   backgrounds are now flattened composites with the logo, nav,
   fog/atmosphere and both buttons already baked in by Alexandra — two
   separate, purpose-built compositions, not one image stretched to fit
   both. The live versions of that same chrome stay in the markup (kept
   rather than deleted, in case either image is swapped out later) but
   are redundant, double-rendered elements now, so they're hidden at
   every width — this has to come after the three rules above it, since
   they share the same specificity and source order decides. .ne-hero-nav
   and .fog are both position:absolute, so hiding them outright doesn't
   move anything else. */
.ne-hero-nav, .ne-hero .fog{ display:none; }
/* .ctas is different: it's an in-flow sibling of h3 inside .copy, and
   .copy used to be centered as that whole h3+ctas group within the
   873/558 frame. display:none on .ctas would drop it from layout, which
   shrinks that group down to just h3 and re-centers — shifting the
   headline about 25px lower than it used to sit, straight into the
   buttons baked into hero-desktop-flat.png (that image was captured
   from the original h3+ctas layout, buttons included, so it expects the
   headline to land exactly where it did before). visibility:hidden
   keeps .ctas' layout space reserved without drawing its (redundant)
   pixels, which keeps that centering math — and so the headline's
   position — identical to before. Mobile's .copy is positioned fresh
   (see below) against hero-mobile-flat.png's own open space, not this
   inherited centering, so .ctas can just be removed from flow there. */
@media (min-width:901px){
  .ne-hero .ctas{ visibility:hidden; }
}
@media (max-width:900px){
  .ne-hero .ctas{ display:none; }
}
@media (prefers-reduced-motion: reduce){
  .ne-hero .fog{ transition:none; }
  .hero-phrase-text{ transition:none; }
}

/* Figma reference for this component specifically (a tall, cropped-edge
   carousel showing ~3-4 people at once, generous gaps, a repositioned
   Pitch Portal button and a single centered caption) — composition,
   spacing, typography and scale below all follow that reference rather
   than the page's general section rhythm. */
.ne-scroll{ background:#f4f4f4; padding:40px 40px 48px; }
.ne-scroll-top{ display:flex; align-items:flex-start; justify-content:space-between; gap:16px; margin-bottom:20px; }
/* "mono-style" per Figma, but per brief this stays inside the site's
   existing two typefaces — Roboto with wide letter-spacing reads as a
   small label/eyebrow without introducing an actual monospace font. */
.ne-scroll .sub{ font-family:var(--font-ui); font-size:11px; letter-spacing:.12em; color:var(--grey); margin:0; text-transform:uppercase; }
.ne-pitch{
  font-family:var(--font-ui); font-size:12px; letter-spacing:.5px; text-transform:uppercase;
  background:#fff84a; color:#1a1c1c; padding:8px 16px; border-radius:2px; text-decoration:none;
  flex:none;
}
/* Smaller and lighter than the page's other section heads, and broken
   into its two sentences explicitly (not left to wrap) per Figma. */
.ne-scroll .head{ font-family:var(--font-ui); font-weight:400; font-size:19px; line-height:1.4; color:var(--black); margin:0 0 28px; }

/* Cutout portraits (transparent PNGs, one person or a pair per file),
   each pre-composed by Alexandra on a shared 1440px-tall canvas so the
   relative scale between people is baked into the source files — every
   image here is rendered at one common height with width left to auto,
   which reproduces that intended scale automatically and never stretches
   or distorts anything. Figures sit on a shared "ground line"
   (align-items:flex-end) exactly like the reference photo. The frame is
   fixed-height and overflow:hidden, and the row is simply much wider than
   it, so figures crop at the left/right edges as they drift through —
   matching Figma rather than ever showing the full lineup at once. */
.ne-track-wrap{ height:460px; overflow:hidden; position:relative; }
.ne-track{
  height:100%;
  display:flex;
  align-items:flex-end;
  gap:56px;
  animation:marquee-horizontal 54s linear infinite;
  width:max-content;
  padding-bottom:4px; /* keeps feet off the very edge of the track box */
}
/* Pause-on-hover is a mouse-only affordance (gives a desktop visitor a
   moment to read a caption before the row keeps drifting). Touch doesn't
   have real hover, but mobile browsers simulate a "stuck" :hover state
   after a tap — scrolling the page with a thumb that brushes the track
   was pausing/resuming the marquee unpredictably, which read as a glitch.
   Gating hover to pointer:fine keeps the row a single continuous motion
   on touch, matching how a touch visitor actually expects it to behave;
   :focus-within stays ungated so keyboard navigation can still pause it. */
@media (hover:hover) and (pointer:fine){
  .ne-track-wrap:hover .ne-track{ animation-play-state:paused; }
}
.ne-track-wrap:focus-within .ne-track{ animation-play-state:paused; }
.ne-track figure{ margin:0; flex:none; width:auto; position:relative; cursor:pointer; }
.ne-track img{ display:block; height:440px; width:auto; }
@keyframes marquee-horizontal{
  from{ transform:translateX(0); }
  to{ transform:translateX(-50%); }
}
@media (prefers-reduced-motion: reduce){
  .ne-track{ animation:none; overflow-x:auto; }
}

/* One caption, centered under the whole frame (not attached to any single
   figure's column) — new-entity.js swaps its text to whichever person is
   nearest center as the row scrolls, or whoever is hovered/focused. The
   reserved min-height keeps the hint line below from shifting as
   captions of different lengths fade in and out. */
.ne-track-caption{
  text-align:center;
  margin-top:20px;
  min-height:42px;
  opacity:0;
  transition:opacity .3s ease;
}
.ne-track-caption.is-visible{ opacity:1; }
.ne-track-caption .collab-name{
  display:block;
  font-family:var(--font-ui);
  font-weight:400;
  font-size:14px;
  color:var(--black);
}
.ne-track-caption .collab-business{
  display:block;
  font-family:var(--font-ui);
  font-size:11px;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--grey);
  margin-top:3px;
}

/* Mobile: a purpose-built composition for this module, not the desktop
   one shrunk or wrapped. Uses its own small grid (5 columns, 16px page
   margins, 12px gutters) rather than the project page's 60/20px --side
   margins — this module is visually self-contained (its own grey card),
   so its internal margins don't need to match the rest of the page.
   Scoped entirely to max-width:900px; nothing here touches a single
   desktop rule above. */
@media (max-width:900px){
  .ne-scroll{ padding:32px 16px; }
  /* "Partner with us" and Pitch Portal share a row and sit on the same
     baseline (flex-start on desktop stacks them awkwardly once the
     button is this much smaller than the headline next to it). */
  .ne-scroll-top{ align-items:center; margin-bottom:20px; }
  .ne-scroll .sub{ font-size:10px; }
  /* Desktop's button (12px / 8px·16px padding) reads oversized against
     a 358px-wide mobile card — scaled down rather than inherited. */
  .ne-pitch{ font-size:10px; padding:6px 12px; }
  /* Desktop's 19px/1.4 is sized for a ~400-600px-wide headline; at mobile
     widths that wraps into a dense block. Smaller size + tighter leading
     keeps the two manual lines (the <br> in the markup is unchanged)
     reading as two short, intentional lines instead of wrapped prose. */
  .ne-scroll .head{ font-size:19px; line-height:1.28; margin:0 0 24px; }
  /* The carousel gets its own mobile viewport: a shorter frame with a
     proportionally shorter image height (240px vs desktop's 440px) and a
     tighter gap (12px, this module's own gutter, vs desktop's 56px) so
     roughly 2 figures sit fully in view with a third cropped at each
     edge — rather than 440px-tall desktop-scale people overflowing a
     390px-wide screen. width:auto on the images (unchanged from desktop)
     still does all the proportion-preserving work: nothing here stretches
     or distorts a single figure, it only changes the common height they
     all scale from. The wrap bleeds to this card's own left/right edges
     (negative margin matching .ne-scroll's 16px padding) so the crop
     reads as a true edge-to-edge band inside the card, matching the
     "crop cleanly at the edges" brief, without the grey card itself
     breaking out of its column. */
  /* Swipe, not auto-scroll: a CSS animation that keeps running underneath
     the page's own vertical scroll is what caused the glitches reported
     here — a transform-animated row inside overflow:hidden is a known
     weak spot for mobile Safari/Chrome (figures briefly not painted after
     the page settles from a scroll) and for native `loading=lazy` (images
     positioned off to the right by the animation, not by real scroll,
     don't reliably count as "near the viewport", so they can stay
     unrequested). Native horizontal scrolling with snap sidesteps both:
     there's no animation to drop a frame of, and every image sits in a
     real scroll container the browser lazy-loads correctly. It's also
     the gesture a visitor already expects for "swipe through people" on
     a phone, where desktop's ambient auto-drift doesn't translate anyway.
     Desktop keeps the marquee unchanged — only this breakpoint switches. */
  .ne-track-wrap{
    height:244px;
    margin:0 -16px;
    overflow-x:auto;
    overflow-y:hidden;
    -webkit-overflow-scrolling:touch;
    scroll-snap-type:x proximity;
  }
  .ne-track{ gap:12px; padding-bottom:4px; animation:none; }
  .ne-track img{ height:240px; }
  .ne-track figure{ scroll-snap-align:start; }
  /* The duplicate, aria-hidden figure set exists only to make the
     infinite marquee loop seamlessly — irrelevant once there's no loop,
     and left in would just be a second, identical, dead-end half of the
     swipe after Russel + Ligaya. */
  .ne-track figure[aria-hidden="true"]{ display:none; }
  /* Independent mobile sizing for the caption, not inherited from
     desktop — slightly smaller type and a tighter reserved min-height
     to match. */
  .ne-track-caption{ margin-top:14px; min-height:36px; }
  .ne-track-caption .collab-name{ font-size:13px; }
  .ne-track-caption .collab-business{ font-size:10px; }
}

/* Click-through pitch deck: a single <img> whose src swaps on click or
   arrow key (new-entity.js), rather than 18 stacked slides. .ne-deck-zone
   is a pair of transparent, borderless buttons stacked over the left/right
   halves of the frame — clicking either advances in that direction, and
   each shows a custom circular-arrow cursor (a small inline SVG, data-URI
   encoded) so it's clear which way a click will go. The deck stops at the
   first/last slide rather than looping (a pitch reads start-to-finish),
   and both zones get a plain default cursor + no-op click at that end
   rather than disappearing, so the frame's footprint never shifts. */
.ne-deck{ outline:none; }
.ne-deck-frame{
  position:relative;
  width:100%;
  aspect-ratio:16/9;
  overflow:hidden;
  background:#111;
}
.ne-deck-slide{
  display:block;
  width:100%;
  height:100%;
  object-fit:contain;
  -webkit-user-select:none;
  user-select:none;
  pointer-events:none;
}
.ne-deck-zone{
  position:absolute;
  top:0;
  bottom:0;
  width:50%;
  margin:0;
  padding:0;
  border:0;
  background:transparent;
  -webkit-appearance:none;
  appearance:none;
}
.ne-deck-zone.prev{
  left:0;
  cursor:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2244%22%20height%3D%2244%22%3E%3Ccircle%20cx%3D%2222%22%20cy%3D%2222%22%20r%3D%2220%22%20fill%3D%22white%22%20fill-opacity%3D%220.92%22%20stroke%3D%22black%22%20stroke-opacity%3D%220.12%22/%3E%3Cpath%20d%3D%22M25%2014%20L17%2022%20L25%2030%22%20fill%3D%22none%22%20stroke%3D%22%231a1a1a%22%20stroke-width%3D%222.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22/%3E%3C/svg%3E") 22 22, pointer;
}
.ne-deck-zone.next{
  right:0;
  cursor:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2244%22%20height%3D%2244%22%3E%3Ccircle%20cx%3D%2222%22%20cy%3D%2222%22%20r%3D%2220%22%20fill%3D%22white%22%20fill-opacity%3D%220.92%22%20stroke%3D%22black%22%20stroke-opacity%3D%220.12%22/%3E%3Cpath%20d%3D%22M19%2014%20L27%2022%20L19%2030%22%20fill%3D%22none%22%20stroke%3D%22%231a1a1a%22%20stroke-width%3D%222.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22/%3E%3C/svg%3E") 22 22, pointer;
}
.ne-deck-zone:disabled{ cursor:default; }

/* New-Entity-only override of the generic .media-row.thirds gap: this strip
   of 6 Instagram posts should read as one tight social-media grid (1px
   between tiles, tighter than the previous 5px per request), tighter
   even than .tight-row below. Single shared rule, no separate mobile
   override exists, so this applies at both breakpoints at once.
   .media-row.thirds elsewhere (e.g. RIA) keeps the standard 20px. */
.media-row.thirds.ne-social{ gap:1px; }

/* New-Entity-only, mobile-only: the last two posters (side by side on
   desktop, via the shared .media-row flex-row default) stack full-width
   instead, per the mobile layout reference — a narrow phone column reads
   better as two full-size posters than two half-width ones. Scoped to
   .ne-posters specifically so it doesn't touch any other .media-row
   pairing on this or other pages (e.g. HDC's own pairs, which have their
   own separate mobile gallery entirely — see .hdc-media-mobile above). */
@media (max-width:900px){
  .media-row.ne-posters{ flex-direction:column; }
}

/* Tight editorial spacing: 6px between every image on a project page,
   instead of the site's usual 20px (.project-media's row gap and each
   .media-row's internal gap). Trialled on HDC first, then approved and
   rolled out to Majo Ideas and New Entity too; RIA keeps the standard
   20px since it was never part of that trial. Higher specificity than
   the shared rules it overrides, so it only ever applies where
   .tight-media/.tight-row are present. */
.project-media.tight-media{ gap:6px; }
.media-row.tight-row{ gap:6px; }

/* HDC only: dedicated mobile gallery (separate markup — see hdc.html)
   swapped in for the desktop one at the mobile breakpoint, same way the
   New Entity hero swaps its own desktop/mobile images. Unconditional
   .hdc-media-mobile{display:none} up top keeps it hidden everywhere by
   default; the max-width:900px rule below is what reveals it, and hides
   its desktop counterpart, only there. */
.hdc-media-mobile{ display:none; }
@media (max-width:900px){
  .hdc-media-desktop{ display:none; }
  .hdc-media-mobile{ display:flex; }
}

/* RIA-only: the walkthrough GIF is just the phone itself (tightly
   cropped, no baked-in backdrop), set inside a fixed-proportion black
   frame — matching the Figma hero block's own measured ratio
   (880:561) — so the block's height stays reasonable regardless of the
   phone image's own tall aspect ratio, instead of growing to whatever
   height a full-width phone would naturally need. The img is sized to
   the frame's height (not width), centered, so it sits flush top-to-
   bottom with black letterboxing left/right — exactly the Figma look.
   box-sizing:border-box keeps the outer frame at the same 880:561
   footprint as before; the 60px top/bottom padding just eats into the
   frame's own content area (shrinking the image slightly) rather than
   growing the block, per Alexandra's request for breathing room above
   and below the phone. */
.ria-walkthrough-frame{
  aspect-ratio:880/561;
  background:#000;
  display:flex;
  align-items:center;
  justify-content:center;
  overflow:hidden;
  box-sizing:border-box;
  padding:60px 0;
}
.ria-walkthrough-frame img.ria-walkthrough{
  width:auto;
  height:100%;
  max-width:100%;
  object-fit:contain;
}

/* RIA-only: draggable/zoomable "board" viewer for the two large Figma
   screen boards (design iterations, and the labeled key-flow strips).
   aspect-ratio matches the source boards' own native 7295x5081 pixel
   size exactly, so the "fit" view (see js/ria.js's fitToContainer) shows
   the whole board with no letterboxing on either axis. overflow:hidden
   crops the oversized <img> inside it; js/ria.js drives the actual
   pan/zoom by writing a translate()/scale() transform onto that <img> —
   this stylesheet only sets up the frame, the cursor states, and the
   hint/reset chrome. touch-action:none stops the browser's own
   scroll/pinch-zoom from fighting the JS on touch devices. */
.board-viewer{
  aspect-ratio:7295/5081;
  position:relative;
  overflow:hidden;
  background:#ece9e4;
  cursor:grab;
  touch-action:none;
}
.board-viewer.is-grabbing{ cursor:grabbing; }
.board-viewer img{
  position:absolute;
  top:0; left:0;
  width:auto; max-width:none; height:auto;
  transform-origin:0 0;
  user-select:none;
  -webkit-user-drag:none;
  pointer-events:none; /* the viewer div owns pan/zoom input, not the img */
}
/* Hint + reset chrome sit above the image; the hint fades out after the
   visitor's first drag/scroll/touch (.has-interacted, set by the JS) so
   it doesn't linger over content once the gesture is understood. */
.board-viewer .board-hint,
.board-viewer .board-reset{
  position:absolute;
  font-family:var(--font-ui);
  font-size:11px;
  letter-spacing:0.4px;
  text-transform:uppercase;
  color:var(--white);
  background:rgba(0,0,0,0.6);
  padding:6px 10px;
  pointer-events:none;
}
.board-viewer .board-hint{
  bottom:12px; left:12px;
  transition:opacity 0.4s ease;
}
.board-viewer.has-interacted .board-hint{ opacity:0; }
.board-viewer .board-reset{
  bottom:12px; right:12px;
  border:0;
  cursor:pointer;
  pointer-events:auto;
  font:inherit;
  letter-spacing:inherit;
  text-transform:inherit;
}
.board-viewer .board-reset:hover{ background:rgba(0,0,0,0.8); }

/* RIA mobile only, per the mobile layout reference: */
@media (max-width:900px){
  /* 1. Tighter spacing between the hero gif and the two boards — 6px,
     not RIA's usual 20px (see the "RIA keeps the standard 20px" note
     above; this is a deliberate, explicit exception for mobile only,
     desktop is untouched). Scoped to .ria-media so no other page's
     .project-media gap is affected. */
  .ria-media{ gap:6px; }

  /* 2. The walkthrough gif "zoomed in": desktop's frame is a wide
     880:561 black letterbox sized for a big, mostly-empty frame around
     a small centered phone (see the comment above
     .ria-walkthrough-frame) — intentional there, but on a narrow mobile
     column it just reads as a tiny phone in a lot of black space.
     Matching the frame's own aspect-ratio to the gif's actual pixel
     ratio (589:1158) instead lets the phone fill the frame edge to
     edge with no side letterboxing, and the padding comes down from
     60px to a much smaller 16px — still a little breathing room, not
     the desktop block's deliberately generous margin. */
  .ria-walkthrough-frame{
    aspect-ratio:589/1158;
    padding:16px 0;
  }

  /* 3. Both draggable board viewers full portrait (9:16), replacing
     desktop's wide 7295:5081 board aspect — a much taller mobile frame
     to explore each board in. js/ria.js's fitToContainer re-measures
     against this new aspect-ratio automatically (it reads the
     container's actual rendered size, not a hardcoded one), so the
     drag/pinch/reset behavior keeps working here with no markup
     changes needed. It does switch its initial-fit math for this
     breakpoint, though (Math.max instead of Math.min) — a plain
     "fit the whole board inside the frame" at this much narrower,
     taller shape leaves most of the frame as empty gray space around
     a small board, so js/ria.js fills the portrait frame edge to edge
     here instead (see the isMobileLayout comment there). */
  .board-viewer{ aspect-ratio:9/16; }
}

/* -------------------------------------------------------------
   About
   ------------------------------------------------------------- */
main.about{ padding-top:56px; }
/* Desktop only: Figma has the intro statement's cap-height beginning
   at y:120 below the viewport top (the fixed nav sits at top:30 and
   is well clear of this). Overrides the generic main.home/project/
   about/playground 88px rule for About specifically — same specificity
   (tag+class), so this wins by appearing later in the stylesheet;
   Home, Project and Playground keep that shared 88px untouched. */
@media (min-width:901px){
  main.about{ padding-top:120px; }
}
/* .about-grid carries both .wrap (max-width:1440, margin:0 auto,
   60px side padding) and its own narrower 7-column span on the same
   element. margin:0 overrides .wrap's margin:0 auto, which would
   otherwise center this box *inside* the already-centered/padded wrap
   instead of pinning it to the page's 60px left edge. */
.about-grid{ margin-left:0; margin-right:0; }
/* The 7-column width is derived from the site's grid (60px margins,
   20px gutters, 12 fluid columns — see --max/--side above), not a
   fixed px number: it tracks the browser width the same way the
   underlying column grid does, and only happens to equal ~762px at
   the 1440px Figma reference width.
     colWidth(w)  = (w - 2*60 - 11*20) / 12 = (w - 340) / 12
     7 columns + 6 internal gutters:
     span(w) = 7*colWidth(w) + 6*20 = (7w - 940) / 12
   At w:1440 that's (10080-940)/12 ≈ 761.7px, matching the Figma
   measurement almost exactly — it's a derived result, not the source
   of truth. box-sizing:content-box (overriding the global border-box
   default for this element only) makes that computed width describe
   the text column itself; padding-right:0 means the box's own right
   edge doesn't add any further inset beyond it. padding-left still
   comes from .wrap (var(--side): 60px desktop), which is what
   positions the column at x:60. max-width:762px is only a ceiling —
   the site's whole grid holds steady rather than keeps widening past
   1440 (see .wrap), so the column stops growing there too instead of
   outgrowing the Figma frame. Scoped to desktop (min-width:901);
   mobile keeps its normal symmetric 20px/20px .wrap padding and full-
   width text, unaffected by any of this. */
@media (min-width:901px){
  .about-grid{
    width:calc((100vw * 7 - 940px) / 12);
    max-width:762px;
    padding-right:0;
    box-sizing:content-box;
  }
}
.about-grid h1{ margin-bottom:24px; }
.about-grid .bio p{ font-size:20px; line-height:24px; margin-bottom:12px; }
.about-links{ margin-top:40px; }
.about-links a{
  display:block;
  font-family:var(--font-ui);
  font-size:14px;
  letter-spacing:0.42px;
  text-transform:uppercase;
  color:var(--black);
  text-decoration:none;
  margin-bottom:6px;
}
.about-links a:hover{ text-decoration:underline; }
/* No max-width here — this inner block was independently capped at
   650px (narrower than the 762px/7-column parent), which is exactly
   the kind of nested constraint that was clipping the Project Index
   short of the full column width. It now just inherits the parent's
   762px content box. */
.about-index{ margin-top:64px; }
.about-index .eyebrow{ margin-bottom:12px; }
.about-index p{ font-family:var(--font-ui); font-size:14px; margin-bottom:8px; text-transform:none; }
/* Project name links back to its case study; yellow highlight on
   hover reuses --active-yellow, the same color the case-study sidebar
   nav already uses for its own active state, so About borrows an
   existing accent rather than introducing a new one. */
.idx-name{
  color:var(--black);
  text-decoration:none;
  box-decoration-break:clone;
  -webkit-box-decoration-break:clone;
  transition:background-color .15s ease;
}
.idx-name:hover{ background:var(--active-yellow); }
.idx-sep{ color:var(--grey); margin:0 2px; }
.idx-role{ color:var(--grey); }
.about-plus{
  display:block;
  width:14px; height:14px;
  color:var(--grey);
  margin-top:40px;
}

/* About's footer: copyright only, no "get in touch" line, no top
   border/divider, and tighter spacing than footer.site elsewhere
   (that one is built around the 2-line contact block this page
   doesn't have). footer.site.about-footer beats footer.site on
   specificity regardless of source order. */
.about-footer .contact-line{ display:none; }
footer.site.about-footer{ border-top:none; margin-top:24px; padding:0 0 48px; }
/* Keep the copyright line pinned to the same x:60 left edge as the
   rest of the About content above it, rather than letting its .wrap
   center independently on viewports wider than 1440px. */
.about-footer .wrap{ margin-left:0; margin-right:0; }

/* -------------------------------------------------------------
   Playground
   -------------------------------------------------------------
   Holding state: a small centered note instead of real content
   (see the comment above playground_content in build.py for why).
   The whole page — header, the centered note, and the footer/
   copyright — is pinned to exactly one viewport with no page
   scroll, on both desktop and mobile, each using the same
   header-flex-footer column body.page-home's single-screen
   treatment already established (see body.page-home above): the
   header keeps its natural (desktop: zero, fixed-position) height,
   main.playground stretches to take whatever's left via flex, and
   the footer sits at its natural height at the very end of the
   column — which is what pins it flush to the bottom edge rather
   than leaving it floating wherever normal document flow happens
   to end. Scoped entirely to body.page-playground so none of this
   touches Home/About/the project pages. */
main.playground{
  display:flex;
  align-items:center;
  justify-content:center;
}
.playground-note{
  font-family:var(--font-ui);
  font-size:17px;
  color:var(--grey);
  text-align:center;
  margin:0;
  white-space:nowrap; /* this line never wraps, so it never recenters mid-animation either */
}
/* The three dots: .dots-anim's text is always the complete "...",
   never retyped — clip-path just hides part of it from view. That
   keeps this a single plain inline span (no inline-block, no
   overflow:hidden), so two things come for free: its own box never
   changes size as more of it is revealed (clip-path is purely
   visual, it doesn't affect layout), which is what actually keeps
   "Still tinkering"'s centered position fixed through the whole
   animation — and its baseline is just the normal text baseline,
   sitting flush with the rest of the sentence in every browser.
   (A first version reserved the width with an inline-block
   overflow:hidden wrapper instead and animated its width — but per
   the CSS box model, an inline-block whose overflow isn't visible
   uses its bottom margin edge as its baseline rather than its
   text's baseline. Chromium rendered it fine regardless; Safari
   followed the spec and the dots floated above the line's actual
   baseline there. clip-path sidesteps the whole issue rather than
   fighting it.) Inset values are tuned to .playground-note's 17px,
   the same widths the old version measured for one/two/three dots. */
.dots-anim{
  clip-path:inset(0 0.65em 0 0); /* "." — clip away the right ~2/3 of "...", the ".." dots after the first */
  animation:playground-dots 1.35s infinite;
}
/* Three held states (~450ms each, the middle of the requested
   400-500ms range) with a ~1%-of-cycle snap between them — short
   enough to read as an instant jump rather than a slide — and a
   matching snap back from the 3-dot state to the 1-dot state at
   the loop seam, so the cycle repeats with no visible pause or
   jump-cut at 100%→0%. */
@keyframes playground-dots{
  0%, 32%  { clip-path:inset(0 0.65em 0 0); } /* "." */
  33%, 65% { clip-path:inset(0 0.30em 0 0); } /* ".." */
  66%, 98% { clip-path:inset(0 0 0 0); }      /* "..." — nothing clipped */
  100%     { clip-path:inset(0 0.65em 0 0); }
}
@media (prefers-reduced-motion: reduce){
  .dots-anim{ animation:none; clip-path:none; } /* show all three dots, static, rather than force motion on visitors who've asked not to see it */
}

/* Desktop: one fixed-height column (header's own fixed-position 0
   flow height, main growing to fill what's left, footer at its
   natural height) instead of main.playground's old plain
   min-height:60vh — that let total page height vary with viewport
   size and never guaranteed everything fit without a scrollbar.
   padding-top:88px on main matches every other page's clearance
   from the fixed nav (see the shared min-width:901 rule up top);
   repeated here because this block's own later main.playground
   rules would otherwise need it listed there to keep winning. */
@media (min-width: 901px){
  body.page-playground{
    height:100vh;
    overflow:hidden;
    display:flex;
    flex-direction:column;
  }
  body.page-playground main.playground{
    flex:1 1 auto;
    min-height:0;
    padding-top:88px;
  }
  body.page-playground footer.site{
    flex:0 0 auto;
    /* .copyright carries no margin/padding of its own below it (see
       the base footer.site .copyright rule), so this padding-bottom
       is the entire gap between its text and the footer's own
       bottom edge — which sits flush against the viewport's bottom
       edge in this single-viewport column, making this value
       exactly the copyright line's distance from the true bottom
       of the screen, per Alexandra's 80px spec. */
    padding-bottom:80px;
  }
  /* Pin the footer's left edge to the same x:60 content margin as
     the rest of the page (matching the header wordmark's left
     edge) instead of letting .wrap center itself independently on
     viewports wider than the 1440px design frame — same fix
     About's footer already uses for the same reason. */
  body.page-playground footer.site .wrap{ margin-left:0; margin-right:0; }
}

/* Mobile: same single-viewport column, dynamic-viewport-height
   aware (100dvh keeps this correct once mobile browser chrome
   shows/hides, same two-line vh-then-dvh fallback body.page-home's
   mobile block uses above). header.site is a normal in-flow block
   here (not fixed, unlike desktop), so it gets flex:0 0 auto to
   keep its own natural height rather than being squashed; main
   fills what's left and centers the note in it; the footer's
   "Get in touch" line and email are sized down specifically for
   this page so the whole footer — contact line, email, and
   copyright — comfortably fits under main in the remaining space
   on a short phone screen without pushing the page into a scroll.
   .wrap's own side padding (var(--side), 20px here) is untouched,
   so the existing mobile margins carry over unchanged. */
@media (max-width: 900px){
  body.page-playground{
    height:100vh;
    height:100dvh;
    overflow:hidden;
    display:flex;
    flex-direction:column;
  }
  body.page-playground header.site{ flex:0 0 auto; }
  body.page-playground main.playground{
    flex:1 1 auto;
    min-height:0;
    padding-top:0;
    padding-bottom:0;
  }
  body.page-playground footer.site{
    flex:0 0 auto;
    margin-top:0;
    padding:24px 0 20px;
  }
  body.page-playground footer.site .contact-line{
    font-size:15px;
    line-height:1.35;
    margin:6px 0 10px;
  }
}

/* -------------------------------------------------------------
   Footer (project/about pages without the sidebar footer)
   ------------------------------------------------------------- */
footer.site{ padding:64px 0 40px; border-top:1px solid var(--grey-line); margin-top:80px; }
footer.site .contact-line{
  font-family:var(--font-ui); font-size:24px; letter-spacing:0.24px; line-height:1.3; margin:8px 0 12px;
}
footer.site a.email{ text-decoration:none; color:var(--black); }
footer.site a.email:hover{ text-decoration:underline; }
footer.site .copyright{ font-family:var(--font-ui); font-size:14px; color:var(--grey); margin:0; }
