/* ==========================================================================
   Magfolio v2 — "Painted Maximalism" theme layer
   --------------------------------------------------------------------------
   Load this AFTER /assets/styles.css. It never removes structure; it repaints
   every surface in Maggie's own visual language: painterly brushstroke bands,
   deep-purple (#221442) cards floating on paint, cream (#fffaee) ink, lavender
   pills, cut-out artwork over rotated colour slabs, and hand-made motion.

   ACCESSIBILITY: every text/background pair below meets WCAG 2.1 AA
   (>=4.5:1 body, >=3:1 large text). See v2/README.md for the contrast table.
   The original site's cream-on-white headings are corrected to cream-on-paint.

   All painted photography is referenced through custom properties so the
   standalone preview (file://) can repoint them at ../dist/media/.
   ========================================================================== */

:root {
  /* --- Maggie's palette, sampled from mag-folio.com + her gouache --------- */
  --deep: #221442;            /* header bar / title cards */
  --deep-2: #2d1b56;
  --cream: #fffaee;           /* her ink on purple */
  --cream-soft: #edd3ec;
  --paper: #fbf6ec;           /* warm gallery paper, not sterile white */
  --lav: #c9a0dc;
  --lav-pale: #e9d6f2;
  --rose: #c0628f;
  --teal: #2f8fa8;
  --ochre: #c89b6a;           /* from the gouache self-portrait ground */
  --crimson: #b3202c;         /* ZBrush */
  --maya: #37a5cc;            /* Autodesk Maya */
  --blender: #e87d0d;         /* Blender */
  --gp: #8a5fbf;              /* Grease Pencil */

  /* --- ink tokens -------------------------------------------------------
     Two families, and every text colour in this sheet comes from one of them.
     Each is measured against the *hardest* surface in its family rather than
     the obvious one, so a token that passes here passes everywhere in the
     theme:

       light family — verified on #d0c3d7, the darkest of the lavender washes
                      painted behind body copy (not --paper). Passing there
                      means it also passes on --paper, --cream, --lav-pale and
                      the #f6e7fb chip tint.
       dark family  — verified on #442c6e, the lightest purple in the header
                      bar and hero wash. Passing there means it also passes on
                      --deep and --deep-2.

     Ratios are WCAG 2.1 contrast: body copy needs 4.5:1, text 24px+ (or 18.66px
     at 700) needs 3:1. Reproduce them with `python v2/preview.py` and
     `window.__audit()` in the console.

     The raw palette hues above (--teal, --rose, --ochre, --crimson, --lav) are
     paint, not ink: at 2.3–3.7:1 on a light ground they fail as text. Use them
     for swashes, slabs, borders and icons, where 3:1 is the bar. Words on a
     light ground take --ink / --ink-soft / --muted / --accent. */
  --ink: #3a2a5e;             /* 7.5:1 on the wash, 11.7:1 on paper */
  --ink-soft: #55417f;        /* 5.1:1 / 8.0:1 */
  --muted: #564a6c;           /* 4.8:1 / 7.5:1 — was #6b5c86, only 3.6:1 on the wash */
  --line: #c9b6da;
  --accent: #682792;          /* 5.4:1 / 8.5:1 */
  --on-deep: var(--cream);          /* 11.0:1 on #442c6e, 16.2:1 on --deep */
  --on-deep-soft: var(--cream-soft);/*  8.3:1 / 12.1:1 */
  --on-deep-muted: var(--lav);      /*  5.2:1 /  7.7:1 — the quiet tier on purple */

  --bg: var(--paper);
  --surface: #fffdf7;
  --tint: var(--lav-pale);

  /* maximalist geometry: irregular, hand-cut */
  --radius: 34px 22px 30px 24px / 24px 30px 22px 34px;
  --radius-sm: 16px 10px 14px 11px / 11px 14px 10px 16px;
  --slab-1: var(--lav);
  --slab-2: var(--teal);
  --slab-3: var(--rose);
  --slab-4: var(--ochre);
  --shadow-paint: 0 2px 0 rgb(34 20 66 / .10), 0 14px 34px -12px rgb(34 20 66 / .38);

  /* --- corner scale: Apple's rounded rectangle, never a stadium ----------
     Two families of surface, deliberately different:

       art surfaces keep the hand-cut --radius / --radius-sm above. Torn paper
       is the collage identity and it is not a control, so Apple's geometry
       does not apply to it.
       controls take the fixed scale below. HIG (Buttons) gives a text button a
       rounded rectangle and requires a flexible-height button to "maintain
       consistent corner radius and padding" as its content grows — so the
       radius is a fixed length per size class, never 999px.

     999px is what made the buttons smush: a pill radius is half the element's
     *shortest* side, so the arc eats the flat edge until a wide CTA on a
     narrow window is nothing but two semicircles. Each value here is ~0.22 of
     the control height it belongs to — the proportion Apple's own iOS app
     icon uses (185 of 1024) — so the corner stays a corner at any width.

     --corner makes them continuous: SwiftUI's RoundedCornerStyle.continuous,
     shipped to CSS as `corner-shape: squircle` (superellipse(2)). Browsers
     without it drop the declaration and render the same border-radius as a
     plain quarter-circle — correct, just less soft. */
  --r-xs: 8px;                /* 36-40px tags and small chips */
  --r-sm: 10px;               /* nav links, form fields, the objective anchor */
  --r-md: 12px;               /* the 44-50px control height: buttons */
  --r-lg: 18px;               /* popovers, panels, menus */
  --r-xl: 26px;               /* full-bleed art frames */
  --r-circle: 50%;            /* icon-only controls stay circular per HIG */
  --corner: squircle;

  /* HIG's minimum hit region is 44x44pt on iOS/iPadOS/macOS/tvOS/watchOS, and
     its iOS type floor is 11pt — nothing on the site is allowed below either.
     These are rem-where-possible so both still scale with the reader's own
     text-size preference, which Apple calls out as essential: an interface
     that ignores it "can be difficult or impossible to use". */
  --hit: 2.75rem;             /* 44px at the default root size */
  --hit-lg: 3.125rem;         /* 50px: primary CTAs */
  --type-floor: 0.6875rem;    /* 11px */

  /* --- hero backdrop: painted in CSS, never in photographs -------------
     The build already ships the art as real <img> tags (.project-hero-media)
     and as JS stage scenes (orbit / flipbook / gallery), so the theme only
     supplies the gouache wash behind them. Hashed /media/ filenames must not
     appear here: they change every time the site is rebuilt.               */
  --hero-paint:
    radial-gradient(120% 130% at 8% -20%, rgb(104 39 146 / .95), transparent 58%),
    radial-gradient(90% 110% at 92% 4%, rgb(47 143 168 / .50), transparent 60%),
    radial-gradient(70% 90% at 62% 112%, rgb(192 98 143 / .55), transparent 62%),
    linear-gradient(168deg, var(--deep-2) 4%, var(--deep) 58%, #1a0f36 100%);

  /* --- hand-made SVG paint: a tapered brush swash + paper grain ---------- */
  --swash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 26' preserveAspectRatio='none'%3E%3Cpath d='M5 15C42 6 74 20 112 13c42-8 74 8 118 1 34-5 56 2 65-2-14 11-46 8-82 13-38 5-74-8-112-2-36 6-66 2-96-8z' fill='currentColor'/%3E%3C/svg%3E");
  --swash-thick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 40' preserveAspectRatio='none'%3E%3Cpath d='M4 22C40 8 78 30 118 20c44-11 78 12 122 2 32-7 50 3 56-2-8 16-44 14-84 20-40 6-78-10-116-3-36 7-64 4-92-17z' fill='currentColor'/%3E%3C/svg%3E");
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  --brush-edge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 60' preserveAspectRatio='none'%3E%3Cpath d='M0 34c60-14 120 12 190 6s110-24 180-18 120 26 190 20 110-26 180-20 120 24 190 18 110-22 170-14l100 12v22H0z' fill='currentColor'/%3E%3C/svg%3E");
  /* one 35mm sprocket hole, tiled down the edges of an empty film window */
  --sprockets: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='30'%3E%3Crect x='3' y='8' width='14' height='14' rx='4.5' fill='%23fffaee'/%3E%3C/svg%3E");
}

/* ==========================================================================
   1 · The whole page sits on painted paper
   ========================================================================== */
body {
  background:
    radial-gradient(120% 60% at 12% -8%, rgb(201 160 220 / .30), transparent 60%),
    radial-gradient(90% 50% at 96% 4%, rgb(47 143 168 / .16), transparent 62%),
    radial-gradient(70% 46% at 50% 108%, rgb(192 98 143 / .16), transparent 60%),
    var(--paper);
  color: var(--ink);
}
/* a faint canvas tooth over everything, like gouache on cold-press paper.
   Kept at .12 so it never drops text contrast below AA. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  background: var(--grain) repeat 0 0 / 220px 220px;
  opacity: .12;
  mix-blend-mode: multiply;
}

h1, h2, h3, h4 { letter-spacing: -0.015em; }
a { text-decoration: underline; text-underline-offset: .18em; }   /* always visible links */

/* Painted swash under every major heading, drawn on as it enters view.
   Swash colours are chosen so heading ink stays >=4.5:1 on top of them. */
h1, h2 { position: relative; display: inline-block; }
h1::after, h2::after {
  content: "";
  position: absolute;
  left: -0.06em;
  right: -0.10em;
  bottom: -0.28em;
  height: 0.34em;
  /* the SVGs are used as MASKS, not backgrounds: a data-URI SVG in
     background-image cannot inherit currentColor and always paints black.
     Masking lets every swash keep its per-heading colour. */
  background-color: currentColor;
  -webkit-mask: var(--swash-thick) no-repeat center / 100% 100%;
  mask: var(--swash-thick) no-repeat center / 100% 100%;
  color: var(--lav);
  z-index: -1;
  opacity: .95;
}
html.js-paint h1::after, html.js-paint h2::after {
  -webkit-mask-size: 0% 100%;
  mask-size: 0% 100%;
  transition: -webkit-mask-size 1.1s cubic-bezier(.22,.9,.24,1) .15s,
              mask-size 1.1s cubic-bezier(.22,.9,.24,1) .15s;
}
html.js-paint h1.reveal-in::after, html.js-paint h2.reveal-in::after,
html.js-paint .reveal-in h1::after, html.js-paint .reveal-in h2::after {
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}
.section:nth-of-type(odd) h2::after, .showcase h2::after { color: var(--teal); }
.section:nth-of-type(even) h2::after { color: var(--rose); }
.site-footer h2::after { color: var(--lav); }

/* ==========================================================================
   2 · Header: her deep-purple bar with the lavender pill, made richer
   ========================================================================== */
.site-header {
  background:
    linear-gradient(rgb(255 255 255 / .05), rgb(255 255 255 / 0)),
    linear-gradient(100deg, var(--deep) 0%, var(--deep-2) 55%, #3a2166 100%);
  border-bottom: 0;
  box-shadow: 0 10px 26px -14px rgb(34 20 66 / .7);
}
.site-header::after {           /* a brushstroke licking up under the bar */
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -14px;
  height: 16px;
  background-color: currentColor;
  -webkit-mask: var(--brush-edge) no-repeat center / 100% 100%;
  mask: var(--brush-edge) no-repeat center / 100% 100%;
  color: var(--deep);
  pointer-events: none;
}
.brand { color: var(--cream); }
/* the base sheet writes the lockup's tagline in --muted, a light-surface ink;
   on the purple bar it measured 1.9:1 — the worst text on the whole site */
.brand-tag { color: var(--on-deep-muted); }
.nav-link {
  color: var(--cream-soft);
  border-radius: 999px;
  padding: .42em 1.05em;
  text-decoration: none;
  white-space: nowrap;
  transition: background .25s, color .25s, transform .25s;
}
.nav-link:hover { background: rgb(255 250 238 / .16); color: var(--cream); transform: translateY(-1px); }
/* the base sheet paints the "Work" dropdown toggle in ink — invisible on purple */
.nav-link.is-section { color: var(--cream-soft); }
.nav-link.is-section:hover { color: var(--cream); }
/* the current-page pill has to come last: .nav-link.is-section is the same
   specificity, so an earlier pill rule lost and rendered cream-soft on
   lav-pale — a measured 1.02:1, i.e. invisible */
.nav-link[aria-current="page"],
.nav-link.is-section[aria-current="page"] {
  background: var(--lav-pale);
  color: var(--deep);
  font-weight: 700;
  box-shadow: 0 3px 0 rgb(34 20 66 / .35);
}
.nav-link[aria-current="page"]:hover,
.nav-link.is-section[aria-current="page"]:hover { background: var(--cream); color: var(--deep); }
/* the click-to-call link lives in the bar on desktop */
.header-call {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: .45em;
  background: var(--lav-pale);
  color: var(--deep);
  font-weight: 700;
  border-radius: 999px;
  padding: .42em 1em;
  text-decoration: none;
  box-shadow: 0 3px 0 rgb(34 20 66 / .4);
  white-space: nowrap;
}
.header-call:hover { background: var(--cream); transform: translateY(-1px); }
.header-call svg { width: 1em; height: 1em; fill: currentColor; }
/* The bar is a tight fit between its own 860px collapse point and ~1330px:
   six links, a brand lockup and the phone pill. This tier tightens spacing
   only — never type below the floor. Under 1000px the phone pill drops its
   number and keeps the icon (aria-label still names the call; the fixed
   call-bar owns small screens). */
@media (min-width: 861px) and (max-width: 1330px) {
  .nav-list { gap: clamp(2px, .7vw, 12px); }
  .nav-link { font-size: .97rem; padding: .4em .72em; }
  .header-call { padding: .35em .75em; }
  .brand-logo { height: clamp(26px, 3.4vw, 36px); }
}
@media (min-width: 861px) and (max-width: 1000px) {
  .header-call span { display: none; }
  /* the tagline is the first thing to go — otherwise the row runs long and
     flex-shrink quietly squeezes the phone pill below its 44pt floor */
  .brand-tag { display: none; }
  /* icon-only still owes the 44pt floor in both axes: a fixed square with a
     centered glyph, not a squeezed pill */
  .header-call {
    padding: 0;
    width: var(--hit);
    justify-content: center;
  }
}
.subnav {
  background: var(--deep-2);
  border: 1px solid rgb(255 250 238 / .16);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-paint);
  overflow: hidden;
}
.subnav a { color: var(--cream-soft); }
.subnav a:hover { background: rgb(255 250 238 / .14); color: var(--cream); }

/* ==========================================================================
   3 · Hero: a full-bleed painted band, collage layers, floating paint
   ========================================================================== */
.hero-3d, .page-hero {
  position: relative;
  isolation: isolate;
  overflow: visible;
  /* every stop in --hero-paint keeps cream text at >=7:1 (worst case 7.3:1
     over the rose wash), so the artwork stages can sit on top unharmed */
  background: var(--hero-paint);
  color: var(--cream);
}
/* canvas tooth + one broad dry-brush drag, so the wash reads as gouache laid
   on cold-press paper rather than a CSS gradient */
/* canvas tooth only. The old drag screen-blended mid-gray noise at .55, which
   fogged the whole band to lavender and sank the cream copy to ~4:1; soft-light
   keeps the gouache grain without the fog, so cream measures >=10:1 on paint. */
.hero-3d::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--grain) repeat;
  opacity: .35;
  mix-blend-mode: soft-light;
  z-index: -1;
  pointer-events: none;
}
/* the copy column arrives with a near-opaque white spotlight from the base
   sheet (rgba(248,251,252,.96) radial) — invisible-ink territory for cream
   text. The painted band is the seat now. */
