/* ==========================================================================
   Steven Sayers — Graphic Designer
   Font: Early Sans Variable (Adobe Fonts). See index.html <head> for the
   Adobe Fonts kit embed slot — Early Sans is a paid/Adobe Fonts typeface,
   so it will only render once you add your own kit code there. Until then
   this falls back to Space Grotesk, which Adobe lists as a close free match.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@300..900&display=swap');

:root{
  /* ---- brand palette, sampled from the concept ---- */
  --teal:        #73ddcf;
  --teal-dark:   #4bc2b2;
  --orange:      #f75222;
  --orange-dark: #dc3f11;
  --blue:        #1b53f4;
  --blue-dark:   #123bb8;
  --cyan:        #00C3C1; /* motion graphics — updated per direct request, now used for circles, heading and button (border+text, since .btn uses currentColor) alike, same pattern as photography's yellow update. Contrast against the page background is ~2.04:1 — better than the original #0decda's ~1.4:1, but still under WCAG's 3:1 minimum even for large text. Flagging as before, applied literally as asked. */
  --cyan-dark:   #0a9b8e; /* no longer referenced now that --cyan is used directly for text too — left defined in case the contrast issue above means this ends up wanted again */
  --yellow:      #F3D00C; /* product photography — updated per direct request, used for both the circles and the heading/button text this time (previously just the circles, with a darkened variant for text — see below). Contrast against the page background is still only ~1.4:1 (WCAG AA needs 4.5:1 for text, 3:1 even for large text), essentially unchanged from the original #fae536's ~1.2:1 — flagging this even though it's being used literally as asked, since the heading/button will be hard to read. */
  --yellow-dark: #918308; /* no longer referenced now that --yellow is used directly for text too — left defined in case the contrast issue above means this ends up wanted again */
  --ink:         #141414;
  --body:        #595b5d;
  --grey-line:   #8c8c8c;
  --paper:       #ffffff;
  --mist-1:      #f5f6f5;
  --mist-2:      #eceeed;

  /* ---- type ---- */
  --font-main: 'early-sans-variable', 'Figtree', 'Helvetica Neue', Arial, sans-serif;
  --w-light:   300;
  --w-medium:  500;
  --w-bold:    700;

  /* ---- layout ---- */
  --nav-h: 84px;
  --container: 1180px;
  --ease: cubic-bezier(.22,.8,.32,1);
}

*,*::before,*::after{ box-sizing:border-box; }
/* scroll-behavior:smooth removed from here — it used to apply to
   every scroll on the page, not just the nav logo's #top jump it was
   actually meant for. That's what was causing the hero video to
   "stick, then start moving" oddly: each new wheel-scroll event
   interrupts and restarts the browser's smooth-scroll animation for
   the previous one before it finishes, so on continuous scrolling the
   page's real scroll position lags increasingly behind the input —
   confirmed directly (rapid scroll calls that should have landed at
   850px were actually landing around 90px). That desync then fed into
   the hero's position:sticky behaviour and the scroll-linked JS below
   (both keyed off real-time scroll position), producing exactly the
   reported glitch. Smooth scrolling for the #top link specifically is
   now handled by JS instead (see main.js), which only ever runs once
   per click rather than fighting every scroll event. */
@media (prefers-reduced-motion: reduce){
  *{ animation-duration:.001ms !important; transition-duration:.001ms !important; }
}

