/* ───────────────────────────────────────────
   RESET & TOKENS
─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* DESIGNER REVIEW: all colour values are placeholder */
  --paper:       #CFBE9E;
  --paper-rgb:    207, 190, 158; /* same value as --paper, split out for use in rgba() (e.g. tinting the wood texture in .wood-surface) */
  --paper-dark:  #e8e0d0;
  --amber:       #5C6437; /* was gold #c8892a — now aliased to the hover green so every old accent use follows the new palette */
  --hover:       #5C6437; /* the one hover-state color, used everywhere something is hovered */
  --amber-light: #e8b84b;
  --rust:        #b84c2e;
  --pink:        #d4897a;
  --sage:        #7a9b80;
  --ink:         #1e1710;
  --ink-mid:     #3d3022;
  --ink-faint:   rgba(30,23,16,0.35);
  --electric:    #3a7dff;

  /* shadow system */
  --sh-base: 0 4px 18px rgba(30,23,16,0.13), 0 1px 5px rgba(30,23,16,0.08);
  --sh-lift: 0 16px 44px rgba(30,23,16,0.21), 0 4px 12px rgba(30,23,16,0.12);

  /* Argent Pixel CF — loaded via the Adobe Fonts (Typekit) kit link in
     index.html <head>. One weight only (400, roman + italic), used for
     the name-mark, all headings, and object labels. Mono stays on
     Google Fonts for small utility text (tabs, meta, captions). */
  --font-serif: 'argent-pixel-cf', Georgia, serif;
  --font-mono:  'DM Mono', 'Courier New', monospace;

  --header-h: 84px; /* persistent top bar height, used to offset content below it */
}

/* Same tint+texture as .wood-surface (not the class itself, since
   that also carries background-attachment:local, meant for a
   scrolling element — html/body never scroll, per overflow:hidden
   above). This is what actually shows through the transparent header
   on every non-home page — #domain-view's own wood-surface layer
   covers everything below the header's clip line, but html/body is
   the ONLY background behind the header's now-empty strip. Without
   this, that strip was just flat --paper with no grain at all. */
html, body {
  width: 100%; height: 100%;
  overflow: hidden;
  background-color: var(--paper);
  background-image:
    radial-gradient(ellipse at 50% 50%, transparent 55%, rgba(30,23,16,0.07) 100%),
    linear-gradient(rgba(var(--paper-rgb), 0.5), rgba(var(--paper-rgb), 0.5)),
    url('assets/background-faded.jpg');
  background-size: cover, cover, 100% auto;
  background-position: center, center, center top;
  background-repeat: no-repeat, no-repeat, repeat-y;
  color: var(--ink);
  font-family: var(--font-serif);
}

/* paper grain — layered noise texture */
body::after {
  content: '';
  position: fixed; inset: 0; z-index: 999;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.055'/%3E%3C/svg%3E");
  background-size: 200px 200px;
  mix-blend-mode: multiply;
}

/* ───────────────────────────────────────────
   LOADER SCREEN
─────────────────────────────────────────── */
#loader {
  position: fixed; inset: 0; z-index: 800;
  background: var(--paper);
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
#loader-name {
  font-family: var(--font-serif);
  font-size: clamp(30px, 5vw, 48px);
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--ink-mid);
  opacity: 0;
}

/* ───────────────────────────────────────────
   PERSISTENT HEADER — name (click = home), socials, nav trigger.
   Fixed above every view. The social icons and nav trigger stay the
   same size on every breakpoint per feedback: nothing there shrinks
   on mobile. The name-mark is the one exception — on a narrow phone
   viewport "abhas misraraj" was wrapping to two lines, and the fix
   requested is to shrink the text (via clamp()) rather than ever let
   it wrap (see .name-mark below). The bar itself ignores pointer
   events so it never blocks clicks on content underneath; only its
   buttons (name, social links, nav trigger) opt back in.
─────────────────────────────────────────── */
#top-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 210;
  height: var(--header-h);
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 28px;
  pointer-events: none;
  opacity: 0; /* revealed by GSAP */
  background: transparent;
  transition: background 0.3s ease;
}
/* The header stays fully transparent everywhere — home AND every
   other page — same as it always did on the home desk. What used to
   sit behind it (domain-view/gallery content) is now clipped off via
   clip-path on #desk/#domain-view themselves (see .wood-surface),
   so there's nothing back there to show through: the "header" is
   really just an optical illusion made of a hard clip line, not an
   opaque bar. Simpler and more robust than the texture-matching /
   scroll-synced version this replaced, which never looked right. */

.name-mark {
  pointer-events: auto;
  background: none; border: none; cursor: pointer;
  display: flex; align-items: center;
  font-family: var(--font-serif);
  font-size: clamp(15px, 4.5vw, 28px);
  white-space: nowrap;
  letter-spacing: -0.07em;
  color: var(--ink-mid);
  opacity: 0.7;
  padding: 10px 6px;
  transition: opacity 0.2s;
}
.name-mark:hover { opacity: 1; color: var(--hover); }
.top-right-cluster {
  pointer-events: auto;
  display: flex; align-items: center;
  gap: 6px;
}
@media (min-width: 681px) {
  .top-right-cluster { gap: 34px; }
}

/* ───────────────────────────────────────────
   SOCIAL LINKS — 44×44 min tap targets, vertically
   centered with the nav trigger via the flex cluster above.
─────────────────────────────────────────── */
.social-row {
  display: flex; align-items: center; gap: 6px;
}
.social-row a {
  width: 44px; height: 44px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  opacity: 0.55;
  transition: opacity 0.2s, background 0.2s;
}
.social-row a:hover { opacity: 1; background: rgba(30,23,16,0.06); }
.social-row img {
  width: 22px; height: 22px;
  display: block;
}

/* ───────────────────────────────────────────
   NAV TRIGGER — 44×44 tap target
─────────────────────────────────────────── */
.nav-trigger {
  width: 44px; height: 44px;
  background: none; border: none; cursor: pointer;
  padding: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 5px;
  transition: opacity 0.2s;
}
.nav-trigger span {
  display: block; width: 24px; height: 1.5px;
  background: var(--ink);
  opacity: 0.5;
  transform-origin: center;
  transition: opacity 0.25s, transform 0.35s cubic-bezier(0.34,1.2,0.64,1), width 0.25s;
}
.nav-trigger:hover span { opacity: 0.8; }
.nav-trigger.open span:nth-child(1) { transform: rotate(45deg) translate(4px, 4px); opacity: 0.8; }
.nav-trigger.open span:nth-child(2) { width: 0; opacity: 0; }
.nav-trigger.open span:nth-child(3) { transform: rotate(-45deg) translate(4px, -4px); opacity: 0.8; }

/* ───────────────────────────────────────────
   NAV MENU — slides in from top-right
─────────────────────────────────────────── */
#nav-menu {
  position: fixed; top: 0; right: 0; z-index: 205;
  width: 320px; height: 100vh;
  background: var(--paper);
  border-left: 1px solid rgba(30,23,16,0.08);
  padding: 110px 40px 40px;
  transform: translateX(100%);
  opacity: 0;
  pointer-events: none;
  overflow-y: auto;
  /* DESIGNER REVIEW: nav reveal style — currently a right-panel slide */
}

#nav-menu.open {
  pointer-events: all;
}

.nav-section {
  margin-bottom: 36px;
}
/* Shared tiny-label treatment — was previously split into three
   slightly different groups (10px/11px, 0.06-0.16em, some not
   uppercase), consolidated from ~14 near-identical one-off
   declarations. All of them now match the "Get in Touch" button
   (.cs-btn)'s original treatment instead — that one read the best of
   the bunch and the others were genuinely too small to read
   comfortably. Each selector keeps its own color/spacing/etc in its
   own rule — only the font treatment moved here. */
