/* ==========================================================================
   Chioma — Main Stylesheet
   Palette: persistent true-black grounds (no white/light sections),
   hot pink as the one accent ("Editorial Spotlight Pink"), warm
   cinematic photo grade applied site-wide. Cormorant Garamond for
   display, Manrope for body/UI.
   ========================================================================== */

/* Cross-document continuity: page navigations cross-fade instead of
   hard-cutting, so moving through the site feels like turning pages in
   one magazine. No-ops silently in browsers without support. */
@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

/* Oct 2026 redesign — persistent dark world (was white/blush, dark
   accents only). See DESIGN.md's Colors section and Palette History
   for the full rationale. Token NAMES and ROLES are unchanged (bg =
   page ground, ink = primary text, accent = primary interactive
   color, on-accent = text atop a solid accent fill) — only the VALUES
   flipped, which is why most component CSS below needed zero edits.
   The two things that DON'T follow that flip automatically:
   (1) anywhere that explicitly borrowed the old light `--color-bg-
   pearl` for "light text on a dark surface" (the nav links, tag
   badges on photos, the lightbox) now uses `--color-ink` directly,
   since ink itself is the light tone now; (2) a few places that need
   to stay a deliberately BRIGHT surface regardless of the dark page
   (the polaroid card's paper mat, the inverse button) use the new
   `--color-ivory-surface` token instead of bg-pearl, since bg-pearl's
   role is now "secondary dark surface," not "bright surface." */
/* Oct 2026 "Editorial Spotlight Pink" repaint — see DESIGN.md's Palette
   History for the full before/after. Sourced from a black/pink palette
   reference (Filmora's black-pink palette guide), not generated: the
   four anchor hexes are #000000, #222222, #FF4F8B, #FFD6E8. This
   palette gives only two distinct near-black tones (vs. the prior
   system's five warm-noir tiers), so --color-bg/--color-accent-deep/
   --color-on-accent intentionally collapse to the same literal
   #000000 — a starker, more graphic "poster black" read than the
   previous warm-noir system had, not an oversight. --color-bg-pearl
   is the one interpolated (not sourced) value, a midpoint needed to
   keep a 3-tier dark-surface system with only 2 given blacks. */
:root {
  --color-bg: #000000;           /* primary dark ground */
  --color-bg-pearl: #111111;     /* secondary dark surface — about-teaser, focus-section, brand-gallery, footer; interpolated midpoint, not a sourced palette value */
  --color-bg-blush: #222222;     /* lightest dark tier — hero, page-header, brand-strip */

  --color-accent: #FF4F8B;       /* hot pink — primary interactive/CTA/link color, sourced directly from the palette */
  --color-accent-soft: #FF85B0;  /* paler rose — decorative/structural only (gradients, hairline tints); derived midpoint between --color-accent and --color-ink */
  --color-accent-deep: #000000;  /* truest black — deepest shadow/gradient stop; same literal value as --color-bg in this palette (see note above) */

  --color-ink: #FFD6E8;          /* primary text — pale pink, sourced directly from the palette */
  --color-ink-soft: #C0718C;     /* secondary/muted text — desaturated rose-mauve, blended from the accent hue toward neutral gray */
  --color-on-accent: #000000;    /* dark text atop solid accent surfaces (buttons) — true black for maximum contrast against the hot-pink fill */

  --color-ivory-surface: #FFD6E8; /* the one deliberately BRIGHT surface in an otherwise all-dark system — polaroid card mat, inverse button fill; same value as --color-ink, named separately because the role (a bright card-like object) differs from "text color" */

  --font-heading: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  --font-body: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;

  --rule-soft: 1px solid rgba(255, 214, 232, 0.14);  /* light hairline, was dark-on-light */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);

  --section-pad: 7rem;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* `overflow-x: hidden` on both html and body: the closed mobile
   `.nav-collapse` drawer sits just off-screen via `transform:
   translateX(...)`, not `display: none`, so without this a visitor
   could scroll the page horizontally and see the drawer bleed into
   view even without opening it (305px of overflow, confirmed via
   `document.documentElement.scrollWidth` vs `clientWidth` at mobile
   widths). Safe with the full-bleed `position: fixed` header — fixed
   elements size against the viewport regardless of this. */
html {
  overflow-x: hidden;
}

body {
  font-family: var(--font-body);
  color: var(--color-ink);
  background-color: var(--color-bg);
  line-height: 1.6;
  overflow-x: hidden;
}

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  line-height: 1.1;
  font-weight: 500;
}

p { color: var(--color-ink); }

a {
  color: inherit;
  text-decoration: none;
}

/* Unified warm cinematic grade — Oct 2026 redesign. Every real photo
   on the site now (favicon/icons are the only non-photo-tag graphics,
   so this is safe as a blanket rule) passes through the same filter
   chain, so a mix of full-color phone shots and pre-existing
   black-and-white shots reads as one deliberately graded body of work
   instead of an assortment of snapshots. saturate() pulls color shots
   toward the same muted family without flattening them to grayscale;
   sepia() nudges everything warm, toward the accent's pink/rose hue
   family; contrast()/brightness() give it a touch more "film," less
   "camera roll." Applied once here rather than per-component so a
   newly added photo anywhere inherits it automatically. */
img {
  max-width: 100%;
  display: block;
  filter: saturate(0.78) sepia(0.16) contrast(1.08) brightness(0.96);
}

.container {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

/* ==========================================================================
   Navigation
   ========================================================================== */

/* Fixed (desktop) so the header floats over the hero/page-header instead
   of pushing it down — hero content starts at the true top of the
   viewport, with the glass bar overlaid on top of it. Translucent so the
   backdrop-filter blur actually has that content passing underneath it
   to blur — a glass surface over a static background would just look
   tinted, not glassy. Reverted to the original in-flow `sticky` at
   ≤900px (see responsive.css) — the overlay-hero treatment is a desktop
   thing; tablet/mobile keep the header pushing content down normally. */
header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 500;
}

/* Base = fully-grown, resting state (top of page, or scroll position 0).
   initStickyNav() in main.js adds/removes .is-compact / .is-shadow /
   .is-hidden as the page scrolls: .is-compact (shrunk) any time
   scrollY > 0, whether scrolling up or down — so the header stays
   shrunk-but-visible on the way back up rather than growing back first —
   and neither (fully grown again) once scrollY returns to 0. .is-shadow
   lifts it off the page ("hovering" rather than flush/flat) any time
   it's not at that resting top position. .is-hidden (desktop only —
   neutralized at ≤900px in responsive.css) slides the header off-screen
   while actively scrolling down, and is removed the instant the user
   scrolls up, so it doesn't permanently occupy space over page content
   but is always one upward scroll away.

   Deliberate exception to the site's "animate transform/opacity, not
   layout properties" rule (see DESIGN.md Don'ts): this transitions
   `padding`, a genuine layout property, in addition to `transform`
   (used for the hide/show slide, per the rule). A transform: scale()
   can't substitute for the padding change — the requested effect is the
   header's real box height changing (so it actually grows/shrinks)
   while the logo/link text stays crisp at native size, not a
   visually-scaled illusion. Scoped to one small, cheap element (a
   single flex row, not a page-wide grid) transitioning at most once per
   scroll-direction change, not a continuous per-frame animation — the
   layout-thrash risk this rule guards against doesn't really apply at
   this scale. Confirmed smooth in-browser. */