.hero-3d-copy::before { content: none; }
/* torn brush edge tearing the painted band into the paper below */
.hero-3d::after, .page-hero::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -24px;
  height: 26px;
  background-color: currentColor;
  -webkit-mask: var(--brush-edge) no-repeat center / 100% 100%;
  mask: var(--brush-edge) no-repeat center / 100% 100%;
  color: var(--deep);
  z-index: 3;
  pointer-events: none;
}

/* project heroes get their art from a real <img> in the build; treat that
   plate like a painting instead of a flat banner */
.project-hero-media img {
  filter: saturate(1.06) contrast(1.04);
  transform: scale(1.015);
}
.project-hero-david,
.project-hero-david .project-hero-media { background: #9ed8d3; }
.project-hero-david .project-hero-media img {
  object-fit: contain;
  object-position: center;
  transform: none;
}
.project-hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--grain) repeat;
  opacity: .2;
  mix-blend-mode: overlay;
  pointer-events: none;
}
/* :not(.objective *) keeps this cream hero ink out of the Objective card, which
   brings its own light ground. Unguarded, `.page-hero p` (0,1,1) outranked the
   card's own `.objective-scope` (0,1,0) and repainted the whole card cream on
   cream — a measured 1.0:1 wherever the brief sits inside a page hero. */