.nav-section-label,
.about-portrait span,
.cs-eyebrow,
.cs-stat-label,
.skills-proficiency-tier-label,
.gallery-tab,
.cs-btn,
.cs-nav-footer button,
.cs-back-top,
.lb-close,
.nav-ext-link,
.lb-counter,
.embed-caption,
.site-footer {
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.nav-section-label {
  color: var(--ink-faint);
  margin-bottom: 14px;
}
.nav-link {
  display: block;
  font-family: var(--font-serif);
  font-size: 20px;
  font-weight: 400;
  color: var(--ink-mid);
  text-decoration: none;
  padding: 4px 0;
  border: none; background: none; cursor: pointer;
  text-align: left;
  width: 100%;
  transition: color 0.2s, transform 0.2s;
  transform: translateX(0);
  opacity: 0; /* animated in */
}
.nav-link:hover { color: var(--hover); transform: translateX(4px); }

.nav-external {
  display: flex; gap: 16px; flex-wrap: wrap;
}
.nav-ext-link {
  color: var(--ink-faint);
  text-decoration: none;
  border: none; background: none; cursor: pointer;
  padding: 0;
  transition: color 0.2s;
}
.nav-ext-link:hover { color: var(--hover); }

/* ───────────────────────────────────────────
   ARTISTIC SKILLS TRACKER LINK — bottom corner
   Was a barely-visible easter egg; now legible but still quiet.
─────────────────────────────────────────── */
/* Same treatment as the object labels — Argent Pixel CF, no mono/
   uppercase/letter-spacing — so these corner links read as part of
   the same family of links as "Photography", "Product Design", etc. */
.skills-tracker-link,
.about-link {
  position: absolute; bottom: 22px; z-index: 40;
  font-family: var(--font-serif);
  font-size: 16px;
  color: var(--ink-mid);
  opacity: 0.75;
  background: none; border: none; cursor: pointer;
  padding: 8px 4px;
  transition: opacity 0.25s, color 0.25s;
}
.skills-tracker-link { left: 28px; }
.about-link { right: 28px; }
.skills-tracker-link:hover,
.about-link:hover { opacity: 1; color: var(--hover); }

/* ───────────────────────────────────────────
   SURFACE / DESK
   padding-top keeps objects clear of the fixed header (percentage
   positions inside resolve against the padded box, so top:0% already
   starts just below the header — no object can slide behind it).
─────────────────────────────────────────── */
/* Wood-desk texture + vignette, shared by every scrollable surface
   (the desk AND every domain view/gallery/etc — not just home).
   Both layers use background-attachment:local so they scroll WITH
   the content and always cover its full height, never just the first
   viewport — that's what was cutting off before the footer. Putting
   the vignette here as a background layer (rather than a separate
   ::after sized to the element's own fixed box) is what fixes it. */
/* The texture TILES (rather than stretching one copy to cover however
   tall the scrollable content gets) — stretching a single image over
   a very long page blew it up into a blurry mess. A fixed tile size
   repeating keeps it crisp no matter how long the page is. The
   vignette gradient stays a single cover layer since gradients don't
   pixelate when scaled. */
/* Texture always fills the full width (background-size: 100% auto)
   and only repeats VERTICALLY (repeat-y) once the page runs past one
   image-height's worth of content — never tiled sideways, and never
   stretched to blur across the whole scroll height either. */
/* The wood photo (background-faded.jpg) has its own baked-in tan
   tone, so simply changing --paper doesn't touch it — it just sits on
   top, unrelated to the color underneath. A flat --paper tint at 75%
   opacity sits between the vignette and the photo so the photo shows
   through only as texture (~25%) rather than setting the actual
   color. */
.wood-surface {
  background-color: var(--paper);
  background-image:
    radial-gradient(ellipse at 50% 50%, transparent 55%, rgba(30,23,16,0.07) 100%),
    linear-gradient(rgba(var(--paper-rgb), 0.5), rgba(var(--paper-rgb), 0.5)),
    url('assets/background-faded.jpg');
  background-size: cover, cover, 100% auto;
  background-position: center, center, center top;
  background-repeat: no-repeat, no-repeat, repeat-y;
  background-attachment: local, local, local;
}
/* Home desk is unchanged — objects can sit right up against/behind
   the (fully transparent) header there, as before. Every OTHER page
   clips its own content+background off at the header's height, so
   nothing ever renders in that strip — the see-through header reads
   as a hard "everything disappears here" line instead of an opaque
   bar, with zero need to match colors/textures/scroll position. */
#domain-view { clip-path: inset(var(--header-h) 0 0 0); }

#desk {
  position: fixed; inset: 0; z-index: 10;
  overflow-x: hidden;
  overflow-y: auto;
}

/* ───────────────────────────────────────────
   OBJECTS — desk items
   All ten objects live inside #desk directly (position:absolute,
   percentage top/left) but their SIZE is a percentage of the viewport
   clamped between a floor and ceiling — see per-object rules below.
   That combination is what keeps spacing visually consistent at every
   breakpoint while letting objects grow generously on large screens
   and never shrink past a legible floor on small ones.
─────────────────────────────────────────── */
.obj {
  position: absolute;
  cursor: pointer;
  opacity: 0; /* GSAP reveals */
  z-index: 20;
}

.obj-img {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
  filter: drop-shadow(0 4px 14px rgba(30,23,16,0.18)) drop-shadow(0 1px 4px rgba(30,23,16,0.1));
}

/* Rotation lives on .obj-inner (and the label, separately) via a
   per-object --rot custom property — NOT on .obj itself — so the
   label's own position/centering logic stays simple while both pieces
   visually sit on the same tilted "plane" as the desk object. */
/* The image itself is never rotated by CSS — it's used exactly as
   supplied. Only the label text underneath carries the --rot tilt. */
.obj-inner {
  position: relative;
  transition: transform 0.4s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.obj-inner .obj-img {
  transition: filter 0.3s ease;
}
.obj:hover .obj-inner {
  transform: translateY(-6px) scale(1.035);
}
.obj:hover .obj-inner .obj-img {
  filter: drop-shadow(0 18px 30px rgba(30,23,16,0.28)) drop-shadow(0 4px 10px rgba(30,23,16,0.16));
}

/* label — visible at rest now (not hover-only); tilted per --rot.
   pointer-events is NOT none here on purpose: hovering/clicking the
   label itself (not just the image) needs to open the same domain,
   via bubbling up to .obj's hover state and onclick handler. */
.obj-label {
  position: absolute;
  bottom: -30px; left: 50%;
  transform: translateX(-50%) rotate(var(--rot, 0deg));
  font-family: var(--font-serif);
  font-size: 16px;
  color: var(--ink-mid);
  opacity: 0.7;
  white-space: nowrap;
  transition: opacity 0.22s ease, color 0.22s, transform 0.22s;
  cursor: pointer;
}
.obj:hover .obj-label { opacity: 1; color: var(--hover); transform: translateX(-50%) translateY(-2px) rotate(var(--rot, 0deg)); }

/* ── individual object placements & sizes ──
   Positions are percentage-based (top/left, relative to #desk) so
   the cluster scales as one unit as the viewport resizes — that's
   what keeps the gaps between objects visually consistent instead of
   stretching apart on wide screens. Width is also percentage-based
   (of the viewport) but clamped so objects grow large on big screens
   without shrinking below a comfortable floor on small ones. Objects
   are packed tighter (less dead space) than earlier passes, and each
   carries a --rot custom property used by both the image and its label.
   DESIGNER REVIEW: positions/rotations loosely matched to the v1 flat-lay. */

/* Positions/sizes read directly off the reference flat-lay at its
   exact 1920×928 breakpoint (top/left as % of viewport, matching the
   image's own proportions pixel-for-pixel; width in vw calibrated so
   it lands on the exact px width at 1920 and scales down gracefully
   below that). top uses max(header-clearance, Y%) — the percentage
   alone matches the reference exactly at normal/tall viewports, and
   the max() only kicks in as a floor on unusually short ones so
   nothing can slide behind the fixed header.
   Label rotation: CSS rotate() is positive-clockwise, but the given
   angles are positive-counterclockwise, so every value is negated
   here (e.g. "6.76° CCW" → rotate(-6.76deg)). */
/* Y% values shifted up ~3.3 points from the raw reference reading so
   the whole cluster sits vertically centered in the viewport (as a
   block) rather than centered only in the space below the header —
   that's what was reading "a little low" before. */
#o-product      { top: max(calc(var(--header-h) + 10px), 15.3%); left: 4.75%;  width: clamp(150px, 15.25vw, 293px); --rot: -6.76deg; }
#o-photography  { top: max(calc(var(--header-h) + 10px), 13.2%); left: 32.75%; width: clamp(150px, 13.75vw, 264px); --rot: -0.63deg; }
#o-writing      { top: max(calc(var(--header-h) + 10px), 7.55%); left: 57.75%; width: clamp(120px, 9.25vw, 178px); --rot: 9.6deg; }
#o-shop         { top: max(calc(var(--header-h) + 10px), 16.8%); left: 85.25%; width: clamp(90px, 6.25vw, 120px);  --rot: 14.33deg; }
#o-games        { top: max(calc(var(--header-h) + 10px), 56.1%); left: 7.5%;   width: clamp(130px, 10.25vw, 197px); --rot: -10.38deg; }
#o-illustration { top: max(calc(var(--header-h) + 10px), 46.3%); left: 24.75%; width: clamp(190px, 18vw, 346px);   --rot: 3.45deg; }
#o-bookshelf    { top: max(calc(var(--header-h) + 10px), 50.4%); left: 46.5%;  width: clamp(105px, 11.25vw, 216px); --rot: 6.62deg; }
#o-film         { top: max(calc(var(--header-h) + 10px), 37.5%); left: 66.5%;  width: clamp(180px, 18vw, 346px);   --rot: -14.5deg; }
#o-music        { top: max(calc(var(--header-h) + 10px), 69.5%); left: 70.25%; width: clamp(90px, 7.75vw, 149px);  --rot: -13.92deg; }
#o-performance  { top: max(calc(var(--header-h) + 10px), 57.7%); left: 85%;    width: clamp(100px, 10.75vw, 206px); --rot: 11.04deg; }

/* Desk objects are positioned continuously (%/vw-based) rather than
   with discrete breakpoints, which mostly self-adjusts fine — but a
   few objects were crowding/overlapping their neighbors specifically
   around 863px wide. Rather than snapping a fixed nudge on/off at
   that width, --bp-t ramps linearly from 0 (at 1422px and above, no
   nudge) to 1 (at 863px and below, the full nudge below is reached
   and then held) — same "clamp + viewport-relative value" fluid-
   scaling idiom the width: clamp(...) rules above already use, just
   solved for a 0..1 progress value instead of a min/max length.
   (100vw / 1px) is a trick to get the viewport width as a plain
   unitless number so it can be combined with the unitless 1422/559
   constants — calc() can't otherwise divide/multiply mismatched
   units. Applied via margin (object + label move together since the
   label lives inside .obj), not transform:translate, since the GSAP
   entrance animation sets an inline transform on .obj itself once it
   finishes, which would silently override a CSS transform here. */
@media (min-width: 681px) {
  :root { --bp-t: clamp(0, calc((1422 - (100vw / 1px)) / 559), 1); }
  #o-illustration { margin: calc(var(--bp-t) * -102px) 0 0 calc(var(--bp-t) * -7px); }
  #o-bookshelf    { margin: calc(var(--bp-t) * 91px) 0 0 calc(var(--bp-t) * -24px); }
  #o-music        { margin: calc(var(--bp-t) * 77px) 0 0 calc(var(--bp-t) * -34px); }
  #o-film         { margin: calc(var(--bp-t) * 62px) 0 0 calc(var(--bp-t) * -40px); }
  #o-writing      { margin: calc(var(--bp-t) * 87px) 0 0 calc(var(--bp-t) * 24px); }
}

/* Label placement per object, matching the reference: most labels
   sit ABOVE their object, a few stay below or shift to one side —
   not a uniform "always centered below" rule. Rotation (--rot)
   still applies the same way regardless of side. */
#o-product .obj-label,
#o-writing .obj-label,
#o-games .obj-label,
#o-music .obj-label {
  top: -30px; bottom: auto;
}
#o-shop .obj-label {
  top: -22px; bottom: auto;
  left: auto; right: -18px;
  transform: rotate(var(--rot, 0deg));
}
#o-shop:hover .obj-label { transform: translateY(-2px) rotate(var(--rot, 0deg)); }
#o-film .obj-label {
  top: -30px; bottom: auto;
  left: 28%;
}