body{
  margin:0;
  font-family:var(--font-main);
  font-weight:var(--w-light);
  font-variation-settings:'wght' var(--w-light);
  color:var(--body);
  background:var(--paper);
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

img{ max-width:100%; display:block; }
a{ color:inherit; text-decoration:none; }
h1,h2,h3{ margin:0; font-weight:var(--w-bold); font-variation-settings:'wght' var(--w-bold); color:var(--ink); }
p{ margin:0; }

.container{ width:100%; max-width:var(--container); margin:0 auto; padding:0 40px; }

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

/* ==========================================================================
   Nav
   ========================================================================== */
.nav{
  position:fixed; top:0; left:0; right:0; z-index:100;
  height:var(--nav-h);
  display:flex; align-items:center;
  background:#ffffff;
  box-shadow:0 1px 0 rgba(0,0,0,.06);
}
.nav-inner{
  width:100%;
  display:grid;
  grid-template-columns:1fr auto 1fr;
  align-items:center;
  padding:0 40px;
}
/* Hamburger menu (index.html only for now — see main.js/CSS comments;
   the plan is to add the same markup to the other four pages once this
   is signed off). Sits in the nav's own left-hand grid track (1fr auto
   1fr), which was previously empty space used only to centre the logo. */
.nav-menu-wrap{ grid-column:1; justify-self:start; position:relative; }

.nav-menu-btn{
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:5px;
  width:40px; height:40px; margin-left:-8px; padding:0; border:0;
  background:transparent; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.nav-menu-btn span{
  display:block; width:20px; height:2px; border-radius:1px; background:#818789;
  transition:transform .28s var(--ease), opacity .2s ease, background-color .2s ease;
}
/* hover always shows teal — same colour and same idea as .nav-contact's
   own hover (var(--teal)) — regardless of whether the menu is open or
   closed; this rule is placed after the aria-expanded one below so it
   wins the tie when both apply (hovering it while it's already open) */
.nav-menu-btn[aria-expanded="true"] span{ background:var(--ink); }
.nav-menu-btn[aria-expanded="true"] span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
.nav-menu-btn[aria-expanded="true"] span:nth-child(2){ opacity:0; }
.nav-menu-btn[aria-expanded="true"] span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }
.nav-menu-btn:hover span{ background:var(--teal); }

/* The panel: fixed to the viewport (not just the button) so it always
   starts exactly at the bottom edge of the header, flush with the true
   left edge of the screen — per direct feedback, rather than floating
   below the button with a gap above it. Only the bottom-right corner is
   rounded, since every other corner sits flush against either the header
   above or the screen's own left edge; no shadow, so it reads as a flat
   extension of the header rather than a card floating over the page.
   Animated open/closed via max-height (rather than display:none/block) so
   the whole thing — height, fade, and a small settle — transitions
   together. 480px is a fixed ceiling comfortably above the real content
   height (4 links + the Projects subhead + 3 project links), which is
   what lets max-height animate smoothly; it isn't tied to the content's
   actual measured size. */
.nav-menu{
  position:fixed; top:var(--nav-h); left:0; z-index:5;
  width:280px;
  background:#fff;
  border-radius:0 0 16px 0;
  max-height:0; opacity:0;
  overflow:hidden; pointer-events:none;
  transition:max-height .34s var(--ease), opacity .22s ease;
}
.nav-menu.is-open{ max-height:480px; opacity:1; pointer-events:auto; }
.nav-menu-inner{ padding:18px 24px 22px; }

/* Page links — same "underline draws in on hover" used on the header's
   own Contact link (.nav-contact::after), reused here for consistency;
   each in that page's own colour, matching its pill/heading elsewhere. */
.nav-menu-link{
  position:relative; display:block; width:max-content; max-width:100%; padding:9px 0;
  font-size:18px; font-weight:var(--w-bold); font-variation-settings:'wght' var(--w-bold);
} /* width:max-content — was stretching full-width (the default for a block element), which made the hover underline below run the full width of the menu rather than just under the word itself; kept display:block (rather than inline-block) so each link still sits on its own line */
.nav-menu-link::after{
  content:''; position:absolute; left:0; right:0; bottom:6px; height:1px; background:currentColor;
  transform:scaleX(0); transform-origin:right; transition:transform .22s ease-out;
}
.nav-menu-link:hover::after, .nav-menu-link:focus-visible::after{ transform:scaleX(1); transform-origin:left; }
.nav-menu-link--orange{ color:var(--orange); }
.nav-menu-link--blue{ color:var(--blue); }
.nav-menu-link--cyan{ color:var(--cyan); }
.nav-menu-link--yellow{ color:var(--yellow); }

.nav-menu-subhead{
  margin:14px 0 6px; padding-top:14px; border-top:1px solid #eceeef;
  font-size:18px; font-weight:var(--w-bold); font-variation-settings:'wght' var(--w-bold);
  color:var(--teal); /* matches .projects h2 — the "Projects" heading at the bottom of the home page, per direct request */
} /* font-size matches .nav-menu-link exactly (18px), per follow-up feedback that 16px still read as smaller */
/* Reverted to smaller/grey (not matched to the main links) — per direct
   follow-up feedback correcting the previous change. The teal underline
   on hover is kept as a small accent, even though the resting text isn't
   teal any more. */
.nav-menu-project{
  position:relative; display:block; width:max-content; max-width:100%; padding:6px 0;
  font-size:14.5px; font-weight:500; color:var(--body);
} /* width:max-content — same underline fix as .nav-menu-link above */
.nav-menu-project::after{
  content:''; position:absolute; left:0; right:0; bottom:3px; height:1px; background:var(--teal);
  transform:scaleX(0); transform-origin:right; transition:transform .2s ease-out;
}
.nav-menu-project:hover::after, .nav-menu-project:focus-visible::after{ transform:scaleX(1); transform-origin:left; }

/* Contact — same idea as the top-right nav-contact link, but reachable
   from the bottom of the panel too (the phone case: reaching across to
   the opposite corner from a menu you've just opened one-handed is a much
   bigger stretch than the same link sitting right here). The divider
   above it matches .nav-menu-subhead's own — same colour, same spacing —
   per direct request ("matching the line below photography"). Envelope
   icon is the one already used at the bottom of the sub-pages, coloured
   teal ("the green colour I've used a lot"); the word next to it shifts
   to that same teal on hover rather than using the underline treatment
   the links above use, since an icon+word pair reads better as a single
   unit than a text-only underline would. */
.nav-menu-contact{
  display:flex; align-items:center; gap:8px; /* left-aligned now, matching the other items — was centred */
  margin-top:14px; padding-top:14px; padding-bottom:8px;
  border-top:1px solid #eceeef;
}
/* grey at rest, the same green-on-hover as the hamburger button itself
   (.nav-menu-btn); the word is the exact same SVG artwork as the
   top-right Contact link (.nav-contact), not real text — that link's
   "CONTACT" is drawn as vector paths rather than set in the page's own
   font, which is why plain text couldn't actually match its grey,
   uppercase look. Sized to the same 16px height it renders at up there. */
.nav-menu-contact svg{ width:26px; height:26px; color:#818789; flex:none; transition:color .2s ease; }
.nav-menu-contact:hover svg, .nav-menu-contact:focus-visible svg{ color:var(--teal); }
.nav-menu-contact-wordmark{ height:16px; width:auto; display:block; }
.nav-menu-contact .nav-menu-contact-wordmark{ width:auto; height:16px; } /* needs to out-specificity ".nav-menu-contact svg" above, which would otherwise also catch this (it's an <svg> too) and force it to the envelope's 26x26 box */
.nav-menu-contact:hover .nav-menu-contact-wordmark .contactfill,
.nav-menu-contact:focus-visible .nav-menu-contact-wordmark .contactfill{ fill:var(--teal); }

.nav-logo{
  grid-column:2;
  justify-self:center;
  display:flex;
  align-items:center;
}
.nav-logo svg{ height:16px; width:auto; display:block; }
.nav-contact{
  grid-column:3;
  justify-self:end;
  display:flex;
  align-items:center;
  position:relative;
  padding-bottom:2px;
  border-bottom:1px solid transparent; /* reserves the 1px the underline sits on so the nav doesn't shift height */
}
.nav-contact::after{
  content:'';
  position:absolute;
  left:0; right:0; bottom:-1px;
  height:1px;
  background:var(--teal);
  transform:scaleX(0);
  transform-origin:right; /* on mouse-out the line wipes off to the right, continuing the left-to-right motion */
  transition:transform .22s ease-out;
}
.nav-contact svg{ height:16px; width:auto; display:block; }
.nav-contact .contactfill{ transition:fill .2s ease; }
.nav-contact:hover::after,
.nav-contact:focus-visible::after{ transform:scaleX(1); transform-origin:left; } /* underline draws in from the left on hover/keyboard focus (~0.22s) */
.nav-contact:hover .contactfill{ fill:var(--teal); } /* was var(--ink) (near-black); now the same teal as the pills and the page-end buttons */

/* ==========================================================================
   Hero video — pinned via position:sticky while the About panel slides up
   to cover it (the parallax reveal). The sticky box itself is always sized
   to the video's exact aspect ratio, so there's never any letterboxing —
   only the *extra* scroll buffer below it is what creates the reveal room,
   and About's negative margin pulls it up to swallow that buffer exactly.
   ========================================================================== */
main{ margin-top:var(--nav-h); }

:root{ --video-h: 40.417vw; } /* 776/1920, the video's own aspect ratio as a height */

.hero{
  position:relative;
  height:calc(var(--video-h) + 25vh); /* video's natural height + scroll-pin buffer — reduced from 60vh, since the video now autoplays on load instead of waiting for a click, that long buffer just meant extra blank scrolling before bio appeared with nothing gained from it */
}
.hero-sticky{
  position:sticky;
  top:var(--nav-h);
  width:100%;
  height:var(--video-h); /* matches the real video exactly — no crop, no letterbox */
  overflow:hidden;
  background:#0d0d0d;
}
.hero-media{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:contain; /* scale to fit, never crop */
  background:#0d0d0d;
  will-change:transform;
}
.hero-sound{
  position:absolute; bottom:24px; right:24px;
  width:52px; height:52px;
  border-radius:50%;
  background:rgba(255,255,255,.95);
  border:none;
  display:flex; align-items:center; justify-content:center;
  cursor:pointer;
  box-shadow:0 6px 20px rgba(0,0,0,.25);
  transition:transform .25s var(--ease), box-shadow .25s var(--ease);
  z-index:1;
}
.hero-sound:hover{
  transform:scale(1.08);
  box-shadow:0 8px 26px rgba(0,0,0,.3);
}
.hero-sound:active{ transform:scale(0.94); }
.hero-sound svg{ width:24px; height:24px; fill:var(--ink); }
/* two icon states swapped via the .is-unmuted class (see main.js) —
   simpler and more reliable than trying to morph one icon between the
   two states */
.hero-sound .icon-unmuted{ display:none; }
.hero-sound.is-unmuted .icon-muted{ display:none; }
.hero-sound.is-unmuted .icon-unmuted{ display:block; }

/* ==========================================================================
   About — pulled up by the same amount as the hero's scroll buffer, so it
   sits flush under the video at rest and slides up to cover it on scroll.
   ========================================================================== */
.about{
  position:relative;
  z-index:2;
  margin-top:calc(-25vh); /* matches the hero's own buffer above (was 60vh, both reduced together) */
  /* min-height AND padding-bottom both extended by the same 100px, not
     padding-bottom alone. That matters: this element can be constrained
     by either the min-height floor or by its own content+padding,
     depending on viewport size, and increasing only padding-bottom
     affects those two cases in *opposite* directions (tried twice,
     failed both times — see the long-form explanation that used to be
     here). Extending both together by the same amount cancels that out
     in either case, while genuinely growing the element by ~100px, so
     more of its own gradient renders — a real extension of the same
     continuous background, not a second element trying to approximate
     it. That approximation (a separate .bio-buffer div) was also tried;
     it couldn't work because the actual colour along .about's bottom
     edge isn't uniform (confirmed by sampling it directly — it visibly
     varies left to right due to the diagonal tint), so no flat colour
     or two-layer gradient in a differently-shaped element could match
     it everywhere at once. */
  min-height:calc(max(60.625vw, 909px) + 14vh + 50px); /* same proportions as the reference background above 1500px (909px is the computed value there) — a floor, not a hard cap, so content can never get clipped at narrower/shorter windows. Below 1500px this stops shrinking further, which also stops pulling Packaging's overlap closer to the bio content as the viewport narrows (Packaging's position is tied to this value when it's the dominant constraint) — same freeze pattern as the text and the bio-assets wrapper above. */
  /* --avatar-size: the exact same clamp .avatar's width used to compute
     directly (clamp(132px, 12.3%, 240px)), but with its % rewritten to
     resolve against clamp(1750px, 100vw, 2560px) — the same frozen width
     .bio-assets (.avatar's real containing block) uses — instead of
     .about's own width. Below 1750px those two widths used to diverge
     (.bio-assets stays frozen at 1750px, .about keeps shrinking with the
     real viewport), so the % in each clamp resolved to two different
     pixel values that quietly drifted apart as the window narrowed —
     widening the gap between the avatar's real size and the clearance
     padding-top reserved for it, until the heading crept up underneath
     it (reported directly, reproduced by resizing down from desktop).
     Defining the width once here and reusing it in both .avatar and this
     padding-top guarantees they can never disagree again. */
  --avatar-size:clamp(132px, calc(.123 * clamp(1750px, 100vw, 2560px)), 240px);
  padding-top:calc(6.06% + var(--avatar-size) + 40px); /* avatar's top offset + its own height (now the shared --avatar-size, not a separately-computed clamp) + gap to the heading, tightened from 65px to 40px to bring the heading closer underneath the now-bigger avatar, per direct request */
  padding-bottom:calc(clamp(80px, 11.46vw, 260px) + 14vh + 50px); /* always kept exactly matched to .next-shape's overlap below (same extra amount there too) — see the comment above and the one on that rule. The added amount (14vh + 50px, not just 100px) folds in what the now-removed .bio-buffer div used to contribute, so Packaging's position doesn't shift from where it currently sits, on top of the new 100px of extra background requested */
  background:
    /* single glow, anchored near the top with a fixed vw value rather
       than a % of this element's own height, so it doesn't drift if
       this element's height changes for any reason */
    linear-gradient(135deg, rgba(197,221,214,.45) 0%, rgba(197,221,214,0) 32%, rgba(197,221,214,0) 68%, rgba(197,221,214,.45) 100%),
    radial-gradient(120% 110% at 45% 24.25vw, #fbfcfb 0%, #f2f5f4 45%, #e4ece9 100%);
  overflow:hidden;
}
.about .container{ width:100%; }

/* wraps avatar + circles, centred, with a width that freezes at
   1750px once the viewport narrows past that point (rather than
   continuing to shrink with it) — above 1750px it just tracks the
   viewport normally. Below that, since this wrapper is now wider than
   its shrinking parent, and .about clips overflow, the frozen assets
   (and the circles bleeding off its edges) get cropped by the
   narrowing viewport instead of scaling down further. Text is a
   separate sibling (.container below), entirely unaffected by this. */
.bio-assets{
  position:absolute;
  top:0; bottom:0;
  left:50%;
  transform:translateX(-50%);
  /* width:clamp(1750px, 100%, 2560px) — frozen below 1750px (same as
     before), tracks the viewport between 1750–2560px, then frozen again
     above 2560px ("2K"/QHD width) so it stops growing there instead of
     indefinitely — past that point widening the window just reveals
     more of .about's own gradient background at the sides, rather than
     the circles continuing to grow forever. */
  width:clamp(1750px, 100%, 2560px);
  overflow:hidden;
}

/* avatar centres on the whole section (matches the reference mockup),
   independent of where the left-aligned text column sits.
   Opacity/transform here are driven directly from scroll position in
   main.js (tied to the pin/reveal progress), not a one-shot class swap —
   a plain "in view" trigger would fire while still hidden behind the
   pinned video, so the fade needs to track the actual reveal. */
.avatar{
  position:absolute;
  top:6.063vw; /* fixed to the section's original 60.625vw reference height, not a % of .about's own (now much taller, thanks to the added buffer) height — a %-based value here would drift down as that buffer grows, same issue the gradient anchor fix addressed */
  left:50%;
  width:var(--avatar-size); /* was clamp(132px, 12.3%, 240px) — that % resolved against .avatar's own containing block (.bio-assets, frozen at clamp(1750px,100vw,2560px)), which isn't the same width .about's padding-top below was using to clear it. --avatar-size (defined on .about) makes both expressions share one width basis so they can never drift apart again — see the padding-top comment. */
  aspect-ratio:1;
  border-radius:50%;
  overflow:hidden;
  box-shadow:none;
  opacity:0;
  transform:translateX(-50%) translateY(32px) scale(.75);
  transition:opacity .6s var(--ease), transform .6s var(--ease);
}
.avatar.is-revealed{
  opacity:1;
  transform:translateX(-50%) translateY(0) scale(1);
}
.avatar img{ width:100%; height:100%; object-fit:cover; }

/* decorative product circles — positions/sizes measured directly off the
   concept: a cascade of three per side (top corner → larger middle →
   lower corner bleeding past the edge), mirrored left/right */
.about-decor{ position:absolute; inset:0; pointer-events:none; z-index:0; }
.circle{
  position:absolute;
  border-radius:50%;
  overflow:hidden;
  opacity:0;
  box-shadow:none;
  transform:scale(.6) translateY(46px);
  transition:opacity .6s var(--ease), transform .6s var(--ease);
}
.circle.is-revealed{ opacity:1; transform:scale(1) translateY(0); }
.circle img{ width:100%; height:100%; object-fit:cover; display:block; }
/* top values fixed to the section's original 60.625vw reference height
   (11%/28%/45% of that), not a % of .about's own current height — since
   .about is now taller (extra scroll-safety buffer), %-based positions
   here would have drifted down into where Packaging overlaps, same
   issue the avatar and the gradient anchor both had */
.circle--1{ width:12%;   aspect-ratio:1; top:6.669vw;  left:4%;   }
.circle--2{ width:16.5%; aspect-ratio:1; top:16.975vw; left:15%;  }
.circle--3{ width:15%;   aspect-ratio:1; top:27.281vw; left:-2%;  }
.circle--4{ width:12%;   aspect-ratio:1; top:6.669vw;  right:4%;  }
.circle--5{ width:16.5%; aspect-ratio:1; top:16.975vw; right:15%; }
.circle--6{ width:15%;   aspect-ratio:1; top:27.281vw; right:-2%; }
/* real time-based stagger, independent of scroll speed — see the JS
   comment on fadeTargets for why this matters on a fast scroll/fling */
.circle--1.is-revealed,.circle--4.is-revealed{ transition-delay:0s; }
.circle--2.is-revealed,.circle--5.is-revealed{ transition-delay:.12s; }
.circle--3.is-revealed,.circle--6.is-revealed{ transition-delay:.24s; }

.about-copy{
  max-width:min(max(28.9vw, 506px), 603px); /* proportional to the 1920 concept canvas, so it matches at any width — widened ~8% across the board (was 26.8vw/469px/558px) since bumping the body copy to semibold pushed it onto a 5th line; floored at the value computed at 1750px (matching where the circles stop scaling and start cropping, so text and images freeze together) so the box doesn't keep narrowing below that point while the font sizes are now frozen there too (see .about-heading etc.); otherwise the now-fixed-size text gets crushed into an ever-narrower box as the viewport keeps shrinking */
  margin:0 auto;
  text-align:left;
  position:relative;
  z-index:1;
}
.reveal-1,.reveal-2,.reveal-3,.reveal-4{
  opacity:0;
  transform:translateY(44px);
  transition:opacity .6s var(--ease), transform .6s var(--ease);
}
.reveal-1.is-revealed,.reveal-2.is-revealed,.reveal-3.is-revealed,.reveal-4.is-revealed{
  opacity:1;
  transform:translateY(0);
}
.reveal-2.is-revealed{ transition-delay:.08s; }
.reveal-3.is-revealed{ transition-delay:.16s; }
.reveal-4.is-revealed{ transition-delay:.24s; }

.about-heading{ color:var(--teal); font-size:clamp(79.8px,4.56vw,95px); font-weight:800; font-variation-settings:'wght' 800; margin-bottom:9px; }
.about-lead{ font-size:clamp(28px,1.6vw,33.3px); font-weight:700; font-variation-settings:'wght' 700; color:var(--body); line-height:1.4; margin-bottom:10px; max-width:none; }
/* These three max caps are deliberately set to stop growing at the exact
   same viewport width (~2082px) that .about-copy's own max-width caps out
   at (558px / 26.8vw — see that rule). If the box stops growing before
   the font does, the font keeps expanding into a now-fixed-width box
   until a word no longer fits and wraps to a new line — a visible jump
   right at that width. Keeping all the caps aligned to one width removes
   that mismatch. If .about-copy's max-width or the 26.8vw ever changes,
   these three need recalculating to match (cap px = vw-rate × 2082 / 100). */
/* min bounds (79.8px/28px/18.4px) are the exact computed values at
   1750px width, not 1450px as before — moved to match the point where
   the bio circles themselves stop scaling and start cropping, so the
   copy freezes at the same breakpoint as the images instead of a
   different one */
.about-body{ font-size:clamp(18.4px,1.05vw,21.9px); font-weight:600; font-variation-settings:'wght' 600; line-height:1.65; color:var(--body); max-width:none; } /* was 500 — a step up to semibold (600), between the named Medium/Bold weights */

/* ==========================================================================
   Next section — curved-top entrance shape for now, content comes later.
   Sits just above the previous section (negative margin) with a wide,
   shallow elliptical top edge, and rises + fades in as it's scrolled to,
   same reveal pattern as everything else on the page.
   ========================================================================== */

/* two-column layout scoped to this section specifically, rather than
   giving it the shared base .feature class — that class also carries
   its own padding/background, which would conflict with .next-shape's
   already-customised versions of both */
.next-shape .container{
  display:grid;
  grid-template-columns:0.85fr 1.15fr; /* restored — widening this to 1fr/1fr for the copy earlier shrank the art column (and the circles in it, since they're sized as % of it) as an unintended side effect */
  gap:40px;
  align-items:center;
  /* reverted to the original baseline (no max-width override here, so
     it uses the shared 1180px cap like every other section) — a wider
     cap was tried to let this content keep growing indefinitely with
     the viewport, which caused real problems (see the min-width:1750px
     block below for the correct, bounded version of "grows a bit above
     1750px" instead). */
}
/* CSS Grid items default to min-width:auto, meaning a track won't
   shrink below its content's own intrinsic width — so as the heading's
   font-size grew, the grid was forced wider than the viewport to fit
   it, pushing content off the left edge. min-width:0 overrides that
   default and lets the column (and the text inside it) actually wrap
   or size down instead of forcing an overflow. This is what actually
   caused the cropping in the screenshot, not the max-width alone. */
.next-shape .feature-copy,
.next-shape .feature-art{
  min-width:0;
}

.feature-art{
  position:relative;
  aspect-ratio:1.76/1; /* measured directly off the reference screenshot's art column (924×526px there), not guessed */
}
.pkg-circle{
  position:absolute;
  border-radius:50%;
  background:var(--orange);
  opacity:0;
  transform:scale(0);
  /* transition, not a @keyframes animation — an animation only plays
     forward and doesn't reverse gracefully: if the trigger class ever
     flickers off (right at the scroll threshold, scrolling back up,
     etc.) it snaps instantly back to invisible instead of fading out,
     which is what was making these appear to "disappear" oddly. A
     transition with an overshoot easing curve gives the same bounce
     feel on the way in and reverses smoothly, same as the text next
     to it. cubic-bezier(.34,1.56,.64,1) is a standard "back out" curve
     — it overshoots past 1 before settling, which reads as a single
     bounce rather than a plain ease. */
  transition:opacity .6s ease, transform .65s cubic-bezier(.34,1.56,.64,1);
}
.pkg-circle--1{ width:58%; aspect-ratio:1; top:-26%; left:-2%; } /* bigger (was 48.2%) and anchored further up-left (was top:-16%, left:4.2%) — growing a corner-anchored circle without moving its anchor would push its far edge further into circle--2, increasing overlap; shifting the anchor outward by roughly the size increase keeps the near edge (and the gap between them) about where it was */
.pkg-circle--2{ width:78%; aspect-ratio:1; bottom:-8%; right:-14%; } /* bigger (was 65%) and anchored further down-right (was bottom:0%, right:-4%), same reasoning as circle--1 */

/* 3D POS reuses Packaging's exact circle sizes/animation/text sizing —
   only the grid order (art first, so it's on the left) and the circle
   colour/positions are different, achieved with overrides scoped to
   .feature--pos rather than duplicating any of the shared rules above */
.feature--pos .container{ grid-template-columns:1.15fr 0.85fr; } /* mirrored ratio — art column first/wider, copy column second */
.feature--pos .pkg-circle{ background:var(--blue); }
.feature--pos .pkg-circle--1{ left:auto; right:-2%; } /* same size/top as the shared rule above, just left<->right flipped */
.feature--pos .pkg-circle--2{ right:auto; left:-14%; } /* same size/bottom as the shared rule above, just right<->left flipped */
.container.pkg-text-in-view .feature-art .pkg-circle--1{
  opacity:1; transform:scale(1);
}
.container.pkg-text-in-view .feature-art .pkg-circle--2{
  opacity:1; transform:scale(1);
  transition-delay:.12s;
}

/* Motion graphics reuses Packaging's exact layout (not mirrored — text
   left, circles right, same as Packaging, unlike POS) and its exact
   circle positions/sizes/animation, so only the colour needs
   overriding, same pattern as .feature--pos above. No cutout images
   yet (no motion-graphics stills exist to crop), just the two circles
   — add pkg-cutout images here later following Packaging/POS's pattern
   once real stills/thumbnails exist (see HANDOFF-NOTES.md for the
   pixel-analysis measuring method used for their positions). */
.feature--motion .pkg-circle{ background:var(--cyan); }
.feature--photography .pkg-circle{ background:var(--yellow); }
/* mirrored layout (art left/copy right, like POS) — the circles now
   mirror too, same as POS's own (bigger circle — pkg-circle--2 — on
   the left, smaller — pkg-circle--1 — on the right, via the same
   left<->right flip mechanism POS uses below), so the blob bulges
   outward away from the text instead of toward it. Only the photos
   stay in their original (non-mirrored) positions, per direct
   feedback — dog/top-left, dehumidifier/top-right,
   checked-duvet/bottom-left, deer/bottom-right, same as the reference,
   just relocated to the left column along with the rest of the art
   block rather than mirrored themselves. */
.feature--photography .container{ grid-template-columns:1.15fr 0.85fr; }
.feature--photography .pkg-circle--1{ left:auto; right:-2%; }
.feature--photography .pkg-circle--2{ right:auto; left:-14%; }

/* product cutouts — positions/sizes re-measured properly this round,
   using connected-component detection against the concept image rather
   than the earlier (much rougher) manual read, and expressed as % of
   the concept's own orange-blob bounding box (so top can go negative —
   several products genuinely poke up above the blob's own top edge in
   the concept). Each carries its own --rot custom property (set inline
   in the HTML) so the rotation is easy to see and tweak per image
   without hunting through this file. Same bounce (overshoot
   cubic-bezier) as the circles, not the plain ease used before, and
   delays start at .8s — after the circles have fully finished landing
   (circle--2 finishes at .12s delay + .65s duration = .77s) rather than
   overlapping their animation — and are shuffled so the pop-in order
   doesn't just match the 1–6 layout order left to right. */
.pkg-cutout{
  position:absolute;
  object-fit:contain;
  opacity:0;
  transform:scale(0) rotate(var(--rot,0deg));
  transition:opacity .35s ease, transform .4s cubic-bezier(.34,1.56,.64,1); /* was .6s/.65s — quicker per-item animation on top of the shorter delays below, so the whole cluster reads faster after the circles */
}
.pkg-cutout--1{ width:45.4%; height:72%;   top:-38%;   left:-3%;   } /* vacuum — nudged left and up */
.pkg-cutout--2{ width:39.2%; height:58.1%; top:-34%;   left:39.6%; } /* Remington — nudged up */
.pkg-cutout--3{ width:38%;   height:54.2%; top:-18%;   left:80%;   } /* dotted duvet set — 10% bigger */
.pkg-cutout--4{ width:36.9%; height:41.1%; top:42%;    left:14%;   } /* baby monitor — nudged left, away from Sleepeezee */
.pkg-cutout--5{ width:36.1%; height:84.4%; top:30%;    left:49.5%; } /* Sleepeezee duvet — 5% bigger, nudged higher */
.pkg-cutout--6{ width:27.6%; height:59.5%; top:45%;    left:92%;   } /* juicer — nudged right and up */
.container.pkg-text-in-view .feature-art .pkg-cutout{
  opacity:1; transform:scale(1) rotate(var(--rot,0deg));
}
/* delays now start right after the circles land (.77s) instead of a
   noticeable pause afterward, and the spread between items is much
   tighter (.04s apart instead of .07s) — same shuffled order as before,
   just compressed */
/* circle--2 (the later of the two) starts at .12s delay and takes .65s
   to land, so its halfway point is .12 + .325 ≈ .45s — cutouts now
   start right there instead of waiting for the circles to fully finish,
   keeping the same tight relative spacing between them */
.container.pkg-text-in-view .feature-art .pkg-cutout--1{ transition-delay:.61s; }
.container.pkg-text-in-view .feature-art .pkg-cutout--2{ transition-delay:.45s; }
.container.pkg-text-in-view .feature-art .pkg-cutout--3{ transition-delay:.65s; }
.container.pkg-text-in-view .feature-art .pkg-cutout--4{ transition-delay:.49s; }
.container.pkg-text-in-view .feature-art .pkg-cutout--5{ transition-delay:.57s; }
.container.pkg-text-in-view .feature-art .pkg-cutout--6{ transition-delay:.53s; }

/* Motion graphics' cutouts — same treatment as .pkg-cutout (identical
   transition/easing), separate class only because these are real
   video/gif elements (object-fit:contain works the same way on
   <video> as it does on <img>, so the base rule is shared). Positions
   measured off the reference screenshot the same way Packaging/POS's
   were (see HANDOFF-NOTES.md) — fit a circle to pkg-circle--2's
   visible (unoccluded) edge in the screenshot to back-calculate
   .feature-art's own box there, then measured each element against
   that. No --rot applied — unlike Packaging's flat product photos,
   these already have their own baked-in presentation (the laptop's
   3D tilt, the cards' own design), so an extra CSS rotation wasn't
   part of the reference and would fight it. */
.motion-cutout{
  position:absolute;
  object-fit:contain;
  opacity:0;
  transform:scale(0) rotate(var(--rot,0deg));
  transition:opacity .35s ease, transform .4s cubic-bezier(.34,1.56,.64,1);
}
.motion-cutout--1{ width:96.6%;   height:88%;   top:-21.5%; left:-9.3%; } /* laptop mockup gif — +5% scale on top of the earlier +8%, re-centred again on the same midpoint each time so it grows outward evenly */
.motion-cutout--2{ width:31.35%;  height:52.24%; top:-32.1%; left:-9.3%; } /* World Oceans Day video — top-left, overlaps the laptop's corner */
.motion-cutout--3{ width:31.1%;   height:51.6%; top:33.2%;  left:87%;   } /* Sweeett video — bottom-right, overlaps the laptop's other corner. Shifted further right (+9.5%, ~60px at typical desktop widths) per direct feedback — re-check clipping at the 1438-963px squeeze breakpoint if nudging this further, see the comment on .next-shape's 1438px block. */
.container.pkg-text-in-view .feature-art .motion-cutout{
  opacity:1; transform:scale(1) rotate(var(--rot,0deg));
}
.container.pkg-text-in-view .feature-art .motion-cutout--1{ transition-delay:.45s; }
.container.pkg-text-in-view .feature-art .motion-cutout--2{ transition-delay:.55s; }
.container.pkg-text-in-view .feature-art .motion-cutout--3{ transition-delay:.63s; }

/* Photography's cutouts — same treatment again, real static photos
   this time. Positions measured the same way as Motion's (fit a
   circle to the visible unoccluded edge of pkg-circle--2 in the
   reference screenshot to back-calculate .feature-art's own box,
   then measured each photo against that box), cross-checked against
   each photo's own source aspect ratio (all four render at their
   true aspect here, not stretched). None of the four actually
   overlap each other in the reference, so DOM order doesn't matter
   for stacking — kept as top-left, top-right, bottom-left,
   bottom-right for readability. Positions below are the original
   (non-mirrored) measurements — Photography's grid is mirrored (art
   left, copy right, matching POS) but the art block itself, circles
   and photos both, is not; it reads exactly as the reference
   concept, just relocated to the other side of the row. */
.photo-cutout{
  position:absolute;
  object-fit:contain;
  opacity:0;
  transform:scale(0) rotate(var(--rot,0deg));
  transition:opacity .35s ease, transform .4s cubic-bezier(.34,1.56,.64,1);
}
.photo-cutout--1{ width:42.4%;  height:70.25%; top:-40.67%; left:4.8%;    } /* pet bed — scaled 5% bigger, centre-preserving, then shifted down 40px (~10.5% of .feature-art height) */
.photo-cutout--2{ width:33.28%; height:55.42%; top:-51.81%; left:56.69%;  } /* dehumidifier — moved 40px left (~6.5% of .feature-art width) and 10px down (~2.7% of height) */
.photo-cutout--3{ width:35.87%; height:59.67%; top:38.92%;  left:-0.52%; } /* checked duvet — moved 20px right (~3.26% of .feature-art width) */
.photo-cutout--4{ width:52.79%; height:65.4%;  top:11.65%;  left:41.51%; z-index:1; } /* deer duvet — 15% larger than before, centre-preserving so it grew outward evenly; also explicitly given a z-index so it can't end up sitting behind its neighbours even though it's already last in DOM order (which alone should put it on top) */
.container.pkg-text-in-view .feature-art .photo-cutout{
  opacity:1; transform:scale(1) rotate(var(--rot,0deg));
}
.container.pkg-text-in-view .feature-art .photo-cutout--1{ transition-delay:.45s; }
.container.pkg-text-in-view .feature-art .photo-cutout--2{ transition-delay:.53s; }
.container.pkg-text-in-view .feature-art .photo-cutout--3{ transition-delay:.6s; }
.container.pkg-text-in-view .feature-art .photo-cutout--4{ transition-delay:.68s; }

.next-shape{
  position:relative;
  z-index:3;
  overflow-x:clip; /* NOT overflow:hidden — that clips vertically too, cutting off the circles/cutouts that are designed to bleed above the section on purpose. overflow-x:clip (distinct from the older overflow-x:hidden, which has a quirk where any non-'visible' axis forces the other axis to 'auto' and still ends up clipping) lets this cut only the horizontal bleed on the right below 1438px, leaving vertical overflow untouched. */
  overflow-y:visible;
  /* scales with viewport width instead of a fixed px, and is kept
     mathematically equal in magnitude to .about's padding-bottom above
     — a fixed overlap either felt too small on wide screens or, being
     fixed, could exceed the (also fixed) buffer on narrower ones and
     start covering the bio's content or circles. Equal + proportional
     fixes both. (Do not change this independently of padding-bottom
     above — they must stay matched exactly, or Packaging ends up
     either covering the circles or sitting with a large gap above it.) */
  margin-top:calc(-1 * clamp(80px, 11.46vw, 260px) - 14vh + 30px); /* 80px total less overlap than .about's matched buffer (14vh + 50px) provides — reducing the overlap is always safe, it only ever creates a bigger gap, never covers anything. */
  padding:18px 0 100px;
  background:#f6f7f7;
  opacity:0;
  transform:translateY(100px);
  transition:opacity .35s var(--ease), transform .35s var(--ease); /* quicker (was .6s) and travels further (was 50px) — on a fast scroll, the old .6s fade left a window where you could see through the still-transparent shape to the bio gradient's end underneath; closing that faster (while keeping the motion visible by starting it further down) reduces how much of that gap is exposed */
}
/* POS's overlap needs to be its own value, not the base .next-shape
   formula above — that formula is specifically matched to sit atop
   .about (which has a large, deliberately-matched padding-bottom
   buffer). POS actually sits atop Packaging, whose own padding-bottom
   is a modest 100px — nowhere near enough to safely absorb that same
   large overlap, which is exactly what was pulling POS up into
   Packaging's own content. No overlap at all here, matching the same
   "reducing overlap is always safe" principle used elsewhere — POS
   just sits with a plain gap after Packaging instead. */
.feature--pos{ margin-top:0; }
/* same reasoning as POS's override just above — Motion sits atop POS,
   not .about, so it gets the same plain zero-overlap treatment rather
   than the base negative-margin formula meant for bridging from .about. */
.feature--motion{ margin-top:0; }
/* same again — Photography sits atop Motion. */
.feature--photography{ margin-top:0; }

/* Packaging gets the same soft tonal glow .about uses at its own
   bottom (the thing that's visible behind Packaging's own circle,
   peeking through where Packaging overlaps .about) — anchored to
   Packaging's own bottom instead, using a plain % position rather
   than .about's fixed-vw anchor, so it sits at the bottom regardless
   of how tall this section renders at a given width/breakpoint. Same
   outer tone as .about's radial-gradient (#e4ece9), layered over the
   shared #f6f7f7 rather than replacing it, so Packaging still matches
   POS everywhere except this one subtle wash near the bottom. */
.feature--packaging{
  background:
    radial-gradient(120% 70% at 50% 100%, rgba(228,236,233,1) 0%, rgba(228,236,233,0) 50%),
    #f6f7f7;
  padding-bottom:250px; /* was the shared 100px — bumped by 150px specifically for Packaging (not POS, not any future .next-shape section) to put more air before POS, per direct request after the min-height fix above made the gap consistent but apparently still too tight. Padding, not margin, so it stays part of Packaging's own box and the ::after extension below (anchored to top:100%) still lines up flush against it. */
}
/* Packaging's background/gradient extended past its own box using a
   pseudo-element rather than actual padding/height, specifically so
   this is purely visual and can't push POS (or anything after it)
   down — position:absolute takes it out of normal flow entirely, so
   it adds no layout height anywhere. Why it's needed: POS starts
   opacity:0 and only fades in once scrolled far enough into view (see
   main.js's updateFade) — if someone's scroll comes to rest in the
   moment after POS's box has structurally started but before that
   fade has triggered, POS's own background isn't painted yet, and
   without this, you'd see straight through both Packaging's box
   (already ended) and POS's not-yet-visible one to the page's plain
   white underneath. 70vh is deliberately generous — cost-free since
   it's layout-neutral, and it only ever shows during that brief
   pre-reveal gap anyway, since POS's own background paints over it
   (POS comes after Packaging in DOM order) the moment POS fades in. */
.feature--packaging::after{
  content:'';
  position:absolute;
  left:0; right:0;
  top:100%;
  height:70vh;
  background:
    linear-gradient(to bottom, rgba(228,236,233,.4) 0%, rgba(228,236,233,0) 70%),
    #f6f7f7;
  pointer-events:none;
}

/* POS gets the exact same three treatments as Packaging just above —
   the tonal glow, the extra padding-bottom, and the pre-reveal
   background extension — now that Motion sits after it and needs the
   same "same distance as Packaging-to-POS" spacing and the same
   protection against the white pre-reveal flash. Kept as separate
   rules mirroring Packaging's rather than generalising both under
   .next-shape, so each section's padding-bottom independently matches
   whichever section comes after it, rather than accidentally coupling
   all current and future .next-shape sections to one shared number. */
.feature--pos{
  background:
    radial-gradient(120% 70% at 50% 100%, rgba(228,236,233,1) 0%, rgba(228,236,233,0) 50%),
    #f6f7f7;
  padding-bottom:250px;
}
.feature--pos::after{
  content:'';
  position:absolute;
  left:0; right:0;
  top:100%;
  height:70vh;
  background:
    linear-gradient(to bottom, rgba(228,236,233,.4) 0%, rgba(228,236,233,0) 70%),
    #f6f7f7;
  pointer-events:none;
}
/* same again, one section further down — Motion now hands off to
   Photography the same way Packaging hands off to POS and POS hands
   off to Motion. */
.feature--motion{
  background:
    radial-gradient(120% 70% at 50% 100%, rgba(228,236,233,1) 0%, rgba(228,236,233,0) 50%),
    #f6f7f7;
  padding-bottom:250px;
}
.feature--motion::after{
  content:'';
  position:absolute;
  left:0; right:0;
  top:100%;
  height:70vh;
  background:
    linear-gradient(to bottom, rgba(228,236,233,.4) 0%, rgba(228,236,233,0) 70%),
    #f6f7f7;
  pointer-events:none;
}

.next-shape.in-view{ opacity:1; transform:translateY(0); }
.next-shape-curve{
  position:absolute;
  top:-119px; left:0;
  width:100%; height:120px;
  display:block;
}
@media (max-width:900px){
  .next-shape-curve{ top:-64px; height:64px; }
}

/* text pops in individually, same pattern as the bio section: hidden by
   default, revealed (with its own delay) once .next-shape's own
   IntersectionObserver trigger adds .in-view */
.pkg-reveal-1,.pkg-reveal-2,.pkg-reveal-3{
  opacity:0;
  transform:translateY(30px);
  transition:opacity .6s var(--ease), transform .6s var(--ease);
}
/* text reveal is now gated on the .feature-copy container's own scroll
   position (see the JS), independent of when the shape itself appeared
   — the delays below are just the internal stagger between heading,
   button, and paragraph once that's triggered */
.container.pkg-text-in-view .pkg-reveal-1{ opacity:1; transform:translateY(0); transition-delay:0s; }
.container.pkg-text-in-view .pkg-reveal-2{ opacity:1; transform:translateY(0); transition-delay:.13s; }
.container.pkg-text-in-view .pkg-reveal-3{ opacity:1; transform:translateY(0); transition-delay:.25s; }

/* POS's product cutouts — same bounce transition/timing as Packaging's
   pkg-cutout (same duration, same easing, same trigger mechanism via
   .container.pkg-text-in-view), just a different class name and its
   own positions measured off the POS reference composite */
.pos-cutout{
  position:absolute;
  object-fit:contain;
  opacity:0;
  transform:scale(0) rotate(var(--rot,0deg));
  transition:opacity .35s ease, transform .4s cubic-bezier(.34,1.56,.64,1);
}
.pos-cutout--1{ width:30.6%; height:71.4%;  top:-33.4%; left:14%;   } /* airmax shipper */
.pos-cutout--2{ width:61.1%; height:76.9%;  top:17.1%;  left:-15.8%;} /* Remington counter display */
.pos-cutout--3{ width:47.9%; height:120.9%; top:-24.6%; left:50.8%; } /* restore floor-standing shipper */
.container.pkg-text-in-view .feature-art .pos-cutout{
  opacity:1; transform:scale(1) rotate(var(--rot,0deg));
}
.container.pkg-text-in-view .feature-art .pos-cutout--1{ transition-delay:.53s; }
.container.pkg-text-in-view .feature-art .pos-cutout--2{ transition-delay:.45s; }
.container.pkg-text-in-view .feature-art .pos-cutout--3{ transition-delay:.61s; }
@media (max-width:900px){
  .next-shape{ margin-top:0; padding:40px 0 70px; min-height:50vh; } /* was -90px, then -20px — still clipped the last line at some widths, since mobile text height isn't precisely predictable. No overlap at all now: guarantees it can never cover content, at the cost of a slightly plainer join where the curve meets the bio section. */
  .feature--packaging{ padding-bottom:170px; } /* same "too close" complaint as the desktop fix above, raised again once seen on tablet/mobile — bumped by 100px here rather than the desktop rule's +150px, since 70px was already smaller to start with on this narrower layout; scoped to Packaging only, same reasoning as the desktop override. */
  .feature--pos{ padding-bottom:170px; } /* matches Packaging's bump just above, same reasoning — keeps the POS-to-Motion gap equal to the Packaging-to-POS gap on tablet/mobile too. */
  .feature--motion{ padding-bottom:170px; } /* same again — keeps the Motion-to-Photography gap matched too. */
}

/* ==========================================================================
   Feature sections — Packaging / 3D Point of Sale
   ========================================================================== */
.feature{
  position:relative;
  background:var(--mist-1);
  padding:120px 0;
  overflow:hidden;
}
.feature .container{
  display:grid;
  grid-template-columns:0.85fr 1.15fr; /* copy (narrow) | art (wide) */
  gap:40px;
  align-items:center;
}
/* pos's art comes first in the markup and needs to be the wide column,
   so the reversed layout swaps the track sizes rather than reordering */
.feature--reverse .container{ grid-template-columns:1.15fr 0.85fr; }

.feature-copy h2{ font-size:clamp(41px,5.75vw,60px); line-height:1.05; margin-bottom:24px; }
.feature-copy h2 a{ color:inherit; text-decoration:none; } /* titles now link to each section's own page (same destination as its "View more" button) — plain, no hover/zoom effect requested, just inherits the h2's own colour instead of the browser's default link blue/underline */
.feature--packaging h2{ color:var(--orange); }
.feature--pos h2{ color:var(--blue); }
.feature--motion h2{ color:var(--cyan); }
.feature--photography h2{ color:var(--yellow); }

.btn{
  --btn-fill:var(--ink);
  position:relative; isolation:isolate; overflow:hidden; /* isolate + overflow keep the sliding fill behind the label and clipped to the pill shape */
  display:inline-flex; align-items:center; justify-content:center;
  height:56px; padding:0 30px; border-radius:999px;
  font-size:17px; font-weight:var(--w-bold); font-variation-settings:'wght' var(--w-bold);
  border:1.5px solid var(--btn-fill); /* was currentColor — that turned the outline white mid-fill (label colour flips to white on hover), so the un-filled half lost its edge. Fixed to the brand colour so the outline stays put and the fill sweeps across inside it. */
  color:var(--ink);
  margin-bottom:26px;
  transition:color .25s ease;
}
/* hover fill: a coloured layer wipes in from the left (and back out to the right on mouse-out), same idea as the nav Contact underline. Replaces the old instant background-colour fill. */
.btn::before{
  content:'';
  position:absolute; inset:0; z-index:-1;
  background:var(--btn-fill);
  transform:scaleX(0);
  transform-origin:right;
  transition:transform .28s ease-out;
}
.btn:hover::before,
.btn:focus-visible::before{ transform:scaleX(1); transform-origin:left; }
.btn:hover,
.btn:focus-visible{ color:#fff; }
.btn--orange{ color:var(--orange); --btn-fill:var(--orange); }
.btn--blue{ color:var(--blue); --btn-fill:var(--blue); }
.btn--cyan{ color:var(--cyan); --btn-fill:var(--cyan); }
.btn--yellow{ color:var(--yellow); --btn-fill:var(--yellow); }
.btn--teal{ color:var(--teal-dark); --btn-fill:var(--teal); }
/* the reveal rule below (.pkg-reveal-2) puts a .13s transition-delay on the button once its section scrolls in, which would make the label colour lag behind the fill on hover — zeroed for the button only. */
.container.pkg-text-in-view .btn.pkg-reveal-2{ transition-delay:0s; }

.feature-copy p{ max-width:44ch; font-size:17.8px; line-height:1.75; font-weight:500; font-variation-settings:'wght' 500; }

/* packaging-specific overrides: bigger heading, chunkier button, text
   nudged left, and the paragraph narrowed (not widened) so it wraps
   onto 5 lines within the now-wider copy column (see the grid
   template-columns change above) instead of 4 */
.next-shape .feature-copy{ margin-left:-60px; }
.feature--pos .feature-copy{ margin-left:0; margin-right:-60px; } /* mirrored — Packaging nudges its (left-side) text further left/outward; POS's text is on the right, so it nudges right/outward instead */
.feature--photography .feature-copy{ margin-left:0; margin-right:-60px; } /* same as POS above — missed this the first time Photography's layout was mirrored, so it was still inheriting the generic -60px margin-left meant for left-side text, pulling it into the art column instead of pushing it outward. */
/* Desktop text-block nudge (viewports above 1438px — the full two-column layout; tablet and phone layouts are untouched, and the 963–1438px "compressed desktop" range has no spare room to nudge into). The left-hand text blocks (Packaging, Motion) move further LEFT so their left edge lines up with the left edge of the circle in the mirrored sections (3D point of sale, Photography), and the right-hand blocks (3D point of sale, Photography) move RIGHT by the same amount.
   --copy-shift is that circle overhang, worked out from the layout itself so the alignment holds at every desktop width instead of only one: the mirrored art column is 1.15/2 of the container's inner width minus the 40px gap ((C - 80 - 40) * 0.575, C being the same clamp(1180px, 67.43vw, 1726px) as .next-shape .container), its blue/cyan/yellow circle overhangs that column's left edge by 14% of its width, and the existing -60px offset is already part of where the left text sits: shift = 0.14 * 0.575 * (C - 120) - 60 = 0.0805 * (C - 120) - 60. That comes to about 25px at 1440px wide, 44px at 2085px and 69px at 2560px+. Margins are used (not transforms) and the opposite-side margin gives back the same amount, so each block keeps exactly the width it had and its text wraps the same as before. */
@media (min-width:1439px){
  .next-shape{ --copy-shift:calc(0.0805 * (clamp(1180px, 67.43vw, 1726px) - 120px) - 60px); }
  .next-shape .feature-copy{ margin-left:calc(-60px - var(--copy-shift)); margin-right:var(--copy-shift); }
  .feature--pos .feature-copy,
  .feature--photography .feature-copy{ margin-left:var(--copy-shift); margin-right:calc(-60px - var(--copy-shift)); }
}
/* below 1750px these clamp to their own floor (72px/68px/18px/16px, the
   same "good" sizes as before), so nothing changes there. Between
   1750–2560px they scale smoothly via the same vw rates as before —
   extended to keep growing all the way to 2560px ("2K"/QHD, the same
   point .bio-assets now caps at above) instead of stopping early around
   ~1950–2150px, so Packaging doesn't finish growing and go static while
   bio's still visibly getting bigger next to it. Above 2560px both
   sides of the page are frozen together. */
.next-shape .feature-copy h2{ font-size:clamp(72px, 4.114vw, 105.3px); }
.next-shape .feature-copy .btn{
  height:clamp(68px, 3.886vw, 99.5px);
  padding:0 44px;
  border-width:3px;
  font-size:clamp(18px, 1.029vw, 26.3px);
}
.next-shape .feature-copy p{ max-width:52ch; font-size:clamp(16px, 0.914vw, 19.1px); } /* ceiling matched to where .about-body (bio's own copy) stops growing — ~2086px — not 2560px like the heading/button/container/circles. Body text stopping earlier than the rest of a layout is normal (bio does the same), so Packaging's copy now mirrors that instead of continuing to grow alongside the bigger elements. */
.next-shape .container{ max-width:clamp(1180px, 67.43vw, 1726px); }

/* below 1438px, the 0.85fr copy column gets too narrow for the fixed
   -60px margin-left to stay safe — it was pushing text past the left
   edge. Below this point the art column now holds a firm minimum width
   instead of continuing to shrink, so once the viewport can't fit both
   columns comfortably, the art side intentionally bleeds off the right
   edge (clipped by .next-shape's new overflow:hidden) while the text
   column stays anchored safely on the left with real padding, matching
   how the bio circles already crop rather than the text being what
   gives. Heading/copy also shrink gradually across this same range
   instead of staying frozen at their 1750px-floor size, down to the
   existing 900px mobile sizes (46px/16px) by the time this range ends. */
@media (max-width:1438px){
  .next-shape .container{
    margin-left:0; /* un-centers — so any overflow from the minmax() below only extends rightward past the viewport, not symmetrically off both sides */
    grid-template-columns: minmax(260px, 0.85fr) minmax(460px, 1.15fr);
  }
  .next-shape .feature-copy{ margin-left:0; }
  .next-shape .feature-copy h2{ font-size:calc(46px + (100vw - 900px) * 0.04833); } /* 46px at 900px (matching the existing mobile override) scaling up to 72px at 1438px (matching the floor just above this breakpoint) */
  .next-shape .feature-copy p{ font-size:16px; } /* already at its floor from the base rule — held flat here, nothing further to shrink without hurting readability */
  /* POS mirrors the layout (art left, copy right), so the crop-margin
     mechanic above — built for Packaging's asymmetric ratio and
     right-edge overflow — would apply backwards here. POS's content is
     just two circles (no cutout images bleeding as aggressively as
     Packaging's do), so it doesn't need the same forced-minmax crop;
     a plain mirrored ratio is enough, plus neutralising the outward
     text push same as Packaging's is neutralised above. */
  .feature--pos .container{ grid-template-columns:1.15fr 0.85fr; margin-left:auto; }
  .feature--pos .feature-copy{ margin-right:0; }
  /* Photography mirrors the layout too, same reasoning as POS just
     above — but unlike POS, its content is four real photos with
     significant bleed (not just two plain circles), so this needs
     re-checking for clipping the same way Motion's crop treatment
     did, rather than assuming POS's simple version is automatically
     enough here. */
  .feature--photography .container{ grid-template-columns:1.15fr 0.85fr; margin-left:auto; }
  .feature--photography .feature-copy{ margin-right:0; }
  /* Motion intentionally gets no override here, unlike POS above —
     it should crop off the right edge the same way Packaging does in
     this range (that's the established, wanted behaviour for this
     breakpoint), not be held back from it. Tried adding a width
     constraint here briefly to reduce the clipping, but that made
     Motion behave inconsistently with Packaging/POS, which wasn't
     wanted — reverted. */
}

/* tablet treatment — below this, side-by-side just doesn't have enough
   room to work well (that's what the 1438px block above was patching
   around). Simpler to actually restack: art on top, centred and at 80%
   width so it scales down proportionally with the viewport (everything
   inside it is %-based off .feature-art already, so this one width
   change moves the whole cluster together), text below it. Overrides
   the 1438px block's minmax()/crop approach entirely at this width —
   that mechanic doesn't apply once it's a single column. */
@media (max-width:962px){
  /* no overflow override needed here — the base rule above already
     does exactly the right thing (overflow-x:clip, overflow-y:visible)
     at every width. */
  .next-shape .container{
    grid-template-columns:1fr;
    margin:0 auto;
  }
  /* the whole cluster (circles + cutouts) treated as one unit, sized
     and positioned to include its own bleed — not just feature-art's
     own nominal box. Every pkg-circle/pkg-cutout is already positioned
     as a % of feature-art, so they scale together proportionally
     automatically; the actual problem was centring feature-art's own
     box while ignoring that the cluster's bleed is asymmetric (circle--2
     and the juicer cutout extend ~2% past the left edge and ~19.6% past
     the right edge). Width and left offset below are solved so the
     cluster's true outer edges — bleed included — sit at exactly 5%
     margin on both sides, not just feature-art's own box. Desktop's
     circle/cutout position values are completely untouched. */
  .next-shape .feature-art{
    order:-1;
    width:74%;
    margin-left:6.48%;
    margin-right:0;
    margin-bottom:40px;
    min-height:0; /* overrides a generic mobile rule (min-height:360px) meant for a different, not-yet-built section that was leaking in here via the shared .feature-art class name and fighting with this element's own aspect-ratio-driven sizing — that mismatch is what was making the circles scale independently of the rest of the block */
  }
  .next-shape .feature-copy{
    width:90%; /* the text block has no bleed to account for, so plain 90%/centred still applies here */
    margin:0 auto;
  }
  .next-shape .feature-copy h2{ font-size:76px; } /* fixed across the whole 560-962px range — the smooth scale was tried but wasn't wanted */
  /* POS has a different bleed profile than Packaging, so it needs its
     own width/offset computed the same way as Packaging's above, not
     Packaging's actual numbers. (The width/margin-left below predate
     the real product cutouts being wired in — they were tuned against
     the two circles alone. Measured against the actual cutout images
     now, the true outer edges land at 6-9% margin rather than the
     original 5% target, which is generous rather than tight, so no
     horizontal clipping results and these values are left as-is.) */
  .feature--pos .feature-art{ width:77.59%; margin-left:15.86%; margin-right:0; }
  /* POS-covers-Packaging bug: feature-art is order:-1 here (art sits
     above the text, same as Packaging), and .feature--pos has
     margin-top:0 (see that rule's own comment) — so there's zero gap
     between Packaging's box ending and POS's feature-art box
     beginning. POS's own cutouts bleed *upward* past their own box
     (.pos-cutout--1 alone is top:-33.4%), and with no gap to absorb
     that, the bleed was landing directly on Packaging's text above,
     not — as originally suspected — Packaging's own cutouts bleeding
     down into POS. Confirmed by measuring rendered bounding boxes at
     several widths across this breakpoint range: the worst-case
     upward bleed holds steady at ~17% of .container's width (16.3% at
     560px up to 16.9% at 800px), so an 18% margin-top clears it with
     a small safety margin at every width in the range, the same
     buffer-against-bleed approach as Packaging's own margin-bottom
     below. */
  .feature--pos .feature-art{ margin-top:18%; }
  /* same mechanism again, one section further down: Motion sits atop
     POS the same way POS sits atop Packaging (margin-top:0, zero
     deliberate gap), and Motion's circles reuse Packaging's own
     positions/bleed (not POS's) — .pkg-circle--1 alone bleeds top:-26%
     of feature-art's height. Measured the same way as POS's fix above:
     holds steady at ~10.6-10.9% of .container's width across this
     breakpoint range, so 13% margin-top clears it with a similar
     safety margin to POS's own fix, even with POS's extra
     padding-bottom (the 170px bump above) already factored in — without
     this, Motion's circles were landing on POS's own text the same way
     POS's cutouts originally landed on Packaging's. */
  .feature--motion .feature-art{ margin-top:15%; } /* bumped from an initial 13% (based on the plain circles alone) to 15% once the real cutout elements were added — .motion-cutout--2 bleeds further (top:-32.1%) than the circles did, and 13% only left ~6px of margin at the 962px edge of this breakpoint, too tight for comfort. 15% restores a healthier margin across the whole range (verified 28px+ everywhere except right at 962px itself). */
  /* Photography, atop Motion — was 13% for the plain circles alone
     (Motion's original pre-image state). Now that the four real photos
     are in, .photo-cutout--2 (the dehumidifier) bleeds much further —
     top:-59.4%, measured at a steady ~24.2-24.9% of .container's width
     across this range — so bumped to 27% for a similar safety margin
     to Motion's own revision, same reasoning throughout. */
  .feature--photography .feature-art{ margin-top:27%; }
  /* horizontal centering — Photography's circles now mirror POS's
     exactly (bigger circle left, smaller right, same left/right
     values), so it was inheriting the generic width:74%/margin-left:
     6.48% meant for Packaging's (un-mirrored) bleed profile instead
     of POS's centering treatment, leaving the whole cluster sitting
     too far toward the left edge on tablet/mobile. Reusing POS's own
     width/margin-left directly, since the circle geometry driving
     that calculation is now identical between the two. */
  .feature--photography .feature-art{ width:77.59%; margin-left:15.86%; margin-right:0; }
}


.feature-art{
  position:relative;
  width:100%;
}
.blob{
  position:absolute;
  opacity:0;
  transform:scale(.4);
  transition:opacity .7s var(--ease), transform .7s var(--ease);
  transition-delay:0s;
}
.feature.in-view .blob{ opacity:1; transform:scale(1); }

/* packaging: wide organic blob behind a loose 3-over-3 cluster of boxes */
.feature--packaging .feature-art{ aspect-ratio:1.66/1; }
.feature--packaging .blob{
  width:98%; height:92%; left:0; top:4%;
  background:var(--orange);
  border-radius:46% 54% 61% 39% / 53% 46% 54% 47%;
}
/* 3D point of sale: rounder blob behind the display unit + tray */
.feature--pos .feature-art{ aspect-ratio:1.5/1; }
.feature--pos .blob{
  width:88%; height:100%; left:0; top:0;
  background:var(--blue);
  border-radius:50%;
}
/* motion graphics: same proportions as Packaging's art column, since
   it reuses Packaging's exact circle layout (no .blob rule needed here
   — that class isn't used by the live markup, same as Packaging/POS's
   .blob rules above; the real circles are .pkg-circle, styled where
   the rest of .pkg-circle lives). */
.feature--motion .feature-art{ aspect-ratio:1.66/1; }
/* product photography: same again, no images yet either. */
.feature--photography .feature-art{ aspect-ratio:1.66/1; }

.cutouts{ position:absolute; inset:0; }
.cutout{
  position:absolute;
  border-radius:10px;
  background:linear-gradient(155deg,#ffffff 0%, #dfe1e0 100%);
  box-shadow:0 20px 36px rgba(20,20,20,.18);
  opacity:0;
  transform:translateY(24px) scale(.88) rotate(var(--rot,0deg));
  transition:opacity .55s var(--ease), transform .55s var(--ease);
  display:flex; align-items:center; justify-content:center;
  overflow:hidden;
}
.cutout img{ width:100%; height:100%; object-fit:cover; }
.cutout-label{
  font-size:10px; letter-spacing:.05em; text-transform:uppercase;
  color:var(--grey-line); text-align:center; padding:8px;
}
.feature.in-view .cutout{ opacity:1; transform:translateY(0) scale(1) rotate(var(--rot,0deg)); }

/* positions read off the concept: 3 boxes riding the top of the blob,
   3 more overlapping lower down, alternating tilt for an unpacked feel */
.feature--packaging .cutout:nth-child(1){ width:27%; height:39%; top:0%;   left:1%;  --rot:-6deg; transition-delay:.30s; background:linear-gradient(155deg,#e9f6f4,#bfe9e2); }
.feature--packaging .cutout:nth-child(2){ width:26%; height:35%; top:0%;   left:33%; --rot:5deg;  transition-delay:.40s; background:linear-gradient(155deg,#fdeee8,#f7c6b3); }
.feature--packaging .cutout:nth-child(3){ width:23%; height:33%; top:3%;   left:61%; --rot:-4deg; transition-delay:.50s; background:linear-gradient(155deg,#fff7e6,#f3e2b8); }
.feature--packaging .cutout:nth-child(4){ width:33%; height:28%; top:47%;  left:6%;  --rot:-8deg; transition-delay:.60s; background:linear-gradient(155deg,#eef1ff,#c9d2f7); }
.feature--packaging .cutout:nth-child(5){ width:20%; height:54%; top:39%;  left:42%; --rot:2deg;  transition-delay:.70s; background:linear-gradient(155deg,#e9e9e9,#c7c7c7); }
.feature--packaging .cutout:nth-child(6){ width:20%; height:35%; top:43%;  left:70%; --rot:6deg;  transition-delay:.80s; background:linear-gradient(155deg,#1c1c1c,#3a3a3a); }

/* pos: a low wide tray front-left, a tall tower stood beside it, and a
   thin poster/pouch peeking in from the top */
.feature--pos .cutout:nth-child(1){ width:60%; height:71%; top:26%; left:0%;   --rot:0deg;  transition-delay:.30s; background:linear-gradient(155deg,#111,#333); }
.feature--pos .cutout:nth-child(2){ width:37%; height:100%; top:0%; left:63%;  --rot:0deg;  transition-delay:.48s; background:linear-gradient(155deg,#0f3d3a,#1f6e63); }
.feature--pos .cutout:nth-child(3){ width:30%; height:28%; top:0%;  left:29%;  --rot:-3deg; transition-delay:.62s; background:linear-gradient(155deg,#e9f6f4,#bfe9e2); }

/* soft section divider curve */
.curve{
  position:absolute; left:0; right:0; height:120px;
  pointer-events:none;
}
.curve--top{ top:-1px; }
.curve svg{ width:100%; height:100%; display:block; }

/* ==========================================================================
   Projects grid
   ========================================================================== */
/* z-index:4 (one above .next-shape's 3) — Photography's own reveal
   animation slides up from translateY(100px) as it fades in, and
   while that's mid-transition its box can transiently extend lower
   than its settled position. Without its own stacking level, Projects
   (no reveal animation, always static) would sit behind that and get
   its top edge visibly covered during the transition — confirmed by
   sampling scroll positions directly (100px overlap, exactly matching
   the pre-reveal transform's offset, in over half the positions
   checked). Giving Projects a higher z-index than any .next-shape
   section keeps it on top regardless of what's mid-animation below it. */
.projects{ padding:140px 0 120px; background:var(--paper); position:relative; z-index:4; }
.projects .container{ max-width:clamp(1180px, 68.21vw, 1726px); } /* was a flat 1180px like every other .container — fine up to ~1730px viewport width (68.21vw solves to exactly 1180px there), but past that it left growing amounts of dead space either side per direct feedback. Scoped to .projects only, not the shared --container variable, since other sections (About, Packaging's text column, etc.) have their own proportional systems already tuned around the fixed 1180px value. 1726px ceiling matches the site's own existing "generously wide" cap used elsewhere (.next-shape .container), rather than growing indefinitely on very wide monitors. */
.projects h2{
  text-align:center; color:var(--teal);
  font-size:clamp(79.8px,4.56vw,95px); font-weight:800; font-variation-settings:'wght' 800;
  margin-bottom:70px;
} /* matches .about-heading's size/weight/colour exactly, per direct request — same clamp values, same 800 weight, same --teal */
.projects-grid{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:40px 32px;
}
.project-card{ display:block; }
.project-thumb{
  border-radius:18px;
  aspect-ratio:1/1; /* was 16/10 (landscape) — your reference screenshot's thumbs read close to square (measured ~1.04:1), so this matches that and reads as noticeably larger/more prominent within the same column width, per direct feedback */
  background:linear-gradient(135deg,#1fb7c9 0%, #2aa6c9 45%, #f7943c 100%);
  margin-bottom:18px;
  overflow:hidden;
  position:relative;
  transition:transform .4s ease;
}
.project-thumb img{
  width:100%; height:100%; object-fit:cover;
  transition:transform .4s ease;
}
/* two-layer zoom on hover, per direct feedback that the image-only
   version wasn't obvious enough: .project-thumb itself now scales up
   a little too (1.03), on top of the image's own larger scale (1.06)
   — the two stack, so the image ends up growing more than the frame,
   keeping the "photo zooming within a frame" read rather than the
   whole card just getting bigger. Growing the frame via transform
   still doesn't shift the title/meta below (transform never affects
   layout/flow) — it's genuinely visually bigger now, not just
   repositioned, but at 1.03 the ~5px of growth on each edge (on a
   ~345px thumb) stays well clear of the 18px gap before the title. */
.project-card:hover .project-thumb{ transform:scale(1.03); }
.project-card:hover .project-thumb img{ transform:scale(1.06); }
.project-title{ color:var(--teal-dark); font-weight:var(--w-medium); font-variation-settings:'wght' var(--w-medium); font-size:16px; margin-bottom:6px; }
.project-meta{ font-size:13px; color:var(--body); line-height:1.6; }
.projects .project-title{ font-size:clamp(16px, 0.925vw, 23.4px); } /* flat until ~1730px viewport width, same point .projects .container starts growing past its own 1180px floor, then scales at the same rate — reaching 23.4px (16 × the container's own 1726/1180 growth ratio) at 2530px, exactly where the container's clamp hits its own ceiling, so title/meta and the grid finish scaling together. Scoped to .projects specifically (not the shared .project-title/.project-meta base) so packaging.html/pos.html's own project grids keep their fixed size. */
.projects .project-meta{ font-size:clamp(13px, 0.752vw, 19px); } /* same approach as .project-title, scaled from 13px by the same ratio */

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer{
  padding:60px 0 40px;
  text-align:center;
  border-top:1px solid var(--mist-2);
}
.site-footer .nav-logo{ position:static; margin-bottom:14px; }
.footer-links{ display:flex; justify-content:center; gap:26px; margin-bottom:22px; }
.footer-links a{ font-size:13px; color:var(--body); border-bottom:1px solid transparent; }
.footer-links a:hover{ color:var(--ink); border-color:var(--teal); }
.footer-copy{ font-size:12px; color:#9a9a9a; }

/* ==========================================================================
   Page-end buttons (home / back-to-top arrow / contact) — replace the old footer (logo / links / copyright) on
   packaging.html, pos.html and photography.html. Scroll animation lives in
   main.js (custom eased scroll so the duration stays short whatever the
   page length). Grey is the same #818789 as the nav logo/contact text.
   ========================================================================== */
.back-to-top-wrap{ display:flex; justify-content:center; align-items:center; gap:clamp(24px, 10vw, 56px); padding:32px 0 20px; } /* bottom trimmed from 56px to 20px — now just the gap above the copyright line below, which carries the page's final bottom space instead */
.photography-page .back-to-top-wrap{ padding-top:0; } /* photography's last row of pills already ends with 80px of its own padding */
.back-to-top-wrap--index{ padding:48px 0 20px; } /* home page: extra breathing room below the Projects grid (which already ends with its own 120px) stays on the top side; bottom trimmed from 120px to 20px to match the pattern above — see .back-to-top-wrap--index + .footer-copy for where that 120px went */
/* copyright line under the page-end buttons, on every page. Reuses the
   old unused .footer-copy styling (colour/size) from the pre-page-end-btn
   footer, just centred and given the bottom padding the buttons used to
   carry themselves, so the page's total bottom space is unchanged. */
.footer-copy{ font-size:12px; color:#9a9a9a; text-align:center; padding:0 0 56px; }
.back-to-top-wrap--index + .footer-copy{ padding-bottom:120px; }
.page-end-btn{
  text-decoration:none;
  display:flex; align-items:center; justify-content:center;
  width:64px; height:64px;
  padding:0; border:0; border-radius:50%;
  background:transparent;
  color:#818789;
  cursor:pointer;
  transition:color .2s ease, transform .2s ease, background-color .2s ease;
  -webkit-tap-highlight-color:transparent;
}
.page-end-btn:hover{ color:var(--teal); background:rgba(115,221,207,.16); transform:translateY(-3px); } /* teal = same var(--teal) as the project pills. Note it's a light colour on the pale page background (~1.7:1 contrast) — fine for a hover state, but the resting grey is what carries legibility. */
.page-end-btn svg{ width:36px; height:36px; display:block; }
.page-end-btn:focus-visible{ outline:2px solid #818789; outline-offset:2px; }

/* ==========================================================================
   Photography page (photography.html) — a dedicated image showcase, not
   a dedicated image showcase with its own left-aligned (not centred)
   title/copy treatment and a gallery of plain images rather than titled
   project cards. The 3D point of sale, motion and packaging pages reuse
   this same template.
   ========================================================================== */
body.photography-page{ background:#f6f7f7; }
.photography-page .container{ max-width:1680px; } /* was the shared 1180px — much wider on desktop specifically (this only ever affects viewports wide enough to hit the cap; tablet/mobile's own padding-driven margin from the shared .container rule is untouched), per direct feedback that the images should run larger/closer to the edge */
.photo-page-hero{
  padding:calc(var(--nav-h) + 80px) 0 60px;
}
.photo-page-hero h1{ font-size:clamp(41px,5.75vw,60px); line-height:1.05; color:var(--yellow); margin-bottom:24px; } /* matches .feature-copy h2 on the home page exactly — same clamp, same line-height, and var(--yellow) rather than the darkened --yellow-dark used elsewhere, since this is copying the literal colour choice from home's title/button rather than re-applying the contrast fix */
/* "Where to next?" — the heading above the bottom pill links on the photography / 3D point of sale / motion pages. Same size and line-height as that page's own title, centred, in the site teal (var(--teal)) on every page rather than each page's own colour. No padding above (the gallery already ends with its own space); below: gives the pills' overhanging artwork room. */
.photo-next-title{ font-size:clamp(41px,5.75vw,60px); line-height:1.05; color:var(--teal); text-align:center; margin:0; padding:0 0 clamp(28px,4vw,52px); }
.photo-page-hero p{ font-size:17.8px; line-height:1.75; font-weight:500; font-variation-settings:'wght' 500; color:var(--body); max-width:44ch; } /* matches .feature-copy p on the home page exactly */

/* one .photo-grid per row of images — mix --2col (3:2 crops) and --3col
   (square crops) rows freely as real photos are added; duplicate whichever
   block matches the next batch of images. Placeholder tiles use the same
   gradient placeholder as .project-thumb elsewhere on the site — replace
   each .photo-tile's content with an <img> (width:100%;height:100%;
   object-fit:cover;display:block — matching .project-thumb img) once real
   photography is available. */
.photo-gallery{ display:flex; flex-direction:column; gap:12px; padding-bottom:60px; } /* gap was 24px — tighter row spacing per direct feedback. padding-bottom is the space between the last row and the "Where to next?" heading; was 120px, halved to 60px per feedback that the gap above the heading was too big */
@media (min-width:1025px){ .photo-gallery{ padding-bottom:110px; } } /* desktop only (tablet and phone layouts, 1024px and below, keep the 60px above): +50px so the heading sits further from the gallery, per feedback */
.photo-grid{ display:grid; gap:12px; } /* was 24px — tighter column spacing to match */
.photo-grid--2col{ grid-template-columns:repeat(2,1fr); }
.photo-grid--3col{ grid-template-columns:repeat(3,1fr); }
.photo-grid--1col{ grid-template-columns:1fr; } /* one full-width row (motion.html: 16:9 videos) */
.photo-grid--feature{ grid-template-columns:repeat(4,1fr); } /* 8 equal tiles, 4 across x 2 rows (sleepeezee.html) — reads as two 2x2 blocks side by side */
/* Small caption text under an image/video row (elaine-wong.html) — sits in
   the same .photo-gallery flex column as the .photo-grid rows, so the
   existing 12px gap already spaces it correctly above and below. */
.photo-caption{ font-size:17.8px; line-height:1.75; font-weight:500; font-variation-settings:'wght' 500; color:var(--body); max-width:70ch; margin:12px 0 40px; } /* matches .photo-page-hero p (the intro paragraph) exactly, per direct feedback */
/* 3D Point of sale page (pos.html) reuses the photography template. Title is blue instead of yellow, and the gallery tiles are flat-colour placeholders (no gradient) until the 3D visuals are ready — change --placeholder to restyle them all at once. */
.photography-page--pos{ --placeholder:#dde1e4; }
.photography-page--pos .photo-page-hero h1{ color:var(--blue); }
.photography-page--pos .photo-page-hero p{ max-width:56ch; } /* wider than the shared 44ch so the longer 3D Point of sale copy takes one line fewer (7 → 6 in testing; 54–58ch all give 6, 56 sits in the middle so a slightly different font width shouldn't tip it back). Photography page keeps 44ch. */
.photography-page--pos .photo-tile{ background:var(--placeholder); }
/* Motion Graphics page (motion.html) — same template again: cyan title (matches the Motion section's own colour on the home page) and the same flat-colour placeholder tiles until the videos are ready. */
.photography-page--motion{ --placeholder:#dde1e4; }
.photography-page--motion .photo-page-hero h1{ color:var(--cyan); }
.photography-page--motion .photo-tile{ background:var(--placeholder); }
/* Packaging page (packaging.html) — same template again: orange title (the packaging section's own colour on the home page) and the same flat-colour placeholder tiles until the images are ready. */
.photography-page--packaging{ --placeholder:#dde1e4; }
.photography-page--packaging .photo-page-hero h1{ color:var(--orange); }
.photography-page--packaging .photo-tile{ background:var(--placeholder); }
.photography-page--packaging .photo-page-hero p{ max-width:56ch; } /* wider than the shared 44ch so the longer packaging copy takes fewer lines (7 → 5 at desktop in testing); same idea as the 3D point of sale page */
/* Elaine Wong Personal trainer project page (elaine-wong.html) — same template again, but an individual project rather than a whole discipline: title colour is the project's own brand blue (#089dc4, not one of the site's shared vars) and the gallery mixes images/video with small caption text underneath some rows (see .photo-caption below). Flat-colour placeholder tiles until real assets are added. */
.photography-page--elaine{ --placeholder:#dde1e4; }
.photography-page--elaine .photo-page-hero h1{ color:#089dc4; }
.photography-page--elaine .photo-page-hero p{ max-width:62ch; }
.photography-page--elaine .photo-tile{ background:var(--placeholder); }
/* Sleepeezee project page (sleepeezee.html) — same template again: title
   uses the site's own --teal (matches this project's pill colour in the
   "Recent projects" rows on every other page), same flat-colour placeholder
   tiles until real assets are added. */
.photography-page--sleepeezee{ --placeholder:#dde1e4; }
.photography-page--sleepeezee .photo-page-hero h1{ color:#375542; }
.photography-page--sleepeezee .photo-page-hero p{ max-width:62ch; }
.photography-page--sleepeezee .photo-tile{ background:var(--placeholder); }
/* Silentnight Electricals project page (silentnight-electricals.html) —
   same template again, duplicated from sleepeezee.html's layout (same
   section structure: 1col image, 2col pair, 8-tile feature grid, merged
   6-tile 3col row, closing 1col video). Title colour is the project's own
   brand cyan (#01acd8, not one of the site's shared vars). Flat-colour
   placeholder tiles and placeholder copy throughout until real assets and
   copy are added. */
.photography-page--electricals{ --placeholder:#dde1e4; }
.photography-page--electricals .photo-page-hero h1{ color:#01acd8; }
.photography-page--electricals .photo-page-hero p{ max-width:62ch; }
.photography-page--electricals .photo-tile{ background:var(--placeholder); }
.photo-tile{
  position:relative;
  overflow:hidden;
  border-radius:4px;
  background:linear-gradient(135deg,#1fb7c9 0%, #2aa6c9 45%, #f7943c 100%);
}
.photo-grid--2col .photo-tile{ aspect-ratio:3/2; }
.photo-grid--3col .photo-tile{ aspect-ratio:1/1; }
.photo-grid--1col .photo-tile{ aspect-ratio:16/9; }
.photo-grid--1col .photo-tile--banner{ aspect-ratio:1920/730; } /* wide banner video (motion.html) — same full width as the 16:9 rows, shorter */
.photo-grid--1col .photo-tile--hero{ aspect-ratio:1920/776; } /* same crop ratio as the home page hero video (elaine-wong.html's opening video) */
.photo-grid--feature .photo-tile{ aspect-ratio:3/2; }
.photo-tile img,
.photo-tile video{ width:100%; height:100%; object-fit:cover; display:block; } /* video tiles (3D page) size the same way as images */

/* Two-image "carousel" tile — two full quality JPEGs, laid out
   side-by-side in one continuous strip (.carousel-track, double the
   tile's width) that slides left/right between them — like a looping
   GIF but without any video file or lossy GIF compression. Driven by
   the small [data-carousel] block in main.js rather than CSS
   @keyframes: a pure-CSS version (first a hard-cut, then a ping-pong
   keyframe animation, then a radio-button hack for the dots) kept
   running into real limits — a seam/flicker at the loop-point snap,
   then a scroll-jump when a hidden radio's label was clicked, then no
   way to tell CSS "this dot is the one actually showing right now" vs
   "just picked" without tracking real animation progress. JS tracks
   the current slide directly, so the dots always reflect the truth
   and transitions are always a smooth slide (just a transform change
   on .carousel-track, which this transition rule animates) rather
   than a jump. First used on silentnight-electricals.html's opening
   tile. */
.photo-tile--carousel .carousel-track{
  display:flex;
  width:200%;
  height:100%;
  transition:transform 1s cubic-bezier(.65,0,.35,1);
}
.photo-tile--carousel .carousel-track img{
  width:50%;
  height:100%;
  flex:none;
}
@media (prefers-reduced-motion: reduce){
  .photo-tile--carousel .carousel-track{ transition:none !important; } /* main.js also skips the autoplay timer for this preference — see the [data-carousel] block */
}

/* Carousel dots — stop the autoplay and jump to a slide. Real
   <button>s (not the old hidden-radio trick), so there's nothing
   invisible for the browser to scroll to. main.js toggles .is-active
   to match whichever slide is actually showing, at all times —
   including during autoplay, not only after a click. */
.carousel-dots{
  position:absolute;
  left:0; right:0;
  bottom:14px;
  display:flex;
  justify-content:center;
  gap:10px;
  z-index:1;
}
.carousel-dot{
  width:12px; height:12px; /* 50% bigger than the original 8px */
  padding:0;
  border-radius:50%;
  background:rgba(255,255,255,.5);
  border:1px solid rgba(0,0,0,.15);
  cursor:pointer;
  transition:background .2s ease, border-color .2s ease;
}
.carousel-dot:hover{ background:rgba(255,255,255,.8); }
.carousel-dot.is-active{ background:var(--teal); border-color:var(--teal-dark); }

.photo-links-wrap{ container-type:inline-size; } /* each .photo-links row sits in this wrapper so the scaling rule further down can size the pills off the row's own available width (100cqw) rather than the viewport — same breakpoints as the natural wrap points, and unaffected by scrollbar width */
.photo-links{
  --u:1px; /* one "design pixel" for everything inside the pills — every pixel value in the .photo-pill rules below is written as N * var(--u), so redefining --u in the container query further down scales the whole pill (text, padding, image offsets, circle images) proportionally */
  display:flex;
  justify-content:center;
  align-items:flex-start;
  flex-wrap:wrap;
  gap:36px 40px; /* re-measured directly off the concept image (pixel-scanned it) — column-gap was 28px, concept's is closer to 42px; row-gap recalculated for the smaller art bleed below (10px overflow each side now, vs 22px before, needs much less clearance than the old 56px) */
  padding:20px 0 80px;
}
.photo-links--projects{ gap:52px 40px; } /* row-gap only matters when the pills stack (single column). Was 100px, sized for the old gradient circles that bled ~44px above/below the pill; the pre-composed images bleed ~20.7px each side (142.4px tall on a 101px pill), so neighbouring pills need at least ~41px between them to keep their art from touching. Tightened 100 → 64 → 52px per direct feedback, leaving ~10px of clear space between one pill's art and the next. Don't go much below ~44px. */
/* pill buttons — reuses the same product cutouts already used on the
   home page's own circles for each section (just smaller exports of
   them), bleeding out past the pill's own edge the same way those
   circles bleed past their own section's boundary. overflow isn't
   clipped anywhere here (no overflow:hidden on .photo-pill) specifically
   so the art can extend past the rounded shape.
   Sizing below was recalculated by pixel-measuring the concept image
   directly (scanned it for pill/text/art boundaries) after the first
   attempt ran notably bigger than intended — pill was 410×132 (concept:
   368×101) and art bled to 1.33× the pill's own height (concept: 1.2×,
   a much subtler overflow). */
.photo-pill{
  position:relative;
  display:flex;
  align-items:center;
  width:100%;
  max-width:calc(368 * var(--u));
  min-height:calc(101 * var(--u));
  padding:calc(16 * var(--u)) calc(28 * var(--u));
  border-radius:999px;
  color:#fff;
  text-decoration:none;
  transition:transform .2s ease-in-out; /* ease in and out, 0.2s, per direct request */
}
.photo-pill:hover{ transform:scale(1.05); } /* 5% zoom, per direct request */ /* one motion for the whole pill now it's a solid shape, rather than the text-only zoom used before */
.photo-pill--orange{ background:var(--orange); }
.photo-pill--blue{ background:var(--blue); }
.photo-pill--cyan{ background:var(--cyan); }
.photo-pill--yellow{ background:var(--yellow); } /* Photography pill on pos.html; label stays white like the rest (low contrast on yellow, same as the yellow View more button) */
.photo-pill-label{
  position:relative;
  z-index:1;
  left:calc(10 * var(--u)); /* shifted right 10px per direct request */
  max-width:calc(115 * var(--u)); /* was 145px — narrowed slightly so "3D Point of sale" wraps after "Point" (matching the concept exactly) instead of after "of"; "Packaging" (one unbreakable word) still clears this fine since it's shorter than either wrapped line here */
  font-size:calc(27.6 * var(--u)); /* was 23px, +20% per direct request */
  font-weight:800;
  font-variation-settings:'wght' 800;
  line-height:1.15;
}
.photo-pill-art{
  position:absolute;
  right:calc(-5 * var(--u));
  top:50%;
  transform:translateY(-50%);
  height:calc(121 * var(--u)); /* was 176px — matches the concept's measured ~1.2× pill-height bleed (121px art on a 101px pill) instead of the earlier, much more dramatic 1.33-1.57× overflow */
  display:flex;
  align-items:center;
  pointer-events:none;
}
.photo-pill-art--single{ height:calc(82 * var(--u)); right:calc(-14 * var(--u)); } /* laptop's own wide aspect ratio still needs a shorter height than the two-image pills to clear the wrapped label, same reasoning as before, just rescaled to the new smaller pill */
.photo-pill-img{ height:100%; width:auto; object-fit:contain; } /* drop-shadow removed per direct request */
.photo-pill-img--back{ margin-right:calc(-24 * var(--u)); z-index:0; }
.photo-pill-img--front{ z-index:1; }
.photo-pill--orange .photo-pill-img--back{ height:75%; position:relative; top:calc(-15 * var(--u)); left:calc(-30 * var(--u)); } /* mat-vac box — 25% smaller, nudged up 15px and left 30px, per direct request */
.photo-pill--orange .photo-pill-img--front{ height:90%; position:relative; left:calc(-20 * var(--u)); top:calc(5 * var(--u)); } /* sleepeezee bag — 10% smaller, nudged left 20px and down 5px, per direct request */
.photo-pill--blue .photo-pill-img--front{ position:relative; left:calc(-20 * var(--u)); } /* restore shipper — nudged left 20px, per direct request */
.photo-pill--blue .photo-pill-img--back{ height:80%; position:relative; left:calc(-20 * var(--u)); top:calc(-20 * var(--u)); } /* airmax shipper — 20% smaller, nudged left 20px and up 20px, per direct request */
.photo-pill--cyan .photo-pill-img--front{ height:143.75%; position:relative; left:calc(-20 * var(--u)); top:calc(-10 * var(--u)); } /* laptop — was 125% bigger/left:-40px; a further 15% bigger (125%×1.15) and shifted right 20px (net left:-20px) and up 10px, per direct request */
/* second row — "Project" pills, same system as .photo-pill above, all
   teal (var(--teal)), with a two-line stacked label (bold "Project" +
   smaller project name) and two circle placeholders instead of product
   cutouts, per direct request. */
.photo-pill--teal{ background:var(--teal); }
.photo-pill--stacked{ padding:calc(14 * var(--u)) calc(28 * var(--u)); } /* two lines of text needs a touch less vertical padding than the single/double-line labels above to keep the same overall pill height */
.photo-pill-label--stacked{ display:flex; flex-direction:column; gap:calc(2 * var(--u)); max-width:calc(150 * var(--u)); }
.photo-pill-label--stacked strong{ font-size:calc(27.6 * var(--u)); font-weight:800; font-variation-settings:'wght' 800; line-height:1.15; }
.photo-pill-label--stacked small{ max-width:calc(140 * var(--u)); font-size:calc(14 * var(--u)); font-weight:600; font-variation-settings:'wght' 600; line-height:1.25; opacity:.92; } /* narrower than the 150px "Project" line above it — was wrapping as "Silentnight electricals / Range", narrowed specifically so it breaks after "Silentnight" instead, matching the concept */
.photo-pill-art--circles{ position:absolute; inset:0; height:auto; transform:none; } /* was its own height/right-offset box; now spans the pill's own edges exactly (pill has no border, so its containing-block padding-box coincides with the pill's visible border-box) so the two circles below can be positioned directly in terms of the pill itself, matching how the new sizing was specified */
.photo-pill-label--narrow small{ max-width:calc(125 * var(--u)); } /* first project pill only: 140px let "Silentnight electricals" fit on one line; 125px is wide enough for "electricals Range" but not "Silentnight electricals", so it wraps as "Silentnight / electricals Range" per the target mock-up. Scoped so the other pills' wrapping is unchanged. */
.photo-pill-circles-img{ position:absolute; top:calc(50% - calc(72 * var(--u))); right:calc(19 * var(--u)); height:calc(144 * var(--u)); width:auto; display:block; } /* pre-composed two-circle image (electrical_circles.png, 247×195 with ~2px transparent margin) replacing the two gradient placeholder circles on the first project pill. Height 144px = the old circle pair's 140.2px cluster height scaled up by the image's own 2px margins (190px of content → 140px), so it occupies the same footprint; right:19px puts the content's right edge where the front circle's was (22px inside the pill, ~3px of that is the image's own margin); vertically centred on the pill's midpoint the same way the circles were. */
.photo-pill-circles-img--elaine{ top:calc(50% - calc(71.2 * var(--u))); right:calc(20.6 * var(--u)); height:calc(142.4 * var(--u)); } /* elaine_circles.png is 258×200 (content 256×198); scaled by the same ~0.712 as sleepeezee below so both land at ~142px tall — the same footprint as the electricals image (which is 141px of visible content) — with right offset adjusted for each file's own transparent margin so the content's right edge sits 22px inside the pill like the others. */
.photo-pill-circles-img--sleepeezee{ top:calc(50% - calc(71.2 * var(--u))); right:calc(21.3 * var(--u)); height:calc(142.4 * var(--u)); } /* sleepeezee_circles.png is 252×200 (content 250×198), same scale/centring as above */
.photo-pill-photo{ position:absolute; top:calc(-10.8 * var(--u)); right:calc(25 * var(--u)); width:calc(126.1 * var(--u)); height:auto; display:block; } /* Photography pill's rectangular photo (deer_pic_small.png, 201×150, no rounding). Placed by measuring the mock-up screenshot: 543px-wide pill scaled to our 368px, photo ≈126×94px, top edge ~11px above the pill, ~25px in from its right edge. */
.photo-pill-circle{
  position:absolute;
  border-radius:50%;
  background:linear-gradient(135deg,#1fb7c9 0%, #2aa6c9 45%, #f7943c 100%); /* same placeholder gradient used for the gallery tiles — swap for a real photo (as a background-image or nested <img> with object-fit:cover) once available */
  overflow:hidden;
}
.photo-pill-circle--back{ width:88.7px; height:88.7px; top:calc(50% - 70.1px); right:97px; } /* whole circle pair is now vertically centred on the pill (cluster spans 140.2px: back circle's top edge is 70.1px above the pill's midpoint, front circle's bottom edge 70.1px below it) using top:calc(50% - Npx) so it stays centred whatever the pill's own height ends up as. Previously sat ~50px too high because .photo-pill-art--circles inherited translateY(-50%) from .photo-pill-art (see transform:none above). Horizontal positions match the target mock-up: back circle right:97px, front circle's right edge 22px inside the pill (was sticking out 16px past it), giving the pair the target's tighter overlap. */
.photo-pill-circle--front{ width:118.2px; height:118.2px; top:calc(50% - 48.1px); right:22px; }
/* Between the row wrapping to 2 columns (available width < 3 pills + 2 gaps = 3×368 + 2×40 = 1184px) and wrapping to a single column (< 2 pills + 1 gap = 776px), keep all three pills on one line and scale the whole row down proportionally instead: --u becomes (available width ÷ 1184), so pills, gaps, text and art all shrink by the same ratio and always exactly fill the row. Above 1184px and below 776px nothing changes from before (1 --u = 1px), so the single-column breakpoint stays exactly where it was. */
@container (min-width:776px) and (max-width:1183.98px){
  .photo-links-wrap .photo-links{ --u:calc(100cqw / 1184); flex-wrap:nowrap; column-gap:calc(40 * var(--u)); }
  .photo-links-wrap .photo-pill{ flex:0 1 auto; width:calc(368 * var(--u)); }
}
/* Single-column layout only (row narrower than 2 pills + 1 gap = 776px): pull the first project pill closer to the Motion Graphics pill above it. The two rows are separate blocks, so the space between them is row 1's padding-bottom (80px) + row 2's padding-top (20px) = 100px; this drops row 1's to 32px, giving 52px — matching the pill-to-pill spacing inside the projects row. Multi-column layouts keep the 100px. */
@container (max-width:775.98px){
  .photo-links-wrap .photo-links:not(.photo-links--projects){ padding-bottom:32px; }
}
@media (max-width:1024px){
  /* dedicated breakpoint just for this, wider than the site's usual
     900px tablet/mobile line — that line is really about when the hero
     pin effect and nav layout switch, not about when this grid gets
     cramped. A landscape tablet (iPad landscape is 1024px) sails past
     900px and would otherwise still get the full desktop layout. Per
     direct feedback, tablet now matches mobile's own pattern exactly:
     3:2 crops go to a single column (two side-by-side reads too
     cramped once each one this narrow), while 1:1 crops stay at two
     across — replaces the old mobile-only (≤560px) single-column rule
     for --2col, since this ≤1024px breakpoint now covers that range too. */
  .photo-grid--2col{ grid-template-columns:1fr; }
  /* Once --2col collapses to a single column, a row that packs its two
     per-image captions in as extra grid children (business card + stickers)
     would otherwise show image, image, caption, caption in DOM order.
     Reorder so each caption follows its own image when stacked — desktop
     is untouched since its 2-column row-major placement doesn't consult
     source order here anyway. */
  .photo-grid--2col > *:nth-child(1){ order:1; }
  .photo-grid--2col > *:nth-child(2){ order:3; }
  .photo-grid--2col > *:nth-child(3){ order:2; }
  .photo-grid--2col > *:nth-child(4){ order:4; }
  .photo-grid--3col{ grid-template-columns:repeat(2,1fr); }
  .photo-grid--feature{ grid-template-columns:repeat(2,1fr); } /* 8 tiles at 2-across instead of 4 — same idea as --3col below */
  /* an isolated run of 3 squares boxed in by 3:2 rows on both sides has
     nowhere to "flow" its odd one out into at 2-across — hides that
     tile at this breakpoint specifically (still shows normally at
     desktop's 3-across layout, where all 3 fit on one line with no
     gap), per direct feedback. Only used on rows that stay a fixed
     .photo-grid--3col of exactly 3 — the big run of consecutive 3col
     rows above/below is merged into one continuous grid instead, so
     its squares flow into each other's gaps naturally rather than
     needing anything hidden. */
  .photo-tile--hide-tablet{ display:none; }
}

/* ==========================================================================
   Video player (motion.html) — poster + big play button, then a control bar
   with play/pause, time, a scrub timeline and mute (no full screen, by design). Behaviour
   is in main.js ([data-vplayer]); state classes: .is-started (has been
   played), .is-playing, .is-idle (playing + pointer still, hides the bar),
   .is-muted.
   ========================================================================== */
.vplayer{ position:relative; width:100%; height:100%; background:#0b1d2a; overflow:hidden; }
.vplayer video{ cursor:pointer; }

/* big play button — white disc; play symbol is dark grey at rest; on hover / keyboard focus it turns the site teal and the disc grows slightly (scale 1.08). */
.vplayer-big{
  position:absolute; left:50%; top:50%;
  width:104px; height:104px; margin:-52px 0 0 -52px;
  padding:0; border:0; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,.94);
  color:#4a5155;
  box-shadow:0 10px 32px rgba(0,0,0,.28);
  cursor:pointer;
  transition:color .2s ease, opacity .25s ease, transform .25s var(--ease);
  -webkit-tap-highlight-color:transparent;
}
.vplayer-big svg{ width:40%; height:40%; margin-left:6%; fill:currentColor; display:block; } /* margin nudges the triangle right so it looks optically centred in the disc */
.vplayer-big:hover,
.vplayer-big:focus-visible{ color:var(--teal); transform:scale(1.08); }
.vplayer-big:focus-visible{ outline:3px solid #fff; outline-offset:3px; }
.vplayer.is-playing .vplayer-big{ opacity:0; pointer-events:none; transform:scale(.9); }

/* control bar — appears once the video has been started; hides itself while playing and the pointer is still */
.vplayer-bar{
  position:absolute; left:0; right:0; bottom:0;
  display:flex; align-items:center; gap:14px;
  padding:48px 22px 14px;
  background:linear-gradient(to top, rgba(6,20,30,.78), rgba(6,20,30,0));
  color:#fff;
  opacity:0; transform:translateY(8px); pointer-events:none;
  transition:opacity .25s ease, transform .25s ease;
}
.vplayer.is-started .vplayer-bar{ opacity:1; transform:none; pointer-events:auto; }
.vplayer.is-started.is-playing.is-idle .vplayer-bar{ opacity:0; transform:translateY(8px); pointer-events:none; }
.vplayer.is-started.is-playing.is-idle{ cursor:none; }
.vplayer-btn{
  flex:none; width:38px; height:38px; padding:0; border:0; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:transparent; color:#fff; cursor:pointer;
  transition:color .2s ease, background-color .2s ease;
  -webkit-tap-highlight-color:transparent;
}
.vplayer-btn svg{ width:24px; height:24px; fill:currentColor; display:block; }
.vplayer-btn:hover,
.vplayer-btn:focus-visible{ color:var(--teal); background:rgba(255,255,255,.12); }
.vplayer-btn:focus-visible{ outline:2px solid var(--teal); outline-offset:1px; }
.vplayer-toggle .icon-pause{ display:none; }
.vplayer.is-playing .vplayer-toggle .icon-play{ display:none; }
.vplayer.is-playing .vplayer-toggle .icon-pause{ display:block; }
.vplayer-mute .icon-muted{ display:none; }
.vplayer.is-muted .vplayer-mute .icon-vol{ display:none; }
.vplayer.is-muted .vplayer-mute .icon-muted{ display:block; }
.vplayer-time{ flex:none; font-size:13px; font-weight:600; font-variant-numeric:tabular-nums; letter-spacing:.02em; white-space:nowrap; }

/* timeline — a styled <input type=range> so it's keyboard- and touch-scrubbable for free; --p (set in main.js) is the played percentage */
.vplayer-seek{
  -webkit-appearance:none; appearance:none;
  flex:1; min-width:0; height:22px; margin:0; padding:0;
  background:transparent; cursor:pointer;
  --p:0%;
}
.vplayer-seek:focus{ outline:none; }
.vplayer-seek::-webkit-slider-runnable-track{ height:4px; border-radius:2px; background:linear-gradient(to right, var(--teal) var(--p), rgba(255,255,255,.35) var(--p)); transition:height .15s ease; }
.vplayer-seek::-moz-range-track{ height:4px; border-radius:2px; background:rgba(255,255,255,.35); }
.vplayer-seek::-moz-range-progress{ height:4px; border-radius:2px; background:var(--teal); }
.vplayer-seek::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; width:14px; height:14px; margin-top:-5px; border:0; border-radius:50%; background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.4); transition:transform .15s ease; }
.vplayer-seek::-moz-range-thumb{ width:14px; height:14px; border:0; border-radius:50%; background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.4); transition:transform .15s ease; }
.vplayer-seek:hover::-webkit-slider-runnable-track,
.vplayer-seek:focus-visible::-webkit-slider-runnable-track{ height:6px; }
.vplayer-seek:hover::-webkit-slider-thumb,
.vplayer-seek:focus-visible::-webkit-slider-thumb{ transform:scale(1.25); background:var(--teal); }
.vplayer-seek:hover::-moz-range-thumb,
.vplayer-seek:focus-visible::-moz-range-thumb{ transform:scale(1.25); background:var(--teal); }
/* players in the 3-across square rows (~400px wide) get a smaller big button; on phones (~165px tiles, 2 across) smaller again with the time readout dropped so the timeline still has room */
.photo-grid--3col .vplayer-big{ width:80px; height:80px; margin:-40px 0 0 -40px; }
@media (max-width:560px){
  .vplayer-big{ width:76px; height:76px; margin:-38px 0 0 -38px; }
  .vplayer-bar{ gap:6px; padding:36px 10px 8px; }
  .vplayer-btn{ width:34px; height:34px; }
  .vplayer-time{ font-size:12px; }
  .photo-grid--3col .vplayer-big{ width:56px; height:56px; margin:-28px 0 0 -28px; }
  .photo-grid--3col .vplayer-time{ display:none; }
}

/* Discourage image saving (belt-and-braces alongside the JS in main.js):
   stops the native drag-to-save gesture and iOS's long-press "Save Image"
   callout menu on every image site-wide. */
img{ -webkit-user-drag:none; user-drag:none; -webkit-touch-callout:none; }

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 900px){
  .container{ padding:0 24px; }
  .nav-inner{ padding:0 20px; }
  .nav-logo svg{ height:13px; } /* was font-size:11px, back when this was text — scaled proportionally (11/16) for the SVG version so it shrinks by the same ratio on mobile */
  .nav-contact svg{ height:13px; } /* matches the logo's mobile size above — same ratio, kept consistent between the two */
  .feature .container{ grid-template-columns:1fr; }
  .feature-art{ order:-1; min-height:360px; }
  .feature--reverse .feature-art{ order:-1; }
  /* avatar's top is a fixed value on mobile too (desktop uses a vw-based
     formula that was never overridden here — that's what let it and the
     heading below collide) — 24px + 110px height + 26px gap is what
     the text's own padding-top below is calculated to clear exactly */
  .avatar{ width:164px; height:164px; top:24px; } /* bigger again per direct request — up from 143px (itself 30% bigger than the original 110px) — see the .about padding-top below, recalculated to match */
  .projects-grid{ grid-template-columns:1fr; }
  /* stacked cards read very wide/heavy at full container width, so
     each card is sized down and centred instead — 60% at 900px (this
     breakpoint's own top edge), scaling progressively down to 90% at
     375px (a standard small-mobile reference width), per direct
     feedback. */
  .projects .project-card{
    width:min(calc(4.8vw + 468px), 90%); /* max-width:90% wasn't actually constraining this inside the 1fr grid track — the explicit calc() width was overflowing both the card and the grid itself rather than being capped (confirmed: rendered width exceeded even the grid's own measured width). Using min() to combine the formula and the percentage cap into one expression for `width` directly sidesteps that, since there's no separate max-width for the grid sizing algorithm to disagree with. */
    margin:0 auto;
  }
  /* cards are narrower now (the width rule above), so the base 16px/13px
     title/meta sizes that were fine at full container width started
     reading small/hard-to-read on top of it, per direct feedback —
     bumped 40%/30% respectively, flat across this whole breakpoint
     rather than scaling further, matching how other text in this same
     block (e.g. .next-shape .feature-copy h2 just above) is held at a
     single fixed size across the stacked range rather than kept fluid. */
  .projects .project-title{ font-size:22.4px; }
  .projects .project-meta{ font-size:16.9px; }
    /* the sticky pin/reveal effect is skipped below 900px — mobile browser
     chrome (address bars showing/hiding) makes vh-based pin heights
     unreliable, so it just becomes a normal stacked layout instead */
  .hero{ height:auto; }
  .hero-sticky{ position:relative; top:auto; }
  .about{ margin-top:0; min-height:auto; padding:202px 0 140px; } /* 202px top = 24 + 164 + 14, clearing the now-bigger avatar above with a tightened gap (was 26px) to sit closer to the heading, per direct request. Bottom bumped from 60px to 140px — with Packaging's mobile overlap now at 0, this gap is the only thing separating the bio content from Packaging, so it needed to be a real gap, not just enough to prevent covering. */
  .about-decor{ display:none; }
  /* the heading/lead/body clamps are floored at their 1750px value
     (see those rules) with no mobile override — meaning below 900px
     they were rendering at a fixed 79.8px/28px/18.4px regardless of
     how narrow the screen actually got, which is what was breaking.
     Reduced to sensible fixed mobile sizes here instead. */
  .about-heading{ font-size:68px; }
  .about-lead{ font-size:24px; }
  .about-body{ font-size:16px; }
  /* Packaging's own heading no longer overridden here — it now scales
     smoothly across the whole 560-962px range in one formula (see the
     962px block), instead of snapping to a fixed size at each of this
     breakpoint and the 560px one below, which read as an abrupt jump */
}
@media (max-width: 560px){
  .hero-sound{ width:44px; height:44px; bottom:16px; right:16px; }
  /* one step smaller again for the narrowest phones */
  .about-heading{ font-size:56px; }
  .about-lead{ font-size:22px; }
  .next-shape .feature-copy h2{ font-size:64px; } /* was fixed at 76px the whole way down — stepping down a bit here too, at the same point bio's own heading steps down */
}