.hero-3d h1, .hero-3d .lead,
.page-hero h1, .page-hero p:not(.objective, .objective *) {
  color: var(--on-deep);
  text-shadow: 0 1px 2px rgb(23 13 48 / .6);
}
.hero-3d .eyebrow, .page-hero .eyebrow {
  color: var(--lav-pale);
  letter-spacing: .22em;
  text-transform: uppercase;
  font-weight: 700;
}
.hero-3d h1 strong, .page-hero h1 strong { color: #ffe9a8; }

/* her pale-lavender pills, sitting on a painted stroke like the original */
.pill-list { position: relative; }
.pill-list::before {
  content: "";
  position: absolute;
  inset: -14px -26px;
  background-color: currentColor;
  -webkit-mask: var(--swash-thick) no-repeat center / 100% 100%;
  mask: var(--swash-thick) no-repeat center / 100% 100%;
  color: rgb(201 160 220 / .5);
  z-index: -1;
  transform: rotate(-1deg);
}
.pill-list li {
  background: var(--lav-pale);
  color: var(--deep);
  border: 0;
  box-shadow: 0 3px 0 rgb(34 20 66 / .4);
  font-weight: 700;
  transform: rotate(-.6deg);
}
.pill-list li:nth-child(2n) { transform: rotate(.8deg); background: #f6e7fb; }

/* Apple's geometry for a text button: a fixed rounded rectangle, never a
   stadium. 999px is half the element's *shortest* side, so the arc ate the flat
   edge and a long CTA on a narrow window smushed into two semicircles; --r-md
   is ~0.22 of the 44px control height — the proportion of an iOS app icon — so
   the corner stays a corner at any width and the label grows by wrapping.
   min-height is the HIG 44pt hit region in rem, so it scales with the reader's
   own text size. §12e carries the rest of the control geometry. */
.btn {
  min-height: var(--hit);
  padding: 0 1.25rem;
  border-radius: var(--r-md);
  line-height: 1.25;
  white-space: normal;
  text-wrap: balance;
  background: var(--lav-pale);
  color: var(--deep);
  font-weight: 700;
  text-decoration: none;
  box-shadow: 0 4px 0 rgb(34 20 66 / .45);
  transition: transform .2s, box-shadow .2s, background .2s;
}
.btn:hover { transform: translateY(-2px) rotate(-.5deg); box-shadow: 0 6px 0 rgb(34 20 66 / .45); }
/* Outlined button. On a light ground it takes --accent ink and hairline (8.5:1
   on paper, 5.4:1 on the lavender wash); on paint it flips to cream. Scoping the
   flip to the dark bands is what keeps it legible — an unscoped cream outline
   measured 1.6:1 against the about page's light section. */
.btn-outline {
  background: transparent;
  color: var(--accent);
  border: 2px solid var(--accent);
  box-shadow: none;
}
.btn-outline:hover { background: rgb(104 39 146 / .1); }

:is(.hero-3d, .page-hero, .project-head, .info-card, .callout, .commission,
    .site-footer) .btn-outline {
  color: var(--on-deep);
  border-color: var(--lav);
}
:is(.hero-3d, .page-hero, .project-head, .info-card, .callout, .commission,
    .site-footer) .btn-outline:hover { background: rgb(255 250 238 / .14); }

/* the 3D stage / portrait becomes a torn painting pinned to the band */
.stage3d, .portrait { position: relative; isolation: isolate; }
.stage-fallback > img, .portrait img {
  border-radius: 6px;
  box-shadow: 0 0 0 10px var(--cream), 0 0 0 12px rgb(34 20 66 / .5), var(--shadow-paint);
  transform: rotate(-1.4deg);
}
.stage-fallback::before, .portrait::before {   /* rotated colour slab behind */
  content: "";
  position: absolute;
  inset: 6% -5% -5% 7%;
  background: var(--rose);
  border-radius: var(--radius);
  transform: rotate(3.4deg);
  z-index: -1;
  opacity: .9;
}
/* painter's tape at the top */
.stage-fallback::after, .portrait::after {
  content: "";
  position: absolute;
  top: -14px; left: 50%;
  width: 92px; height: 26px;
  background: rgb(255 250 238 / .8);
  transform: translateX(-50%) rotate(-3deg);
  box-shadow: 0 2px 6px rgb(34 20 66 / .25);
  z-index: 2;
}

.avatar-greet a {
  background: var(--cream);
  color: var(--deep);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-paint);
  padding: .5em .9em;
  display: inline-block;
  transform: rotate(-2deg);
}

/* floating paint specks injected by maximal.js */
.speck-field { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 1; }
.speck-field i {
  position: absolute;
  border-radius: 50%;
  background: currentColor;
  opacity: .45;
  animation: speck-float 9s ease-in-out infinite alternate;
}
@keyframes speck-float {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(var(--dx, 12px), var(--dy, -26px), 0) scale(1.25); }
}

/* ==========================================================================
   4 · Cards & figures: cut-out artwork over rotated colour slabs
   ========================================================================== */