/* Pixel nudges on top of the placements above — measured against the
   reference flat-lay at the 1920×928 desktop breakpoint. */
#o-product .obj-label      { left: calc(50% - 19px); top: -15px; }
#o-photography .obj-label  { bottom: -15px; }
#o-writing .obj-label      { left: calc(50% + 34px); top: -14px; }
/* Percentage-based (not fixed px) on purpose: #o-film's width is a
   clamp() that shrinks a lot between breakpoints, and a fixed top
   offset stayed the same regardless — so at narrower desktop widths
   the label sat further down INSIDE the now-shorter image instead of
   below it. % of the object's own box scales with it. (64px top /
   40px left were measured against the object's rendered size at the
   1920px reference breakpoint — 346px wide × ~230px tall.) */
#o-film .obj-label         { left: 16.4%; top: 27.8%; }
#o-shop .obj-label         { right: -53px; top: 32px; }
#o-music .obj-label        { left: calc(50% - 25px); top: -13px; }
#o-performance .obj-label  { left: calc(50% - 26px); bottom: -9px; }
#o-bookshelf .obj-label    { left: calc(50% - 13px); bottom: -25px; }
#o-illustration .obj-label { left: calc(50% - 31px); bottom: -20px; }
#o-games .obj-label        { left: calc(50% - 23px); top: -15px; }

/* ───────────────────────────────────────────
   DOMAIN VIEW — full / near-full screen.
   No in-view back button — the persistent header name is the way home.
─────────────────────────────────────────── */
#domain-view {
  position: fixed; inset: 0; z-index: 100;
  opacity: 0;
  pointer-events: none;
  overflow-y: auto;
}
#domain-view.active { pointer-events: all; }

.dv-content {
  min-height: 100vh;
  padding: calc(var(--header-h) + 50px) 60px 100px;
  max-width: 880px;
  margin: 0 auto;
  opacity: 0;
}
/* wide modifier — grids/embeds/image-heavy views get much more room;
   prose-heavy views (Writing, About) stay at the narrower reading width */
.dv-content.wide {
  max-width: min(1700px, 96vw);
}

/* Same fixed size as the header name-mark — page titles shouldn't
   dominate the screen, the content underneath should. */
.dv-title {
  font-family: var(--font-serif);
  font-size: 28px;
  font-weight: 400; line-height: 1.1;
  color: var(--ink-mid);
  opacity: 0.7;
  margin-bottom: 24px;
}
.cs-detail .dv-title { font-size: 24px; }

/* Photography / Illustration & Collage: tighter top spacing and
   side padding so the images are the centerpiece, not the header —
   the left/right margin matches the gap between tiles themselves. */
.dv-content.gallery-view {
  padding-top: calc(var(--header-h) + 26px);
  padding-left: 18px;
  padding-right: 18px;
}
@media (min-width: 700px) {
  .dv-content.gallery-view { padding-left: 26px; padding-right: 26px; }
}
.gallery-view .dv-title { margin-bottom: 18px; }
.gallery-view .gallery-tabs { margin-bottom: 22px; }

.dv-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 24px;
  margin-bottom: 60px;
}
.dv-card {
  background: var(--paper-dark);
  border-radius: 4px;
  aspect-ratio: 4/3;
  display: flex; align-items: flex-end;
  padding: 16px;
  transition: transform 0.3s cubic-bezier(0.34,1.3,0.64,1), box-shadow 0.25s;
  cursor: pointer;
  overflow: hidden;
  position: relative;
}
.dv-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--sh-lift);
}
.dv-card-label {
  font-family: var(--font-serif);
  font-size: 15px; color: var(--ink-mid); opacity: 0.85;
}

/* Shared body-copy treatment (Georgia 17px/1.85) — used for every
   prose paragraph, bullet list, and icon-row across the site
   (.dv-prose, .cs-p, .cs-list, .cs-icon-row, .about-list,
   .embed-empty) so long-form text reads consistently regardless of
   which page or component it's in. .cs-card-blurb stays smaller on
   purpose — it's compact index-card copy, not reading copy. */
.dv-prose {
  font-family: Georgia, serif;
  font-size: 17px; line-height: 1.85;
  color: var(--ink-mid);
  max-width: 640px;
}
.dv-prose p { margin-bottom: 1.3em; }
.dv-prose a { color: var(--amber); }

/* ───────────────────────────────────────────
   GALLERY — Photography, Illustration & Collage
   True CSS columns (not a row-aligned grid): each image keeps its own
   aspect ratio and simply fills its column's width — nothing is
   cropped, and rows don't line up, which gives the staggered/masonry
   look. Fixed at 3 columns from tablet width up; images just get
   wider (not more numerous) on large screens.
─────────────────────────────────────────── */
.gallery-tabs {
  display: flex;
  flex-wrap: wrap;
  margin-bottom: 32px;
}
/* Below the mobile breakpoint, wrapping means every category pill
   stacks into its own row — with a dozen+ categories that's a lot of
   vertical space before any actual photos show. Scroll horizontally
   in one row instead, same as a native app's filter-chip row. */