.site-nav {
  position: relative;
  padding: 2.5rem 0;
  /* saturate(160%) + 0.78 alpha were tuned for the old lavender-tinted
     background: boosting saturation of a faint lavender bleed-through
     read as rich glass. Against the new blush-pink background it instead
     boosted the pink into a muddy brown/taupe cast (confirmed live,
     Oct 2026 critique) — not a hex-value problem, a blend problem.
     Higher opacity + no saturation boost reads as genuine soft black. */
  background: rgba(0, 0, 0, 0.9);
  backdrop-filter: blur(18px) saturate(100%);
  -webkit-backdrop-filter: blur(18px) saturate(100%);
  border-bottom: 1px solid rgba(255, 214, 232, 0.14);
  transition: padding 0.45s var(--ease-out-expo), box-shadow 0.45s var(--ease-out-expo), transform 0.45s var(--ease-out-expo);
}

.site-nav.is-shadow {
  box-shadow: 0 20px 40px -18px rgba(0, 0, 0, 0.45);
}

.site-nav.is-hidden {
  transform: translateY(-100%);
}

.site-nav.is-compact {
  padding: 1.75rem 0;
}

/* Content row, capped to the same 1200px column as the rest of the page
   (via the shared .container class) — kept separate from .site-nav so
   the glass background/blur/scroll-shrink padding above can span the
   full viewport width edge-to-edge instead of being capped with it. */
.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Ambient "water droplet" rings — decorative only (aria-hidden),
   painted first so the logo/links/toggle (normal DOM flow, no z-index
   needed) stack above them automatically. Kept to faint pearl-tinted
   rings, never blue, per the Soft-Noir palette rule (see DESIGN.md). */
.glass-ripples {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.glass-ripples span {
  position: absolute;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 1px solid rgba(255, 214, 232, 0.35);
  opacity: 0;
  animation: ripple-pulse 4s ease-out infinite;
}

/* top values hug the bar's top/bottom edges (~15%/85%) rather than its
   vertical center, where nav link text sits — at the old ~25-70% band,
   a ring's horizontal position collided with different link labels at
   different viewport widths (confirmed: one ring sat on "BRANDS" at a
   wide viewport, another on "ABOUT" at a narrower one). Edge-hugging
   avoids the text band entirely regardless of viewport or which links
   happen to fall under which ring. */
.glass-ripples span:nth-child(1) { left: 8%;  top: 15%; animation-delay: 0s;    width: 12px; height: 12px; }
.glass-ripples span:nth-child(2) { left: 26%; top: 85%; animation-delay: 1.1s;  width: 18px; height: 18px; }
.glass-ripples span:nth-child(3) { left: 42%; top: 15%; animation-delay: 2.2s;  width: 10px; height: 10px; }
.glass-ripples span:nth-child(4) { left: 58%; top: 85%; animation-delay: 0.6s;  width: 16px; height: 16px; }
.glass-ripples span:nth-child(5) { left: 80%; top: 15%; animation-delay: 1.8s;  width: 13px; height: 13px; }
.glass-ripples span:nth-child(6) { left: 92%; top: 85%; animation-delay: 2.9s;  width: 15px; height: 15px; }

@keyframes ripple-pulse {
  0% { transform: scale(0.3); opacity: 0; }
  12% { opacity: 0.55; }
  100% { transform: scale(7); opacity: 0; }
}

.site-nav .logo {
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 600;
  font-size: 1.7rem;
  color: var(--color-accent-soft);
  letter-spacing: 0.01em;
}

.site-nav ul {
  display: flex;
  gap: 2.25rem;
  list-style: none;
}

/* Light text — the header glass is now the same dark noir as the mobile
   drawer (rgba(0, 0, 0,X)), nearly identical to --color-ink, which
   would be invisible-on-invisible if links kept their original ink
   color from the site's light-background default. */
.site-nav ul li a {
  position: relative;
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-ink);
  padding-bottom: 0.35rem;
}

.site-nav ul li a::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--color-accent-soft);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.4s var(--ease-out-expo);
}

.site-nav ul li a:hover::after,
.site-nav ul li a:focus-visible::after {
  transform: scaleX(1);
}

.nav-collapse {
  display: contents;
}

/* Backdrop scrim behind the mobile off-canvas drawer (see .nav-collapse's
   ≤600px override below). Hidden/inert on desktop, where .nav-collapse
   is just `display: contents` and never opens. */
.nav-scrim {
  /* top/left + vw/vh (not `inset: 0`) deliberately — .nav-scrim's
     ancestor .site-nav has `backdrop-filter`, which makes .site-nav
     the containing block for fixed descendants' *percentage* offsets
     (top/right/bottom/left as %, which `inset: 0` shorthand computes
     as). That collapsed this scrim to .site-nav's own ~44px header-bar
     height instead of the full page, since `bottom: 0` resolved against
     .site-nav's bottom edge, not the viewport's. vw/vh units sidestep
     this entirely — they're always viewport-relative regardless of
     containing block, which is also why .nav-collapse's `height: 100vh`
     (below) was unaffected by the same trap. */
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(0, 0, 0, 0.35);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s var(--ease-out-expo);
  z-index: 55;
}

.nav-scrim.open {
  opacity: 1;
  visibility: visible;
}

.nav-toggle {
  position: relative;
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 24px;
  padding: 4px;
  background: none;
  border: none;
  cursor: pointer;
}

.nav-toggle-bar {
  display: block;
  width: 100%;
  height: 2px;
  background: var(--color-accent-soft);
  transition: transform 0.35s var(--ease-out-expo), opacity 0.35s var(--ease-out-expo);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bar:first-child {
  transform: translateY(3.5px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bar:last-child {
  transform: translateY(-3.5px) rotate(-45deg);
}

.nav-toggle:focus-visible,
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

/* ==========================================================================
   Buttons / links
   ========================================================================== */

.btn-primary {
  position: relative;
  display: inline-block;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-on-accent);
  background: var(--color-accent);
  padding: 1.05rem 2.1rem;
  border-radius: 2px;
  box-shadow: 0 14px 28px -14px rgba(0, 0, 0, 0.55);
  transition: transform 0.4s var(--ease-out-expo), box-shadow 0.4s var(--ease-out-expo);
}

.btn-primary:hover {
  transform: translateY(-3px);
  box-shadow: 0 20px 34px -12px rgba(0, 0, 0, 0.6);
}

/* Dark fill + ivory text, not the old light-fill version — this
   button only ever appears inside .cta-close (confirmed: no other
   usage site-wide), and .cta-close's own background is now the full
   rose accent-bleed instead of dark noir. An ivory-on-rose button
   there reads as barely-there (both light-ish tones); a dark button
   is the one thing that actually contrasts against a rose section. */
.btn-primary-inverse {
  position: relative;
  display: inline-block;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ivory-surface);
  background: var(--color-on-accent);
  padding: 1.05rem 2.1rem;
  border-radius: 2px;
  box-shadow: 0 14px 28px -14px rgba(0, 0, 0, 0.35);
  transition: transform 0.4s var(--ease-out-expo), box-shadow 0.4s var(--ease-out-expo);
}

.btn-primary-inverse:hover {
  transform: translateY(-3px);
  background: var(--color-accent-deep);
}

.btn-secondary {
  position: relative;
  display: inline-block;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-accent);
  padding-bottom: 0.35rem;
  transition: color 0.3s var(--ease-out-expo);
}