.card, .tile, .figure, .gallery > li, .zoom {
  position: relative;
  isolation: isolate;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.card::before, .tile::before, .figure::before, .gallery > li::before {
  content: "";
  position: absolute;
  inset: 5% -4% -4% 6%;
  background: var(--slab, var(--slab-1));
  border-radius: var(--radius);
  transform: rotate(-2.6deg);
  z-index: -1;
  transition: transform .35s cubic-bezier(.22,.9,.24,1);
}
.card:nth-child(4n+1) { --slab: var(--slab-1); }
.card:nth-child(4n+2) { --slab: var(--slab-2); }
.card:nth-child(4n+3) { --slab: var(--slab-3); }
.card:nth-child(4n+4) { --slab: var(--slab-4); }
.card:hover::before, .tile:hover::before, .figure:hover::before { transform: rotate(-4.4deg) scale(1.02); }

.card-media, .tile img, .figure img, .zoom img {
  border-radius: 4px;
  box-shadow: 0 0 0 8px var(--cream), 0 0 0 9px rgb(34 20 66 / .35), var(--shadow-paint);
  transform: rotate(.7deg);
  transition: transform .35s cubic-bezier(.22,.9,.24,1);
  background: var(--surface);
}
.card:nth-child(2n) .card-media, .gallery > li:nth-child(2n) img { transform: rotate(-.9deg); }
.card:hover .card-media, .tile:hover img, .figure:hover img { transform: rotate(0deg) scale(1.025); }

.card-media--contain { background: #9ed8d3; }
.card-media--contain img,
.card:hover .card-media--contain img {
  object-fit: contain;
  transform: none;
}
.card-body { padding: 1.2rem 1.15rem 1.35rem; background: transparent; }
.card-body h3 { color: var(--deep); }
.card-body p { color: var(--ink-soft); }
.card-more { color: var(--accent); font-weight: 700; }

/* denser, more collage-like grids */
.card-grid { gap: clamp(2.2rem, 4.5vw, 4rem) clamp(1.6rem, 3vw, 2.8rem); }
.track { gap: clamp(1.8rem, 3.4vw, 3rem); }

.icon-btn {
  background: var(--deep);
  color: var(--cream);
  border: 0;
  box-shadow: 0 4px 0 rgb(34 20 66 / .35);
  border-radius: 50%;
}
.icon-btn:hover { background: var(--accent); transform: translateY(-2px); }

/* ==========================================================================
   5 · A painted marquee ribbon between hero and work
   ========================================================================== */
.paint-marquee {
  position: relative;
  z-index: 3;
  margin: -1.2rem 0 2.6rem;
  padding: .8rem 0;
  background: linear-gradient(95deg, var(--deep), #3a2166 60%, var(--deep-2));
  color: var(--cream);
  transform: rotate(-1.3deg) scaleX(1.04);
  box-shadow: var(--shadow-paint);
  overflow: hidden;
}
.paint-marquee .mq-track {
  display: flex;
  gap: 2.6rem;
  width: max-content;
  animation: mq-scroll 34s linear infinite;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  font-size: .92rem;
}
.paint-marquee .mq-track span { display: inline-flex; align-items: center; gap: 2.6rem; }
.paint-marquee .mq-track span::after { content: "✦"; color: var(--lav); }
.paint-marquee:hover .mq-track, .paint-marquee:focus-within .mq-track { animation-play-state: paused; }
@keyframes mq-scroll { to { transform: translateX(-50%); } }

/* ==========================================================================
   6 · Project / about / paintings pages
   ========================================================================== */
.page-hero { padding-block: clamp(3.5rem, 9vw, 7rem); }
.project-head, .info-card, .callout {
  background: linear-gradient(120deg, var(--deep), var(--deep-2));
  color: var(--cream);
  border: 0;
  border-radius: var(--radius);
  box-shadow: var(--shadow-paint);
  transform: rotate(-.5deg);
}
.project-head { padding: clamp(1.25rem, 3vw, 2rem); }
.project-head h1, .info-card h2, .info-card h3, .callout h2, .callout h3 { color: var(--cream); }
.project-head p, .info-card p, .callout p { color: var(--cream-soft); }

/* the original put cream headings on white (illegible). Cream now only ever
   sits on paint; on paper we use deep purple. */
.project h2, .project h3 { color: var(--deep); }
.project h2::after { color: var(--ochre); }
/* …but the painted title cards keep cream ink. This must come AFTER the rule
   above: same specificity, and .project-head h2 lives inside .project. */
.project-head h2, .project-head h3,
.info-card h2, .info-card h3,
.callout h2, .callout h3 { color: var(--cream); }

.embed, .sprite-row > li {
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 8px var(--cream), var(--shadow-paint);
}
.pager a {
  background: var(--lav-pale);
  color: var(--deep);
  border-radius: 999px;
  font-weight: 700;
  box-shadow: 0 4px 0 rgb(34 20 66 / .35);
}

.gallery.project-gallery {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(.8rem, 1.8vw, 1.25rem);
  margin-top: clamp(2rem, 5vw, 4rem);
}
.project-gallery > li {
  grid-column: span 3;
  min-width: 0;
  margin: 0;
  padding: .65rem;
  background: var(--cream);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-paint);
}
.project-gallery > li::before { content: none; }
.project-gallery > li:only-child { grid-column: 1 / -1; }
.project-gallery-5 > li:nth-last-child(-n + 3) { grid-column: span 2; }
.project-gallery figure {
  display: grid;
  grid-template-rows: 1fr auto;
  height: 100%;
}
.project-gallery .zoom {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  background: #120d1c;
  border-radius: var(--r-sm);
}
.project-gallery .zoom img,
.project-gallery > li:nth-child(2n) img,
.project-gallery .zoom:hover img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform: none;
  box-shadow: none;
}
.project-gallery figcaption {
  min-height: 3.2rem;
  padding: .8rem .45rem .25rem;
  color: var(--ink);
  font-weight: 600;
}
@media (max-width: 760px) {
  .project-gallery > li,
  .project-gallery-5 > li:nth-last-child(-n + 3) { grid-column: 1 / -1; }
}

/* About: the avatar becomes a painted portrait in a taped, gilded frame */
.about-grid .stage3d, .about-grid .stage-avatar { isolation: isolate; }
.about-grid .stage-fallback > img {
  box-shadow: 0 0 0 12px var(--cream), 0 0 0 14px var(--ochre), var(--shadow-paint);
  border-radius: 4px;
  transform: rotate(-1deg);
}

/* ==========================================================================
   6b · The Objective brief card — one recognisable object on every project
   --------------------------------------------------------------------------
   Markup comes from tools/objectives.py (v2/objectives.py). It reads as a
   cream index card pinned to the painting with tape: same card, same target
   mark, same lavender-swashed "Objective" heading on every page, so a
   recruiter scanning seven projects always knows where to look.

   `isolation: isolate` is load-bearing: the global h2::after swash uses
   z-index:-1, which would otherwise paint *behind* the card's own background.
   ========================================================================== */
/* --- placement: the brief docks BELOW the hero, in normal flow ------------
   The card used to be injected inside .project-hero-copy. Measured in a
   1581x1145 window that stretched the hero to ~1100px purely to hold a
   593x606 opaque cream card, covering 22% of the full-bleed artwork. Nothing
   is positioned over the painting now: objectives.py emits
   <aside class="objective objective-dock"> immediately after the hero's closing
   </section>, in the document flow, on paper — where its cream ground reads as
   an index card on the desk instead of a placard over the artwork.

   With the card gone the hero falls back to its min-height, so raise it: the
   point of the move is to show more artwork, not less. */
.project-hero { min-height: clamp(480px, 76vh, 800px); }
.project-hero-copy { padding-bottom: clamp(30px, 6vh, 66px); }

/* The hero keeps a one-line chip pointing at the card. It sits in flow with the
   headline, in the same family as the nav's current-page pill, so it reads as
   part of the painted bar rather than as new chrome — never absolutely
   positioned over the artwork. --deep on --lav-pale is 12.4:1. The arrow says
   "the brief is below". */
.objective-anchor {
  display: inline-flex;
  align-items: center;
  gap: .5em;
  min-height: var(--hit);
  margin-top: 1.2rem;
  padding: .5em max(1.15em, 18px);
  background: var(--lav-pale);
  color: var(--deep);
  border-radius: var(--r-sm);
  corner-shape: var(--corner);
  font-size: .95rem;
  font-weight: 700;
  letter-spacing: .04em;
  line-height: 1.25;
  text-decoration: none;
  box-shadow: 0 3px 0 rgb(34 20 66 / .4);
  transition: transform .2s, box-shadow .2s, background .2s;
}
.objective-anchor:hover {
  background: var(--cream);
  transform: translateY(-1px);
  box-shadow: 0 4px 0 rgb(34 20 66 / .4);
}
.objective-anchor:focus-visible { outline: 3px solid var(--cream); outline-offset: 3px; }
.objective-anchor-mark { flex: none; width: 1.35em; height: 1.35em; color: var(--accent); }
.objective-anchor-arrow { flex: none; width: .95em; height: .95em; opacity: .72; }

/* The docked card. It lands as a direct child of <main>, outside .container, so
   it brings its own gutter and its own measure: a painted band across the page,
   capped at 62rem and centred, so the cream card still reads as one object
   rather than a wall of cream. The negative top margin tucks its taped edge
   under the hero's torn brush stroke, which is what makes hero + brief look
   like a single thing laid on the desk.

   Scoped `.objective.objective-dock` because both classes sit on the same
   element here; an unscoped `.objective-dock` would also catch a wrapper-style
   dock (the card inside an .objective-dock <div>), and the two need different
   jobs — the wrapper gets breathing room, the card gets a measure. */
.objective.objective-dock {
  max-width: min(62rem, calc(100% - 2 * var(--gutter, 1.5rem)));
  margin: -1.2rem auto 2.4rem;
  padding: 1.6rem clamp(1.5rem, 4vw, 2.6rem) 1.5rem 2.4rem;
  transform: rotate(-.35deg);
}
/* a band is wide, so cap the reading measure inside it: the brief and each spec
   line stay at a comfortable line length while the card itself stays full */
.objective.objective-dock .objective-text,
.objective.objective-dock .objective-spec dd { max-width: 68ch; }
/* wrapper-style dock (older markup / native port): room below the hero, and the
   card inside it keeps its own 58ch measure, left-aligned in its .container */
.objective-dock:not(.objective) { padding-block: clamp(34px, 7vh, 76px) clamp(6px, 2vh, 24px); }
.objective-dock:not(.objective) .objective { margin: 0; }
.objective-dock:not(.objective) + .objective-dock:not(.objective) { padding-top: 0; }

/* the stock veil fades to transparent too early: the lead washed out over
   light artwork. A heavier middle stop keeps cream-soft text >=4.5:1. */
.project-hero::after {
  background: linear-gradient(180deg,
    rgb(23 13 48 / .40) 0%,
    rgb(23 13 48 / .68) 52%,
    rgb(23 13 48 / .88) 100%);
}

.objective {
  position: relative;
  isolation: isolate;
  max-width: 58ch;
  margin: 1.7rem 0 0;
  padding: 1.35rem 1.5rem 1.3rem 2rem;
  background: linear-gradient(168deg, #fffdf6 12%, var(--cream));
  color: var(--ink);
  border-radius: 6px 14px 8px 12px / 12px 6px 14px 8px;
  box-shadow: 0 0 0 2px var(--lav), var(--shadow-paint);
  transform: rotate(-.6deg);
  scroll-margin-top: 96px;
}
.objective:target { box-shadow: 0 0 0 3px var(--crimson), var(--shadow-paint); }

/* the painted purple spine, like a file tab */
.objective::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 13px;
  background: linear-gradient(180deg, var(--deep) 8%, var(--accent));
  border-radius: 6px 0 0 12px;
}
/* painter's tape pinning it to the page */
.objective::after {
  content: "";
  position: absolute;
  top: -13px; right: 30px;
  width: 108px; height: 27px;
  background: rgb(201 160 220 / .82);
  box-shadow: 0 2px 6px rgb(34 20 66 / .28);
  transform: rotate(2.6deg);
  z-index: 3;
}

.objective-head { display: flex; align-items: flex-start; gap: .85rem; }
.objective-mark {
  flex: none;
  width: 40px; height: 40px;
  padding: 7px;
  color: var(--crimson);              /* 6.4:1 on the pale disc */
  background: var(--lav-pale);
  border-radius: 52% 48% 46% 54% / 48% 52% 50% 46%;
  box-shadow: 0 0 0 2px rgb(179 32 44 / .3);
  transform: rotate(-7deg);
}
.objective-head-text { min-width: 0; }
/* id selector so the section-level teal/rose swash rules can't out-rank it */
.objective h2#objective-title {
  margin: 0;
  color: var(--deep);                 /* 15.4:1 on cream */
  font-size: clamp(1.1rem, 2.3vw, 1.42rem);
  letter-spacing: .14em;
  text-transform: uppercase;
}
.objective h2#objective-title::after { color: var(--lav); }
.objective-scope {
  margin: .7em 0 0;
  color: var(--ink-soft);             /* 8.1:1 on cream */
  font-size: .8rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
}