@media (max-width: 680px) {
  .gallery-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    margin-left: -22px; margin-right: -22px;
    padding-left: 22px; padding-right: 22px;
  }
  .gallery-tabs::-webkit-scrollbar { display: none; }
  .gallery-tab { flex-shrink: 0; }
}
.gallery-tab {
  color: var(--ink-faint);
  background: none;
  border: 1px solid rgba(30,23,16,0.16);
  border-radius: 100px;
  padding: 10px 18px;
  margin: 4px;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.gallery-tab:hover { color: var(--ink-mid); border-color: rgba(30,23,16,0.32); }
.gallery-tab.active {
  color: var(--paper);
  background: var(--ink);
  border-color: var(--ink);
}

/* True CSS column-masonry (not a row-based grid) — each image keeps
   its own aspect ratio and simply fills its column's width, giving
   the staggered/interlocking look. Order in the data arrays matters:
   a column fills top-to-bottom before moving to the next, so an item
   that should never land at the top of a column (see the Faces
   comment above) needs to sit near the end of its list. */
.gallery-grid {
  column-count: 2;
  column-gap: 18px;
  margin-bottom: 40px;
}
@media (min-width: 700px) {
  .gallery-grid { column-count: 3; column-gap: 26px; }
}
.gallery-tile {
  break-inside: avoid;
  margin-bottom: 18px;
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  display: block;
  transition: transform 0.3s cubic-bezier(0.34,1.3,0.64,1), box-shadow 0.25s;
}
@media (min-width: 700px) {
  .gallery-tile { margin-bottom: 26px; }
}
.gallery-tile:hover {
  transform: translateY(-3px) scale(1.015);
  box-shadow: var(--sh-lift);
}
.gallery-tile img {
  width: 100%; height: auto;
  display: block;
}

/* Shared fade-in for grid images/videos (Photography, Illustration,
   Games, Bookshelf, Performance) — without this, tiles pop in
   abruptly and at different times as each one finishes loading,
   which reads as a jarring "flash" across the grid. Applied via JS
   once each element's `load`/`loadeddata` event fires (see the
   capture-phase listener in script.js), so it fades in exactly when
   it's actually ready rather than on a fixed timer. */
.fade-img { opacity: 0; transition: opacity 0.4s ease; }
.fade-img.loaded { opacity: 1; }

/* lightbox */
#lightbox {
  position: fixed; inset: 0; z-index: 300;
  background: rgba(14,10,6,0.94);
  display: flex; align-items: center; justify-content: center;
  opacity: 0;
  pointer-events: none;
}
#lightbox.active { pointer-events: all; }
.lb-stage {
  max-width: 84vw; max-height: 80vh;
  display: flex; align-items: center; justify-content: center;
}
.lb-stage img, .lb-stage video {
  max-width: 84vw; max-height: 80vh;
  width: auto; height: auto;
  border-radius: 2px;
  box-shadow: 0 24px 60px rgba(0,0,0,0.5);
}
.lb-close {
  position: absolute; top: 20px; right: 20px;
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  color: rgba(255,255,255,0.55);
  background: none; border: none; cursor: pointer;
  transition: color 0.2s;
}
.lb-close:hover { color: #fff; }
.lb-counter {
  position: absolute; top: 30px; left: 30px;
  color: rgba(255,255,255,0.4);
}
.lb-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 48px; height: 48px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.2);
  background: rgba(255,255,255,0.04);
  color: rgba(255,255,255,0.7);
  font-family: var(--font-mono);
  font-size: 18px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.2s, border-color 0.2s, transform 0.2s;
}
.lb-arrow:hover { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.4); }
.lb-arrow.prev { left: 24px; }
.lb-arrow.next { right: 24px; }
.lb-arrow:active { transform: translateY(-50%) scale(0.92); }

@media (max-width: 680px) {
  .lb-arrow { width: 40px; height: 40px; }
  .lb-arrow.prev { left: 10px; }
  .lb-arrow.next { right: 10px; }
}

/* ───────────────────────────────────────────
   PRODUCT DESIGN — case study index (image-first, like the galleries)
   + case study detail (centered prose, nothing cropped)
─────────────────────────────────────────── */
.cs-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 56px;
  margin-bottom: 40px;
}
@media (min-width: 700px) {
  .cs-grid { grid-template-columns: 1fr 1fr; gap: 48px 40px; }
}
/* Bounded aspect ratio (not the image's natural size) so both cards —
   image, title, blurb, buttons — fit inside one desktop viewport
   without scrolling. The image itself is never cropped to fill that
   box — it's letterboxed (object-fit:contain, centered) so the whole
   thing stays visible either way. */
/* No fill — just an outline "card", so the PNG's own (often transparent)
   background shows the page behind it rather than a white/paper box. */
/* No outline — the card should look like it's simply resting on the
   wood until hovered, when the lift + shadow makes it read as rising
   off the surface. */
.cs-card-image {
  width: 100%;
  aspect-ratio: 16/10;
  border-radius: 6px;
  overflow: hidden;
  cursor: pointer;
  margin-bottom: 16px;
  display: flex; align-items: center; justify-content: center;
  transition: transform 0.3s cubic-bezier(0.34,1.3,0.64,1), box-shadow 0.25s;
}
.cs-card-image:hover { transform: translateY(-3px); box-shadow: var(--sh-lift); }
/* width/height:100% + object-fit:contain (not max-width/max-height
   with auto dimensions) — the auto-dimension approach is the one
   that was cropping the top/bottom of these PNGs on real iOS Safari
   even though it rendered fine in desktop responsive/device-emulation
   mode; this is the more broadly-supported pattern for a flex item
   that must never be cropped. */
.cs-card-image img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* Fragments' source image reads much larger than the app-store cards
   at the same box size — scale it down and let it float centered. */
.cs-card-image.fragments img { width: 66%; height: 66%; }
.cs-card-title {
  display: block;
  font-family: var(--font-serif);
  font-size: 24px;
  color: var(--ink-mid);
  opacity: 0.7;
  margin-bottom: 10px;
  cursor: pointer;
  text-decoration: none;
}
.cs-card-title:hover { color: var(--hover); opacity: 1; }
.cs-card-blurb {
  font-family: Georgia, serif;
  font-size: 15px; line-height: 1.7;
  color: var(--ink-mid);
  margin-bottom: 18px;
}
.cs-card-actions { display: flex; flex-wrap: wrap; margin: -6px; }
.cs-btn {
  /* Explicit, since .cs-btn is used on both <button> (default
     inline-block, margins work fine) and <a> (default inline, which
     silently ignores top/bottom margin — the bug behind inconsistent
     spacing above/below link-based buttons like "Get in Touch"). */
  display: inline-flex; align-items: center; justify-content: center;
  padding: 12px 20px;
  margin: 6px;
  border-radius: 100px;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
  border: 1px solid var(--ink);
}
.cs-btn-primary { background: var(--ink); color: var(--paper); }
.cs-btn-primary:hover { background: var(--hover); border-color: var(--hover); }
.cs-btn-secondary { background: none; color: var(--ink-mid); }
.cs-btn-secondary:hover { border-color: var(--hover); color: var(--hover); }

/* case study detail — centered prose column, images never cropped */
/* Everything in a case study reads left-aligned — including the
   headers — per feedback; centering was making it feel disconnected
   from the rest of the site (which is left-aligned throughout). */
.cs-detail {
  max-width: 720px;
  margin: 0 auto;
  text-align: left;
}
.cs-detail .dv-title { text-align: left; }

/* Hero — full-bleed, cropped shorter than the source photo's natural
   (very tall) aspect ratio, with the app icon / title / store button
   layered directly on top instead of sitting in plain text above it.
   This is the very first thing on the page, so it's also what
   replaces the old Role/Team/Timeline/Platform block — that
   information already lives in Background/Role below, and stacking
   it again up here was just dead space before anything interesting
   loads. */
/* Pulls up by the extra 50px .dv-content normally pads on top of the
   header clearance (30px at the mobile breakpoint, see below) so the
   hero sits flush against the header instead of leaving a gap — the
   header-height clearance itself is untouched, so nothing renders
   behind the fixed header. */