.btn-secondary::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--color-accent-soft);
  transition: background 0.3s var(--ease-out-expo);
}

.btn-secondary:hover {
  color: var(--color-ink);
}

.btn-secondary:hover::after {
  background: var(--color-accent);
}

.text-link {
  color: var(--color-accent);
  font-weight: 600;
  border-bottom: 1px solid var(--color-accent-soft);
  padding-bottom: 2px;
  transition: border-color 0.3s var(--ease-out-expo), color 0.3s var(--ease-out-expo);
}

.text-link:hover {
  color: var(--color-ink);
  border-color: var(--color-accent);
}

/* ==========================================================================
   Section labels
   ========================================================================== */

.kicker {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin-bottom: 1.25rem;
}

.kicker::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--color-accent-soft);
  opacity: 0.6;
}

.section-kicker {
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 600;
  font-size: 1.5rem;
  color: var(--color-accent);
  margin-bottom: 0.75rem;
}

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

.hero {
  background: var(--color-bg-blush);
  padding: var(--section-pad) 0;
  overflow: hidden;
}

.hero-grid {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: 1.5rem;
  align-items: center;
}

.hero-copy {
  position: relative;
  z-index: 2;
}

.hero-title {
  /* Oversized past the usual 6rem display cap on purpose: the brief's own
     cover-line-bleeding-behind-the-panel concept only reads at masthead
     scale, so the type has to reach toward the visual to sell it. */
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 600;
  font-size: clamp(4.5rem, 13vw, 9rem);
  line-height: 0.9;
  letter-spacing: -0.02em;
  color: var(--color-ink);
  position: relative;
  z-index: 2;
}

.hero-title .accent {
  color: var(--color-accent);
}

.hero-tagline {
  font-family: var(--font-body);
  font-size: 1.05rem;
  line-height: 1.75;
  color: var(--color-ink-soft);
  max-width: 42ch;
  margin-top: 1.75rem;
}

.hero-actions {
  display: flex;
  align-items: center;
  gap: 2rem;
  flex-wrap: wrap;
  margin-top: 2.75rem;
}

.hero-visual {
  position: relative;
  z-index: 1;
  /* max-width caps the polaroid stack's rendered size — without it, the
     stack's `aspect-ratio: 3/4` was deriving its height purely from the
     grid column's full bled width (~845px at common desktop widths),
     making it ~1126px tall against .hero-copy's ~413px. .hero-grid's
     `align-items: center` then split that ~713px gap into huge empty
     space above AND below the text block (confirmed via
     getBoundingClientRect during the Oct 2026 critique). Capping the
     width the stack is allowed to bleed to keeps the signature
     oversized-photo-bleeding-past-the-column effect while bringing the
     two columns' heights close enough that `align-items: center`
     actually reads as centered, not lopsided. */
  max-width: 560px;
  margin-left: -8rem;
  margin-right: calc(-1 * max(0px, (100vw - 1200px) / 2 + 1.5rem));
}

/* ==========================================================================
   Polaroid stack (hero — real photos, replaces the placeholder silhouette
   in the same slot/aspect-ratio/entrance animation)
   ========================================================================== */

/* Same footprint as the old .silhouette-frame so the hero layout doesn't
   shift: js/main.js's initPolaroidStack() drives the scatter/cycle/reshuffle
   via the --tx/--ty/--rot/--drag custom properties below. Fallback (no JS)
   is the fixed nth-child fan so the hero never shows a dead stack of
   identical, unrotated photos. */
.polaroid-stack {
  position: relative;
  aspect-ratio: 3 / 4;
  touch-action: pan-y;
}

.polaroid-stack:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 6px;
}

.polaroid-card {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 72%;
  aspect-ratio: 4 / 5;
  background: var(--color-ivory-surface);
  padding: 0.6rem 0.6rem 1.6rem;
  border-radius: 2px;
  box-shadow: 0 32px 60px -22px rgba(0, 0, 0, 0.5);
  cursor: grab;
  user-select: none;
  transform: translate(-50%, -50%) translate(var(--tx, 0%), var(--ty, 0%)) translate(var(--drag, 0px), 0) rotate(var(--rot, 0deg));
  transition: transform 0.6s var(--ease-out-expo), opacity 0.5s var(--ease-out-expo);
}

.polaroid-card:active {
  cursor: grabbing;
}

.polaroid-card img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Biased toward the top third rather than dead-center: these are
     portrait phone photos with the face/subject in the upper portion
     and plainer clothing/background below, and a center crop on a
     back-stack card (which only reveals a thin sliver past the
     scatter offset) could land squarely on a dark, featureless area —
     confirmed on mirror-selfie-04.jpg, whose default-centered crop
     landed on near-black jeans and read as a broken/empty card (Oct
     2026 critique). Check each new polaroid photo against this
     default before assuming it crops well; override per-image via an
     inline style if it doesn't, same pattern as the portfolio grid's
     "Per-image crop overrides" in PROJECT_NOTES.md. */
  object-position: 50% 22%;
}

.polaroid-caption {
  position: absolute;
  left: 0.6rem;
  right: 0.6rem;
  bottom: 0.5rem;
  font-family: var(--font-body);
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
  text-align: center;
}

/* No-JS / pre-JS fallback fan — fixed, not random, so the layout is stable
   before initPolaroidStack() applies its own scatter values. */
.polaroid-card:nth-child(1) { --rot: -6deg; --tx: -6%;  --ty: 2%;  z-index: 6; }
.polaroid-card:nth-child(2) { --rot: 4deg;  --tx: 6%;   --ty: -4%; z-index: 5; }
.polaroid-card:nth-child(3) { --rot: -4deg; --tx: -3%;  --ty: 6%;  z-index: 4; }
.polaroid-card:nth-child(4) { --rot: 7deg;  --tx: 5%;   --ty: 5%;  z-index: 3; }
.polaroid-card:nth-child(5) { --rot: -8deg; --tx: -5%;  --ty: -5%; z-index: 2; }
.polaroid-card:nth-child(6) { --rot: 9deg;  --tx: 3%;   --ty: -7%; z-index: 1; }

/* ==========================================================================
   Page header (About / Portfolio / Brands / Contact masthead-lite)
   ========================================================================== */

.page-header {
  background: var(--color-bg-blush);
  padding: calc(var(--section-pad) * 0.85) 0 var(--section-pad);
}