.objective-text {
  margin: 1.05rem 0 0;
  max-width: 56ch;
  color: var(--ink);                  /* 12.9:1 on cream */
  font-size: clamp(1.02rem, 1.5vw, 1.13rem);
  font-weight: 600;
  line-height: 1.6;
}

.objective-spec {
  display: grid;
  gap: .72rem;
  margin: 1.15rem 0 0;
  padding-top: 1rem;
  border-top: 2px dashed rgb(104 39 146 / .3);
}
.objective-spec > div {
  display: grid;
  grid-template-columns: minmax(7rem, 9rem) 1fr;
  gap: .25rem .95rem;
  align-items: start;
}
.objective-spec dt {
  margin: 0;
  padding-top: .4em;
  color: var(--muted);                /* 5.8:1 on cream */
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.objective-spec dd { margin: 0; color: var(--ink); font-weight: 600; }

.objective-chips, .objective-deliverables {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.objective-chips li {
  padding: .26em .8em;
  background: var(--lav-pale);
  color: var(--deep);                 /* 12.4:1 */
  border-radius: 999px;
  box-shadow: 0 2px 0 rgb(34 20 66 / .22);
  font-size: .82rem;
  font-weight: 700;
  transform: rotate(-.7deg);
}
.objective-chips li:nth-child(2n) { background: #f6e7fb; transform: rotate(.9deg); }
.objective-deliverables { gap: .3rem 1.1rem; }
.objective-deliverables li {
  position: relative;
  padding-left: 1.4em;
  font-size: .93rem;
}
/* a small teal brush dash as the bullet */
.objective-deliverables li::before {
  content: "";
  position: absolute;
  left: 0; top: .58em;
  width: .78em; height: .4em;
  background-color: currentColor;
  -webkit-mask: var(--swash) no-repeat center / 100% 100%;
  mask: var(--swash) no-repeat center / 100% 100%;
  color: var(--teal);                 /* 3.6:1 non-text marker */
}

.objective-context {
  margin: 1.15rem 0 0;
  padding: .8rem 1rem;
  background: var(--lav-pale);
  border-left: 5px solid var(--accent);
  border-radius: var(--radius-sm);
  color: var(--ink);                  /* 9.2:1 on lavender-pale */
  font-size: .95rem;
  line-height: 1.55;
}
.objective-context strong {
  color: var(--accent);               /* 6.9:1 on lavender-pale */
  font-size: .76rem;
  letter-spacing: .1em;
  text-transform: uppercase;
}

@media (max-width: 720px) {
  .objective { margin-top: 1.3rem; padding: 1.2rem 1.1rem 1.15rem 1.5rem; transform: rotate(-.35deg); }
  /* the dock keeps its tuck under the hero but gives back the wide-band padding,
     so the spec rows still have room on a phone */
  .objective.objective-dock { margin: -.8rem auto 1.8rem; padding: 1.25rem 1.1rem 1.15rem 1.5rem; }
  .objective::before { width: 10px; }
  .objective::after { right: 16px; width: 88px; }
  .objective-spec > div { grid-template-columns: 1fr; gap: .3rem; }
  .objective-spec dt { padding-top: 0; }
}
@media print {
  .objective { transform: none; box-shadow: none; break-inside: avoid; }
  .objective::after { display: none; }
}

/* ==========================================================================
   7 · Real application icons orbiting the avatar
   ========================================================================== */
.app-orbit { position: absolute; inset: 0; pointer-events: none; z-index: 5; }
.app-icon {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--s, 62px);
  height: var(--s, 62px);
  margin: calc(var(--s, 62px) / -2);
  pointer-events: auto;
  display: grid;
  place-items: center;
  background: linear-gradient(150deg, #fffdf7, #f3e9da);
  color: var(--brand, var(--deep));
  border: 0;
  border-radius: 38% 62% 55% 45% / 50% 44% 56% 50%;
  box-shadow: 0 0 0 3px var(--cream), 0 0 0 5px rgb(34 20 66 / .35), 0 10px 22px -8px rgb(34 20 66 / .5);
  transform: rotate(var(--r, 0deg));
  animation: icon-bob 6.5s ease-in-out infinite;
  animation-delay: var(--d, 0s);
  transition: transform .3s cubic-bezier(.22,.9,.24,1), box-shadow .3s;
  cursor: pointer;
}
.app-icon svg { width: 62%; height: 62%; fill: currentColor; }
.app-icon:hover, .app-icon:focus-visible {
  transform: rotate(0deg) scale(1.16);
  box-shadow: 0 0 0 3px var(--cream), 0 0 0 6px var(--brand, var(--deep)), 0 14px 26px -8px rgb(34 20 66 / .55);
  z-index: 6;
}
.app-icon .app-icon-label {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) rotate(-2deg);
  background: var(--deep);
  color: var(--cream);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  padding: .28em .7em;
  border-radius: 999px;
  white-space: nowrap;
  opacity: 0;
  transition: opacity .25s;
  pointer-events: none;
}
.app-icon:hover .app-icon-label, .app-icon:focus-visible .app-icon-label { opacity: 1; }
@keyframes icon-bob {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -12px; }
}

/* restyle the existing skill chips as painted tiles too */
.avatar-tool, .tool-icon {
  border-radius: 38% 62% 55% 45% / 50% 44% 56% 50%;
  box-shadow: 0 0 0 3px var(--cream), 0 6px 16px -6px rgb(34 20 66 / .5);
}

/* ==========================================================================
   8 · Marginalia & stickers — the handmade clutter that says "artist"
   ========================================================================== */
.sticker {
  display: inline-block;
  background: var(--cream);
  color: var(--deep);
  border-radius: 999px;
  padding: .3em .9em;
  font-weight: 700;
  font-size: .8rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  box-shadow: 0 3px 0 rgb(34 20 66 / .35);
  transform: rotate(-2deg);
}
.sticker.alt { background: var(--lav); color: var(--deep); transform: rotate(1.6deg); }
.marginalia { font-style: italic; color: var(--ink-soft); transform: rotate(-1.2deg); }

/* ==========================================================================
   9 · Start a project + pay now (commissions)
   ========================================================================== */
.commission {
  position: relative;
  isolation: isolate;
  background: linear-gradient(140deg, var(--deep), var(--deep-2) 70%, #170d30);
  color: var(--cream-soft);
  border-radius: var(--radius);
  box-shadow: var(--shadow-paint);
  padding: clamp(1.6rem, 4vw, 3rem);
  transform: rotate(-.4deg);
}
.commission h2, .commission h3 { color: var(--cream); }
.commission h2::after { color: var(--lav); }
.commission p, .commission li { color: var(--cream-soft); }
.commission a:not(.btn) { color: var(--lav-pale); }

.commission-grid { display: grid; gap: clamp(1.4rem, 3vw, 2.4rem); grid-template-columns: 1.1fr .9fr; }
@media (max-width: 900px) { .commission-grid { grid-template-columns: 1fr; } }

/* service picker: painted tiles, one per kind of work */
.svc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .8rem; }
.svc-list li { position: relative; }
.svc-list input[type="radio"], .svc-list input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 100%; height: 100%;
  margin: 0;
  cursor: pointer;
}
.svc-list label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  background: rgb(255 250 238 / .08);
  border: 2px solid rgb(255 250 238 / .22);
  border-radius: var(--radius-sm);
  padding: .8rem 1rem;
  cursor: pointer;
  color: var(--cream);
  font-weight: 700;
  transition: background .2s, border-color .2s, transform .2s;
}
.svc-list label small { display: block; font-weight: 400; color: var(--cream-soft); }
.svc-list .svc-price { color: #ffe9a8; font-weight: 700; white-space: nowrap; }
.svc-list input:checked + label {
  background: var(--lav-pale);
  color: var(--deep);
  border-color: var(--cream);
  transform: rotate(-.6deg);
}
.svc-list input:checked + label small { color: var(--ink-soft); }
.svc-list input:checked + label .svc-price { color: var(--accent); }
.svc-list input:focus-visible + label { outline: 3px solid var(--lav); outline-offset: 3px; }

/* the running quote card */
.quote-card {
  background: var(--cream);
  color: var(--ink);
  border-radius: var(--radius);
  padding: 1.3rem 1.4rem;
  box-shadow: 0 0 0 4px rgb(34 20 66 / .35), var(--shadow-paint);
  transform: rotate(.6deg);
  position: sticky;
  top: calc(var(--header-h, 76px) + 16px);
}
.quote-card h3 { color: var(--deep); }
.quote-card .quote-total {
  font-size: clamp(1.8rem, 1.2rem + 2vw, 2.6rem);
  font-weight: 700;
  color: var(--deep);
  line-height: 1.1;
}
.quote-card .quote-note { color: var(--ink-soft); font-size: .9rem; }
.quote-card dl { display: grid; grid-template-columns: 1fr auto; gap: .3rem 1rem; margin: 0 0 1rem; }
.quote-card dt { color: var(--ink-soft); }
.quote-card dd { margin: 0; font-weight: 700; color: var(--deep); }

/* pay + call actions */
.pay-row { display: flex; flex-wrap: wrap; gap: .8rem; align-items: center; margin-top: 1rem; }
.btn-pay {
  background: var(--blender);
  color: #241300;                 /* 7.1:1 on the orange */
  font-weight: 700;
  min-height: var(--hit-lg);
  border-radius: var(--r-md);
  padding: .8em 1.5em;
  text-decoration: none;
  box-shadow: 0 5px 0 rgb(34 20 66 / .5);
  display: inline-flex;
  align-items: center;
  gap: .5em;
}
.btn-pay:hover { transform: translateY(-2px); box-shadow: 0 7px 0 rgb(34 20 66 / .5); }
.btn-pay svg { width: 1.1em; height: 1.1em; fill: currentColor; }
.btn-call {
  background: var(--cream);
  color: var(--deep);
  min-height: var(--hit-lg);
  border-radius: var(--r-md);
  padding: .8em 1.4em;
  font-weight: 700;
  text-decoration: none;
  box-shadow: 0 5px 0 rgb(34 20 66 / .5);
  display: inline-flex;
  align-items: center;
  gap: .5em;
}
.btn-call:hover { transform: translateY(-2px); }
.btn-call svg { width: 1.05em; height: 1.05em; fill: currentColor; }

/* FAQ (also emitted as FAQPage schema) */
.faq details {
  background: rgb(255 250 238 / .07);
  border: 1px solid rgb(255 250 238 / .2);
  border-radius: var(--radius-sm);
  padding: .8rem 1rem;
  margin-bottom: .7rem;
}
.faq summary { cursor: pointer; font-weight: 700; color: var(--cream); }
.faq details[open] { background: rgb(255 250 238 / .12); }
.faq p { margin: .6rem 0 0; }

/* ==========================================================================
   10 · Click-to-call: always within reach
   ========================================================================== */
.call-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 70;
  display: none;
  background: linear-gradient(95deg, var(--deep), var(--deep-2));
  color: var(--cream);
  padding: .7rem 1rem calc(.7rem + env(safe-area-inset-bottom, 0px));
  box-shadow: 0 -8px 24px -10px rgb(34 20 66 / .6);
}
.call-bar a {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .6em;
  color: var(--cream);
  font-weight: 700;
  text-decoration: none;
  font-size: 1.05rem;
}
.call-bar svg { width: 1.2em; height: 1.2em; fill: currentColor; }
@media (max-width: 860px) {
  .call-bar { display: block; }
  body { padding-bottom: 64px; }      /* keep footer text clear of the bar */
  .header-call { display: none; }
}