.cs-hero {
  width: 100vw;
  position: relative; left: 50%; transform: translateX(-50%);
  margin: -50px 0 48px;
  overflow: hidden;
}
.cs-hero > img {
  width: 100%; height: 50vh; min-height: 300px; max-height: 460px;
  object-fit: cover; display: block;
}
.cs-hero-overlay {
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(10,8,4,0.65), rgba(10,8,4,0) 65%);
  /* Covers the full hero (inset:0) but is mostly transparent above the
     gradient, and sits after .cs-back-top in the DOM — without this it
     silently blocks clicks on the "All case studies" button underneath. */
  pointer-events: none;
}
.cs-hero-overlay > * { pointer-events: auto; }
/* Same column as the rest of the case study (.cs-detail, 720px,
   centered) — the hero itself is full-bleed (100vw) so it can't just
   inherit .cs-detail's width/position, and re-deriving .dv-content's
   max-width/96vw-cap/padding breakpoints here in a second formula
   would drift out of sync with them. alignCsHeroBody() in script.js
   measures .cs-detail's real rendered box instead and sets this
   element's left/width to match exactly; position:absolute here is
   just what makes that left/width apply. The static width below is
   only a no-JS/pre-measurement fallback. */
.cs-hero-body {
  position: absolute;
  bottom: 40px;
  left: 22px;
  width: min(720px, calc(100% - 44px));
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 14px;
}
.cs-hero-icon { width: 56px; height: 56px; border-radius: 14px; box-shadow: 0 4px 14px rgba(0,0,0,0.35); }
.cs-hero-title {
  font-family: var(--font-serif);
  font-size: clamp(30px, 5vw, 48px);
  font-weight: 400; line-height: 1.05;
  color: #fff;
  margin: 0;
}
.cs-hero .cs-btn {
  background: #fff; color: var(--ink); border-color: #fff;
  margin: 0;
}
.cs-hero .cs-btn:hover { background: #f2ede2; }
/* "← All case studies" now sits on top of the hero itself (top-left,
   white) instead of taking up its own row above it — frees up the
   hero to actually fill that space. */
.cs-hero .cs-back-top {
  position: absolute; top: 20px; left: clamp(20px, 5vw, 72px);
  margin-bottom: 0;
  color: rgba(255,255,255,0.85);
  text-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.cs-hero .cs-back-top:hover { color: #fff; }
@media (max-width: 680px) {
  .cs-hero { margin-top: -30px; }
}

/* section headers get real breathing room above them — this is what
   was missing before ("more spacing above them") */
.cs-block { margin: 64px 0 48px; }
.cs-eyebrow {
  color: var(--amber);
  margin-bottom: 16px;
}
.cs-h2 {
  font-family: var(--font-serif);
  font-size: clamp(22px, 3.4vw, 32px);
  font-weight: 400;
  color: var(--ink-mid);
  opacity: 0.7;
  margin-bottom: 18px;
}
.cs-p {
  font-family: Georgia, serif;
  font-size: 17px; line-height: 1.85;
  color: var(--ink-mid);
}
.cs-p + .cs-p { margin-top: 1.2em; }
.cs-p a, .cs-list a { color: var(--hover); }
.cs-list {
  text-align: left;
  margin: 0;
  font-family: Georgia, serif;
  font-size: 17px; line-height: 1.85;
  color: var(--ink-mid);
  padding-left: 22px;
}
.cs-list li { margin-bottom: 0.5em; }
.cs-icon-list {
  display: flex; flex-direction: column; gap: 14px;
  max-width: 520px; margin: 0 0 28px;
}
/* bold — on the real page these render as heading tags, not plain
   paragraphs, which is why they read bolder than the surrounding
   body copy (matches both Awards & Features and Results, both built
   from this same 'iconlist' block type) */
.cs-icon-row {
  display: flex; align-items: flex-start; gap: 12px;
  font-family: Georgia, serif;
  font-size: 17px; line-height: 1.6; font-weight: 700;
  color: var(--ink);
  text-align: left;
}
.cs-icon-row .icon { font-size: 18px; flex-shrink: 0; }
.cs-icon-row a { color: var(--hover); }

/* sub-heading within a section (e.g. "Teach a user how improve AI
   results") — one step down from .cs-h2's section titles */
.cs-h3 {
  font-family: var(--font-serif);
  font-size: clamp(19px, 2.6vw, 24px);
  font-weight: 400;
  color: var(--ink-mid);
  opacity: 0.7;
  margin: 40px 0 16px;
}

/* Full-bleed — breaks out of the 720px reading column to span the
   entire browser width edge to edge (not just the content column),
   matching the handful of hero/vibe/diagram images and demo videos
   on the real case study that read as much bigger, "the whole
   screen" moments. Works regardless of how deep it's nested since
   it's centered against the viewport, not its parent. */
.cs-full-bleed {
  width: 100vw;
  position: relative;
  left: 50%;
  transform: translateX(-50%);
  margin: 48px 0;
}
.cs-full-bleed img, .cs-full-bleed video {
  width: 100%; height: auto; display: block;
}
.cs-full-bleed video { background: #000; }
.cs-full-bleed.lightboxable { cursor: pointer; }
/* the WWF partnership clip — an ambient, always-looping background
   moment rather than something to watch with controls, so it's
   capped to viewport height and allowed to crop (object-fit:cover)
   instead of ever letterboxing */
.cs-full-bleed.ambient video {
  height: 82vh; max-height: 720px; width: 100%;
  object-fit: cover;
}
/* the "vibe" shot — same idea as .ambient (capped height, allowed to
   crop) but for a static image; it was rendering at its full natural
   height edge to edge, which on a tall source photo read as far too
   dominant next to everything else on the page. */
.cs-full-bleed.capped img {
  height: 70vh; max-height: 640px; width: 100%;
  object-fit: cover;
}

/* full-bleed single image — natural size, never cropped */
.cs-img-full { max-width: 100%; margin: 40px 0; }
.cs-img-full img { width: 100%; height: auto; display: block; border-radius: 4px; }

/* Media (video or image) paired beside its own text — fits within
   the normal 720px reading column (not a wider breakout), same as
   everything else on the page, so the whole pairing reads as one
   block at the same width as the text above and below it. */
.cs-split {
  display: flex; gap: 24px; align-items: center;
  margin: 72px 0;
}
.cs-split.media-right { flex-direction: row-reverse; }
.cs-split-media { flex: 1 1 220px; min-width: 0; text-align: center; }
.cs-split-media.lightboxable { cursor: pointer; }
/* All three (both demo videos + the forget-me-not screenshot) share
   one fixed height now — they were each sizing to their own natural
   height up to 80vh, which made the video column read as much
   bigger than the paragraph text sitting next to it. */
.cs-split-media img, .cs-split-media video {
  max-width: 100%; height: 46vh; max-height: 420px;
  width: auto;
  display: inline-block;
  border-radius: 8px;
  box-shadow: var(--sh-base);
}
/* the forget-me-not screenshot is a transparent PNG, not a screen
   recording — the card frame (radius + shadow) was reading as a
   translucent box behind it rather than a natural photo edge */
.cs-split-media.plain img { border-radius: 0; box-shadow: none; }
.cs-split-content { flex: 1 1 300px; min-width: 0; }
.cs-split-content .cs-h3 { margin-top: 0; }
@media (max-width: 560px) {
  .cs-split {
    flex-direction: column !important;
    gap: 20px;
  }
  .cs-split-media img, .cs-split-media video { height: 42vh; max-height: 360px; }
}

/* a row of images (e.g. testimonial screenshots) — wraps, each at its
   own natural aspect ratio, nothing forced into a fixed box */
.cs-img-row {
  display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 16px;
  margin: 40px 0;
}
.cs-img-row img {
  height: 260px; width: auto; max-width: 100%;
  border-radius: 4px;
  box-shadow: var(--sh-base);
}

/* horizontal, tap/swipe-through carousel — used for testimonial rows
   instead of stacking every screenshot in one wrapped block */
.cs-carousel-wrap { position: relative; margin: 40px 0; }
.cs-carousel {
  display: flex; gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding: 4px 4px 16px;
  -webkit-overflow-scrolling: touch;
}
.cs-carousel::-webkit-scrollbar { height: 6px; }
.cs-carousel::-webkit-scrollbar-thumb { background: rgba(30,23,16,0.15); border-radius: 3px; }
.cs-carousel img {
  height: 300px; width: auto; flex-shrink: 0;
  scroll-snap-align: center;
  border-radius: 4px;
  box-shadow: var(--sh-base);
}
.cs-carousel-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(30,23,16,0.16);
  background: var(--paper);
  color: var(--ink-mid);
  font-family: var(--font-mono);
  font-size: 16px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: border-color 0.2s, color 0.2s;
  box-shadow: var(--sh-base);
}
.cs-carousel-arrow:hover { border-color: var(--hover); color: var(--hover); }
.cs-carousel-arrow.prev { left: -8px; }
.cs-carousel-arrow.next { right: -8px; }
@media (max-width: 900px) {
  .cs-carousel-arrow { display: none; }
}

/* Awards & Features (left) / badges (right) — a real two-column
   pairing like the source page, fit within the normal 720px reading
   column (not a wider breakout) so the whole block matches the width
   of everything above/below it. The badges column is auto-width
   (just as wide as a badge + its number needs) so the AWARDS text is
   the side that wraps when space is tight, not the badges. */
.cs-awards-grid {
  display: grid; grid-template-columns: 1fr auto;
  gap: 40px;
  align-items: start;
  margin: 64px 0 48px;
}
/* grid items default to min-width:auto, which lets long unbroken
   content (the award links) force a column wider than its fair
   share — that's what was overflowing/breaking the layout at
   in-between desktop widths */
.cs-awards-grid > * { min-width: 0; }
@media (max-width: 560px) {
  .cs-awards-grid { grid-template-columns: 1fr; gap: 32px; }
}

/* Two plain text sections side by side (Challenge / Role) — same
   pattern as .cs-awards-grid, just an even split since both sides
   are comparable amounts of text. */
/* A little wider than the plain 720px reading column — Challenge and
   Role were reading cramped fit exactly to it — using the same
   viewport-centering breakout as .cs-full-bleed, just capped much
   narrower than that (860px, not the full width). */
.cs-text-grid {
  width: 100vw; max-width: 860px;
  position: relative; left: 50%; transform: translateX(-50%);
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 40px;
  align-items: start;
  margin: 64px 0 48px;
}
.cs-text-grid > * { min-width: 0; }
.cs-text-grid .cs-block { margin-top: 0; }
@media (max-width: 560px) {
  .cs-text-grid {
    grid-template-columns: 1fr; gap: 40px;
    width: 100%; max-width: 100%; left: auto; transform: none;
  }
}
/* badges stack vertically in their column — see .cs-badge below for
   the actual badge+number pairing */
.cs-badges {
  display: flex; flex-direction: column; gap: 40px;
}
.cs-badge {
  display: flex; align-items: center; gap: 18px;
}
.cs-badge img { width: 128px; height: 128px; flex-shrink: 0; }
.cs-stat-value {
  font-family: var(--font-serif);
  font-size: clamp(28px, 4vw, 44px);
  color: var(--amber);
  line-height: 1;
}
.cs-stat-label {
  color: var(--ink-faint);
  margin-top: 8px;
}

/* same treatment as .cs-h2, and the one deliberate exception to the
   "everything is left-aligned" rule — it reads better centered over
   a centered row of logos */
/* same level as .cs-h3 (e.g. "AI is frustrating when it's a black
   box") on purpose — on the source page both are h2 tags, one level
   below the major section headers (Background, Awards & Features,
   etc.), not the same size as those */
.cs-featured-heading {
  font-family: var(--font-serif);
  font-size: clamp(19px, 2.6vw, 24px);
  font-weight: 400;
  color: var(--ink-mid);
  opacity: 0.7;
  text-align: center;
  margin: 56px 0 20px;
}
/* Full-bleed, full color, real tap targets — this was a small,
   grayscale, opacity-dimmed row before, which read as an
   afterthought rather than actual press logos. Breaks out to the
   full browser width like .cs-full-bleed so there's room for each
   logo to be genuinely large. */
.cs-logo-strip {
  width: 100vw;
  position: relative; left: 50%; transform: translateX(-50%);
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: 32px 64px;
  padding: 8px 24px;
  margin-bottom: 8px;
}
/* Sized by WIDTH (not height) on purpose, matching the source page —
   each logo keeps its own natural aspect ratio, so a wide wordmark
   (Bay Nature) reads shorter and a squarer mark (VOA) reads taller,
   instead of every logo being forced to one uniform height regardless
   of shape. max-height is just a safety cap for unusually square/tall
   logos, not the driving dimension. */
.cs-logo-strip img { width: 140px; height: auto; max-height: 70px; object-fit: contain; }
/* Bay Nature's wordmark is unusually wide/flat (7:1), so sizing it by
   width like the rest left it reading much shorter than the others —
   National Geographic's mark at that same width — so it's sized by
   HEIGHT instead, matched to National Geographic's own rendered
   height at each breakpoint (140px/108px wide -> ~45px/~35px tall,
   from National Geographic's own aspect ratio), letting its width
   follow from that instead of the shared 140px/108px column. */
.cs-logo-strip img[src*="bay-nature"] { width: auto; height: 34px; max-height: none; }
/* VOA reads visually heavier/taller than the others at any strict
   aspect-matched height, so this is picked by eye against the rest of
   the row rather than matched to any one other logo. */
.cs-logo-strip img[src*="voa"] { width: auto; height: 44px; max-height: none; }
.cs-logo-strip a {
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
  transition: opacity 0.2s;
}
.cs-logo-strip a:hover { opacity: 0.7; }
@media (max-width: 700px) {
  .cs-logo-strip img { width: 108px; max-height: 54px; }
  .cs-logo-strip img[src*="bay-nature"] { width: auto; height: 26px; }
  .cs-logo-strip img[src*="voa"] { width: auto; height: 34px; }
}

.cs-nav-footer {
  display: flex; justify-content: space-between; align-items: center;
  padding-top: 40px; margin-top: 20px;
  border-top: 1px solid rgba(30,23,16,0.12);
}
.cs-nav-footer button {
  color: var(--ink-faint);
  background: none; border: none; cursor: pointer;
  transition: color 0.2s;
}
.cs-nav-footer button:hover { color: var(--hover); }

/* same "← All case studies" link, repeated at the top of a case study
   so there's a quick way back without scrolling to the bottom */
.cs-back-top {
  display: block;
  margin-bottom: 32px;
  color: var(--ink-faint);
  background: none; border: none; cursor: pointer;
  transition: color 0.2s;
}
.cs-back-top:hover { color: var(--hover); }

@media (max-width: 680px) {
  .cs-img-row img { height: 200px; }
  .cs-carousel img { height: 220px; }
}

/* ───────────────────────────────────────────
   SHELF — Games & Bookshelf share this pattern:
   a cover image with a short editable note beneath it. Same component,
   different data array per domain (see GAMES / BOOKS in script.js).
─────────────────────────────────────────── */
/* GAMES — own row-major grid: fixed column width, each cover keeps
   its own natural height (no crop, no stretch, no upscaling past
   source resolution). Column count grows with viewport, capped at 6
   per request, with a tight-enough min width to keep the page from
   reading sparse. */
.games-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-items: start;
  gap: 16px;
  margin-bottom: 40px;
}
@media (min-width: 480px) { .games-grid { grid-template-columns: repeat(3, 1fr); gap: 18px; } }
@media (min-width: 700px) { .games-grid { grid-template-columns: repeat(4, 1fr); gap: 22px; } }
@media (min-width: 1000px) { .games-grid { grid-template-columns: repeat(5, 1fr); gap: 24px; } }
@media (min-width: 1300px) { .games-grid { grid-template-columns: repeat(6, 1fr); gap: 26px; } }
.games-tile {
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  display: block;
  transition: transform 0.3s cubic-bezier(0.34,1.3,0.64,1), box-shadow 0.25s;
}
.games-tile:hover { transform: translateY(-3px); box-shadow: var(--sh-lift); }
.games-tile img { width: 100%; height: auto; display: block; }

/* BOOKSHELF — the Games grid, mirrored: Games fixes each cover's
   WIDTH (column) and lets height follow its own aspect ratio; Books
   fixes each cover's HEIGHT and lets width follow instead, wrapping
   with plain flex-wrap. That height (--books-row-height, set in
   script.js's sizeBooksRow) is solved once from the first 6 covers so
   a row of 6 fills the shelf edge-to-edge at the largest breakpoint —
   every other row reuses that exact same height, so narrower
   viewports just naturally fit fewer covers per row via flex-wrap
   (6 -> 3 continuously as the window resizes), the same way Games'
   column count degrades. Below 700px switches to a fixed 3-column
   grid instead (see the mobile query below) since flex-wrap's uneven
   last-row leftovers read cramped at phone widths. */
.books-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-items: start;
  gap: 12px;
  margin-bottom: 40px;
}
@media (min-width: 480px) and (max-width: 699px) {
  .books-row { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 700px) {
  .books-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 22px; }
}
.books-tile {
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  display: block;
  transition: transform 0.3s cubic-bezier(0.34,1.3,0.64,1), box-shadow 0.25s;
}
.books-tile:hover { transform: translateY(-3px); box-shadow: var(--sh-lift); }
.books-tile img { width: 100%; height: auto; display: block; }
@media (min-width: 700px) {
  .books-tile img { width: auto; height: var(--books-row-height, 250px); max-width: 100%; }
}

/* ───────────────────────────────────────────
   EMBEDS — Film (video), Playlist (Spotify), Writing (Substack)
─────────────────────────────────────────── */
.embed-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 40px;
  margin-bottom: 40px;
}
@media (min-width: 800px) {
  .embed-grid { grid-template-columns: 1fr 1fr; gap: 44px 36px; }
}
.embed-video {
  position: relative;
  width: 100%;
  aspect-ratio: 16/9;
  border-radius: 6px;
  overflow: hidden;
  background: var(--paper-dark);
  box-shadow: var(--sh-base);
}
.embed-video.portrait { aspect-ratio: 9/16; }
.embed-video iframe, .embed-video blockquote {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: none;
}
.embed-caption {
  color: var(--ink-faint);
  margin-top: 10px;
}
/* ───────────────────────────────────────────
   PERFORMANCE — custom layout (not the shared .gallery-grid). The
   video is given full priority: height:100% of the row + width:auto
   renders it at its complete natural size (nothing cropped, nothing
   shrunk to make room). It doesn't flex-shrink, so whatever width is
   left over is split evenly between the two photo columns (flex:1
   each) — one full, uncropped photo per column. Order fixed in the
   markup: photo, photo, video.
─────────────────────────────────────────── */
.performance-row {
  display: flex;
  gap: 24px;
  align-items: stretch;
  justify-content: center;
  height: calc(100vh - var(--header-h) - 140px);
  max-height: 720px;
}
.performance-tile {
  border-radius: 6px;
  overflow: hidden;
  cursor: pointer;
  box-shadow: var(--sh-base);
  transition: transform 0.3s cubic-bezier(0.34,1.3,0.64,1), box-shadow 0.25s;
}
.performance-tile:hover { transform: translateY(-3px); box-shadow: var(--sh-lift); }
/* Photo columns: each takes an equal share of whatever width the
   video leaves behind — a masonry column, not a box the photo has to
   fit inside. The photo always fills the FULL column width at its
   own aspect ratio, top-aligned, with no height limit — it's free to
   run past the row's height (and off the bottom of the viewport) at
   its full width rather than ever being shrunk or cropped to fit. */