.page-header-grid {
  display: grid;
  grid-template-columns: 1.2fr 0.8fr;
  gap: 3rem;
  align-items: center;
}

.page-header .kicker {
  animation: fade-up 0.6s var(--ease-out-expo) both;
}

.page-header h1 {
  font-style: italic;
  font-size: clamp(2.75rem, 6vw, 4.5rem);
  margin: 0.5rem 0 1.25rem;
  color: var(--color-ink);
  animation: ink-reveal 0.8s var(--ease-out-expo) both 0.2s;
}

.page-header-intro {
  font-size: 1.05rem;
  line-height: 1.75;
  color: var(--color-ink-soft);
  max-width: 52ch;
  animation: fade-up 0.7s var(--ease-out-expo) both 0.4s;
}

.page-header-visual {
  animation: fade-up 0.8s var(--ease-out-expo) both 0.3s;
}

/* Real photo used in every non-hero page-header (About/Portfolio/Brands/
   Contact) — replaces what used to be a smaller .silhouette-frame
   panel. js/main.js's initHeaderPhoto() randomly picks one of a fixed set
   of photos into the .page-header-photo img on each load (fresh pairing
   per visit, not a fixed per-page assignment). Two-layer composition
   echoes the hero's silhouette-back/front bleed: a hot-pink clipped shape
   peeks out from behind a slightly rotated, larger-than-the-box photo
   card, rather than a flat single rectangle. Neither layer clips to the
   .page-header-photo-frame box (no overflow: hidden there) so the back
   shape's offset is actually visible, same as the hero's silhouette-frame
   never clipping its own children. */
.page-header-photo-frame {
  position: relative;
  aspect-ratio: 3 / 4;
}

.page-header-photo-back {
  position: absolute;
  inset: 0;
  border-radius: 2px;
  background: linear-gradient(155deg, var(--color-accent-soft) 0%, var(--color-bg-blush) 85%);
  clip-path: polygon(20% 0%, 100% 8%, 86% 100%, 2% 90%);
  opacity: 0.7;
  transform: translate(6%, 5%);
}

.page-header-photo {
  position: absolute;
  inset: 0;
  border-radius: 2px;
  overflow: hidden;
  /* Thick dark frame, not a hairline — Oct 2026 redesign, echoing the
     actor-portfolio reference's framed About photo. `border` (not
     box-shadow) so the photo `img` below, positioned `inset: 0`
     within this box, is genuinely inset from it rather than just
     outlined — the photo sits recessed inside the frame, not
     floating next to it. */
  border: 10px solid var(--color-accent-deep);
  box-shadow: 0 32px 60px -22px rgba(0, 0, 0, 0.5);
  transform: rotate(-4deg) translate(-4%, -3%) scale(1.06);
}

.page-header-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.page-header-photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 75% 15%, rgba(255, 214, 232, 0.3), transparent 55%),
    linear-gradient(195deg, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.28) 100%);
  mix-blend-mode: soft-light;
  pointer-events: none;
}

/* ==========================================================================
   Bio
   ========================================================================== */

.bio-section {
  background: var(--color-bg);
  padding: var(--section-pad) 0;
}

.bio-grid {
  display: grid;
  grid-template-columns: 0.6fr 1.4fr;
  gap: 3rem;
}

.bio-copy p {
  color: var(--color-ink-soft);
  line-height: 1.8;
  max-width: 65ch;
  margin-bottom: 1.25rem;
}

.glance-list {
  margin-top: 1.5rem;
  border-top: var(--rule-soft);
}

.glance-item {
  padding: 1.25rem 0;
  border-bottom: var(--rule-soft);
}

.glance-item dt {
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
  margin-bottom: 0.4rem;
}

.glance-item dd {
  font-family: var(--font-body);
  font-size: 0.95rem;
  color: var(--color-ink);
  margin: 0;
}

/* ==========================================================================
   Focus areas (editorial contributor-list, not icon cards)
   ========================================================================== */

/* position: relative — containing block for the reused .glass-ripples
   decorative texture (Oct 2026 layout pass: these long text-only list
   sections, with no photography at all, read as flat empty stretches
   on the persistent dark ground, especially on mobile where there's
   no asymmetric grid to lean on and everything just stacks as plain
   text — confirmed by the user, mobile prioritized). Reuses the exact
   ambient-ring motif already established in the nav rather than
   inventing a new decorative device. */
.focus-section {
  position: relative;
  background: var(--color-bg-pearl);
  padding: var(--section-pad) 0;
}

/* Oct 2026 "What I Offer lacks character" pass: this was a plain
   hairline-divided text grid (deliberately, per the system's original
   "no icon+heading+text cards" rule) — the user explicitly asked for
   cards this time, a direct reversal of that rule, not a
   misunderstanding of it. See the amended Don't rule in DESIGN.md.
   Reused on all three pages that share this component (Home/About/
   Contact), not just the one instance asked about, so the site
   doesn't end up with two different treatments of the same pattern. */
.focus-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
  margin-top: 2.5rem;
  counter-reset: focus-counter;
}

.focus-item {
  counter-increment: focus-counter;
  padding: 2.5rem 2rem;
  border-radius: 2px;
  /* Background is a translucent ivory wash over whatever the section's
     own background is, not a new hex — reads as "a card lifted off
     this surface" regardless of which dark tier it sits on (reused on
     .focus-section's --color-bg-pearl here, but stays correct if this
     component ever lands on a different section background). Edge
     reuses the same box-shadow-as-border device as photo tiles. */
  background: rgba(255, 214, 232, 0.04);
  box-shadow: 0 0 0 1px rgba(255, 214, 232, 0.12);
  transition: transform 0.4s var(--ease-out-expo), box-shadow 0.4s var(--ease-out-expo), background 0.4s var(--ease-out-expo);
}

.focus-item:hover {
  transform: translateY(-6px);
  background: rgba(255, 214, 232, 0.07);
  box-shadow: 0 24px 44px -24px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 214, 232, 0.18);
}

/* Large muted italic numeral above each item — same italic-serif
   "Section Label" register the rest of the site already uses (not a
   new typeface or an icon), just deployed at a bigger, quieter scale
   so the list reads as editorial "contributor page" numbering rather
   than a plain heading+paragraph repeated three times. */
.focus-item h3::before {
  content: counter(focus-counter, decimal-leading-zero);
  display: block;
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 500;
  font-size: 2.5rem;
  line-height: 1;
  color: var(--color-ink-soft);
  opacity: 0.55;
  margin-bottom: 0.9rem;
}

.focus-item h3 {
  font-style: italic;
  font-size: 1.5rem;
  color: var(--color-accent);
  margin-bottom: 0.75rem;
}

.focus-item p {
  color: var(--color-ink-soft);
  line-height: 1.7;
}

/* ==========================================================================
   About teaser
   ========================================================================== */

.about-teaser {
  background: var(--color-bg-pearl);
  padding: var(--section-pad) 0;
}