/* ==========================================================================
   11 · Footer: her dark-purple Get In Touch world, expanded
   ========================================================================== */
.site-footer {
  position: relative;
  background: linear-gradient(160deg, var(--deep) 0%, var(--deep-2) 60%, #170d30 100%);
  color: var(--cream-soft);
  margin-top: 3rem;
}
.site-footer::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: -24px;
  height: 26px;
  background-color: currentColor;
  -webkit-mask: var(--brush-edge) no-repeat center / 100% 100%;
  mask: var(--brush-edge) no-repeat center / 100% 100%;
  color: var(--deep);
  pointer-events: none;
}
.site-footer h2, .site-footer h3 { color: var(--cream); }
.site-footer p, .site-footer li, .site-footer a, .site-footer .muted { color: var(--cream-soft); }
.site-footer a:hover { color: var(--cream); }
.footer-cta {
  background: rgb(255 250 238 / .06);
  border: 1px solid rgb(255 250 238 / .16);
  border-radius: var(--radius);
  transform: rotate(-.4deg);
}
.form input, .form textarea {
  background: var(--cream);
  color: var(--deep);
  border: 0;
  border-radius: var(--radius-sm);
}
.form label { color: var(--cream); font-weight: 700; }
.form-status { color: var(--lav-pale); }
.btn-light { background: var(--lav-pale); color: var(--deep); box-shadow: 0 4px 0 rgb(0 0 0 / .35); }
.footer-bottom { border-top: 1px solid rgb(255 250 238 / .16); }
/* The footer carries no notes about how the page was made — no build-process
   commentary anywhere on the site, ever. maximal.js deletes the node outright;
   this hides it first (no flash) and covers the case where the line is still
   emitted. Swap-time patch: delete the <p class="small">Hand-built…</p> line
   in tools/build.py so it never ships again. */
.footer-bottom > p.small { display: none; }

/* ==========================================================================
   12 · Motion: reveals that feel painted on, not slid in
   ========================================================================== */