.performance-photo-tile {
  flex: 1 1 0;
  min-width: 0;
  align-self: flex-start;
}
.performance-photo-tile img {
  width: 100%; height: auto;
  display: block;
}
/* Video: full row height, natural (auto) width — its true size,
   never cropped. */
.performance-video-wrap {
  height: 100%;
  flex-shrink: 0;
}
.performance-video-wrap video {
  height: 100%; width: auto;
  object-fit: contain;
  display: block;
}
@media (max-width: 900px) {
  .performance-row {
    flex-direction: column;
    height: auto; max-height: none;
  }
  /* Shrink-wrapped to the video itself (not stretched to the full
     row width, and no card box behind it) so the clickable area and
     visible frame are exactly the video, matching how the photo
     tiles behave. width:auto + max-width/max-height (not width:100%)
     avoids the same stretch bug fixed above for the photos. */
  .performance-video-wrap {
    width: auto;
    height: auto;
    align-self: center;
    border-radius: 0;
    box-shadow: none;
  }
  .performance-video-wrap video { width: auto; height: auto; max-width: 100%; max-height: 55vh; }
  /* Reset the desktop flex:1 1 0 (which governs HEIGHT once the row
     switches to a column) — otherwise flex-basis:0 collapses each
     photo tile to zero height and overflow:hidden clips the image
     out entirely. Tile stays shrink-wrapped to the image (not
     stretched to the full row width) so the clickable area matches
     the photo exactly, same as desktop. */
  .performance-photo-tile { flex: 0 0 auto; align-self: center; }
  /* width:100% + height:auto + max-height together would stretch a
     tall photo (the height cap kicks in without shrinking width to
     match, distorting the aspect ratio) — width:auto + both max-
     dimensions lets the browser pick whichever constraint binds and
     scale proportionally instead. */
  .performance-photo-tile img { width: auto; height: auto; max-width: 100%; max-height: 45vh; }
}