/* Oct 2026 layout pass: was a plain 2-column text/text split, which on
   the all-dark ground (no light/dark section alternation to lean on
   anymore) read as a huge flat empty stretch — confirmed by the user
   ("too flat and plain"). Added a photo column reusing the exact
   .page-header-photo-frame markup/CSS from the page-header component
   (no new styles needed) so the homepage's About teaser gets the same
   visual anchor every other page already has at its own header. */
.about-grid {
  display: grid;
  grid-template-columns: 0.8fr 0.75fr 1.05fr;
  gap: 3rem;
  align-items: start;
}

.about-grid .page-header-photo-frame {
  /* margin: 0 auto — without it, a block element narrower than its
     column just hugs the column's start (left) edge per normal block
     layout, and .page-header-photo's own rotate/scale/translate
     transform on top of that shifts it left again, landing it almost
     flush against the viewport edge with a big dead gap on the right
     — confirmed via elementFromPoint at mobile width (390px): the
     photo's visual footprint ran roughly x:-18 to x:306, nowhere near
     centered in a 372px content column. Mobile-prioritized per the
     user; this centers it at every width. */
  margin: 0 auto;
  max-width: 280px;
}

.about-grid h2 {
  font-size: clamp(2rem, 4vw, 2.75rem);
  margin-bottom: 1.5rem;
  max-width: 14ch;
}

.about-copy p {
  color: var(--color-ink-soft);
  line-height: 1.8;
  max-width: 62ch;
  margin-bottom: 1.25rem;
}

/* ==========================================================================
   Portfolio teaser
   ========================================================================== */

.work-teaser {
  background: var(--color-bg);
  padding: var(--section-pad) 0;
}

.work-intro {
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  gap: 4rem;
  align-items: end;
  margin-bottom: 3.5rem;
}

.work-intro h2 {
  font-size: clamp(2rem, 4vw, 2.75rem);
  max-width: 14ch;
}

.work-intro p {
  color: var(--color-ink-soft);
  line-height: 1.8;
  max-width: 58ch;
}

.work-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-auto-rows: minmax(140px, 1fr);
  gap: 1.25rem;
}

/* Oct 2026 "rectangles aren't doing any justice" pass. Was a flat
   rectangular tile — border-radius, a 1px hairline edge, two-layer
   overlay gradient, otherwise completely static — the one place on
   the site presenting real photography with no trace of the system's
   signature "peeking accent shape" device (see DESIGN.md's Shapes
   section) and no interactive feedback at all. Split each tile into
   two stacked layers instead of one flat box: `.work-panel-accent` is
   a solid hot-pink layer filling the tile; `.work-panel-photo` sits on
   top holding the actual photo, clipped with an asymmetric
   corner-notch `clip-path` (a different corner per tile, see the
   per-variant rules below) so the pink layer shows through at that one
   cut corner — the same "accent shape peeks out from behind the
   photo" idea the hero/page-header device uses, expressed as a clean
   chamfered notch instead of a rotated photo over a clipped polygon,
   since rotating individual tiles inside this tight asymmetric grid
   risked them visually colliding with their neighbors. `.work-tag`
   stays a direct child of `.work-panel` (not `.work-panel-photo`), so
   it's never subject to the clip-path — which is also why every
   notch corner below is deliberately NOT top-left, where the tag
   always sits. */
.work-panel {
  position: relative;
  min-height: 200px;
}

.work-panel--large {
  grid-column: span 4;
  grid-row: span 2;
}

.work-panel--a {
  grid-column: span 2;
  grid-row: span 1;
}

.work-panel--b {
  grid-column: span 2;
  grid-row: span 1;
}

.work-panel-accent {
  position: absolute;
  inset: 0;
  border-radius: 2px;
  background: var(--color-accent);
}

.work-panel-photo {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: 2px;
  /* box-shadow, not border — doesn't add to layout box size like a
     real border would, so it doesn't fight the grid's gap math. Gives
     each tile a defined edge against the page background and against
     neighboring tiles whose own edge content (sky, pale clothing)
     could otherwise blend straight into the page with only the grid
     gap for separation (Oct 2026 critique: "no proper borders"). Also
     clipped by the same corner-notch path as the photo it wraps, so
     the edge reads as one continuous chamfered frame, not a rectangle
     with a notch cut out of its corner. */
  box-shadow: 0 0 0 1px rgba(255, 214, 232, 0.18);
  transition: transform 0.5s var(--ease-out-expo), box-shadow 0.4s var(--ease-out-expo);
}

.work-panel:hover .work-panel-photo {
  transform: scale(1.035);
  box-shadow: 0 24px 44px -24px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 214, 232, 0.24);
}

.work-panel--large .work-panel-photo {
  clip-path: polygon(0 0, 100% 0, 100% 88%, 90% 100%, 0 100%);
}

.work-panel--a .work-panel-photo {
  clip-path: polygon(0 0, 88% 0, 100% 14%, 100% 100%, 0 100%);
}

.work-panel--b .work-panel-photo {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 14% 100%, 0 86%);
}

.work-panel .swatch,
.work-panel img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.work-panel--large .swatch {
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-deep) 70%);
}

.work-panel--a .swatch {
  background: linear-gradient(200deg, var(--color-accent-soft) 0%, var(--color-accent) 100%);
}

.work-panel--b .swatch {
  background: linear-gradient(20deg, var(--color-bg-blush) 0%, var(--color-accent-soft) 100%);
}

.work-panel-photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 75% 15%, rgba(255, 214, 232, 0.3), transparent 55%),
    linear-gradient(195deg, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.28) 100%);
  mix-blend-mode: soft-light;
  pointer-events: none;
}

.work-tag {
  position: absolute;
  top: 1.1rem;
  left: 1.1rem;
  font-family: var(--font-body);
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink);
  background: rgba(0, 0, 0, 0.72);
  padding: 0.4rem 0.85rem;
  border-radius: 100px;
}

.work-panel--large .work-tag {
  color: var(--color-ink);
  background: rgba(0, 0, 0, 0.45);
}

.work-note {
  margin-top: 1.25rem;
  font-size: 0.8rem;
  color: var(--color-ink-soft);
}

.work-cta {
  margin-top: 1rem;
  text-align: right;
}

/* ==========================================================================
   Portfolio (contact-sheet grid)
   ========================================================================== */

.portfolio-section {
  background: var(--color-bg);
  padding: var(--section-pad) 0;
}

.portfolio-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 2.5rem;
}

.portfolio-filters button {
  position: relative;
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
  background: transparent;
  border: 1px solid var(--color-accent-soft);
  border-radius: 100px;
  padding: 0.6rem 1.25rem;
  cursor: pointer;
  transition: background 0.3s var(--ease-out-expo), color 0.3s var(--ease-out-expo), border-color 0.3s var(--ease-out-expo);
}

.portfolio-filters button:hover {
  color: var(--color-ink);
  border-color: var(--color-accent);
}

.portfolio-filters button.active {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-on-accent);
}

.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.25rem;
}