html.js-paint .scroll-3d {
  opacity: calc(.15 + .85 * var(--enter, 1));
  transform:
    perspective(900px)
    rotateX(calc((1 - var(--enter, 1)) * 10deg))
    rotateZ(calc((1 - var(--enter, 1)) * var(--rz, -1.4deg)))
    translateY(calc((1 - var(--enter, 1)) * 34px));
}
html.js-paint .scroll-3d.from-right { --rz: 1.4deg; }

/* ==========================================================================
   12b · Pieces that maximal.js injects into the page
   ========================================================================== */
.app-icon, .app-icon .app-icon-label, .header-call, .call-bar a,
.btn-pay, .btn-call, .nav-link-cta, .quote-mail { text-decoration: none; }

/* the rebranded chips swap stroke line-art for solid brand marks */
.tool-icon svg { fill: currentColor; stroke: none; }
.avatar-tool-label { line-height: 1.25; text-align: center; }

/* "Start a project" is the one nav item that should shout */
.nav-link-cta {
  background: var(--lav-pale);
  color: var(--deep);
  font-weight: 700;
  box-shadow: 0 3px 0 rgb(34 20 66 / .4);
}
.nav-link-cta:hover {
  background: var(--cream);
  color: var(--deep);
  transform: translateY(-1px);
}
.nav-commissions { margin-left: .4rem; }

/* quote card: email fallback + deposit line */
.quote-mail {
  display: inline-flex;
  align-items: center;
  gap: .5em;
  margin-top: .7rem;
  color: var(--accent);
  font-weight: 700;
}
.quote-mail:hover { color: var(--deep); }
.quote-deposit-line { color: var(--ink-soft); font-size: .95rem; margin: .2rem 0 0; }
.quote-deposit-line b { color: var(--deep); }

/* ==========================================================================
   12c · The story reel — film windows, and the GoomVille act
   --------------------------------------------------------------------------
   maximal.js regroups /story/ into two animatic acts followed by one GoomVille
   act, and drops a painted film window under each animatic that has no cut
   yet. Every video window on the site gets the same treatment: a cream-framed
   screen at the full width of its column, so a turntable or an animatic reads
   as a cinema window instead of a thumbnail.

   The base sheet caps .video-row figures at 420px — a 1920x1080 turntable in a
   postage stamp. A row holds one film on this site, so let it fill the row;
   if a second film ever joins one, the flex row halves them and both still
   get the frame.
   ========================================================================== */
.video-row .figure { max-width: 100%; }
.video-row .figure video,
.story-act .figure video,
.film-slot .film-screen {
  width: 100%;
  border-radius: var(--r-md);
  corner-shape: var(--corner);
  box-shadow: 0 0 0 8px var(--cream), var(--shadow-paint);
}
.video-row .figure video,
.story-act .figure video { aspect-ratio: 16 / 9; object-fit: contain; background: #000; }

/* an embedded film (YouTube, Canva) gets the same cream cinema frame; the
   iframe fills it edge to edge */
.film-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: var(--r-md);
  corner-shape: var(--corner);
  box-shadow: 0 0 0 8px var(--cream), var(--shadow-paint);
  overflow: hidden;
}
.film-frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* an empty slot is still a window: deep paint, sprocket holes, a hand-drawn
   play ring. Nothing about it says "broken player". */
.film-slot { margin: 1.15rem 0 0; }
.film-screen {
  position: relative;
  aspect-ratio: 16 / 9;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: .5rem;
  padding: clamp(20px, 5vw, 52px) clamp(36px, 8vw, 84px);
  text-align: center;
  background:
    radial-gradient(120% 95% at 50% 0%, rgb(104 39 146 / .5) 0%, transparent 62%),
    linear-gradient(168deg, var(--deep-2) 0%, #170d30 82%);
  border-radius: var(--r-md);
  corner-shape: var(--corner);
  box-shadow: 0 0 0 8px var(--cream), var(--shadow-paint);
  overflow: hidden;
  transform: rotate(-.35deg);
}
.film-screen::before, .film-screen::after {
  content: "";
  position: absolute;
  top: 12px; bottom: 12px;
  width: 20px;
  background: var(--sprockets) repeat-y center top / 20px 30px;
  opacity: .5;
}
.film-screen::before { left: 10px; }
.film-screen::after { right: 10px; }
.film-screen-play {
  width: clamp(46px, 7vw, 66px);
  height: auto;
  color: var(--ochre);
  filter: drop-shadow(0 3px 0 rgb(0 0 0 / .35));
}
.film-screen-title {
  margin: 0;
  color: var(--cream);
  font-size: clamp(1.05rem, 2.2vw, 1.35rem);
  font-weight: 800;
  letter-spacing: .02em;
}
.film-screen-note {
  margin: 0;
  max-width: 36ch;
  color: var(--lav);
  font-size: .92rem;
  font-weight: 600;
  line-height: 1.5;
}
.film-slot figcaption, .story-act .figure figcaption { color: var(--ink-soft); }

/* the GoomVille act: one heading, the film, then everything painted for it */
.story-act-intro { max-width: 62ch; }
.story-act h3 { margin: 2.4rem 0 1rem; }
.story-act .gallery { margin: 0; }

/* ==========================================================================
   12d · The troupe & the painted portrait
   --------------------------------------------------------------------------
   maximal.js §0 retires the rendered avatar: the home hero is now five of
   Maggie's own characters pinned to the painted band like studio postcards, and
   the About hero is her real gouache self-portrait, in the taped frame the
   stage already had. The model stays on disk — this is a change of composition,
   not a deletion, so a better sculpt can be lit again.

   Each card is placed by custom properties written inline per card: --x/--y
   against the stage box, --w as a share of its width, --rot the angle of the
   pin, --z the stack, --dz its parallax depth, --d the offset of its breathing,
   --pos where its face sits inside the 4:3 crop. Percentages of the stage
   rather than of the viewport, so the whole collage scales with the hero.

   rotate / scale / translate are separate properties on purpose. They compose
   with each other, and none of them overwrites the Ken Burns animation running
   on the <img> inside — a shorthand `transform` would have killed it.
   ========================================================================== */
.troupe { position: absolute; inset: 0; }
.troupe-card {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  z-index: var(--z);
  margin: 0;
  padding: clamp(6px, 1.1vw, 12px);
  background: var(--cream);
  border-radius: var(--r-sm);
  corner-shape: var(--corner);
  box-shadow: var(--shadow-paint);
  rotate: var(--rot);
  translate: calc(var(--mx, 0) * var(--dz) * 1px) calc(var(--my, 0) * var(--dz) * 1px);
  transition: rotate .45s cubic-bezier(.22,.9,.24,1),
              scale .45s cubic-bezier(.22,.9,.24,1),
              translate .45s cubic-bezier(.22,.9,.24,1),
              box-shadow .45s;
}
.troupe-card img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  object-position: var(--pos, 50% 38%);
  border-radius: var(--r-xs);
  box-shadow: none;
  transform: none;
  /* the crop drifts a hair, so a still frame still feels like a room with air
     in it; --d staggers the five so they never breathe in step */
  animation: troupe-breathe 16s ease-in-out var(--d, 0s) infinite alternate;
}
.troupe-card--contain { background: #9ed8d3; }
.troupe-card--contain img {
  object-fit: contain;
  animation: none;
}
@keyframes troupe-breathe {
  from { transform: scale(1.05); }
  to   { transform: scale(1.13) translate3d(-1.5%, -1.2%, 0); }
}
/* straightening a postcard you put your finger on */
.troupe-card:hover, .troupe-card:focus-within {
  rotate: 0deg;
  scale: 1.035;
  z-index: 9;
  box-shadow: 0 3px 0 rgb(34 20 66 / .12), 0 22px 44px -14px rgb(34 20 66 / .5);
}

/* The greeting used to hang beside a rendered head, at 70% across and a third
   down. That spot is now the zombie postcard's forehead. The one part of the
   stage that no card reaches and the copy column never grows into is the bottom
   right — the copy is capped at min(46%, 600px) on the left, and every card
   ends above 86% of the box — so the pill sits there, pinned to the collage
   like the label on the back of a frame. */
.home-hero .avatar-greet {
  left: auto;
  right: 6%;
  top: auto;
  bottom: 3.5%;
  max-width: min(34ch, 42%);
  transform: none;
}
.home-hero .avatar-greet.is-on { transform: none; }

/* -- About: the gouache self-portrait in the stage -------------------------
   The button keeps the lightbox main.js already bound to it, so all this does
   is mat it: a cream border, a slight lean, and her face seated high in the
   crop because the gouache is a half-length portrait. */
.about-grid .stage-avatar .stage-fallback { padding: 3%; }
.stage-portrait {
  display: block;
  width: 100%;
  height: 100%;
  padding: clamp(8px, 1.4vw, 14px);
  border: 0;
  background: var(--cream);
  border-radius: var(--r-md);
  corner-shape: var(--corner);
  box-shadow: var(--shadow-paint);
  rotate: -1deg;
  overflow: hidden;
  cursor: zoom-in;
}
.stage-portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 24%;
  border-radius: var(--r-sm);
  box-shadow: none;
  transform: none;
  animation: troupe-breathe 22s ease-in-out infinite alternate;
}
/* the shelf caption, kept with the portrait when the figure moved up into the
   hero — a painting without its line is just a crop */