/* Video page — one row of 4 on desktop (all visible, no wrapping),
   2×2 on mobile. Each cell sizes to its own content (no forced
   aspect-ratio box for Instagram, which self-sizes via embed.js), so
   there's no leftover blank space around a smaller video. */
.video-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  align-items: start;
  margin-bottom: 40px;
}
@media (max-width: 900px) {
  .video-grid { grid-template-columns: repeat(2, 1fr); gap: 14px; }
}
.video-tile { min-width: 0; }
.video-tile > iframe.instagram-media,
.video-tile > blockquote.instagram-media {
  width: 100% !important;
  min-width: 0 !important;
  margin: 0 !important;
}
.embed-empty {
  font-family: Georgia, serif;
  font-size: 17px; line-height: 1.85;
  color: var(--ink-mid);
  max-width: 560px;
  padding: 40px 0;
}
.embed-empty code {
  font-family: var(--font-mono);
  font-size: 13px;
  background: var(--paper-dark);
  padding: 2px 6px;
  border-radius: 3px;
}

/* full-screen-feeling wrapper for the single-embed views (Playlist, Writing) —
   fills the space below the header rather than reading as a small
   widget lost in a mostly-empty page */
.embed-fullscreen {
  min-height: calc(100vh - var(--header-h));
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  padding: 40px 20px;
}
.embed-fullscreen .dv-title { text-align: center; }

/* ───────────────────────────────────────────
   PLAYLIST MODAL — the only way to reach the playlist; tapping the
   iPod on the desk opens it over whatever's currently on screen.
─────────────────────────────────────────── */
/* No card, no title bar, no cream background — just the embed itself
   rising up flush with the bottom edge of the screen (like a drawer),
   a faint dimming behind it, and a plain white circular close button.
   GSAP animates translateY (slides up to open, back down to close). */
#playlist-modal {
  position: fixed; inset: 0; z-index: 320;
  background: rgba(14,10,6,0.28);
  display: flex; align-items: flex-end; justify-content: center;
  opacity: 0;
  pointer-events: none;
}
#playlist-modal.active { pointer-events: all; }
.playlist-panel {
  position: relative;
  width: 100%; max-width: 480px;
  padding: 0 16px;
}
.playlist-modal-close {
  position: absolute; top: -52px; right: 16px;
  width: 40px; height: 40px;
  border-radius: 50%;
  border: none; background: #fff;
  color: var(--ink);
  cursor: pointer;
  font-family: var(--font-mono); font-size: 14px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.25);
  transition: transform 0.2s;
}
.playlist-modal-close:hover { transform: scale(1.08); }

.spotify-embed {
  width: 100%;
  max-width: 760px;
  border-radius: 12px 12px 0 0;
  overflow: hidden;
  box-shadow: 0 -20px 50px rgba(0,0,0,0.3);
}
.spotify-embed iframe { display: block; width: 100%; height: min(660px, 72vh); border: none; }
.playlist-panel .spotify-embed { margin: 0; }

.substack-cta {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--font-mono);
  font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--paper);
  background: var(--ink);
  padding: 16px 28px;
  border-radius: 100px;
  text-decoration: none;
  margin-bottom: 36px;
  transition: background 0.2s;
}
.substack-cta:hover { background: var(--hover); }
.substack-embed {
  width: 100%;
  max-width: 560px;
  border-radius: 6px;
  overflow: hidden;
  box-shadow: var(--sh-lift);
}
.substack-embed iframe { display: block; width: 100%; height: min(480px, 60vh); border: none; }

/* ───────────────────────────────────────────
   ABOUT VIEW
─────────────────────────────────────────── */
/* top block — portrait (+ Get in Touch under it) on the left, header
   and the first two intro paragraphs on the right. Everything below
   this (the two lists, skills preview) runs the full column width. */
/* .about-page treats every top-level piece (portrait+intro, each
   list section, the sign-off) as one equally-spaced sequence — see
   the > * + * rule below — instead of each block carrying its own
   one-off margin. */
.about-page { text-align: left; }
/* The visible gap after "choreography" (54px) was already bigger
   than the other section gaps (32px) — not because its own margin
   was bigger, but because .about-top's box bottom follows the taller
   portrait column, not the shorter intro text, adding ~22px of
   overhang on top of the same 32px margin every other section had.
   Rather than shrink that one down, every OTHER section now gets the
   bigger 54px margin to match it; .about-top keeps the smaller 32px
   box-margin specifically because its 22px of built-in overhang is
   what gets it to the same 54px of actual visible space. */