.portfolio-item {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  border-radius: 2px;
  overflow: hidden;
  border: none;
  padding: 0;
  cursor: pointer;
  text-align: left;
  /* box-shadow, not the `border` above (left at none — a real border
     would add to the box's layout size and disturb the grid's aspect
     ratios). See .work-panel's comment for why: defines each tile's
     edge against the page and against neighbors whose own edges could
     otherwise blend straight into the gap. */
  box-shadow: 0 0 0 1px rgba(255, 214, 232, 0.18);
}

.portfolio-item[hidden] {
  /* Author styles always beat the UA [hidden] rule regardless of
     specificity, so `.portfolio-item { display: block }` above silently
     defeats `item.hidden = true` in main.js unless restated here with
     higher specificity — otherwise filtered-out tiles stay in the grid
     as invisible blank boxes instead of closing up. */
  display: none;
}

.portfolio-item--wide {
  grid-column: span 2;
  aspect-ratio: 16 / 9;
}

.portfolio-item .swatch,
.portfolio-item img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.5s var(--ease-out-expo);
}

.portfolio-item:hover .swatch,
.portfolio-item:focus-visible .swatch,
.portfolio-item:hover img,
.portfolio-item:focus-visible img {
  transform: scale(1.04);
}

.portfolio-item::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 75% 15%, rgba(255, 214, 232, 0.3), transparent 55%),
    linear-gradient(195deg, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.28) 100%);
  mix-blend-mode: soft-light;
  pointer-events: none;
}

/* accent -> accent-deep used to be this swatch's stops, but both are
   near-black with almost no luminance gap (#171212 to #0B0808) — it
   rendered as a flat void, indistinguishable from a broken image
   (confirmed via computed styles during the Oct 2026 critique).
   Paired with accent-soft instead, matching every other swatch's
   dark+light pattern so it actually shows a visible gradient. */
/* Both re-paired with accent-deep instead of accent/accent-soft —
   post-redesign, accent and accent-soft are both light rose tones
   (promoted/paled respectively when noir became the page ground), so
   pairing them together left almost no luminance range, same flat-
   gradient problem swatch-1 had pre-redesign for the opposite reason
   (see the Oct 2026 critique comment below). */
.swatch-1 { background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-deep) 100%); }
.swatch-2 { background: linear-gradient(200deg, var(--color-accent-soft) 0%, var(--color-accent-deep) 100%); }
.swatch-3 { background: linear-gradient(20deg, var(--color-bg-blush) 0%, var(--color-accent-soft) 100%); }
.swatch-4 { background: linear-gradient(160deg, var(--color-accent-deep) 0%, var(--color-accent-soft) 100%); }
.swatch-5 { background: linear-gradient(210deg, var(--color-bg-blush) 0%, var(--color-accent) 90%); }
.swatch-6 { background: linear-gradient(45deg, var(--color-accent) 0%, var(--color-bg-blush) 100%); }

.portfolio-item .work-tag {
  z-index: 1;
}

.portfolio-item-view {
  position: absolute;
  right: 1.1rem;
  bottom: 1.1rem;
  z-index: 1;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink);
  background: rgba(0, 0, 0, 0.55);
  padding: 0.4rem 0.85rem;
  border-radius: 100px;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.3s var(--ease-out-expo), transform 0.3s var(--ease-out-expo);
}

.portfolio-item:hover .portfolio-item-view,
.portfolio-item:focus-visible .portfolio-item-view {
  opacity: 1;
  transform: translateY(0);
}

/* ==========================================================================
   Brand strip
   ========================================================================== */

.brand-strip {
  position: relative;
  background: var(--color-bg-blush);
  padding: var(--section-pad) 0;
  text-align: center;
}

.brand-strip .kicker {
  justify-content: center;
}

.brand-strip .kicker::after {
  display: none;
}

/* Oct 2026 "lacks character" pass: was plain centered italic links
   floating in open space — nothing distinguished them as partner
   credits rather than any other rose link on the page. Bounding
   hairlines turn it into a proper masthead "credits line," and the
   numbered-index device (counter-reset here, same pattern/scale as
   .focus-item above) gives each name the same editorial weight. */
.brand-row {
  counter-reset: brand-counter;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 3.5rem;
  flex-wrap: wrap;
  margin-top: 2.25rem;
  padding: 2.5rem 0;
  border-top: var(--rule-soft);
  border-bottom: var(--rule-soft);
}

.brand-row a {
  counter-increment: brand-counter;
  display: block;
  text-align: center;
}

.brand-row a::before {
  content: counter(brand-counter, decimal-leading-zero);
  display: block;
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 500;
  font-size: 2.5rem;
  line-height: 1;
  color: var(--color-ink-soft);
  opacity: 0.55;
  margin-bottom: 0.9rem;
}

.brand-row a span {
  display: inline-block;
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 600;
  font-size: 1.6rem;
  color: var(--color-accent);
  border-bottom: 1px solid transparent;
  padding-bottom: 0.3rem;
  transition: color 0.3s var(--ease-out-expo), border-color 0.3s var(--ease-out-expo);
}

.brand-row a:hover span {
  color: var(--color-ink);
  border-color: var(--color-accent-soft);
}

.brand-strip .work-cta {
  text-align: center;
  margin-top: 2.75rem;
}

/* ==========================================================================
   Brand campaign-preview gallery (static, non-lightbox)
   ========================================================================== */

.brand-gallery {
  background: var(--color-bg-pearl);
  padding: var(--section-pad) 0;
}

.brand-gallery-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.25rem;
  margin-top: 2rem;
}

.gallery-tile {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: 2px;
  overflow: hidden;
  /* See .work-panel's comment above for why box-shadow instead of a
     real border. */
  box-shadow: 0 0 0 1px rgba(255, 214, 232, 0.18);
}

.gallery-tile .swatch {
  position: absolute;
  inset: 0;
}

.gallery-tile::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 75% 15%, rgba(255, 214, 232, 0.3), transparent 55%),
    linear-gradient(195deg, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.28) 100%);
  mix-blend-mode: soft-light;
  pointer-events: none;
}

/* ==========================================================================
   Brand index (partner-credits rows)
   ========================================================================== */

.brand-index-section {
  background: var(--color-bg);
  padding: var(--section-pad) 0;
}

.brand-index-list {
  border-top: var(--rule-soft);
}

.brand-index-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding: 2rem 0;
  border-bottom: var(--rule-soft);
  transition: transform 0.3s var(--ease-out-expo);
}

.brand-index-row:hover,
.brand-index-row:focus-visible {
  transform: translateX(0.5rem);
}

.brand-index-name {
  flex: 0 0 auto;
  min-width: 220px;
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 600;
  font-size: 2rem;
  color: var(--color-accent);
}

.brand-index-desc {
  flex: 1;
  color: var(--color-ink-soft);
}

.brand-index-arrow {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
  white-space: nowrap;
  transition: color 0.3s var(--ease-out-expo);
}

.brand-index-row:hover .brand-index-arrow,
.brand-index-row:focus-visible .brand-index-arrow {
  color: var(--color-accent);
}