.stage-caption {
  margin: .8rem 0 0;
  text-align: center;
  color: var(--ink-soft);
  font-size: .92rem;
  font-weight: 600;
}

/* a phone holds three postcards, not five: the two at the back would be
   slivers, so they go, and the remaining three are re-seated to fill the
   shorter, wider box without hanging off it */
@media (max-width: 860px) {
  .troupe-card:nth-child(1), .troupe-card:nth-child(2) { display: none; }
  .troupe-card:nth-child(3) { --x: 1%; --y: 4%; --w: 46%; rotate: -5deg; }
  .troupe-card:nth-child(5) { --x: 52%; --y: 0%; --w: 47%; rotate: 4deg; }
  .troupe-card:nth-child(4) { --x: 26%; --y: 44%; --w: 46%; rotate: -2deg; }
  /* under the collage, in flow, as its caption: at this width the copy column
     is the full hero, so there is no clear corner left to pin the pill into */
  .home-hero .avatar-greet {
    position: relative;
    z-index: 3;
    right: auto;
    bottom: auto;
    max-width: min(34ch, 92%);
    margin: 1.1rem auto 1.9rem;
    text-align: center;
  }
}
@media (prefers-reduced-motion: reduce) {
  .troupe-card, .troupe-card img, .stage-portrait img { animation: none !important; }
  .troupe-card { translate: 0 0; transition: none; }
  .troupe-card img, .stage-portrait img { transform: none; }
}

/* ==========================================================================
   12e · Apple HIG control geometry
   --------------------------------------------------------------------------
   One rule for every control on the site: a fixed corner per size class, a
   44pt hit region, an 11pt type floor, and content that grows by wrapping
   rather than by shrinking or truncating. The radius scale and the reasoning
   behind it live in :root (§ corner scale); this is where it gets applied.
   ========================================================================== */
/* Rows of controls wrap. A row that cannot wrap turns two CTAs into one
   truncated CTA and one off-screen one — the failure HIG's flexible-height
   guidance exists to prevent. */
.btn-row, .avatar-bubble-links, .pay-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .875rem;
}
.btn-row { row-gap: 1rem; }

/* The 44pt floor for the small controls that are not .btn: the nav's chips, the
   call link, the dropdown's rows, the pause and close buttons. Icon-only
   controls get the full square, since a 40px circle is 40px of target in both
   axes and a finger is not. */
.nav-link, .nav-link-cta, .header-call, .subnav a, .pager a,
.avatar-self, .objective-anchor { min-height: var(--hit); }
.avatar-pause { min-height: var(--hit); padding-inline: 1.1rem; }
.avatar-bubble-close {
  width: var(--hit);
  height: var(--hit);
  min-height: var(--hit);
  border-radius: var(--r-circle);
}
.icon-btn { min-width: var(--hit); min-height: var(--hit); }

/* 11pt floor: the base sheet writes the lockup's tagline at 10.5px, the only
   type on the site under the floor. It is a rem token, so it now scales with
   the reader's text-size preference instead of being pinned in pixels. */
.brand-tag { font-size: var(--type-floor); letter-spacing: .14em; }

/* Display type tightens as it grows — the same optical correction Apple applies
   to its large titles — and a headline is balanced so its last line is not a
   single orphaned word. Body copy keeps the ragged-right measure it needs;
   balancing a paragraph makes it harder to read, not easier. */
h1, h2 { letter-spacing: -.022em; }
h1 { text-wrap: balance; }

/* Continuous corners, where the browser has them. `corner-shape: squircle` is
   SwiftUI's RoundedCornerStyle.continuous: the corner curve keeps going past
   the arc's end instead of stopping dead at a tangent, which is most of why
   Apple's rectangles look soft and everyone else's look round. Everywhere else
   the declaration is dropped and the same border-radius renders as a plain
   quarter-circle — correct, just less soft. Only surfaces with a radius of
   their own are listed; the torn-paper --radius slabs are collage, not
   controls, and stay hand-cut. */
@supports (corner-shape: squircle) {
  .btn, .btn-light, .btn-pay, .btn-call, .icon-btn,
  .avatar-self, .avatar-pause, .objective-anchor, .pager a,
  .svc-list label, .faq details,
  .avatar-bubble, .avatar-tool, .tool-icon,
  .project-head, .info-card, .callout, .commission, .quote-card,
  .objective-context, .subnav, .embed, .lightbox-card,
  .nav-link, .nav-link-cta, .header-call, .pill-list li, .sticker,
  .objective-chips li, .app-icon .app-icon-label {
    corner-shape: var(--corner);
  }
}

/* ==========================================================================
   12f · Touch: a label that needs a hover is a label nobody reads
   --------------------------------------------------------------------------
   The base sheet only reveals the tool labels on :hover / :focus-visible, which
   is fine for a pointer and useless on glass — there is no hover on a phone,
   and a tap fires the tool instead of showing its name. Under (hover: none) the
   label becomes part of the chip: static, always painted, on a deep-plum pill
   that keeps cream text at 16.2:1 whether the chip is lit or not.
   ========================================================================== */
@media (hover: none) {
  .avatar-tool { width: auto; min-width: 78px; }
  .avatar-ui .avatar-tool .avatar-tool-label,
  .stage-avatar.is-3d + .avatar-ui .avatar-tool .avatar-tool-label {
    position: static;
    top: auto;
    left: auto;
    display: block;
    max-width: 100%;
    padding: 3px 8px;
    background: var(--deep);
    color: var(--cream);
    border-radius: var(--r-xs);
    corner-shape: var(--corner);
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .02em;
    line-height: 1.3;
    text-align: center;
    white-space: normal;
    opacity: 1;
    transform: none;
    pointer-events: none;
  }
  /* the orbiting application marks have the same problem: their names only
     appear on hover, so on a phone four icons are four unlabelled blobs */
  .app-icon .app-icon-label { opacity: 1; }
}

/* ==========================================================================
   13 · Responsive + reduced motion
   ========================================================================== */
/* The collage is painted to overshoot the canvas — the marquee's tilted
   corners, drifting specks, orbiting labels all break the edge on purpose.
   The page itself must not grow a scrollbar because of them. clip, not
   hidden: clip doesn't turn the page into a scroll container, so the sticky
   header keeps sticking (and older browsers that lack it simply scroll). */
html, body { overflow-x: clip; }
@media (max-width: 860px) {
  .app-icon { --s: 46px; }
  .stage-fallback::before, .portrait::before { inset: 4% -3% -3% 5%; }
  .paint-marquee { transform: rotate(-1deg) scaleX(1.02); }
  .quote-card { position: static; }
}
@media (prefers-reduced-motion: reduce) {
  .app-icon, .paint-marquee .mq-track, .speck-field i { animation: none !important; }
  html.js-paint h1::after, html.js-paint h2::after {
    -webkit-mask-size: 100% 100% !important;
    mask-size: 100% 100% !important;
    transition: none;
  }
  html.js-paint .scroll-3d { transform: none; opacity: 1; }
  .card-media, .tile img, .figure img { transition: none; }
}