.about-page > * + * { margin-top: 54px; }
.about-page > .about-top + * { margin-top: 32px; }
.about-top {
  display: flex; flex-direction: column; gap: 28px;
}
@media (min-width: 760px) {
  .about-top { flex-direction: row; align-items: flex-start; gap: 56px; }
}
.about-portrait-col { display: flex; flex-direction: column; align-items: center; gap: 18px; flex-shrink: 0; width: 100%; }
@media (min-width: 760px) {
  .about-portrait-col { max-width: 320px; }
}
.about-portrait {
  width: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--paper-dark);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
.about-portrait img { width: 100%; height: 100%; object-fit: cover; }
.about-portrait span {
  color: var(--ink-faint);
}
.about-intro { flex: 1; min-width: 0; text-align: left; }
.about-intro .dv-title { text-align: left; margin-bottom: 24px; }
.about-intro .dv-prose { max-width: 620px; }

.about-page section h3 {
  font-family: var(--font-serif);
  font-size: 19px;
  color: var(--ink-mid);
  opacity: 0.7;
  margin: 0 0 12px;
}
.about-list {
  font-family: Georgia, serif;
  font-size: 17px; line-height: 1.85;
  color: var(--ink-mid);
  max-width: 680px;
  padding-left: 20px;
  /* browsers give <ul> a default ~1em bottom margin — left in place,
     that stacks with .about-section-btn's own margin-top instead of
     collapsing (only adjacent block boxes collapse; .cs-btn is now
     inline-flex), inflating the gap above "Get in Touch" past the
     other section-button gap. */
  margin-bottom: 0;
}
.about-list li { margin-bottom: 0.4em; }
/* A childless-margin ul with no padding/border lets its last li's
   own bottom margin collapse straight through it — the ul's box ends
   up rendering ~6.8px "taller" than its content, throwing off the
   exact 20px gap above .about-section-btn. */
.about-list li:last-child { margin-bottom: 0; }
.about-list a { color: var(--hover); }
/* Space above a section's own button (Get in Touch / View Artistic
   Skills Tracker) — deliberately smaller than the 32px between
   sections (.about-page > * + *) so the button still reads as
   belonging to the section above it, not a new one. */
.about-section-btn { margin-top: 20px; }
.about-signoff {
  font-family: Georgia, serif;
  font-size: 17px; line-height: 1.85;
  color: var(--ink-mid);
}
/* Footer gap after the sign-off matches the between-section spacing
   above, instead of the smaller default the footer uses elsewhere. */
/* .dv-content's own 100px bottom padding would otherwise stack with
   this margin (padding never collapses with a sibling's margin), so
   the visible gap here would be 132px, not 32 — zero it out just for
   this view so the footer's margin-top alone gives the exact
   between-section spacing. */
#dv-content.about-view { padding-bottom: 0; }
#dv-content.about-view + .site-footer { margin-top: 54px; }

/* shared skills-year rows — used on both /about (a preview) and the
   full Artistic Skills Tracker page */
.skills-year {
  display: flex; gap: 28px; align-items: baseline;
  padding-bottom: 20px;
  border-bottom: 1px solid rgba(30,23,16,0.1);
}
.skills-year-label {
  font-family: var(--font-serif);
  font-size: 22px; color: var(--amber);
  width: 132px; flex-shrink: 0;
  white-space: nowrap;
}
.skills-list {
  font-family: var(--font-mono);
  font-size: 13px; letter-spacing: 0.03em;
  color: var(--ink-mid);
  line-height: 1.8;
}

/* ───────────────────────────────────────────
   ARTISTIC SKILLS TRACKER — full page (content, not the old
   constellation visualization)
─────────────────────────────────────────── */
.skills-years-list {
  display: flex; flex-direction: column; gap: 22px;
  margin-bottom: 56px;
}
.skills-proficiency {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
}
@media (min-width: 640px) {
  .skills-proficiency { grid-template-columns: repeat(3, 1fr); }
}
.skills-proficiency-tier-label {
  color: var(--ink-faint);
  margin-bottom: 10px;
}
.skills-proficiency-list {
  font-family: var(--font-serif);
  font-size: 16px;
  color: var(--ink-mid);
  line-height: 1.7;
}

/* ───────────────────────────────────────────
   TRANSITION FLASH — analog film feel
─────────────────────────────────────────── */
#transition-flash {
  position: fixed; inset: 0; z-index: 600;
  background: var(--paper);
  opacity: 0;
  pointer-events: none;
}

/* ───────────────────────────────────────────
   NAV OVERLAY BACKDROP
─────────────────────────────────────────── */
#nav-backdrop {
  position: fixed; inset: 0; z-index: 195;
  background: rgba(30,23,16,0.22);
  opacity: 0; pointer-events: none;
  transition: opacity 0.35s;
}
#nav-backdrop.open { opacity: 1; pointer-events: all; }

/* ───────────────────────────────────────────
   FOOTER — on every page. Pushed below the first viewport on the
   desk (margin-top:100vh) so it's never visible until the user
   scrolls, per feedback; domain views just get it after their content.
─────────────────────────────────────────── */
.site-footer {
  position: relative; z-index: 5;
  margin-top: 100vh;
  padding: 36px 28px 40px;
  text-align: center;
  text-transform: none;
  color: var(--ink-faint);
  display: flex; flex-direction: column; gap: 8px;
}
.site-footer a { color: var(--ink-faint); text-decoration: none; transition: color 0.2s; }
.site-footer a:hover { color: var(--hover); }
#domain-view .site-footer { margin-top: 0; }

/* ───────────────────────────────────────────
   MOBILE — desk becomes a card grid/list.
   Header, name size, social/nav tap targets stay identical to
   desktop per feedback; only the desk surface itself adapts.
─────────────────────────────────────────── */
@media (max-width: 680px) {
  /* Edge-to-edge 2-column grid, every card exactly the same size —
     object image on top, label underneath, nothing rotated. */
  #desk {
    overflow-y: auto; overflow-x: hidden;
    padding-top: var(--header-h);
    display: grid;
    grid-template-columns: 1fr 1fr;
  }

  .obj {
    position: relative !important;
    top: auto !important; left: auto !important;
    width: auto !important;
    margin: 0;
    transform: none !important;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 10px;
    aspect-ratio: 1 / 1.05;
    padding: 14px 10px;
    opacity: 0; /* GSAP reveals */
  }
  /* grid order — matches the requested reading order rather than DOM order */
  #o-product      { order: 1; }
  #o-photography  { order: 2; }
  #o-writing      { order: 3; }
  #o-illustration { order: 4; }
  #o-shop         { order: 5; }
  #o-bookshelf    { order: 6; }
  #o-music        { order: 7; }
  #o-games        { order: 8; }
  #o-film         { order: 9; }
  #o-performance  { order: 10; }

  .site-footer { order: 13; grid-column: 1 / -1; margin-top: 20px; }

  /* On mobile these were floating absolutely over the grid (and the
     tracker link was landing below the footer). Instead they're just
     two more grid cells — same footprint as an object card, just
     text-only, sitting after Film/Performance. */
  .about-link,
  .skills-tracker-link {
    position: relative !important;
    top: auto !important; left: auto !important; right: auto !important; bottom: auto !important;
    aspect-ratio: 1 / 1.05;
    display: flex; align-items: center; justify-content: center;
    text-align: center;
    padding: 14px 10px;
    font-size: 19px;
    opacity: 0.9;
  }
  .about-link { order: 11; }
  .skills-tracker-link { order: 12; }

  .obj-inner {
    width: 80%;
    flex: 1;
    min-height: 0;
    display: flex; align-items: center; justify-content: center;
    transform: none !important;
  }
  .obj-img {
    max-width: 100%; max-height: 100%;
    width: auto; height: auto;
    object-fit: contain;
    filter: none;
  }
  .obj:hover .obj-inner .obj-img { filter: none; }
  .obj-label {
    position: static;
    transform: none !important;
    opacity: 0.9 !important;
    color: var(--ink-mid) !important;
    font-size: 15px;
    text-align: center;
    white-space: normal;
  }

  .dv-content { padding: calc(var(--header-h) + 30px) 22px 60px; }
  .cs-image-row { grid-template-columns: 1fr; }
  .cs-index-item { gap: 16px; }
  .cs-index-thumb { width: 92px; height: 68px; }
  #nav-menu { width: 100%; }
}