.brand-back-link {
  display: inline-block;
  margin-bottom: 1.5rem;
  font-size: 0.85rem;
}

/* ==========================================================================
   Closing CTA
   ========================================================================== */

.cta-close {
  background: var(--color-accent);
  color: var(--color-on-accent);
  padding: var(--section-pad) 0;
  text-align: center;
}

.cta-close h2 {
  font-size: clamp(2.25rem, 5vw, 3.5rem);
  color: var(--color-on-accent);
  margin-bottom: 1.5rem;
}

.cta-close p {
  /* Dark, not light — .cta-close's background is var(--color-accent),
     now a vivid hot pink (the one full-color-bleed moment in an
     otherwise dark site), so its body copy needs dark text like its
     heading (--color-on-accent) does, just at reduced opacity for a
     secondary-text weight. Was light ivory at 0.85 when the CTA
     background was dark noir, pre-redesign. */
  color: rgba(0, 0, 0, 0.72);
  max-width: 46ch;
  margin: 0 auto 2.5rem;
  line-height: 1.75;
}

/* ==========================================================================
   Contact
   ========================================================================== */

.contact-section {
  background: var(--color-bg);
  padding: var(--section-pad) 0 calc(var(--section-pad) * 1.3);
}

.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 3rem;
  padding-top: 3rem;
  border-top: var(--rule-soft);
}

.contact-email {
  display: inline-block;
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 600;
  font-size: clamp(1.75rem, 3.5vw, 2.5rem);
  color: var(--color-accent);
  word-break: break-word;
  transition: color 0.3s var(--ease-out-expo);
}

.contact-email:hover {
  color: var(--color-ink);
}

.contact-socials {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 0.5rem;
}

.contact-social {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  font-family: var(--font-body);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--color-ink);
  transition: color 0.3s var(--ease-out-expo);
}

.contact-social span {
  font-weight: 400;
  color: var(--color-ink-soft);
}

.contact-social:hover {
  color: var(--color-accent);
}

/* ==========================================================================
   Footer
   ========================================================================== */

/* Oct 2026 "basically nonexistent" pass — was a single centered
   copyright line. Built from the same pieces the rest of the site
   already uses (hairlines, uppercase meta labels, text-links) rather
   than inventing a new footer-specific visual language: a real
   wordmark + tagline, the real nav repeated, and the real contact
   details already confirmed for contact.html (business email,
   Instagram, TikTok) — nothing fabricated. */
.site-footer {
  background: var(--color-bg-pearl);
  border-top: var(--rule-soft);
  padding-top: var(--section-pad);
}

/* Oct 2026 restructure, then same-day reorder: the original 3-up grid
   (brand | nav | connect) put the quick-message form in its own
   full-width row below, reading as a fourth stack of plain text with
   nothing to separate it or give it weight — flagged as "flat."
   Rebuilt as two zones: .footer-contact (brand blurb, then the form
   card stacked directly beneath it) on the left, .footer-link-columns
   (Navigate/Connect side by side) on the right. Per feedback, the form
   needed to read before Navigate/Connect rather than after — grouping
   it with the brand block (both "about/reach us") against the nav
   links (all "where to go") also reads as a cleaner split than the
   form being a disconnected third element. At the ≤900px collapse
   this stacking order falls out automatically: brand, then form, then
   Navigate/Connect. */
.footer-top {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: 4rem;
  align-items: start;
  padding-bottom: 3rem;
  border-bottom: var(--rule-soft);
}

.footer-contact {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.footer-link-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2rem;
}

.footer-wordmark {
  display: inline-block;
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 600;
  font-size: 1.9rem;
  color: var(--color-ink);
}

.footer-brand p {
  margin-top: 1rem;
  color: var(--color-ink-soft);
  max-width: 32ch;
  line-height: 1.7;
  font-size: 0.95rem;
}

.footer-label {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
  margin-bottom: 1.25rem;
}

.footer-links {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.footer-links a {
  color: var(--color-ink);
  font-size: 0.95rem;
  transition: color 0.3s var(--ease-out-expo);
}

.footer-links a:hover {
  color: var(--color-accent);
}

/* Quick-message form card, Oct 2026 restructure. Real functionality
   within the site's static/no-backend constraint (see PRODUCT.md):
   submits via `mailto:` (initFooterForm() in main.js), which opens
   the visitor's own mail client with the message pre-filled rather
   than silently posting to a server — there is no backend here to
   post to, and wiring up a third-party form service (Formspree,
   Netlify Forms, etc.) would be a real architectural addition needing
   its own account/approval, not something to add unasked. Underline-
   style inputs (no boxes/borders-all-around), matching the system's
   flat, hairline-divided input language even inside the card surface.
   The surface itself reuses the same tinted-background + 1px-ring
   language `.focus-item` established for cards — see DESIGN.md's
   Editorial List section — rather than inventing a second card style.
   Fields stack full-width (not the old wrapping flex row) since the
   card is a narrower column than the old full-bleed row was. */
.footer-form-card {
  background: rgba(255, 79, 139, 0.06);
  box-shadow: 0 0 0 1px rgba(255, 214, 232, 0.14);
  border-radius: 2px;
  padding: 2.25rem 2rem;
}

.footer-form-card .footer-label {
  color: var(--color-accent);
}

.footer-form {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  margin-top: 1.25rem;
}

.footer-form-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.footer-form label {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
}

.footer-form input {
  background: transparent;
  border: none;
  border-bottom: var(--rule-soft);
  padding: 0.5rem 0;
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: 0.95rem;
  transition: border-color 0.3s var(--ease-out-expo);
}

.footer-form input::placeholder {
  color: var(--color-ink-soft);
  opacity: 0.7;
}

.footer-form input:focus {
  outline: none;
  border-bottom-color: var(--color-accent);
}

/* .btn-primary was only ever used on <a> tags before (no native
   button chrome to fight); this is the first <button> usage, so a
   browser's default border/cursor still need an explicit reset on
   top of .btn-primary's own styling. Full-width and bottom-anchored
   so it reads as the card's one clear call to action, not a trailing
   link. */
.footer-form .btn-primary {
  width: 100%;
  text-align: center;
  border: none;
  cursor: pointer;
  margin-top: 0.4rem;
}

.footer-form-note {
  margin-top: 0.85rem;
  font-size: 0.78rem;
  color: var(--color-ink-soft);
  min-height: 1.2em;
}

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 1rem;
  padding: 1.75rem 0;
  font-size: 0.8rem;
  color: var(--color-ink-soft);
}

.footer-bottom a {
  color: var(--color-ink-soft);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color 0.3s var(--ease-out-expo);
}

.footer-bottom a:hover {
  color: var(--color-accent);
}

/* ==========================================================================
   Entrance motion (single authored moment, CSS-only so it never depends
   on JS/observer timing and never leaves content stuck invisible)
   ========================================================================== */

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal {
  animation: fade-up 0.9s var(--ease-out-expo) both;
}

.reveal-delay-1 {
  animation-delay: 0.15s;
}

/* ==========================================================================
   Hero focal moment: the masthead opens like a lookbook cover — the
   silhouette panel swings open on its left edge like a page turning,
   while the wordmark inks in with a left-to-right wipe. This is the one
   authored sequence the THESIS asks for; every other section keeps the
   quieter shared fade-up above.
   ========================================================================== */

@keyframes ink-reveal {
  from { clip-path: inset(0 100% 0 0); }
  to { clip-path: inset(0 0 0 0); }
}

@keyframes page-open {
  from {
    opacity: 0;
    transform: perspective(1600px) rotateY(-26deg) scale(0.94);
  }
  to {
    opacity: 1;
    transform: perspective(1600px) rotateY(0) scale(1);
  }
}

.hero .kicker {
  animation: fade-up 0.6s var(--ease-out-expo) both;
}

/* Typewriter: JS (initTypewriter in main.js) wraps each character of
   .hero-title .typewriter in its own .letter span with a computed
   animation-delay, so every glyph fades/settles in whole rather than
   being clipped mid-character by a fixed-step width mask — that
   fixed-width-per-step approach reads as jerky on a proportional
   italic face where letters vary widely in width. If JS never runs,
   the plain text node underneath is already fully visible, so nothing
   here can leave the wordmark permanently hidden. */
.hero-title .letter {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.12em);
  transition: opacity 0.22s var(--ease-out-expo), transform 0.22s var(--ease-out-expo);
}

.hero-title .letter.is-in {
  opacity: 1;
  transform: translateY(0);
}

.hero-title .caret {
  display: inline-block;
  width: 2px;
  height: 0.78em;
  margin-left: 0.05em;
  background: var(--color-accent);
  vertical-align: -0.05em;
  animation: caret-blink 0.9s step-end infinite;
}

@keyframes caret-blink {
  50% {
    opacity: 0;
  }
}

.hero-tagline {
  animation: fade-up 0.7s var(--ease-out-expo) both 0.55s;
}

.hero-actions {
  animation: fade-up 0.7s var(--ease-out-expo) both 0.7s;
}

.hero-visual .silhouette-frame,
.hero-visual .polaroid-stack {
  transform-origin: left center;
  animation: page-open 0.9s var(--ease-out-expo) both 0.15s;
}

/* ==========================================================================
   Portfolio filter transitions
   ========================================================================== */

.portfolio-grid [data-category] {
  transition: opacity 0.35s var(--ease-out-expo), transform 0.35s var(--ease-out-expo);
}

.portfolio-grid [data-category].is-hidden {
  opacity: 0;
  transform: scale(0.96);
  pointer-events: none;
}

/* ==========================================================================
   Lightbox
   ========================================================================== */

.lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3.5rem 2rem 2rem;
  background: rgba(0, 0, 0, 0.82);
  opacity: 0;
  visibility: hidden;
  overflow-y: auto;
  transition: opacity 0.35s var(--ease-out-expo), visibility 0s linear 0.35s;
}

.lightbox-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.35s var(--ease-out-expo);
}

.lightbox-content {
  /* Width is capped by viewport height too (converted through the 4:5
     panel ratio), not just viewport width, so the panel plus its close
     button and caption never overflow a short viewport. */
  position: relative;
  width: min(90vw, 720px, calc((100vh - 6rem) * 4 / 5));
  margin: auto;
  transform: scale(0.94);
  transition: transform 0.4s var(--ease-out-expo);
}

.lightbox-overlay.is-open .lightbox-content {
  transform: scale(1);
}

.lightbox-panel {
  width: 100%;
  aspect-ratio: 4 / 5;
  border-radius: 2px;
  position: relative;
  overflow: hidden;
  box-shadow: 0 50px 90px -30px rgba(0, 0, 0, 0.65);
}

.lightbox-panel::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 75% 15%, rgba(255, 214, 232, 0.3), transparent 55%),
    linear-gradient(195deg, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.28) 100%);
  mix-blend-mode: soft-light;
  pointer-events: none;
}

.lightbox-panel img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.lightbox-panel .work-tag {
  color: var(--color-ink);
  background: rgba(0, 0, 0, 0.4);
}

.lightbox-caption {
  margin-top: 1rem;
  font-size: 0.8rem;
  color: var(--color-ink);
  text-align: center;
}

.lightbox-close {
  position: absolute;
  top: -2.75rem;
  right: 0;
  background: none;
  border: none;
  color: var(--color-ink);
  font-size: 1.9rem;
  line-height: 1;
  cursor: pointer;
  transition: transform 0.3s var(--ease-out-expo);
}

.lightbox-close:hover {
  transform: rotate(90deg);
}

/* ==========================================================================
   Ripple (tap/click feedback on solid buttons, filter pills, and tiles)
   ========================================================================== */

.ripple-surface {
  /* No `position` here on purpose: `.lightbox-close` already declares
     `position: absolute` elsewhere in this file, and this rule's later
     source order would otherwise win the specificity tie and break it.
     Each ripple-eligible selector sets its own `position` below instead. */
  overflow: hidden;
}

.ripple {
  position: absolute;
  border-radius: 50%;
  background: var(--ripple-color, rgba(0, 0, 0, 0.22));
  transform: scale(0);
  pointer-events: none;
  animation: ripple-expand 0.6s var(--ease-out-expo);
}

@keyframes ripple-expand {
  to {
    transform: scale(1);
    opacity: 0;
  }
}

/* .btn-primary's fill is now the light-mid rose accent (was dark
   noir), so its ripple flipped dark to stay visible against it —
   same reasoning as .cta-close p above. */
.btn-primary {
  --ripple-color: rgba(0, 0, 0, 0.3);
}

/* .btn-primary-inverse's fill stays the one deliberately bright
   surface (--color-ivory-surface), so its dark ripple is still
   correct post-redesign — unlike .portfolio-filters button below,
   split out because a transparent pill now sits on the dark page
   itself and needs a light ripple instead. */
.btn-primary-inverse {
  --ripple-color: rgba(255, 214, 232, 0.25);
}

.portfolio-filters button {
  --ripple-color: rgba(255, 214, 232, 0.3);
}

.portfolio-item,
.lightbox-close {
  --ripple-color: rgba(255, 214, 232, 0.35);
}

/* Flipped light — was a dark ripple on the already-dark nav glass
   (low visibility even before the redesign; fixed while already
   auditing every ripple color for the new dark-everywhere world). */
.nav-toggle {
  --ripple-color: rgba(255, 214, 232, 0.3);
}

@media (prefers-reduced-motion: reduce) {
  .ripple {
    display: none;
  }

  .glass-ripples {
    display: none;
  }

  .nav-collapse {
    transition: none;
  }

  .reveal,
  .hero .kicker,
  .hero-title,
  .hero-tagline,
  .hero-actions,
  .hero-visual .polaroid-stack {
    animation: none;
  }

  .hero-title .letter {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .polaroid-card {
    transition: none;
  }

  .hero-title .caret {
    display: none;
  }

  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none !important;
  }

  .lightbox-content,
  .lightbox-close:hover {
    transition: none;
    transform: none;
  }
}
