/* Generated by includes/asset-bundle.php. Do not edit.
   Edit assets/css/components/*.css instead; this is rebuilt
   automatically whenever one of them changes. */

/* ===== assets/css/fonts.css ===== */
/* =========================================================
   Self-hosted fonts
   =========================================================
   Mirrors what the old Google Fonts link served, so nothing
   looks different, but the page no longer waits on (or sends
   every visitor's IP to) fonts.googleapis.com.

   Each file is a variable font covering a weight range, which
   is why one @font-face per character set replaces Google's
   one-per-weight list. Only latin and latin-ext are kept; any
   other script falls back to the system font for those
   characters.

   Space Grotesk tops out at 700. The site asks for 800 in a few
   places, and Google silently never served it, so those
   headings have always rendered at 700. Same here.

   The files are cached for a year (.htaccess), so a changed
   font must get a new filename, not be overwritten in place.

   All three families are SIL Open Font License 1.1; the licence
   and copyright notice travel inside each .woff2's metadata.
   ========================================================= */

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 600 700;
  font-display: swap;
  src: url("/assets/fonts/space-grotesk-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 600 700;
  font-display: swap;
  src: url("/assets/fonts/space-grotesk-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/assets/fonts/dm-sans-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/assets/fonts/dm-sans-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/assets/fonts/jetbrains-mono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/assets/fonts/jetbrains-mono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


/* ===== assets/css/style.css ===== */
/* =========================================================
   FluffyFile — Design Tokens
   ========================================================= */
:root {
  --bg-base: oklch(0.13 0.035 280);
  --bg-surface: oklch(0.19 0.055 285);
  --bg-surface-alt: oklch(0.25 0.075 288);
  --border-subtle: oklch(0.58 0.19 292 / 26%);
  --border-strong: oklch(0.58 0.19 292 / 40%);
  --text-primary: oklch(0.96 0.02 250);
  --text-muted: oklch(0.76 0.045 265);
  /* 72%, not 60%. At 60% this measured 3.88:1 on --bg-base, and it is
     what the footer copyright, all three footer legal links and the
     tool credits are painted in. 72% takes it to about 5.1:1. */
  --text-faint: oklch(0.76 0.045 265 / 72%);

  /* L 0.53, not 0.58. White text on the old value measured 4.13:1,
     under the 4.5:1 AA threshold, and this token is the fill behind the
     primary CTA, the back-to-top button, the process dots and every
     checkbox accent. Every one of its uses is a
     background or an accent-color - never a border or a glow - so
     darkening it is safe, and takes them all to 5.12:1 in one line. The
     gradient and shadow tokens hold their own oklch literals and are
     unaffected. */
  --accent-primary: oklch(0.53 0.19 292);
  --accent-secondary: oklch(0.85 0.15 202);
  --accent-sky: oklch(0.79 0.11 232);
  --accent-gold: oklch(0.78 0.135 84);
  --accent-gold-alt: oklch(0.63 0.09 62);
  --accent-error: oklch(0.7 0.19 25);
  --press-tint: oklch(0.85 0.15 202 / 14%);

  --gradient-primary: linear-gradient(100deg, oklch(0.58 0.19 292) 0%, oklch(0.7 0.16 262) 45%, oklch(0.85 0.15 202) 100%);
  --gradient-gold: linear-gradient(135deg, var(--accent-gold-alt), var(--accent-gold));
  --gradient-hero: radial-gradient(120% 90% at 50% 0%, oklch(0.42 0.17 288 / 48%) 0%, oklch(0.26 0.09 282 / 24%) 38%, oklch(0.13 0.035 280 / 0%) 72%);

  /* Primary CTA (.btn-primary). Violet into indigo and back, painted
     wider than the button so hover can slide it sideways. Both stops sit
     at or below L 0.53, so the light label clears AA across the whole
     fill: 5.12:1 on the violet, 6.03:1 on the indigo. Keep any new stop
     that dark - see the --accent-primary note above. */
  --accent-indigo: oklch(0.48 0.16 266);
  --gradient-cta: linear-gradient(110deg, var(--accent-primary) 0%, var(--accent-indigo) 50%, var(--accent-primary) 100%);
  --border-cta: oklch(0.7 0.16 292 / 45%);
  --shadow-cta: 0 1px 0 0 oklch(0.97 0.03 240 / 24%) inset, 0 12px 28px -14px oklch(0.58 0.19 292 / 70%);
  --shadow-cta-hover: 0 1px 0 0 oklch(0.97 0.03 240 / 24%) inset, 0 16px 36px -12px oklch(0.6 0.2 292 / 95%);

  --font-heading: 'Space Grotesk', system-ui, -apple-system, sans-serif;
  --font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;

  --nav-height: 76px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 22px;
  /* One step beyond --radius-lg, for surfaces that should read as round
     rather than as rounded rectangles. Currently the services bubbles.
     Deliberately a token rather than an ad-hoc clamp() at the use site,
     so the shape scale stays a scale. */
  --radius-bubble: clamp(2rem, 5vw, 3.25rem);
  --radius-pill: 999px;

  --shadow-glow-violet: 0 0 0 1px oklch(0.85 0.15 202 / 26%), 0 1px 0 0 oklch(0.97 0.03 240 / 18%) inset, 0 18px 44px -18px oklch(0.58 0.19 292 / 65%);
  --shadow-glow-cyan: 0 0 0 1px oklch(0.85 0.15 202 / 26%), 0 26px 70px -30px oklch(0.85 0.15 202 / 45%);
  --shadow-card: 0 10px 30px rgba(0, 0, 0, 0.35);

  --transition-base: 0.3s ease;
  --transition-fast: 0.15s ease;

  --container-width: 1200px;
  --section-padding: clamp(2.5rem, 3vw, 3rem);

  /* Stacking scale. Every overlay in the site reads from this ladder so
     the order is declared in one place instead of emerging from whatever
     sequence the modal partials happen to be required in. The seven
     dialogs below --z-modal-backdrop/--z-modal are genuinely peers: only
     one is ever open at a time, apart from the legal pair, which is why
     the legal family gets its own rung above them. */
  --z-card-hover: 30;
  --z-back-to-top: 90;
  --z-nav-backdrop: 104;
  --z-nav-panel: 105;
  /* Above the mobile panel on purpose. .site-nav creates a stacking
     context, so the close button and the dropdowns inside it cannot
     climb past the panel on their own no matter what z-index they
     declare - the bar itself has to outrank it. Before this, the X
     stayed clickable only because the panel is offset below the bar. */
  --z-nav: 106;
  --z-dropdown: 110;
  --z-modal-backdrop: 200;
  --z-modal: 201;
  --z-modal-legal-backdrop: 210;
  --z-modal-legal: 211;
}

/* =========================================================
   Reset
   ========================================================= */
*, *::before, *::after { box-sizing: border-box; }

/* overflow-x: clip, not hidden. With hidden on both html and body, body
   becomes a scroll container of its own that never scrolls (the viewport
   does), and every position: sticky inside it stops sticking - the nav
   and the pricing table header included. clip trims the sideways
   overflow the same way without making a scroll container. hidden stays
   first as the fallback for browsers without clip (Safari before 16),
   which keep the old behaviour. */
html { scroll-behavior: smooth; overflow-x: hidden; overflow-x: clip; }

body {
  margin: 0;
  background: var(--bg-base);
  color: var(--text-primary);
  font-family: var(--font-body);
  line-height: 1.6;
  overflow-x: hidden;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

html.nav-open { overflow: hidden; }
html.commission-modal-open { overflow: hidden; }
html.commission-auth-modal-open { overflow: hidden; }
html.terms-modal-open { overflow: hidden; }
html.review-modal-open { overflow: hidden; }
html.chat-modal-open { overflow: hidden; }
html.account-modal-open { overflow: hidden; }
html.report-modal-open { overflow: hidden; }

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; margin: 0; padding: 0; }
/* The `transition` here is the base easing for the press effect defined
   in the Buttons section below. Any class that declares its own
   `transition` shorthand overrides this, so those append `scale` to
   their own lists instead. */
button { font: inherit; cursor: pointer; }

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  margin: 0;
  line-height: 1.15;
  letter-spacing: -0.02em;
}

p { margin: 0; }

/* =========================================================
   Layout utilities
   ========================================================= */

/* Text for screen readers only. Clipped rather than display:none or
   visibility:hidden, both of which take the text out of the
   accessibility tree along with the layout. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* First focusable thing on the page, off-screen until focused. Without
   it a keyboard visitor tabs through seven nav links on every page
   before reaching the content. */
.skip-link {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  z-index: calc(var(--z-nav) + 1);
  padding: 0.7rem 1.2rem;
  border-radius: var(--radius-sm);
  background: var(--bg-surface);
  border: 1px solid var(--border-strong);
  color: var(--text-primary);
  font-size: 0.9rem;
  font-weight: 600;
  transform: translateY(-160%);
  transition: transform var(--transition-fast);
}

.skip-link:focus-visible { transform: translateY(0); }

.container {
  width: 100%;
  max-width: var(--container-width);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 4vw, 2.5rem);
}

section {
  scroll-margin-top: var(--nav-height);
  padding-block: var(--section-padding);
  position: relative;
}

.section-header { max-width: 640px; margin-bottom: clamp(2.5rem, 6vw, 3.5rem); }

.section-header-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 1rem;
  margin-bottom: clamp(2.5rem, 6vw, 3.5rem);
}

.section-header-row .section-header { margin-bottom: 0; }

.section-meta {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--accent-sky);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-secondary);
  background: oklch(0.85 0.15 202 / 8%);
  border: 1px solid oklch(0.85 0.15 202 / 25%);
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius-pill);
  margin-bottom: 1.25rem;
}

.section-title {
  font-size: clamp(1.9rem, 3.2vw + 1rem, 2.75rem);
  font-weight: 700;
  margin-bottom: 0.85rem;
}

.section-subtitle {
  color: var(--text-muted);
  font-size: clamp(1rem, 0.5vw + 0.9rem, 1.125rem);
}

/* =========================================================
   Icons

   Sized in em so an icon tracks the font-size of whatever it sits
   in, exactly like the Font Awesome <i> elements it replaced.

   fill: currentColor is what makes a single sprite work in every
   colour on the site. The symbols in assets/images/icons.svg carry
   no fill of their own, and fill inherits into the shadow tree that
   <use> creates, so an icon takes the colour of its container with
   no per-icon rule.
   ========================================================= */
.icon {
  width: 1em;
  height: 1em;
  fill: currentColor;
  vertical-align: -0.125em;
  flex-shrink: 0;
}

.text-gradient {
  background: var(--gradient-primary);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* =========================================================
   Buttons
   ========================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.9rem 1.75rem;
  border-radius: var(--radius-pill);
  font-weight: 600;
  font-size: 0.95rem;
  border: 1px solid transparent;
  transition: transform var(--transition-fast), box-shadow var(--transition-base), border-color var(--transition-base), color var(--transition-base);
  white-space: nowrap;
}

.btn:hover { transform: translateY(-3px); }

/* =========================================================
   Press feedback: a colour wash, never motion

   A shrink-on-press (`scale: 0.96`) used to live here and was removed:
   while `scale` animates, Chrome moves the button onto a GPU layer and
   redraws its text and 1px border differently, then switches back to
   normal painting when the animation ends. That switch was a visible
   flash on every click. Opacity and filter trigger the same swap, so
   they're out too.

   Instead a translucent cyan wash (the same hue as the hover borders
   and focus rings) is painted over the button's background while it's
   held. Only the background repaints - text and borders are untouched,
   so there is nothing to flash. It's a background-image layer, so it
   sits on top of whatever background-color the button already has
   and leaves box-shadow glows alone. It snaps on and off with no
   transition (gradients can't interpolate), like a native button.

   The :is()/:not() stack puts this at (0,3,0) specificity, above
   two-class states like `.account-modal-tab.is-active` that set the
   `background` shorthand and would otherwise wipe the wash. Excluded:
   buttons styled as bare text or icons, where a tinted box would read
   as a glitch rather than a press. A button with a gradient background
   of its own has to re-stack the wash on top of it, since both live in
   background-image.
   ========================================================= */
:is(button, .btn):active:not(:disabled, [aria-disabled="true"], .rf-star, .review-modal-link-btn, .nav-toggle) {
  background-image: linear-gradient(var(--press-tint), var(--press-tint));
}

/* The violet stays as background-color under the gradient, so anything
   that clears background-image still leaves a primary-looking button. */
.btn-primary {
  background-color: var(--accent-primary);
  background-image: var(--gradient-cta);
  background-size: 220% 100%;
  background-position: 0% 50%;
  border-color: var(--border-cta);
  color: var(--text-primary);
  box-shadow: var(--shadow-cta);
  transition: transform var(--transition-fast), box-shadow var(--transition-base), border-color var(--transition-base), color var(--transition-base), background-position 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

.btn-primary:hover:not(:disabled) {
  background-position: 100% 50%;
  box-shadow: var(--shadow-cta-hover);
}

.btn-primary:focus-visible { outline: 2px solid var(--accent-secondary); outline-offset: 3px; }

/* The press rule above sets background-image to the wash alone, which
   would flatten this button to plain violet while held. Same (0,3,0)
   weight and later in the file, so this wins and stacks the wash on top
   of the gradient instead. */
.btn-primary:active:not(:disabled, [aria-disabled="true"]) {
  background-image: linear-gradient(var(--press-tint), var(--press-tint)), var(--gradient-cta);
}

/* Every disabled primary button on the site is a form mid-send (sign in,
   commission, review, report, chat), usually with its spinner showing.
   So it keeps its look rather than greying out, and just stops reacting:
   no lift, no slide, no brighter glow. */
.btn-primary:disabled { cursor: progress; }
.btn-primary:disabled:hover { transform: none; }

.btn-outline {
  background: transparent;
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.btn-outline:hover {
  border-color: oklch(0.85 0.15 202 / 60%);
  color: var(--accent-secondary);
}

/* =========================================================
   Effect utilities (ported from the reference's Tailwind @utility blocks)
   ========================================================= */
.gilded {
  border: 1px solid transparent;
  background-image: linear-gradient(var(--bg-surface), var(--bg-surface)),
    linear-gradient(140deg, oklch(0.85 0.15 202 / 50%), oklch(0.45 0.16 290 / 22%) 45%, oklch(0.7 0.18 292 / 40%));
  background-origin: border-box;
  background-clip: padding-box, border-box;
}

.ember-sheen { position: relative; overflow: hidden; }

.ember-sheen::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(60% 40% at 15% 0%, oklch(0.85 0.15 202 / 14%), transparent 70%);
  pointer-events: none;
}

.sheen-hover { position: relative; overflow: hidden; }

.sheen-hover::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 65%;
  background: linear-gradient(90deg, transparent, oklch(0.95 0.05 220 / 14%), transparent);
  transform: translateX(-160%) skewX(-28deg);
  pointer-events: none;
}

.sheen-hover.is-sweeping::before { animation: sheen-sweep 0.9s ease-out; }

.grid-lines {
  background-image:
    linear-gradient(to right, oklch(0.85 0.15 202 / 10%) 1px, transparent 1px),
    linear-gradient(to bottom, oklch(0.85 0.15 202 / 10%) 1px, transparent 1px);
  background-size: 56px 56px;
  mask-image: radial-gradient(80% 60% at 50% 0%, black, transparent 75%);
  -webkit-mask-image: radial-gradient(80% 60% at 50% 0%, black, transparent 75%);
}

/* =========================================================
   Scroll reveal (driven by assets/js/modules/scroll-reveal.js
   via IntersectionObserver). Without JS nothing lifts the
   opacity, so index.php carries a <noscript> style that
   shows every [data-reveal] element outright.
   ========================================================= */
[data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

[data-reveal="left"] { transform: translateX(-24px); }
[data-reveal="right"] { transform: translateX(24px); }

[data-reveal].is-revealed {
  opacity: 1;
  transform: translate(0, 0);
}

@layer utilities {
  @supports (color: color-mix(in lab, red, red)) {
    .bg-surface\/40 {
      background-color: color-mix(in oklab, var(--bg-surface) 40%, transparent);
      border-top: 1px solid var(--border-subtle);
      border-bottom: 1px solid var(--border-subtle);
    }
  }
}

/* =========================================================
   Keyframes
   ========================================================= */
@keyframes sheen-sweep {
  0% { transform: translateX(-160%) skewX(-28deg); }
  100% { transform: translateX(240%) skewX(-28deg); }
}

/* =========================================================
   Responsive
   ========================================================= */
@media (max-width: 640px) {
  .section-header-row { flex-direction: column; align-items: flex-start; }
}

/* =========================================================
   Accessibility — reduced motion
   ========================================================= */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  html { scroll-behavior: auto; }
}


/* ===== assets/css/components/ambient-background.css ===== */
/* =========================================================
   Ambient background (page-wide, fixed) — blobs + grid + embers
   ========================================================= */
.ambient-bg {
  position: fixed;
  inset: 0;
  z-index: -5;
  overflow: hidden;
  pointer-events: none;
}

/* The soft blobs are radial gradients, not blurred discs. They used to be
   solid circles under filter: blur(120-140px) while animating scale(),
   which makes the browser recompute a huge Gaussian blur every frame -
   the heaviest thing on the page, and slow enough on desktop Safari to
   stall the whole tab. A gradient is drawn once and then only moved,
   which the compositor does for free.

   Each box is the old circle grown by the old blur radius on every side,
   with the gradient holding full strength over the old core and fading
   out across the old blur band, so the size, colour and position read
   the same as before. */
.ambient-blob { position: absolute; }

.ambient-blob-1 {
  top: -17.5rem; left: -15.5rem; width: 49rem; height: 49rem;
  background: radial-gradient(closest-side, oklch(0.58 0.19 292 / 12%) 0%, oklch(0.58 0.19 292 / 12%) 35%, oklch(0.58 0.19 292 / 5%) 65%, transparent 100%);
  animation: drift-slow 26s ease-in-out infinite;
}

.ambient-blob-2 {
  top: calc(33% - 8rem); right: -18rem; width: 46rem; height: 46rem;
  background: radial-gradient(closest-side, oklch(0.85 0.15 202 / 10%) 0%, oklch(0.85 0.15 202 / 10%) 30%, oklch(0.85 0.15 202 / 4%) 62%, transparent 100%);
  animation: drift-slower 34s ease-in-out infinite;
}

.ambient-blob-3 {
  bottom: -22.75rem; left: calc(33% - 8.75rem); width: 45.5rem; height: 45.5rem;
  background: radial-gradient(closest-side, oklch(0.78 0.135 84 / 10%) 0%, oklch(0.78 0.135 84 / 10%) 23%, oklch(0.78 0.135 84 / 4%) 58%, transparent 100%);
  animation: breathe 12s ease-in-out infinite;
}

/* The grid drifts by transform, not background-position. Animating
   background-position repaints the whole viewport every frame; moving a
   layer 56px (one grid cell, so the loop is seamless) is a composite.
   The lines live on ::before so the fade-out mask from .grid-lines stays
   put on the parent instead of sliding with them. */
.ambient-grid {
  position: absolute;
  inset: 0;
  opacity: 0.18;
  overflow: hidden;
  background-image: none;
}

.ambient-grid::before {
  content: '';
  position: absolute;
  top: -56px;
  left: -56px;
  right: 0;
  bottom: 0;
  background-image:
    linear-gradient(to right, oklch(0.85 0.15 202 / 10%) 1px, transparent 1px),
    linear-gradient(to bottom, oklch(0.85 0.15 202 / 10%) 1px, transparent 1px);
  background-size: 56px 56px;
  animation: grid-drift 14s linear infinite;
}

.ember {
  position: absolute;
  bottom: -3rem;
  border-radius: 50%;
  background: var(--accent-secondary);
  box-shadow: 0 0 8px 2px oklch(0.85 0.15 202 / 45%);
  animation-name: ember;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* =========================================================
   Keyframes
   ========================================================= */
@keyframes drift-slow {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(6rem, 3rem, 0) scale(1.12); }
}

@keyframes drift-slower {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); }
  50% { transform: translate3d(-5rem, -4rem, 0) scale(0.95); }
}

@keyframes breathe {
  0%, 100% { opacity: 0.5; transform: scale(0.95); }
  50% { opacity: 1; transform: scale(1.1); }
}

@keyframes grid-drift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(56px, 56px, 0); }
}

@keyframes ember {
  0% { transform: translate3d(0, 0, 0); opacity: 0; }
  12% { opacity: 0.9; }
  70% { opacity: 0.55; }
  100% { transform: translate3d(var(--ember-drift, 20px), -105vh, 0); opacity: 0; }
}

/* =========================================================
   Phones and touch devices

   This layer was the single most expensive thing on the site on
   mobile. Three elements running filter: blur(120-140px) over
   surfaces up to 544px square, each animating scale() - which
   invalidates the compositor's cached blur every frame, so the
   Gaussian is recomputed at 60fps - plus a full-viewport masked
   gradient animating background-position (a paint, not a
   composite), plus nine glowing embers. Thirteen infinite
   animations, all running before the first paint of any content,
   under a sticky nav that was backdrop-filtering the result.

   Below the layout breakpoint, all of it is replaced by a single
   static radial gradient in the same violet family. One paint, no
   blur, no animation. Desktop is untouched.

   (hover: none) is included so tablets and touch laptops get the
   cheap version too, not just narrow screens.
   ========================================================= */
@media (max-width: 900px), (hover: none) {
  .ambient-bg {
    background:
      radial-gradient(60% 40% at 8% 0%, oklch(0.58 0.19 292 / 14%), transparent 70%),
      radial-gradient(50% 35% at 100% 38%, oklch(0.85 0.15 202 / 10%), transparent 70%),
      radial-gradient(55% 30% at 45% 100%, oklch(0.78 0.135 84 / 8%), transparent 70%);
  }

  .ambient-blob,
  .ember { display: none; }

  .ambient-grid { opacity: 0.1; }
  .ambient-grid::before { animation: none; }
}


/* ===== assets/css/components/nav.css ===== */
/* =========================================================
   Nav
   ========================================================= */
/* Near-opaque instead of backdrop-filter: blur, on every screen size.
   The nav is sticky and sits over the animated ambient background for
   the whole visit, so a backdrop blur has to re-sample and re-blur what
   is behind it on every frame. Phones dropped it for that reason first;
   once the nav actually started sticking on desktop too (overflow-x:
   clip in style.css), the same per-frame cost landed on desktop
   browsers, worst in Safari. Behind an 80%-dark bar the blur was barely
   visible anyway. */
.site-nav {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  height: var(--nav-height);
  display: flex;
  align-items: center;
  background: oklch(0.13 0.035 280 / 96%);
  border-bottom: 1px solid var(--border-subtle);
}

.nav-inner {
  width: 100%;
  max-width: var(--container-width);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 4vw, 2.5rem);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

.logo {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-heading);
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-primary);
}

.logo-mark {
  height: 32px;
  width: auto;
  flex-shrink: 0;
}

.logo-accent {
  background: var(--gradient-primary);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.nav-links { display: flex; align-items: center; gap: 2rem; }

.nav-link {
  position: relative;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--text-muted);
  padding-block: 0.4rem;
  transition: color var(--transition-fast);
}

.nav-link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  background: var(--gradient-primary);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--transition-base);
}

.nav-link:hover, .nav-link.active { color: var(--accent-secondary); }
.nav-link:hover::after, .nav-link.active::after { transform: scaleX(1); }

.nav-actions { display: flex; align-items: center; gap: 1rem; }
.nav-cta { padding: 0.65rem 1.4rem; font-size: 0.875rem; }

.nav-toggle {
  display: none;
  background: none;
  border: none;
  color: var(--text-primary);
  font-size: 1.4rem;
  padding: 0.4rem;
  z-index: var(--z-dropdown);
}

/* A bottom sheet, not a floating card: anchored flush to the screen's own
   edges (left/right/bottom) so it reads as attached to the device rather
   than adrift with margins on every side. Only the top corners round off;
   the bottom ones stay square since they meet the viewport edge. */
.nav-mobile-panel {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: min(680px, 85vh);
  background: linear-gradient(165deg, var(--bg-surface-alt), var(--bg-surface));
  border: 1px solid var(--border-strong);
  border-bottom: none;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--shadow-glow-violet);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transform: translateY(100%);
  transition: transform var(--transition-base);
  z-index: var(--z-nav-panel);
}

.nav-mobile-handle {
  width: 40px;
  height: 4px;
  margin: 0.65rem auto 0.15rem;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--border-strong);
}

.nav-mobile-scroll {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1rem;
  padding: 0.75rem clamp(1.25rem, 5vw, 1.75rem) max(1.5rem, calc(1rem + env(safe-area-inset-bottom)));
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.nav-mobile-scroll::-webkit-scrollbar { display: none; }

/* Overflow cue for a panel that hides its scrollbar: now a plain layer
   pinned to the shell's own bottom, since the handle above needs to stay
   put while only .nav-mobile-scroll scrolls beneath it. */
.nav-mobile-fade {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2.75rem;
  background: linear-gradient(to top, var(--bg-surface), oklch(0.19 0.055 285 / 0%));
  pointer-events: none;
}

html.nav-open .nav-mobile-panel { transform: translateY(0); }

.nav-mobile-group {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.85rem 0.75rem;
  background: oklch(0.19 0.055 285 / 42%);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  animation: navGroupIn 0.32s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.nav-mobile-group[hidden] { display: none; }

@keyframes navGroupIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.nav-mobile-scroll > *:nth-child(1) { animation-delay: 0s; }
.nav-mobile-scroll > *:nth-child(2) { animation-delay: 0.05s; }
.nav-mobile-scroll > *:nth-child(3) { animation-delay: 0.1s; }
.nav-mobile-scroll > *:nth-child(4) { animation-delay: 0.15s; }
.nav-mobile-scroll > *:nth-child(5) { animation-delay: 0.2s; }

.nav-mobile-label {
  padding-inline: 0.25rem;
  font-family: var(--font-heading);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.nav-mobile-label-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.nav-mobile-label-row .nav-mobile-label { padding-inline: 0; }

.nav-mobile-count-pill {
  padding: 0.15rem 0.55rem;
  border-radius: var(--radius-pill);
  background: oklch(0.85 0.15 202 / 16%);
  color: var(--accent-secondary);
  font-size: 0.7rem;
  font-weight: 700;
}

.nav-mobile-links {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.nav-mobile-links .nav-link {
  display: flex;
  align-items: center;
  padding: 0.65rem 0.75rem;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: 1.05rem;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.nav-mobile-links .nav-link::after { content: none; }

.nav-mobile-links .nav-link:hover {
  background: oklch(0.85 0.15 202 / 10%);
  color: var(--text-primary);
}

.nav-mobile-links .nav-link.active,
.nav-mobile-links .nav-link[aria-current="page"] {
  background: var(--accent-primary);
  color: var(--text-primary);
  box-shadow: var(--shadow-glow-violet);
}

.nav-mobile-messages {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  max-height: 220px;
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.nav-mobile-messages::-webkit-scrollbar { display: none; }

.nav-mobile-message-item {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.6rem 0.7rem;
  border-radius: var(--radius-sm);
  transition: background var(--transition-fast);
}

.nav-mobile-message-item:hover { background: oklch(0.85 0.15 202 / 10%); }

.nav-mobile-message-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  overflow: hidden;
  border-radius: 50%;
  background: var(--gradient-primary);
  color: var(--text-primary);
  font-weight: 700;
  font-size: 0.8rem;
}

.nav-mobile-message-avatar img { width: 100%; height: 100%; object-fit: cover; }

.nav-mobile-message-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
  flex: 1;
}

.nav-mobile-message-title {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.nav-mobile-message-meta {
  font-size: 0.72rem;
  color: var(--text-faint);
}

.nav-mobile-message-badge {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.3rem;
  height: 1.3rem;
  padding: 0 0.35rem;
  border-radius: var(--radius-pill);
  background: var(--accent-primary);
  color: var(--text-primary);
  font-size: 0.7rem;
  font-weight: 700;
}

.nav-mobile-promo {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1.1rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: oklch(0.19 0.055 285 / 50%);
  box-shadow: 0 0 0 1px oklch(0.85 0.15 202 / 18%);
  animation: navGroupIn 0.32s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.nav-mobile-promo-text {
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--text-muted);
  text-align: center;
}

.nav-mobile-promo-cta {
  align-self: center;
}

.nav-mobile-footer {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding: 0.85rem 0.75rem;
  background: oklch(0.19 0.055 285 / 42%);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  animation: navGroupIn 0.32s cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* Guests: Sign in + Sign up, side by side. */
.nav-mobile-auth {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.6rem;
}

.nav-mobile-auth .btn { padding-inline: 1rem; }

@media (prefers-reduced-motion: reduce) {
  .nav-mobile-group, .user-chip-mobile, .nav-mobile-footer, .nav-mobile-promo {
    animation: none;
  }
}

.nav-backdrop {
  position: fixed;
  inset: 0;
  background: oklch(0.13 0.035 280 / 60%);
  backdrop-filter: blur(4px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-base);
  z-index: var(--z-nav-backdrop);
}

html.nav-open .nav-backdrop { opacity: 1; pointer-events: auto; }

/* =========================================================
   Responsive
   ========================================================= */
@media (max-width: 1100px) {
  .nav-links, .nav-actions .desktop-only { display: none; }
  .nav-toggle { display: block; }
}

@media (min-width: 1101px) {
  .nav-mobile-panel, .nav-backdrop { display: none; }
}

/* =========================================================
   Phones and touch devices

   The mobile menu's backdrop swaps its small blur for a flatter scrim.
   (.site-nav itself no longer blurs at any size, see the top of this
   file.)
   ========================================================= */
@media (max-width: 900px), (hover: none) {
  .nav-backdrop {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: oklch(0.13 0.035 280 / 72%);
  }
}


/* ===== assets/css/components/hero.css ===== */
/* =========================================================
   Hero
   ========================================================= */
.hero {
  display: flex;
  align-items: center;
  padding-top: 2rem;
  overflow: hidden;
  padding-bottom: 75px;
}

.hero-grid-lines { position: absolute; inset: 0; opacity: 0.4; z-index: -4; }
.hero-bg { position: absolute; inset: 0; z-index: -3; background: var(--gradient-hero); }

.hero-grid {
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: center;
}

/* 600 with tight tracking rather than 800: size and spacing carry the
   hierarchy, and it sets the headline on two lines on desktop instead
   of three. */
.hero-title {
  font-size: clamp(2.3rem, 2.3vw + 1.5rem, 3.45rem);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.04;
  text-wrap: balance;
  margin-bottom: 1.35rem;
}

.hero-title .word { display: inline-block; }

.hero-subtitle {
  font-size: clamp(1.02rem, 0.4vw + 0.94rem, 1.15rem);
  line-height: 1.7;
  color: var(--text-muted);
  max-width: 46ch;
  margin-bottom: 2.25rem;
}

.hero-ctas { display: flex; flex-wrap: wrap; gap: 1rem; }

/* Availability, from the artist roster. The dot is static: it states
   open or booked, and a pulse would repaint forever to say the same. */
.hero-status {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 1rem 0.45rem 0.85rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-subtle);
  background-color: color-mix(in oklab, var(--bg-surface) 70%, transparent);
  font-size: 0.88rem;
  font-weight: 500;
  color: var(--text-muted);
  margin-bottom: 1.6rem;
}

.hero-status strong { color: var(--text-primary); font-weight: 600; }
.hero-status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-faint); }
.hero-status.is-open .hero-status-dot { background: var(--accent-secondary); box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent-secondary) 18%, transparent); }
.hero-status.is-closed .hero-status-dot { background: var(--accent-gold-alt); }

/* =========================================================
   Texture layers

   Three canvases stacked in 3D: base colors at the bottom, markings
   and the glow map above it on transparent ground, so stacked they
   read as one finished sheet and pulled apart they read as layers.

   Everything that moves is a transform on a composited layer: the rig
   follows the mouse through --tilt-x/--tilt-y, and --gap spreads the
   sheets. The canvases are painted once and never repainted by motion.
   ========================================================= */
.hero-layers {
  position: relative;
  margin: 0;
  display: grid;
  justify-items: center;
  gap: 1.2rem;
  visibility: hidden;
}

.hero-layers.is-live { visibility: visible; }

.hero-layers-halo {
  position: absolute;
  inset: -4% -10% 18%;
  border-radius: 50%;
  background: radial-gradient(closest-side, color-mix(in oklab, var(--accent-primary) 42%, transparent), color-mix(in oklab, var(--accent-primary) 12%, transparent) 60%, transparent 100%);
  pointer-events: none;
  z-index: -1;
}

.hero-layers-scene {
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  --gap: 3px;
  position: relative;
  width: min(100%, 27rem);
  aspect-ratio: 1 / 0.95;
  perspective: 1500px;
}

.hero-layers.is-spread .hero-layers-scene { --gap: 82px; }

/* Left of centre on purpose: the labels hang off the sheets' right-hand
   edge, so it is the stack plus its labels that sits centred in the
   column. Centring the sheets alone pushed the glow map's label past the
   viewport at 1024px. */
.hero-layers-rig {
  position: absolute;
  left: 4%;
  right: 32%;
  top: 30%;
  aspect-ratio: 1;
  transform-style: preserve-3d;
  transform: rotateX(calc(56deg + var(--tilt-x))) rotateZ(calc(-38deg + var(--tilt-y)));
  transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

.hero-layer {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: translateZ(calc(var(--i) * var(--gap)));
  transition: transform 0.9s cubic-bezier(0.16, 1, 0.3, 1);
  border-radius: var(--radius-sm);
}

.hero-layer canvas { display: block; width: 100%; height: 100%; border-radius: inherit; }

.hero-layer--base { box-shadow: var(--shadow-glow-violet); }

/* The upper sheets are clear film: a faint tint and edge while apart,
   nothing at all once stacked, so the stack reads as a single sheet. */
.hero-layer--clear {
  background-color: color-mix(in oklab, var(--bg-surface-alt) 22%, transparent);
  outline: 1px solid color-mix(in oklab, var(--accent-secondary) 34%, transparent);
  outline-offset: -1px;
  transition: transform 0.9s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.4s ease, outline-color 0.4s ease;
}

.hero-layers:not(.is-spread) .hero-layer--clear { background-color: transparent; outline-color: transparent; }

/* Labels sit in the sheet's plane but are counter-rotated by the rig's
   own angles (tilt included), so they always face the viewer. */
.hero-layer-label {
  position: absolute;
  left: 100%;
  top: 50%;
  transform-origin: left center;
  transform: translate(14px, -50%) rotateZ(calc(38deg - var(--tilt-y))) rotateX(calc(-56deg - var(--tilt-x)));
  transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s ease;
  white-space: nowrap;
  pointer-events: none;
  padding: 0.35rem 0.65rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background-color: color-mix(in oklab, var(--bg-surface) 92%, transparent);
  font-size: 0.8rem;
  line-height: 1.35;
  color: var(--text-muted);
}

.hero-layer-label strong { display: block; color: var(--text-primary); font-weight: 600; font-size: 0.86rem; }

.hero-layers:not(.is-spread) .hero-layer-label { opacity: 0; }
.hero-layers.is-spread .hero-layer:nth-child(2) .hero-layer-label { transition-delay: 0s, 0.15s; }
.hero-layers.is-spread .hero-layer:nth-child(3) .hero-layer-label { transition-delay: 0s, 0.3s; }

.hero-layers-controls { display: grid; justify-items: center; gap: 0.6rem; }

/* Utility-scale segmented control, matching the Services chips. */
.hero-layers-toggle {
  display: inline-flex;
  gap: 0.35rem;
  padding: 0.3rem;
  border-radius: calc(var(--radius-sm) + 0.3rem);
  border: 1px solid var(--border-subtle);
  background-color: color-mix(in oklab, var(--bg-surface) 70%, transparent);
}

.hero-layers-toggle button {
  padding: 0.45rem 0.85rem;
  border-radius: var(--radius-sm);
  background-color: transparent;
  border: 1px solid transparent;
  color: var(--text-muted);
  font-size: 0.84rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}

.hero-layers-toggle button:hover { color: var(--text-primary); }

.hero-layers-toggle button:focus-visible {
  outline: 2px solid var(--accent-secondary);
  outline-offset: 2px;
}

.hero-layers-toggle button[aria-pressed="true"] {
  background-color: var(--bg-surface-alt);
  border-color: color-mix(in oklab, var(--accent-secondary) 55%, transparent);
  color: var(--text-primary);
}

.hero-layers-note {
  max-width: 38ch;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--text-muted);
  text-align: center;
}

/* =========================================================
   Responsive
   ========================================================= */
@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; }
  /* The headline leads on every width: the stack comes after the CTAs,
     so a phone visitor reads what the site sells before any art. */
  .hero-layers-scene { width: min(100%, 22rem); }

  /* The labels hang off the sheets' right edge. On a narrow screen there
     is no room past a full-size stack, so the sheets shrink and sit left
     of centre, leaving the right-hand side for the labels. */
  .hero-layers-rig { left: 13%; right: 37%; top: 34%; }
  .hero-layers.is-spread .hero-layers-scene { --gap: 64px; }
  .hero-layer-label { padding: 0.3rem 0.55rem; font-size: 0.72rem; }
  .hero-layer-label strong { font-size: 0.78rem; }
}

@media (max-width: 640px) {
  .hero-ctas .btn { width: 100%; }
}

/* Label names alone where the column is too narrow for the two-line
   labels: phones, and small laptops just above the stacking breakpoint,
   where the glow map's label otherwise ran off the viewport at 1024px.
   The note under the toggle already says what the layers are for. */
@media (max-width: 480px), (min-width: 901px) and (max-width: 1100px) {
  .hero-layer-label-desc { display: none; }
}

/* =========================================================
   Intro animation

   Replaces the GSAP timeline that used to drive this. GSAP was 70KB
   of CDN JavaScript whose only consumer on the whole site was that
   one five-tween timeline, which is a lot of library to load before
   the hero can move.

   The .is-intro class is added by hero-animation.js and only when
   motion is allowed, which matters more than it looks: the global
   prefers-reduced-motion block collapses animation-duration but NOT
   animation-delay, so a reduced-motion visitor would otherwise stare
   at an invisible hero for the length of the longest delay before it
   snapped in. Gating the class means no class, no start state, no
   delay, content just renders.

   Same principle as scroll-reveal.js: the hidden start state only
   exists while the class is present, so a JS failure means "no
   animation", never "no content".

   The layers' own moment (stacked, then pulled apart) is timed by
   hero-layers.js to land after the stack has risen in.
   ========================================================= */
@keyframes hero-rise {
  from { opacity: 0; translate: 0 20px; }
  to { opacity: 1; translate: 0 0; }
}

@keyframes hero-rise-word {
  from { opacity: 0; translate: 0 40px; }
  to { opacity: 1; translate: 0 0; }
}

.hero.is-intro .hero-status,
.hero.is-intro .hero-title .word,
.hero.is-intro .hero-subtitle,
.hero.is-intro .hero-ctas .btn,
.hero.is-intro .hero-layers {
  animation-fill-mode: backwards;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

.hero.is-intro .hero-status { animation-name: hero-rise; animation-duration: 0.6s; }

.hero.is-intro .hero-title .word { animation-name: hero-rise-word; animation-duration: 0.7s; }
.hero.is-intro .hero-title .word:nth-child(1) { animation-delay: 0.30s; }
.hero.is-intro .hero-title .word:nth-child(2) { animation-delay: 0.38s; }
.hero.is-intro .hero-title .word:nth-child(3) { animation-delay: 0.46s; }
.hero.is-intro .hero-title .word:nth-child(4) { animation-delay: 0.54s; }
.hero.is-intro .hero-title .word:nth-child(5) { animation-delay: 0.62s; }
.hero.is-intro .hero-title .word:nth-child(6) { animation-delay: 0.70s; }
.hero.is-intro .hero-title .word:nth-child(7) { animation-delay: 0.78s; }
.hero.is-intro .hero-title .word:nth-child(n+8) { animation-delay: 0.86s; }

.hero.is-intro .hero-subtitle { animation-name: hero-rise; animation-duration: 0.6s; animation-delay: 1.15s; }

.hero.is-intro .hero-ctas .btn { animation-name: hero-rise; animation-duration: 0.5s; }
.hero.is-intro .hero-ctas .btn:nth-child(1) { animation-delay: 1.45s; }
.hero.is-intro .hero-ctas .btn:nth-child(n+2) { animation-delay: 1.55s; }

.hero.is-intro .hero-layers { animation-name: hero-rise; animation-duration: 0.75s; animation-delay: 0.5s; }

/* Reduced motion: the stack starts pulled apart and stays still. The
   tilt is not wired up at all in that case (hero-layers.js), and these
   make sure a toggle click swaps states without a glide. */
@media (prefers-reduced-motion: reduce) {
  .hero-layers-rig,
  .hero-layer,
  .hero-layer--clear,
  .hero-layer-label { transition: none; }
}


/* ===== assets/css/components/services.css ===== */
/* =========================================================
   Services ("What we do")

   A picker beside a bench. The picker holds the services, each with
   what you get and who it suits; the bench (services-atlas.js) shows
   what each one actually produces: the layers a custom texture is
   painted in, a before/after of a recolor and, when 3D modeling is on
   sale, the site's critter head to spin (critter-view.js).

   The two columns are one height. The cards share the picker column's
   height, and the bench never changes height when the service or a
   control changes: everything that swaps (the control groups, the
   notes, the wordings of the panel's label) sits in one grid cell, the
   plates that come and go sit on the sheet itself, and the model's
   canvas shares the sheet's square box.

   With three services the rows that are not picked fold to their name
   and description (.is-fold, set by the partial), so three fit beside
   the panel and the choice reads as one open card. With two, both stay
   open, as they always have.

   Nothing here animates on its own and nothing is blurred. The sheet
   repaints when a control changes, and the model only draws while it
   is spinning, morphing or changing shading, so the section costs
   nothing between interactions (see the render-cost notes in
   ambient-background.css and nav.css).
   ========================================================= */
.services-body {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: stretch;
}

/* No script, no bench: the picker takes the full width instead of
   sitting beside an empty column. */
.services-body:has(.services-bench[hidden]) { grid-template-columns: minmax(0, 1fr); }

/* ---------------------------------------------------------
   Picker
   --------------------------------------------------------- */
.services-picker-col { display: flex; flex-direction: column; min-width: 0; }
.services-picker { flex: 1; display: flex; flex-direction: column; gap: 0.75rem; }

/* Each card takes its own content's height first, then an equal share of
   whatever the column has left. That spare height goes to the "you get"
   block (the 1fr row), so the cue stays on the card's bottom edge.

   background-color, not the background shorthand, so the site-wide
   press wash (a background-image on :active) still shows on these. */
.service-row {
  flex: 1 1 auto;
  display: grid;
  grid-template-rows: auto auto 1fr;
  gap: 0.6rem;
  width: 100%;
  min-width: 0;
  padding: 1.5rem 1.5rem 1.4rem;
  text-align: left;
  color: var(--text-primary);
  background-color: transparent;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  transition: background-color var(--transition-base), border-color var(--transition-base), box-shadow var(--transition-base), flex-grow 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.service-row:hover { border-color: var(--border-strong); background-color: oklch(0.19 0.055 285 / 40%); }

.service-row:focus-visible {
  outline: 2px solid var(--accent-secondary);
  outline-offset: 3px;
}

.service-row.is-selected {
  background-color: var(--bg-surface-alt);
  border-color: oklch(0.58 0.19 292 / 65%);
  box-shadow: var(--shadow-glow-violet);
}

.service-row-top { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem 0.75rem; }

.service-row-name {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 1.35rem;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

/* The one gold accent in the section. */
.service-row-tag {
  font-family: var(--font-mono);
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--accent-gold);
  background: oklch(0.78 0.135 84 / 15%);
  padding: 0.25rem 0.65rem;
  border-radius: var(--radius-pill);
}

.service-row-desc { color: var(--text-muted); font-size: 0.95rem; line-height: 1.6; max-width: 48ch; }

/* What you get and the cue, in a wrapper that can fold shut: the outer
   grid animates its one row from 1fr to 0fr and the inner box clips. */
.service-row-more { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease; }
.service-row-more-in { display: grid; grid-template-rows: 1fr auto; min-height: 0; overflow: hidden; }

/* What you get, one line each, then who the service suits. */
.service-row-get { display: grid; align-content: start; gap: 0.5rem; margin-top: 0.35rem; }
.service-row-get-label { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-muted); }
.service-row-features { display: grid; gap: 0.45rem; }
.service-row-feature { display: grid; grid-template-columns: 1rem minmax(0, 1fr); gap: 0.45rem; font-size: 0.9rem; line-height: 1.45; color: var(--text-muted); }
.service-row-feature::before { content: '\2713'; color: var(--accent-secondary); font-size: 0.8rem; line-height: 1.6; }
.service-row-best { margin-top: 0.4rem; padding-top: 0.75rem; border-top: 1px solid var(--border-subtle); font-size: 0.88rem; line-height: 1.5; color: var(--text-muted); }

/* Once the picker is live, the rows that aren't selected step back so
   the set reads as a choice rather than equal cards. Without the script
   all stay at full strength. */
.services-picker.is-live .service-row:not(.is-selected) .service-row-desc,
.services-picker.is-live .service-row:not(.is-selected) .service-row-get-label,
.services-picker.is-live .service-row:not(.is-selected) .service-row-feature,
.services-picker.is-live .service-row:not(.is-selected) .service-row-best { color: var(--text-faint); }

.service-row-cue {
  display: none;
  margin-top: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--accent-secondary);
}
.services-picker.is-live .service-row-cue { display: block; opacity: 0; transition: opacity var(--transition-base); }
.services-picker.is-live .service-row.is-selected .service-row-cue { opacity: 1; }

/* Folded: the rows not selected keep their name, tag and description,
   and hand the column's spare height to the open one. Above the tablet
   break only; side by side, flex-grow is the cards' width. */
.services-picker.is-live.is-fold .service-row:not(.is-selected) .service-row-more { grid-template-rows: 0fr; opacity: 0; }
@media (min-width: 901px) {
  .services-picker.is-live.is-fold .service-row:not(.is-selected) { flex-grow: 0; }
}

.services-foot { margin-top: 0.9rem; padding-left: 0.25rem; font-size: 0.9rem; color: var(--text-muted); }
.services-foot a { color: var(--accent-secondary); font-weight: 600; }
.services-foot a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* ---------------------------------------------------------
   Bench
   --------------------------------------------------------- */
/* Where the cards need more height than the panel (a narrower laptop,
   where their text wraps more), the panel is stretched to match and
   spreads the difference evenly over its own spacing, so no one gap is
   left standing. */
.services-bench {
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  gap: 1.1rem;
  padding: 1.1rem;
  min-width: 0;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-subtle);
  background:
    radial-gradient(90% 60% at 100% 0%, oklch(0.42 0.17 288 / 18%), transparent 60%),
    oklch(0.19 0.055 285 / 60%);
}

/* The label has one wording per service, and the note beside it one per
   picture. All sit in one grid cell each, so the bar is as wide as the
   longest whichever is showing and wraps the same way for all. */
.services-bench-bar { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 0.25rem 0.75rem; padding-inline: 0.25rem; }
.services-bench-labels,
.services-bench-notes { display: grid; }
.services-bench-label { grid-area: 1 / 1; font-family: var(--font-mono); font-size: 0.72rem; font-weight: 600; color: var(--text-muted); }
.services-bench-note { grid-area: 1 / 1; font-size: 0.78rem; color: var(--text-faint); }

.services-atlas {
  position: relative;
  aspect-ratio: 1;
  width: 100%;
  overflow: hidden;
  border-radius: var(--radius-md);
  border: 1px solid oklch(0.58 0.19 292 / 22%);
  background: #0d0a17;
}

.services-atlas canvas { display: block; width: 100%; height: 100%; transition: opacity 0.22s ease; }
.services-atlas canvas.is-swapping { opacity: 0.35; }

/* The model shares the sheet's box: it sits on top, and the two trade
   places with visibility, so the box's aspect ratio rules both. pan-y
   leaves vertical swipes to the page, as the split input does. */
.services-atlas canvas[data-model-canvas] {
  position: absolute;
  inset: 0;
  touch-action: pan-y;
  cursor: grab;
  transition: opacity 0.22s ease, transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
.services-atlas canvas[data-model-canvas]:active { cursor: grabbing; }
.services-atlas canvas[data-model-canvas]:focus-visible { outline: 2px solid var(--accent-secondary); outline-offset: -3px; border-radius: var(--radius-md); }
.services-atlas canvas[data-model-canvas].is-entering { opacity: 0; transform: scale(0.94); }
.services-atlas:not(.is-model) canvas[data-model-canvas],
.services-atlas.is-model canvas[data-atlas-canvas] { visibility: hidden; }

/* Before/after: a real range input covers the sheet, so dragging
   anywhere, touch and arrow keys all work natively; the visible line and
   knob follow its value. pan-y keeps vertical swipes scrolling the page. */
.services-split-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: ew-resize;
  touch-action: pan-y;
}

.services-split-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--split, 50%);
  width: 2px;
  margin-left: -1px;
  background: oklch(0.97 0.03 240 / 85%);
  box-shadow: 0 0 12px oklch(0.85 0.15 202 / 60%);
  pointer-events: none;
}

.services-split-handle::after {
  content: '\2194';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 36px;
  height: 36px;
  margin: -18px 0 0 -18px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--bg-surface-alt);
  border: 1px solid oklch(0.97 0.03 240 / 70%);
  color: var(--text-primary);
  font-size: 1rem;
}

.services-split-input:focus-visible + .services-split-handle::after {
  outline: 2px solid var(--accent-secondary);
  outline-offset: 3px;
}

/* Small plates on the sheet: its size, and which side of a comparison is
   which. They sit on the sheet, not under it, so they can come and go
   without the panel changing height. Nearly solid, because the paint
   under them can be any colour. */
.services-plate {
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background: color-mix(in oklch, var(--bg-base) 90%, transparent);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--text-primary);
}

.services-readout { position: absolute; top: 0.6rem; left: 0.6rem; pointer-events: none; z-index: 1; }

/* Raised clear of the corner label the sheet itself carries. */
.services-atlas-legend {
  position: absolute;
  left: 0.6rem;
  right: 0.6rem;
  bottom: 1.75rem;
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  pointer-events: none;
}

/* ---------------------------------------------------------
   Controls. Utility scale (small radius), not the pill scale
   the site keeps for CTAs.

   The groups share one grid cell, and so do the notes. The one not in
   use keeps its space: it is hidden with visibility, which also takes
   it out of the tab order and away from screen readers. So the panel is
   exactly as tall for one service as for another.
   --------------------------------------------------------- */
.services-controls-stack,
.services-notes { display: grid; }

.services-controls { grid-area: 1 / 1; display: flex; flex-direction: column; gap: 1rem; padding-inline: 0.25rem; }
.services-control-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem 0.9rem; }
.services-control-label { min-width: 5.25rem; font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-faint); }
.services-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.services-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-sm);
  background-color: transparent;
  border: 1px solid var(--border-subtle);
  color: var(--text-muted);
  font-size: 0.84rem;
  font-weight: 600;
  transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}

.services-chip:hover { color: var(--text-primary); border-color: var(--border-strong); }

.services-chip:focus-visible {
  outline: 2px solid var(--accent-secondary);
  outline-offset: 2px;
}

.services-chip[aria-pressed="true"],
.services-chip[aria-checked="true"] {
  background-color: var(--bg-surface-alt);
  border-color: oklch(0.85 0.15 202 / 55%);
  color: var(--text-primary);
}

.services-chip-dot { width: 8px; height: 8px; border-radius: 50%; border: 1px solid var(--text-faint); }
.services-chip[aria-pressed="true"] .services-chip-dot { background: var(--accent-secondary); border-color: var(--accent-secondary); }

.services-swatch { display: inline-flex; width: 26px; height: 16px; overflow: hidden; border-radius: 4px; border: 1px solid oklch(0.97 0.03 240 / 25%); }
.services-swatch i { flex: 1; }

.services-control-note { max-width: 58ch; padding-inline: 0.25rem; font-size: 0.82rem; line-height: 1.5; color: var(--text-faint); }
.services-notes .services-control-note { grid-area: 1 / 1; }

.services-controls[hidden] { visibility: hidden; }
.services-notes .services-control-note[hidden],
.services-bench-label[hidden],
.services-bench-note[hidden] { display: block; visibility: hidden; }

/* The public site has no global [hidden] rule, and these set their own
   display, which would otherwise win over the browser's [hidden]. */
.services-bench[hidden],
.services-atlas-legend[hidden] { display: none; }

/* =========================================================
   Responsive
   ========================================================= */
/* Tablets: the cards sit side by side above the panel, and the panel
   turns on its side, sheet on the left and controls beside it. Stacked
   in one column two open cards were half empty and the sheet filled
   the screen. Folded rows stack instead: one tall open card beside two
   short ones read wrong side by side. */
@media (max-width: 900px) {
  .services-body { grid-template-columns: minmax(0, 1fr); }
  .services-picker { flex-direction: row; }
  .service-row { flex: 1 1 0; min-width: 0; padding: 1.25rem; }
  .service-row-name { font-size: 1.2rem; }
  .services-picker.is-live.is-fold { flex-direction: column; }
  .services-picker.is-live.is-fold .service-row { flex: 0 0 auto; }

  .services-bench {
    display: grid;
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;
    grid-template-areas: "sheet bar" "sheet controls" "sheet notes";
    gap: 1.1rem 1.25rem;
  }
  .services-bench-bar { grid-area: bar; }
  .services-atlas { grid-area: sheet; align-self: start; }
  .services-controls-stack { grid-area: controls; align-self: center; }
  .services-notes { grid-area: notes; }
}

/* Phones: everything in one column, the sheet full width. */
@media (max-width: 640px) {
  .services-picker { flex-direction: column; }
  .service-row { flex: 0 0 auto; }
  .services-bench { display: flex; }
  .services-atlas,
  .services-controls-stack { align-self: auto; }
}


/* ===== assets/css/components/profile-kit.css ===== */
/* =========================================================
   Profile kit - small pieces shared by profile.php,
   artist-profile.php and the homepage's artist section (the star
   meter and the utility buttons). Page layouts live in profile.css,
   artist-profile.css and the artist-deck files.
   ========================================================= */

/* ---- Utility-scale buttons (card actions, sort, viewer controls).
   radius-sm, the design language's nav/utility scale; pills stay for
   the marketing CTAs. No global <button> reset exists, so background
   and border are always set explicitly. ---- */
.util-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 0.9rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background-color: var(--bg-surface);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 0.84rem;
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}

.util-btn:hover { border-color: color-mix(in oklab, var(--accent-secondary) 60%, transparent); color: var(--accent-secondary); }
.util-btn:focus-visible { outline: 2px solid var(--accent-secondary); outline-offset: 2px; }
.util-btn[hidden] { display: none; }

/* Text-weight action (Report to HR): present, never competing. */
.quiet-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 0.6rem;
  border: 0;
  border-radius: var(--radius-sm);
  background-color: transparent;
  color: var(--text-faint);
  font-family: var(--font-body);
  font-size: 0.82rem;
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  transition: color var(--transition-fast);
}

.quiet-btn:hover { color: var(--accent-error); }
.quiet-btn:focus-visible { outline: 2px solid var(--accent-secondary); outline-offset: 2px; }

/* ---- A count beside a heading ("3", "6 pieces") ---- */
.count-tag {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 500;
  line-height: 1;
  color: var(--text-faint);
}

/* ---- Star meter: a faint row with a gold row clipped to the score on
   top, so fractional averages (3.8) read true. --v is 0-100%. ---- */
.star-meter {
  position: relative;
  display: inline-flex;
  gap: 3px;
  font-size: 0.95rem;
  color: color-mix(in oklab, var(--text-muted) 28%, transparent);
}

.star-meter-fill {
  position: absolute;
  inset: 0 auto 0 0;
  display: inline-flex;
  gap: inherit;
  width: var(--v, 0%);
  overflow: hidden;
  color: var(--accent-gold);
}

.star-meter--sm { gap: 2px; font-size: 0.72rem; }

/* ---- Blank UV sheet: an unpainted texture sheet, the empty state
   for "no work yet" and "no commissions yet". blank-sheet.js paints
   the islands; without it the grid alone still reads as a sheet. ---- */
.blank-sheet {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-subtle);
  background-color: var(--bg-base);
  background-image:
    linear-gradient(to right, color-mix(in oklab, var(--accent-sky) 10%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in oklab, var(--accent-sky) 10%, transparent) 1px, transparent 1px);
  background-size: 12.5% 12.5%;
}


/* ===== assets/css/components/artist-deck.css ===== */
/* =========================================================
   Artists ("Pick your artist") - the table of cards

   At rest this is the flat table: every artist has the same size card,
   on the same level, and no card touches another. The small tilt and
   nudge belong to the seat, not to the artist.

   The seats come from artist_table_layout() (artist-table-layout.php)
   as custom properties, one set per tier:

     xs  under 350px of section width   2 cards per row
     sm  350px and up                   3
     md  640px and up                   4
     lg  900px and up                   6

   A pick does not move a card out of the grid. The script
   (artist-deck-stage.js) writes a pose on each card instead: where it
   sits, how it is turned and how big it is. With no pose written, the
   rules below are the whole story, which is also what a browser without
   the script shows.

   The table and the stage (artist-deck-stage.css) share one grid cell,
   so the section is as tall as the taller of the two and nothing has to
   set a height by hand.

   Render cost: every move is transform and opacity, nothing is blurred,
   and nothing loops.
   ========================================================= */
.artists-body {
  container: artists / inline-size;
  position: relative;
  /* The stage has to sit above the cards whether or not the table is
     leaning, so the order is stated here and not left to the transform. */
  isolation: isolate;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

/* ---------------------------------------------------------
   Filter (utility scale, small radius)
   --------------------------------------------------------- */
.artist-filter {
  display: inline-flex;
  gap: 0.25rem;
  padding: 0.25rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background-color: color-mix(in oklch, var(--bg-surface) 60%, transparent);
  transition: opacity var(--transition-base);
}

.artist-filter[hidden] { display: none; }
/* Set aside while someone is picked: it sorts the table, and the table
   is not what is on show. */
.artist-filter[inert] { opacity: 0.4; }

.artist-filter button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.9rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background-color: transparent;
  color: var(--text-muted);
  font-size: 0.86rem;
  font-weight: 600;
}

.artist-filter button:hover { color: var(--text-primary); }
.artist-filter button:focus-visible { outline: 2px solid var(--accent-secondary); outline-offset: 2px; }

.artist-filter button[aria-pressed="true"] {
  background-color: var(--bg-surface-alt);
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.artist-filter-count { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-faint); }

/* ---------------------------------------------------------
   The table
   --------------------------------------------------------- */
/* A card is 5/6 of its two tracks; the sixth left over is the gutter,
   half on each side. The wrap caps a card at 13.5rem, so a short roster
   gets normal cards in the middle rather than giant ones. The explicit
   width matters: as a grid item with auto margins it would otherwise
   shrink to its content. */
.artist-table-wrap {
  --tracks: var(--tracks-sm);
  --tilt-k: 0.6;
  grid-area: 1 / 1;
  width: 100%;
  max-width: calc(13.5rem * (0.6 * var(--tracks) - 0.2));
  margin-inline: auto;
}

.artist-table {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(var(--tracks), minmax(0, 1fr));
  /* Every row as tall as the tallest, so one long name grows every card
     and not just its own. */
  grid-auto-rows: 1fr;
  width: calc(100% * 3 * var(--tracks) / (3 * var(--tracks) - 1));
  margin-inline: calc(-50% / (3 * var(--tracks) - 1));
  /* The whole table leans with the pointer, every card by the same
     amount (artist-deck.js). */
  transform: translate(calc(var(--lean-x, 0) * 7px), calc(var(--lean-y, 0) * 5px));
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.artist-card {
  --mat: 0.4rem;
  --c: var(--c-sm);
  --r: var(--r-sm);
  grid-column: var(--c) / span 2;
  grid-row: var(--r);
  justify-self: center;
  width: 83.333%;
  /* Percentages of the two tracks: 0.28 of a card between rows. */
  margin-block: 11.5%;
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--mat);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background-color: var(--bg-surface);
  box-shadow: var(--shadow-glow-violet);
  color: inherit;
  cursor: pointer;
  /* Individual transform properties, so the lift composes with the tilt
     and the script can drive them one by one. */
  rotate: calc(var(--tilt, 0deg) * var(--tilt-k));
  translate: 0 calc(var(--nudge, 0) * var(--tilt-k) * 1%);
  transition:
    translate 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    rotate 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    scale 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    opacity var(--transition-base),
    box-shadow var(--transition-base),
    border-color var(--transition-base);
}

/* A strip under the card that belongs to the card. The hover lift moves
   the card up; without this its bottom edge would slip out from under the
   pointer, hover would end, the card would drop back, and it would flutter. */
.artist-card::after {
  content: "";
  position: absolute;
  inset: 100% 0 auto;
  height: 0.5rem;
}

.artist-card:focus-visible {
  rotate: calc(var(--tilt, 0deg) * var(--tilt-k) * 0.4);
  translate: 0 calc(var(--nudge, 0) * var(--tilt-k) * 1% - 0.4rem);
  border-color: color-mix(in oklch, var(--accent-secondary) 45%, transparent);
  outline: 2px solid var(--accent-secondary);
  outline-offset: 3px;
}

/* Hover only where there is a pointer that hovers. A touch screen keeps
   :hover on the last thing tapped, which would leave a card that was just
   put back looking half picked. */
@media (hover: hover) {
  .artist-card:hover {
    rotate: calc(var(--tilt, 0deg) * var(--tilt-k) * 0.4);
    translate: 0 calc(var(--nudge, 0) * var(--tilt-k) * 1% - 0.4rem);
    border-color: color-mix(in oklch, var(--accent-secondary) 45%, transparent);
  }
}

/* Filtered out: still in its seat and still pickable, just stepped back. */
.artist-card.is-dimmed { opacity: 0.32; }
.artist-card.is-dimmed:focus-visible { opacity: 0.6; }

@media (hover: hover) {
  .artist-card.is-dimmed:hover { opacity: 0.6; }
}

/* Set by the script only, while the cards wait to be dealt. Without the
   script the class never appears and the cards are simply there. */
.artist-table.is-undealt .artist-card { opacity: 0; }

/* The card's insides are spans (it is a link that becomes a button), so
   each one sets its own display. */
.artist-portrait {
  position: relative;
  display: block;
  flex: none;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border-radius: calc(var(--radius-md) - var(--mat));
  background: var(--bg-surface-alt);
}

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

/* The one colour here that is not a token: the hue is the artist's own
   (artists-data.php), the same recipe the commission form uses. */
.artist-portrait-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(120% 120% at 50% 18%, oklch(0.62 0.14 var(--avatar-hue, 292) / 55%) 0%, var(--bg-surface-alt) 72%);
  color: var(--text-primary);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.4rem;
}

/* Room for two lines of name, the rating and two lines of status on every
   card. A name longer than that makes the plate taller, and since the rows
   share one height, every card's plate grows with it: the pictures stay
   the same size for everyone. */
.artist-plate {
  flex: 1 1 auto;
  display: grid;
  align-content: center;
  gap: 0.3rem;
  min-height: 6.5rem;
  padding: 0.5rem 0.35rem 0.35rem;
  text-align: left;
}

.artist-name { display: block; font-weight: 700; font-size: 0.8rem; line-height: 1.2; overflow-wrap: anywhere; }

/* The average of every Quality and Speed mark, and how many reviews it
   comes from. An artist with no reviews yet has no such line. Gold is the
   rating's colour in this section and nothing else's. */
.artist-rating {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.66rem;
  line-height: 1.2;
  white-space: nowrap;
}

.artist-rating .icon { color: var(--accent-gold); font-size: 0.92em; vertical-align: 0; }
.artist-rating-score { font-family: var(--font-mono); font-weight: 600; font-variant-numeric: tabular-nums; }
.artist-rating-count { font-family: var(--font-mono); color: var(--text-faint); }

.artist-status {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  color: var(--text-muted);
  font-size: 0.64rem;
  font-weight: 600;
  line-height: 1.3;
}

/* The dot is real state, the same three colours as the badge. */
.artist-status-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--text-faint); translate: 0 -1px; }
.artist-status--available .artist-status-dot { background: var(--accent-secondary); box-shadow: 0 0 8px var(--accent-secondary); }
.artist-status--at_capacity .artist-status-dot { background: var(--accent-gold-alt); }

/* ---------------------------------------------------------
   Under the table: where a visitor with no favourite goes
   --------------------------------------------------------- */
.artist-rest-row {
  grid-area: 2 / 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.9rem 1.5rem;
  margin-top: 1.25rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border-subtle);
  transition: opacity var(--transition-base);
}

.artist-rest-row p { max-width: 56ch; color: var(--text-muted); font-size: 0.92rem; line-height: 1.55; }
.artist-rest-row .btn { padding: 0.8rem 1.3rem; font-size: 0.88rem; }

/* ---------------------------------------------------------
   Tiers
   --------------------------------------------------------- */
@container artists (max-width: 349.98px) {
  .artist-table-wrap { --tracks: var(--tracks-xs); }
  .artist-card { --c: var(--c-xs); --r: var(--r-xs); }
}

@container artists (min-width: 640px) {
  .artist-table-wrap { --tracks: var(--tracks-md); --tilt-k: 1; }
  .artist-card { --c: var(--c-md); --r: var(--r-md); }
  .artist-portrait-fallback { font-size: 1.9rem; }
  .artist-name { font-size: 0.92rem; }
  .artist-rating { font-size: 0.74rem; }
  .artist-status { font-size: 0.7rem; }
}

@container artists (min-width: 900px) {
  .artist-table-wrap { --tracks: var(--tracks-lg); }
  .artist-card { --c: var(--c-lg); --r: var(--r-lg); }
}

/* ---------------------------------------------------------
   While a pick is open (classes set by artist-deck-stage.js)
   --------------------------------------------------------- */
/* The table stops leaning: the stage beside it does not lean, and the
   front card has to stay on its slot. */
.artists-body.is-staging .artist-table { transform: translate(0, 0); }

/* A pose is written inline and flown there by the script, so the card's
   own pose transitions are off from the first pick until the last card
   is back in its seat. Left on, they would run under every flight and
   show whenever a flight was not in charge. */
.artists-body.is-staging .artist-card {
  transition:
    opacity var(--transition-base),
    box-shadow var(--transition-base),
    border-color var(--transition-base);
}

/* One read of where everything sits in the layout. !important beats the
   inline pose and any flight that is running, so the boxes measured are
   the grid's own. */
.artists-body.is-measuring .artist-card {
  translate: none !important;
  rotate: none !important;
  scale: none !important;
}

/* The filter's dimming is put aside: a see-through card in a hand shows
   the cards behind it. */
.artists-body.is-staged .artist-card.is-dimmed { opacity: 1; }

/* In front: lit. Still a button (picking it again puts everyone back). */
.artist-card.is-front {
  border-color: color-mix(in oklch, var(--accent-secondary) 70%, transparent);
  box-shadow: var(--shadow-glow-cyan);
}

/* In the hand: stepped back a shade, so the front card is the bright one.
   A veil over the card, not opacity, for the same reason as above. */
.artist-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  background: var(--bg-base);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-base);
}

.artist-card.is-fanned::before { opacity: 0.34; }

/* Once the hand has settled, a card in it rises along its own lean under
   the pointer and loses its veil. The script writes the direction (--ux,
   --uy) with the pose; the pose itself reads --hx and --hy. */
.artists-body.is-settled .artist-card.is-fanned {
  transition:
    translate 0.28s cubic-bezier(0.16, 1, 0.3, 1),
    opacity var(--transition-base),
    box-shadow var(--transition-base),
    border-color var(--transition-base);
}

@media (hover: hover) {
  .artists-body.is-settled .artist-card.is-fanned:hover {
    --hx: calc(var(--ux, 0) * 1.4rem);
    --hy: calc(var(--uy, 0) * 1.4rem);
    border-color: color-mix(in oklch, var(--accent-secondary) 45%, transparent);
  }

  .artists-body.is-settled .artist-card.is-fanned:hover::before { opacity: 0; }
}

.artists-body.is-staged .artist-rest-row { opacity: 0; visibility: hidden; }

/* Reduced motion: the table does not lean and a hover changes the border
   only (the global reduced-motion rule already makes any remaining change
   instant). The resting tilt stays; it is not motion. The script skips
   every flight, so a pick is one instant change. */
@media (prefers-reduced-motion: reduce) {
  .artist-table { transform: none; }

  .artist-card:hover,
  .artist-card:focus-visible {
    rotate: calc(var(--tilt, 0deg) * var(--tilt-k));
    translate: 0 calc(var(--nudge, 0) * var(--tilt-k) * 1%);
  }

  .artists-body.is-settled .artist-card.is-fanned:hover { --hx: 0px; --hy: 0px; }
}

/* Printed or saved with a pick open, or before the table was dealt: the
   table as it rests. !important because a pose is an inline style, and no
   transition, or a card in the hand would print part-way to its seat. */
@media print {
  .artist-table.is-undealt .artist-card { opacity: 1; }

  .artist-card {
    transition: none !important;
    translate: 0 0 !important;
    rotate: 0deg !important;
    scale: 1 !important;
    z-index: auto !important;
    opacity: 1 !important;
  }

  .artist-card::before { display: none; }
  .artist-stage { display: none !important; }
}


/* ===== assets/css/components/artist-deck-stage.css ===== */
/* =========================================================
   Artists - the stage: where a picked artist is shown

   A scaffold laid over the table (same grid cell, artist-deck.css). It
   holds three things: an empty slot the picked card flies to, the sheet
   with that artist's details, and a small bar of controls. The cards
   themselves stay in the table; artist-deck-stage.js reads where the slot
   is and moves them with poses.

   It takes no room and no clicks until someone is picked: display none
   at rest, and pointer-events only on the sheet and the bar, so the slot
   never swallows a click meant for the card sitting in it.

   Three layouts, on the container queries from artist-deck.css:

     under 640px   slot, bar and sheet stacked, the hand a crown over the slot
     640px and up  slot beside the sheet
     900px and up  room on the left for the hand, slot, sheet

   --fan tells the script which hand to deal: crown (spread over the top
   of the slot) or left (fanned out to the left).
   ========================================================= */
.artist-stage {
  --slot-w: min(11.5rem, 46cqi);
  --fan: crown;
  --crown: 5.25rem;
  grid-area: 1 / 1;
  align-self: start;
  position: relative;
  z-index: 2;
  display: none;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "slot" "bar" "sheet";
  justify-items: center;
  row-gap: 0.9rem;
  /* Set by the script: how far down the table the stage opens. At the
     top, or on a big team around the row that was picked. */
  margin-top: var(--stage-top, 0px);
  padding-top: var(--crown);
  pointer-events: none;
}

.artists-body.is-staging .artist-stage { display: grid; }

/* A soft light on the table behind the front card, so the card reads as
   lifted off it. One fixed gradient that only fades in and out; the
   script says where the slot is (--spot-x, --spot-y). It sits under the
   cards, which is why it belongs to the section and not to the stage. */
.artists-body::before {
  --spot-w: 13rem;
  --spot-h: 17rem;
  content: "";
  position: absolute;
  inset: -2rem 0;
  z-index: 0;
  background: radial-gradient(var(--spot-w) var(--spot-h) at var(--spot-x, 50%) calc(var(--spot-y, 50%) + 2rem), color-mix(in oklch, var(--accent-primary) 30%, transparent), transparent 72%);
  /* Faded out towards the section's sides, where the light would
     otherwise be cut off in a straight line when the slot sits at the
     edge. A mask only reads how solid its colour is, so any opaque
     token does. */
  -webkit-mask-image: linear-gradient(to right, transparent, var(--bg-base) 14%, var(--bg-base) 86%, transparent);
  mask-image: linear-gradient(to right, transparent, var(--bg-base) 14%, var(--bg-base) 86%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.6s ease;
}

.artists-body.is-staged::before { opacity: 1; }

@container artists (min-width: 640px) {
  .artists-body::before { --spot-w: 19rem; --spot-h: 21rem; }
}

@container artists (min-width: 900px) {
  .artists-body::before { --spot-w: 26rem; --spot-h: 22rem; }
}

/* Where the front card lands. Its shape is the card's own (the script
   writes --card-ratio), so slot and card always agree. */
.artist-stage-slot {
  grid-area: slot;
  width: var(--slot-w);
  aspect-ratio: var(--card-ratio, 0.62);
  /* Clear of the fixed nav when the script scrolls the stage into view. */
  scroll-margin-block: calc(var(--nav-height) + var(--crown) + 1rem) 1rem;
}

/* ---------------------------------------------------------
   The bar: back, previous, next, and a line that names the card
   under the pointer
   --------------------------------------------------------- */
.artist-stage-bar {
  grid-area: bar;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  opacity: 0;
  visibility: hidden;
  pointer-events: auto;
  transition: opacity var(--transition-base), visibility 0s linear var(--transition-base);
}

/* It waits for the sheet (--open-delay, written by the script): on a
   narrow stage the others are shuffled right where the bar is. */
.artists-body.is-staged .artist-stage-bar {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--transition-base) var(--open-delay, 0ms), visibility 0s linear var(--open-delay, 0ms);
}

.artist-bar-steps { display: inline-flex; gap: 0.3rem; }

/* Custom buttons: background and border are set by .util-btn, which has
   no global reset to lean on. */
.artist-bar-step { justify-content: center; width: 2.75rem; height: 2.75rem; padding: 0; }
.artist-bar-step[data-artist-step="-1"] .icon { rotate: 180deg; }
.artist-bar-back { min-height: 2.75rem; }

.artist-stage-caption {
  flex: 1 1 100%;
  min-height: 1.3em;
  color: var(--text-faint);
  font-size: 0.8rem;
  line-height: 1.3;
  text-align: center;
}

/* ---------------------------------------------------------
   The sheet: the picked artist's details (content in
   artist-deck-leaf.css)
   --------------------------------------------------------- */
.artist-sheet {
  grid-area: sheet;
  justify-self: stretch;
  position: relative;
  min-width: 0;
  padding: 1.1rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(70% 90% at 100% 0%, color-mix(in oklch, var(--accent-primary) 20%, transparent), transparent 70%),
    var(--bg-surface);
  box-shadow: var(--shadow-glow-violet);
  /* Folded shut until someone is picked. The script unfolds it (a hinge
     on the edge that meets the card) and leaves no transform behind, so
     the text sits on whole pixels once it is open. */
  opacity: 0;
  visibility: hidden;
  pointer-events: auto;
  /* How the sheet looks folded shut, read by the script: hinged along its
     top edge here, along the edge beside the card once they sit side by
     side. */
  --sheet-folded: perspective(1200px) rotateX(-58deg);
  transform-origin: 50% 0%;
  /* Clear of the fixed nav when focus moves into it. */
  scroll-margin-block: calc(var(--nav-height) + 1rem) 1rem;
}

.artists-body.is-staged .artist-sheet { opacity: 1; visibility: visible; }

/* ---------------------------------------------------------
   640px and up: the slot beside the sheet
   --------------------------------------------------------- */
@container artists (min-width: 640px) {
  .artist-stage {
    --slot-w: min(16.5rem, 37cqi);
    --crown: 5.5rem;
    grid-template-columns: var(--slot-w) minmax(0, 1fr);
    grid-template-areas:
      "slot sheet"
      "bar  sheet";
    grid-template-rows: auto 1fr;
    column-gap: 1rem;
    justify-items: stretch;
    align-items: start;
  }

  .artist-stage-bar { justify-content: space-between; }
  .artist-bar-step { width: 2.4rem; height: 2.4rem; }
  .artist-bar-back { min-height: 2.4rem; padding: 0.5rem 0.75rem; font-size: 0.8rem; }
  .artist-stage-caption { text-align: left; }
  .artist-sheet { --sheet-folded: perspective(1400px) rotateY(-68deg); padding: 1.4rem 1.5rem; transform-origin: 0% 50%; }
}

/* ---------------------------------------------------------
   900px and up: the hand has its own room on the left
   --------------------------------------------------------- */
@container artists (min-width: 900px) {
  .artist-stage {
    --slot-w: min(19rem, 27cqi);
    --fan: left;
    --crown: 5.25rem;
    grid-template-columns: minmax(0, 1fr) var(--slot-w) minmax(0, 31rem);
    grid-template-areas:
      "hand slot sheet"
      "hand bar  sheet";
    column-gap: 1.1rem;
  }

  .artist-bar-back { padding: 0.55rem 0.9rem; font-size: 0.84rem; }
}

/* ---------------------------------------------------------
   One artist on the team: no table to pick from, so the card and the
   sheet are simply shown, with or without the script.
   --------------------------------------------------------- */
.artists-body.is-sole { row-gap: 1rem; }
.artists-body.is-sole .artist-stage { display: grid; grid-area: 2 / 1; padding-top: 0; grid-template-columns: minmax(0, 1fr); grid-template-areas: "sheet"; }
.artists-body.is-sole .artist-stage-slot,
.artists-body.is-sole .artist-stage-bar { display: none; }
.artists-body.is-sole .artist-sheet { opacity: 1; visibility: visible; }

/* With room, the card on the left and the sheet beside it. Both stay in
   the one grid cell (the section cannot restyle itself from its own
   container query), so the sheet makes its room with a margin. */
@container artists (min-width: 640px) {
  .artists-body.is-sole .artist-table-wrap { justify-self: start; align-self: center; margin-inline: 0; }
  .artists-body.is-sole .artist-stage { grid-area: 1 / 1; align-self: center; max-width: 62rem; margin-left: 15.5rem; }
}

/* Reduced motion: the script skips the unfold, and the bar's delayed
   entrance must not leave a gap where nothing can be clicked. */
@media (prefers-reduced-motion: reduce) {
  .artist-stage-bar,
  .artists-body.is-staged .artist-stage-bar { transition: none; }
}


/* ===== assets/css/components/artist-deck-leaf.css ===== */
/* =========================================================
   Artists - a leaf: one artist's details inside the sheet

   Every artist's leaf is in the page from the start (the commission
   button in it has to exist when commission-modal.js binds its triggers
   on load). The script only shows the one that was picked.

   At 640px and up all leaves sit in the same grid cell and the ones not
   showing are only hidden from view, so the sheet is as tall as its
   tallest leaf and nothing moves when the pick changes. Below that the
   leaves differ too much in height (a new artist's is a few lines), so
   the sheet fits the one that is showing.

   The words and the look follow the artist's profile page: the same
   facts under the same labels, at a smaller scale.
   ========================================================= */
.artist-leaf {
  display: none;
  flex-direction: column;
  gap: 0.85rem;
  min-width: 0;
}

.artist-leaf.is-active { display: flex; }

@container artists (min-width: 640px) {
  .artist-sheet {
    display: grid;
    /* At least as tall as the card beside it. */
    min-height: calc(var(--slot-w) / var(--card-ratio, 0.62));
  }

  .artist-leaf { grid-area: 1 / 1; display: flex; visibility: hidden; }
  .artist-leaf.is-active { visibility: visible; }
}

/* Each line of a leaf rises in after the one above it. --leaf-delay is
   written by the script: longer when the sheet is still unfolding. */
.artist-leaf.is-entering > * {
  animation: artist-leaf-line 0.42s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(var(--leaf-delay, 0ms) + var(--line, 0) * 35ms);
}

.artist-leaf > :nth-child(2) { --line: 1; }
.artist-leaf > :nth-child(3) { --line: 2; }
.artist-leaf > :nth-child(4) { --line: 3; }
.artist-leaf > :nth-child(5) { --line: 4; }
.artist-leaf > :nth-child(6) { --line: 5; }
.artist-leaf > :nth-child(n + 7) { --line: 6; }

@keyframes artist-leaf-line {
  from { opacity: 0; translate: 0 10px; }
  to { opacity: 1; translate: 0 0; }
}

/* ---- Name, status, role ---- */
.artist-leaf-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.9rem; }

.artist-leaf-name {
  font-size: 1.55rem;
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.1;
  overflow-wrap: anywhere;
}

.artist-leaf-name:focus-visible { outline: 2px solid var(--accent-secondary); outline-offset: 4px; border-radius: 2px; }

/* Availability badge. Opaque, no backdrop-filter. */
.artist-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  background: var(--bg-surface);
  font-size: 0.68rem;
  font-weight: 600;
  white-space: nowrap;
}

.artist-badge-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-faint); }
.artist-badge--available .artist-badge-dot { background: var(--accent-secondary); box-shadow: 0 0 8px var(--accent-secondary); }
.artist-badge--at_capacity .artist-badge-dot { background: var(--accent-gold-alt); }

.artist-leaf-role {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.9rem;
  margin-top: -0.35rem;
  color: var(--text-muted);
  font-size: 0.95rem;
  line-height: 1.4;
}

.artist-leaf-since { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-faint); }

/* Their own words, three lines at most: the profile has the rest. */
.artist-leaf-bio {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  max-width: 58ch;
  font-size: 0.95rem;
  line-height: 1.55;
  text-wrap: pretty;
}

/* ---- Reviews: the average, then the two marks it is made of ---- */
.artist-leaf-reviews {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.75rem;
  padding-block: 0.9rem;
  border-block: 1px solid var(--border-subtle);
}

.artist-leaf-score { display: flex; align-items: center; gap: 0.75rem; }

.artist-leaf-score-num {
  font-family: var(--font-heading);
  font-size: 2.3rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
}

.artist-leaf-score-side { display: grid; gap: 0.3rem; }
.artist-leaf-score-based { font-size: 0.78rem; line-height: 1.2; color: var(--text-faint); }

.artist-leaf-axes { display: grid; gap: 0.35rem; margin: 0; }
.artist-leaf-axes > div { display: grid; grid-template-columns: 3.6rem auto auto; align-items: center; justify-content: start; column-gap: 0.6rem; }
.artist-leaf-axes dt { font-size: 0.8rem; line-height: 1.2; color: var(--text-muted); }
.artist-leaf-axes dd { display: contents; }
.artist-leaf-axis-num { font-family: var(--font-mono); font-size: 0.74rem; font-weight: 600; font-variant-numeric: tabular-nums; }

.artist-leaf-reviews-link {
  margin-left: auto;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--accent-secondary);
  text-decoration: underline;
  text-decoration-color: color-mix(in oklch, var(--accent-secondary) 40%, transparent);
  text-underline-offset: 0.2em;
}

.artist-leaf-reviews-link:hover { text-decoration-color: var(--accent-secondary); }
.artist-leaf-reviews-link:focus-visible { outline: 2px solid var(--accent-secondary); outline-offset: 3px; border-radius: 2px; }

/* ---- Facts: what they do, what they can work on, their queue ---- */
.artist-leaf-facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  align-items: center;
  gap: 0.6rem 1.1rem;
  margin: 0;
}

.artist-leaf-facts dt {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.artist-leaf-facts dd { margin: 0; min-width: 0; }

.artist-leaf-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }

.artist-leaf-chips li {
  padding: 0.2rem 0.65rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  background: var(--bg-surface-alt);
  font-size: 0.76rem;
  line-height: 1.4;
}

.artist-leaf-queue { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.75rem; }
.artist-leaf-slots { display: inline-flex; flex-wrap: wrap; gap: 0.25rem; }
.artist-leaf-slot { width: 1.4rem; height: 0.5rem; border-radius: var(--radius-pill); border: 1px dashed var(--border-strong); }
.artist-leaf-slot.is-taken { border: 0; background: var(--gradient-primary); }
.artist-leaf-queue-text { color: var(--text-muted); font-size: 0.82rem; }

/* ---- What happens if you book them, and the two buttons ---- */
.artist-leaf-foot { display: grid; gap: 0.85rem; margin-top: auto; padding-top: 0.25rem; }
.artist-leaf-status { max-width: 58ch; color: var(--text-muted); font-size: 0.88rem; line-height: 1.55; }
.artist-leaf-actions { display: flex; flex-wrap: wrap; gap: 0.55rem; }

/* On a phone the buttons take the full width, and a long name may wrap
   the label rather than push the sheet wider. */
.artist-leaf-actions .btn {
  flex: 1 1 100%;
  padding: 0.8rem 1.3rem;
  font-size: 0.88rem;
  white-space: normal;
  text-align: center;
}

@container artists (max-width: 639.98px) {
  .artist-leaf-facts { grid-template-columns: minmax(0, 1fr); gap: 0.35rem; }
  .artist-leaf-facts dd { margin-bottom: 0.45rem; }
  .artist-leaf-reviews-link { margin-left: 0; }
}

@container artists (min-width: 640px) {
  .artist-leaf-name { font-size: 1.8rem; }
  .artist-leaf-actions .btn { flex: 0 1 auto; }
}

@media (prefers-reduced-motion: reduce) {
  .artist-leaf.is-entering > * { animation: none; }
}


/* ===== assets/css/components/process.css ===== */
/* =========================================================
   Process - connected step-line (deliberately not another
   equal-column card grid, since Services sits right above this
   and already uses that layout family)
   ========================================================= */
.process-line { display: flex; }

.process-step {
  position: relative;
  flex: 1;
  padding: 2.5rem 1.25rem 0;
  text-align: center;
}

.process-step::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent-primary);
  box-shadow: 0 0 0 5px oklch(0.58 0.19 292 / 16%);
  z-index: 1;
}

.process-step::after {
  content: '';
  position: absolute;
  top: 6px;
  left: 50%;
  width: 100%;
  height: 2px;
  background: var(--border-subtle);
}

.process-step:last-child::after { display: none; }

.process-title { font-size: 1.05rem; font-weight: 700; margin-bottom: 0.5rem; }
.process-desc { color: var(--text-muted); font-size: 0.9rem; }

/* =========================================================
   Responsive - vertical timeline under 768px
   ========================================================= */
@media (max-width: 768px) {
  .process-line { flex-direction: column; gap: 2rem; }

  .process-step { padding: 0 0 0 2rem; text-align: left; }

  .process-step::before { top: 0.3rem; left: 0; transform: none; }

  .process-step::after {
    top: 0.3rem;
    left: 6px;
    width: 2px;
    height: calc(100% + 2rem);
    transform: none;
  }
}


/* ===== assets/css/components/pricing.css ===== */
/* =========================================================
   Pricing

   Structure follows the premium pricing pages it was modelled on
   (Linear, with Raycast and Vercel for the comparison table and the
   questions): equal plan cards on shared baselines, the featured plan
   one step up the surface ladder, "Everything in X, plus" lists, a
   grouped comparison table, short questions. Palette, glow and pill
   buttons stay FluffyFile's own.
   ========================================================= */
.pricing {
  /* What the section band actually paints (bg-surface/40 over the page),
     as a solid colour for the sticky table header to sit on. */
  --pricing-ground: color-mix(in oklab, var(--bg-surface) 40%, var(--bg-base));
}

/* The switch between the price lists, and the way a list arrives when it
   is switched to, live in pricing-switch.css. */

.pricing-empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.25rem;
  max-width: 480px;
  padding: 2rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  color: var(--text-muted);
}

/* =========================================================
   Plan cards

   Each card spans seven rows of the plans grid and takes them as a
   subgrid (intro, price, spec, button, stats, lead-in, list), so
   those rows line up across every card in the row: prices on one
   line, buttons on one line, lists starting together.
   ========================================================= */
.pricing-plans {
  display: grid;
  grid-template-columns: repeat(var(--plan-count, 4), minmax(0, 1fr));
  gap: 1rem;
}

.pricing-card-scene {
  display: grid;
  grid-row: span 7;
  grid-template-rows: subgrid;
  row-gap: 0;
  perspective: 1200px;
}

.pricing-card {
  grid-row: span 7;
  display: grid;
  grid-template-rows: subgrid;
  row-gap: 0;
  position: relative;
  padding: 1.75rem 1.5rem 1.6rem;
  border: 1px solid oklch(0.58 0.19 292 / 22%);
  border-radius: var(--radius-lg);
  background: oklch(0.19 0.055 285 / 55%);
  transition: border-color var(--transition-base), box-shadow var(--transition-base);
}

/* Browsers without subgrid: a plain column per card. Rows no longer
   line up across cards, but every card still reads correctly. */
@supports not (grid-template-rows: subgrid) {
  .pricing-card-scene,
  .pricing-card { display: flex; flex-direction: column; }
  .pricing-card { flex: 1; }
}

.pricing-card:hover,
.pricing-card:focus-within { border-color: var(--border-strong); }

/* Featured: one step up the surface ladder plus the violet glow, rather
   than a louder colour. */
.pricing-card.is-featured {
  --card-rest-shadow: var(--shadow-glow-violet);
  background: var(--bg-surface-alt);
  border-color: oklch(0.58 0.19 292 / 65%);
  box-shadow: var(--shadow-glow-violet);
}

/* ---------------------------------------------------------
   Tilt. pricing-card-tilt.js adds .is-tiltable and writes the
   --tilt-* properties every frame, so transform carries no
   transition here (one would lag behind the pointer). Without
   the class (no JS, reduced motion) the card stays flat and
   only its border answers the pointer.

   The card tilts as one flat plane. It deliberately has no
   transform-style: preserve-3d: Firefox mis-sorts preserve-3d
   layers after the first 3D context on a page, painting every
   later card's own background over its contents, so three of
   the four cards rendered blank and their buttons could not be
   clicked. Depth comes from parallax instead (below).
   --------------------------------------------------------- */
.pricing-card.is-tiltable {
  transform: rotateX(var(--tilt-rx, 0deg)) rotateY(var(--tilt-ry, 0deg)) translateZ(var(--tilt-lift, 0px));
  transition: border-color var(--transition-base);
  box-shadow:
    0 0 0 1px oklch(0.85 0.15 202 / calc(var(--tilt-go, 0) * 22%)),
    var(--tilt-sx, 0px) var(--tilt-sy, 24px) 60px -34px oklch(0.85 0.15 202 / calc(var(--tilt-go, 0) * 45%)),
    var(--card-rest-shadow, 0 0 #0000);
}

/* Depth by parallax: raised layers slide toward the pointer, further
   the higher they sit, which is where a raised layer would appear to
   move on a real tilting card. --tilt-dx/--tilt-dy run -1 to 1 and ease
   back to 0 with the tilt, so at rest everything sits in place. Plain
   2D translates, so no browser has any 3D layers to sort. */
.pricing-card.is-tiltable [data-depth="1"] { transform: translate(calc(var(--tilt-dx, 0) * 3px), calc(var(--tilt-dy, 0) * 3px)); }
.pricing-card.is-tiltable [data-depth="2"] { transform: translate(calc(var(--tilt-dx, 0) * 6px), calc(var(--tilt-dy, 0) * 6px)); }
.pricing-card.is-tiltable [data-depth="3"] { transform: translate(calc(var(--tilt-dx, 0) * 10px), calc(var(--tilt-dy, 0) * 10px)); }

/* Plain alpha, no mix-blend-mode: a blend layer has to be recomposited
   against whatever is behind it, which here is the animated ambient
   background, every frame, on all four cards. */
.pricing-card-glare {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background:
    radial-gradient(circle at var(--tilt-gx, 50%) var(--tilt-gy, 30%), oklch(0.97 0.03 240 / 14%), transparent 50%),
    linear-gradient(115deg, transparent 30%, oklch(0.85 0.15 202 / 5%) 48%, transparent 62%);
  opacity: var(--tilt-go, 0);
}

.pricing-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }

.pricing-name { font-family: var(--font-body); font-size: 1rem; font-weight: 700; }

.pricing-tag {
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-gold);
  background: oklch(0.78 0.135 84 / 15%);
  padding: 0.3rem 0.65rem;
  border-radius: var(--radius-pill);
  white-space: nowrap;
  font-family: var(--font-mono);
}

.pricing-desc { margin-top: 0.4rem; color: var(--text-muted); font-size: 0.88rem; line-height: 1.5; }

.pricing-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 0.5rem;
  margin-top: 1.4rem;
}

.price-value {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: clamp(2.2rem, 1.2vw + 1.6rem, 2.8rem);
  line-height: 1;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
}

.price-tilde { color: var(--text-faint); font-weight: 500; margin-left: 1px; }

.price-period { font-size: 0.85rem; color: var(--text-muted); }

/* The spec strip keeps the mono voice. The line under it says the same
   thing in everyday words (includes/pricing-glossary.php). */
.pricing-spec { margin-top: 0.6rem; }

.pricing-spec-line {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-sky);
}

.pricing-spec-plain { display: block; margin-top: 0.4rem; font-size: 0.8rem; line-height: 1.45; color: var(--text-muted); }

.pricing-card-action { margin-top: 1.4rem; }

/* .btn:hover lifts the button 3px; inside a tilting card that fights the
   depth layer it sits on, so the lift is dropped here. */
.pricing-card-cta { width: 100%; padding: 0.8rem 1.25rem; font-size: 0.875rem; }
.pricing-card-cta:hover { transform: none; }

.pricing-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
  margin: 1.5rem 0 0;
  padding-top: 1.1rem;
  border-top: 1px solid oklch(0.58 0.19 292 / 22%);
}

.pricing-stats:not(:has(div)) { margin: 0; padding: 0; border: 0; }

.pricing-stats div { display: flex; flex-direction: column; gap: 0.15rem; }
.pricing-stats dt { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-faint); }
.pricing-stats dd { margin: 0; font-size: 0.88rem; font-weight: 600; }

.pricing-lead { margin-top: 1.25rem; font-size: 0.84rem; color: var(--text-faint); }

.pricing-features { margin-top: 0.6rem; display: flex; flex-direction: column; gap: 0.55rem; }

.pricing-features li {
  display: grid;
  grid-template-columns: 1rem minmax(0, 1fr);
  gap: 0.5rem;
  font-size: 0.88rem;
  line-height: 1.4;
  color: var(--text-primary);
}

.pricing-features li::before {
  content: '\2713';
  color: var(--accent-secondary);
  font-size: 0.8rem;
  line-height: 1.55;
}

/* A feature with a plain-word note: the name, then the note under it. */
.pricing-feature { display: flex; flex-direction: column; gap: 0.05rem; }
.pricing-gloss { font-size: 0.8rem; color: var(--text-muted); }

/* =========================================================
   Quote band (tiers with no fixed price, e.g. Batch / Bulk)
   ========================================================= */
.pricing-quote {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 1.25rem 2.5rem;
  margin-top: 1rem;
  padding: 1.75rem clamp(1.5rem, 3vw, 2.25rem);
  border: 1px solid oklch(0.58 0.19 292 / 22%);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(90% 140% at 100% 0%, oklch(0.42 0.17 288 / 22%), transparent 60%),
    oklch(0.19 0.055 285 / 55%);
}

.pricing-quote-name { font-size: 1.3rem; font-weight: 600; letter-spacing: -0.02em; }
.pricing-quote-desc { margin-top: 0.35rem; color: var(--text-muted); font-size: 0.95rem; }

.pricing-quote-features { display: flex; flex-wrap: wrap; gap: 0.4rem 1.25rem; margin-top: 0.6rem; }
.pricing-quote-features li { font-size: 0.85rem; color: var(--text-muted); }
.pricing-quote-features li::before { content: '\2713'; color: var(--accent-secondary); margin-right: 0.45rem; font-size: 0.8rem; }

.pricing-quote-side { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
.pricing-quote-price { display: flex; flex-direction: column; gap: 0.1rem; }
.pricing-quote-label { font-family: var(--font-heading); font-weight: 600; font-size: 1.6rem; letter-spacing: -0.03em; line-height: 1.1; }
.pricing-quote-price .price-period { font-family: var(--font-mono); font-size: 0.72rem; }

/* =========================================================
   Assurances
   ========================================================= */
.pricing-assurances {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1rem 2rem;
  margin-top: 2.25rem;
}

.pricing-assurances li {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding-left: 1rem;
  border-left: 1px solid var(--border-strong);
}

.pricing-assurances strong { font-size: 0.92rem; font-weight: 600; }
.pricing-assurances span { font-size: 0.85rem; color: var(--text-muted); }

/* =========================================================
   Comparison table
   ========================================================= */
.pricing-compare { margin-top: clamp(4.5rem, 8vw, 6.5rem); }

/* The questions below (pricing-faq.css) share this header. */
.pricing-compare-head,
.pricing-faq-head { display: flex; flex-direction: column; gap: 0.6rem; max-width: 60ch; }

.pricing-compare-head { margin-bottom: 1.75rem; }

.pricing-compare-title,
.pricing-faq-title {
  font-size: clamp(1.7rem, 2vw + 1rem, 2.35rem);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.08;
}

.pricing-compare-sub,
.pricing-faq-sub { color: var(--text-muted); }

/* The link to the questions that explain the table's terms. */
.pricing-compare-sub a { color: var(--accent-secondary); font-weight: 600; }
.pricing-compare-sub a:hover { text-decoration: underline; text-underline-offset: 3px; }

.pricing-compare-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 0.9rem;
}

.pricing-compare-table th,
.pricing-compare-table td {
  padding: 0.75rem 1rem;
  text-align: left;
  vertical-align: middle;
}

.pricing-compare-corner { width: 26%; }

/* Sticks under the nav so the plan names stay in view while scrolling
   a long table. Needs a solid background: the band behind is translucent. */
.pricing-compare-table thead th {
  position: sticky;
  top: var(--nav-height);
  z-index: 2;
  padding-block: 1rem;
  vertical-align: bottom;
  background: var(--pricing-ground);
  border-bottom: 1px solid var(--border-strong);
}

.pricing-compare-table thead th[data-col] > * { display: block; }
.pricing-compare-plan { font-weight: 700; font-size: 0.95rem; }
.pricing-compare-price {
  margin-top: 0.15rem;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 1.15rem;
  letter-spacing: -0.02em;
  color: var(--text-muted);
}

.pricing-compare-table .pricing-compare-cta {
  display: inline-flex;
  margin-top: 0.6rem;
  padding: 0.55rem 1rem;
  font-size: 0.8rem;
}

.pricing-compare-table tbody th[scope="row"] { font-weight: 500; color: var(--text-primary); }
.pricing-compare-table tbody th[scope="row"] small { display: block; margin-top: 0.1rem; font-size: 0.78rem; font-weight: 400; color: var(--text-muted); }

.pricing-compare-group th {
  padding-top: 1.9rem;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  border-bottom: 1px solid oklch(0.58 0.19 292 / 22%);
}

.pricing-compare-table td { transition: background var(--transition-base); }
.pricing-compare-table thead th[data-col] { transition: background var(--transition-base); }

/* The featured column keeps a faint lift; pricing-compare.js adds
   .is-hot to every cell of the plan being hovered, on a card or in
   the table. */
.pricing-compare-table td.is-featured { background: oklch(0.25 0.075 288 / 35%); }
.pricing-compare-table thead th.is-featured { background: color-mix(in oklab, var(--pricing-ground) 70%, var(--bg-surface-alt)); }
.pricing-compare-table td.is-hot { background: oklch(0.53 0.19 292 / 16%); }
.pricing-compare-table thead th.is-hot { background: color-mix(in oklab, var(--pricing-ground) 78%, var(--accent-primary)); }

.pricing-compare-spec {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--accent-sky);
}

.pricing-yes { color: var(--accent-secondary); font-weight: 600; }

.pricing-no {
  display: inline-block;
  width: 12px;
  height: 1px;
  vertical-align: middle;
  background: var(--text-faint);
  opacity: 0.5;
}

/* =========================================================
   Responsive
   ========================================================= */
@media (max-width: 1080px) {
  .pricing-plans { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
  /* The table scrolls sideways instead of squeezing five columns. The
     header can't stick to the page inside a scroll box, so the feature
     names stick to the left edge instead. */
  .pricing-compare-scroll { overflow-x: auto; scrollbar-width: thin; }
  .pricing-compare-table { min-width: 760px; }
  .pricing-compare-table thead th { position: static; }
  .pricing-compare-table tbody th[scope="row"],
  .pricing-compare-corner {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--pricing-ground);
  }

  .pricing-quote { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 640px) {
  .pricing-plans { grid-template-columns: minmax(0, 1fr); }
}


/* ===== assets/css/components/pricing-switch.css ===== */
/* =========================================================
   Pricing switch

   The pricing header when there is more than one price list: the
   heading on the left and, beside it, a pill track with one option per
   list and a thumb that slides under the list being shown.

   The server prints the options as plain links (?prices=blender), with
   the right list already showing, so the switch works with scripts off.
   pricing-switch.js turns the links into tabs and flips the lists in
   place. Both states are styled here: aria-current marks the shown
   list as the server prints it, aria-selected once the script runs.

   Also here, because they only matter once there is a second list:
   the stacked price for a row with a four-figure price, and the way a
   list arrives when it is switched to.
   ========================================================= */

/* The shared .section-header-row with more air between its two halves,
   and a heading that gives way before the switch does. Once both no
   longer fit on one line, the switch wraps under the heading. */
.pricing .section-header-row { gap: 1.5rem 2rem; }
.pricing .section-header-row .section-header { flex: 1 1 22rem; min-width: 0; }

/* Both sentences sit in one grid cell and only one shows, so the header
   is the same height for either list and the switch never moves under
   the pointer when it is flipped. */
.pricing-subtitles { display: grid; }
.pricing-subtitles > p { grid-area: 1 / 1; }
.pricing-subtitles > p[aria-hidden="true"] { visibility: hidden; }

.pricing-switch-wrap { position: relative; flex: 0 1 29rem; min-width: 0; }

/* Marketing scale, so a pill. Two equal columns for the two lists: the
   thumb is as wide as one column and moves by its own width, so nothing
   has to be measured. */
.pricing-switch {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  padding: 5px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  background-color: color-mix(in oklab, var(--bg-base) 82%, transparent);
}

/* One step up the surface ladder with the cyan hairline the site's other
   segmented controls use. The violet stays with the featured card. */
.pricing-switch-thumb {
  position: absolute;
  top: 5px;
  bottom: 5px;
  left: 5px;
  width: calc((100% - 10px) / 2);
  border: 1px solid color-mix(in oklab, var(--accent-secondary) 55%, transparent);
  border-radius: var(--radius-pill);
  background-color: var(--bg-surface-alt);
  box-shadow: 0 1px 0 0 color-mix(in oklab, var(--text-primary) 14%, transparent) inset;
  transform: translateX(calc(var(--switch-index, 0) * 100%));
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Positioned, so the words paint above the thumb. The name and the
   "from" price share a line, with the few words about the list under
   them. */
.pricing-switch-option {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.1rem 0.6rem;
  min-height: 44px;
  padding: 0.6rem 1.4rem 0.65rem;
  border-radius: var(--radius-pill);
  color: var(--text-muted);
  transition: color var(--transition-fast);
}

.pricing-switch-option:focus-visible {
  outline: 2px solid var(--accent-secondary);
  outline-offset: 2px;
}

.pricing-switch-label { font-weight: 600; font-size: 1rem; letter-spacing: -0.01em; }

.pricing-switch-from {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: var(--accent-sky);
}

.pricing-switch-sub {
  grid-column: 1 / -1;
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-faint);
  transition: color var(--transition-fast);
}

.pricing-switch-option:hover,
.pricing-switch-option:is([aria-current="true"], [aria-selected="true"]) { color: var(--text-primary); }

.pricing-switch-option:is([aria-current="true"], [aria-selected="true"]) .pricing-switch-sub { color: var(--text-muted); }

/* =========================================================
   Four-figure prices

   A row holding a price of $1,000 or more stacks the period under
   every price in that row, so "$1,749~" keeps its line and no card
   wraps on its own.
   ========================================================= */
.pricing-plans[data-price-wide] .pricing-price { flex-direction: column; align-items: flex-start; gap: 0.35rem; }
.pricing-plans[data-price-wide] .price-value { white-space: nowrap; }

/* =========================================================
   A list arriving

   The cards of the list that was switched to rise one after another,
   and the quote band after them. pricing-switch.js adds .is-entering;
   the list the page opens with never gets it, and under reduced motion
   no list does.
   ========================================================= */
.pricing-plans.is-entering .pricing-card-scene {
  animation: pricing-plan-rise 0.55s cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(var(--plan-i, 0) * 70ms);
}

.pricing-plans.is-entering ~ .pricing-quote { animation: pricing-plan-rise 0.55s cubic-bezier(0.16, 1, 0.3, 1) 0.28s both; }

@keyframes pricing-plan-rise {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: none; }
}

/* =========================================================
   Responsive
   ========================================================= */
@media (max-width: 640px) {
  /* The row is a column here (style.css). The switch takes the full
     width, so both options stay easy to hit with a thumb. */
  .pricing .section-header-row { align-items: stretch; }
  .pricing .section-header-row .section-header,
  .pricing-switch-wrap { flex: none; }
  .pricing-switch-option { padding-inline: 1.1rem; }
}

@media (max-width: 520px) {
  /* Narrow phones keep the name and the "from" price, stacked. */
  .pricing-switch-sub { display: none; }
  .pricing-switch-option { grid-template-columns: minmax(0, 1fr); gap: 0; padding-block: 0.5rem; }
}

/* =========================================================
   The sheet and the mesh

   A small picture rides on the chosen option and reaches over the
   section's top edge: the flat texture sheet on the painting list, the
   wireframe head on the building list. pricing-critter.js draws both
   from the careers page's critter. It moves with the thumb, by
   transform. The script shows it and marks the wrap, so without
   scripts there is no picture and no room kept for one.
   ========================================================= */
.pricing-perch {
  position: absolute;
  left: 0;
  bottom: calc(100% - 0.5rem);
  display: flex;
  justify-content: center;
  width: 50%;
  pointer-events: none;
  transform: translateX(calc(var(--switch-index, 0) * 100%));
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.pricing-perch[hidden] { display: none; }
.pricing-perch canvas { display: block; width: 8.5rem; height: 8.5rem; }

/* Once the switch has wrapped under the heading (the row is narrower
   than both parts and their gap), the picture needs room above it. The
   row is the container, so this follows the row and not the window. */
.pricing .section-header-row { container: pricing-header / inline-size; }
@container pricing-header (max-width: 52.99rem) {
  .pricing-switch-wrap.has-perch { margin-top: 7rem; }
}

@media (max-width: 640px) {
  .pricing-perch canvas { width: 6rem; height: 6rem; }
  .pricing-switch-wrap.has-perch { margin-top: 5.25rem; }
}


/* ===== assets/css/components/pricing-faq.css ===== */
/* =========================================================
   Pricing questions

   Two levels of native details. A category is a quiet row with its
   title, a count and a line of what is inside; open, it shows its
   questions. A closed question is a hairline row. The open one lifts
   onto its own surface, one step up the ladder rather than a new
   colour, and its answer shows the point as well as saying it (the ~ on
   a real price, where a discount code goes, rounds and days per tier,
   the quote band in small). Closed, the whole block is three rows.
   Header type is shared with the comparison table in pricing.css.
   ========================================================= */
.pricing-faq {
  --faq-hairline: oklch(0.58 0.19 292 / 22%);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
  gap: 2rem 4rem;
  align-items: start;
  margin-top: clamp(4.5rem, 8vw, 6.5rem);
}

/* The heading holds still under the nav while the answers open. */
.pricing-faq-aside {
  position: sticky;
  top: calc(var(--nav-height) + 1.5rem);
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.pricing-faq-ask {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
  padding-left: 1rem;
  border-left: 1px solid var(--border-strong);
}

.pricing-faq-ask strong { font-size: 0.95rem; font-weight: 600; }
.pricing-faq-ask span { font-size: 0.88rem; color: var(--text-muted); }

.pricing-faq .btn { padding: 0.7rem 1.25rem; font-size: 0.875rem; }

/* ---------- Rows ---------- */
.pricing-faq-list { display: flex; flex-direction: column; min-width: 0; }

.pricing-faq-list summary { list-style: none; cursor: pointer; border-radius: var(--radius-md); }
.pricing-faq-list summary::-webkit-details-marker { display: none; }

.pricing-faq-list summary:focus-visible {
  outline: 2px solid var(--accent-secondary);
  outline-offset: -2px;
}

/* A link can land on the block or on either level (pricing-faq.js);
   keep the target clear of the fixed nav. */
.pricing-faq,
.faq-cat,
.faq-q { scroll-margin-top: calc(var(--nav-height) + 1rem); }

/* Categories: the first level. */
.faq-cat { border-top: 1px solid var(--faq-hairline); }
.faq-cat:last-child { border-bottom: 1px solid var(--faq-hairline); }

.faq-cat-summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.25rem 1rem;
  padding: 1.25rem 1.1rem;
}

.faq-cat-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.8rem; }

.faq-cat-title {
  font-family: var(--font-heading);
  font-size: 1.22rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  transition: color var(--transition-fast);
}

.faq-cat-count { font-family: var(--font-mono); font-size: 0.68rem; color: var(--accent-sky); }
.faq-cat-topics { grid-column: 1; font-size: 0.88rem; line-height: 1.5; color: var(--text-muted); }
.faq-cat-summary .pricing-faq-icon { grid-column: 2; grid-row: 1 / span 2; }
.faq-cat:not([open]) .faq-cat-summary:hover .faq-cat-title { color: var(--accent-secondary); }

/* Open, the questions themselves say what is inside. */
.faq-cat[open] .faq-cat-topics { display: none; }

.faq-cat-body { display: flex; flex-direction: column; padding-bottom: 1.1rem; }

/* Questions: the second level. */
.faq-q {
  border-top: 1px solid var(--faq-hairline);
  border-radius: 0;
  transition: background var(--transition-base), border-color var(--transition-base), border-radius var(--transition-base);
}

.faq-q[open] {
  background: color-mix(in oklab, var(--bg-surface) 70%, transparent);
  border-color: transparent;
  border-radius: var(--radius-md);
  box-shadow: 0 0 0 1px var(--border-subtle);
}

/* The hairline next to an open card would double its edge. */
.faq-q[open] + .faq-q { border-top-color: transparent; }

.faq-q-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.1rem;
  font-weight: 600;
  font-size: 0.98rem;
}

.faq-q:not([open]) .faq-q-summary { color: var(--text-muted); transition: color var(--transition-fast); }
.faq-q:not([open]) .faq-q-summary:hover { color: var(--text-primary); }

.pricing-faq-q { transition: transform var(--transition-base); }
.faq-q:not([open]) .faq-q-summary:hover .pricing-faq-q { transform: translateX(4px); }

/* The + in a ring, turning to an x. */
.pricing-faq-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 1px solid var(--border-subtle);
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: 1rem;
  line-height: 1;
  color: var(--text-faint);
  transition: transform var(--transition-base), color var(--transition-base), border-color var(--transition-base);
}

.faq-q .pricing-faq-icon { width: 26px; height: 26px; font-size: 0.9rem; }

.pricing-faq-list summary:hover .pricing-faq-icon { border-color: var(--border-strong); color: var(--text-primary); }

.faq-cat[open] .faq-cat-summary .pricing-faq-icon,
.faq-q[open] .faq-q-summary .pricing-faq-icon {
  transform: rotate(45deg);
  color: var(--accent-secondary);
  border-color: color-mix(in oklab, var(--accent-secondary) 45%, transparent);
}

/* ---------- Answers ---------- */
.pricing-faq-a {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  padding: 0 1.1rem 1.4rem;
}

.pricing-faq-a p { max-width: 58ch; color: var(--text-muted); line-height: 1.65; }

@media (prefers-reduced-motion: no-preference) {
  .faq-cat[open] .faq-cat-body,
  .faq-q[open] .pricing-faq-a { animation: pricing-faq-in 0.4s cubic-bezier(0.16, 1, 0.3, 1); }
}

@keyframes pricing-faq-in {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}

/* Terms and what they mean, each tagged with the tiers that have it. */
.faq-defs { display: grid; gap: 0.9rem; margin: 0; }

.faq-defs div {
  display: grid;
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
  gap: 0.2rem 1.25rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--faq-hairline);
}

.faq-defs div:first-child { border-top: 0; padding-top: 0; }
.faq-defs dt { display: flex; flex-direction: column; gap: 0.2rem; font-weight: 600; font-size: 0.92rem; }
.faq-defs dt small { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 600; color: var(--accent-sky); }
.faq-defs dd { margin: 0; color: var(--text-muted); font-size: 0.92rem; line-height: 1.6; }

/* Short labels in a row: the avatar bases, the license items. */
.faq-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.faq-chips li {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  font-size: 0.82rem;
  color: var(--text-muted);
}

/* The ~, shown on a real price. */
.pricing-faq-tilde {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.25rem;
  padding: 1rem 1.1rem;
  border-radius: var(--radius-sm);
  background: var(--bg-base);
}

.pricing-faq-tilde-price {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 2.3rem;
  line-height: 1;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
}

.pricing-faq-tilde-mark {
  display: inline-block;
  position: relative;
  top: -0.06em;
  margin-left: 4px;
  padding: 0.02em 0.14em 0.1em;
  line-height: 0.9;
  color: var(--accent-secondary);
  background: oklch(0.85 0.15 202 / 12%);
  border-radius: var(--radius-sm);
}

.pricing-faq-tilde-note { display: flex; flex-direction: column; gap: 0.1rem; }

.pricing-faq-tilde-note b {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-sky);
}

.pricing-faq-tilde-note span { font-size: 0.85rem; color: var(--text-faint); }

/* Where a discount code goes: the steps to paying, the current one lifted. */
.pricing-faq-steps { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.65rem; font-size: 0.85rem; }

.pricing-faq-step {
  padding: 0.45rem 0.75rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  color: var(--text-muted);
}

.pricing-faq-step.is-here { color: var(--text-primary); border-color: var(--border-strong); background: var(--bg-surface-alt); }
.pricing-faq-step small { margin-left: 0.4rem; font-family: var(--font-mono); font-size: 0.7rem; color: var(--accent-sky); }
.pricing-faq-arrow { color: var(--text-faint); }

/* Rounds of changes, and delivery times, per tier. */
.pricing-faq-rounds {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  gap: 0.6rem 1.1rem;
  align-items: center;
}

.pricing-faq-rounds li { display: contents; }
.pricing-faq-rounds-tier { font-size: 0.9rem; color: var(--text-muted); }
.pricing-faq-rounds li.is-featured .pricing-faq-rounds-tier { color: var(--text-primary); }

.pricing-faq-rounds-count {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.pricing-faq-pips { display: flex; gap: 5px; }

.pricing-faq-pips i {
  width: 16px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--accent-secondary);
}

/* The quote band, in small. */
.pricing-faq-quote {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.9rem 1.25rem;
  padding: 0.9rem 1rem 0.9rem 1.1rem;
  border-radius: var(--radius-sm);
  background:
    radial-gradient(90% 140% at 100% 0%, oklch(0.42 0.17 288 / 22%), transparent 60%),
    var(--bg-base);
}

.pricing-faq-quote-name { font-weight: 600; }
.pricing-faq-quote-price { margin-top: 0.1rem; font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-faint); }

/* ---------- Responsive and motion ---------- */
@media (max-width: 900px) {
  .pricing-faq { grid-template-columns: minmax(0, 1fr); }
  .pricing-faq-aside { position: static; }
}

@media (max-width: 560px) {
  .faq-defs div { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 480px) {
  .faq-cat-summary { padding: 1.1rem 0.9rem; }
  .faq-q-summary { padding: 0.95rem 0.9rem; }
  .pricing-faq-a { padding: 0 0.9rem 1.25rem; }
  .pricing-faq-tilde-price { font-size: 2rem; }
  /* Six full-size pips plus a long tier name don't fit a phone row. */
  .pricing-faq-rounds { column-gap: 0.75rem; }
  .pricing-faq-pips { gap: 4px; }
  .pricing-faq-pips i { width: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  .faq-q,
  .faq-q-summary,
  .faq-cat-title,
  .pricing-faq-q,
  .pricing-faq-icon { transition: none; }
}


/* ===== assets/css/components/about.css ===== */
/* =========================================================
   About: the founders as a duo

   Both portraits large and overlapping in the middle, each person's
   details on their own side, facing in. Hovering either side brings that
   portrait forward with a halo and quiets the other (the halo and lift
   carry over from the old profile cards' hover). Social buttons are
   social-buttons.css, shared with Contact.
   ========================================================= */
.about-header {
  margin-inline: auto;
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
  text-align: center;
}

.about-header .section-title { text-wrap: balance; }

/* Links the founders' section to the roster it credits. */
.about-roster-link {
  color: var(--accent-secondary);
  text-decoration: underline;
  text-underline-offset: 3px;
  font-weight: 600;
}

.about-duo {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas: "a portraits b";
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3.5rem);
}

/* ---------- Portraits ---------- */
.about-portraits {
  --size: clamp(9rem, 14vw, 12rem);
  grid-area: portraits;
  position: relative;
  display: flex;
  align-items: center;
  padding-block: 1.5rem;
}

.about-portraits-glow {
  position: absolute;
  inset: -30% -25%;
  border-radius: 50%;
  background: radial-gradient(closest-side, color-mix(in oklab, var(--accent-primary) 45%, transparent), color-mix(in oklab, var(--accent-primary) 12%, transparent) 60%, transparent 100%);
  pointer-events: none;
}

/* Each portrait sits in a slot with its orbit of social buttons. The
   offsets live on the slot, so the orbit centres on its portrait. The
   slot's translate makes it a stacking context, so bringing a portrait
   forward means raising its slot, not the portrait inside it. */
.about-portrait-slot { position: relative; z-index: 1; }

.about-duo:has(.about-person--a:hover, .about-portrait--a:hover) .about-portrait-slot--a,
.about-duo:has(.about-person--b:hover, .about-portrait--b:hover) .about-portrait-slot--b { z-index: 2; }

.about-portrait-slot.is-open { z-index: 3; }

.about-portrait {
  position: relative;
  z-index: 1;
  width: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
  padding: 4px;
  background: var(--gradient-primary);
  transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.35s ease, filter 0.35s ease;
}

/* Tilted and offset like a pair of stickers, the second overlapping the
   first by a fifth of its width. */
.about-portrait-slot--a { translate: 0 -0.9rem; }
.about-portrait-slot--b { margin-left: calc(var(--size) * -0.22); translate: 0 1.1rem; }
.about-portrait--a { rotate: -3deg; }
.about-portrait--b { rotate: 3deg; }

/* A portrait with links is a toggle button (about-orbit.js adds the
   role); it's a div, so it needs no browser button chrome reset. */
.about-portrait[role="button"] { cursor: pointer; -webkit-tap-highlight-color: transparent; }

.about-portrait[role="button"]:focus-visible {
  outline: 2px solid var(--accent-secondary);
  outline-offset: 4px;
}

.about-portrait img,
.about-portrait-initials {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 3px solid var(--bg-base);
}

.about-portrait img { object-fit: cover; background: var(--bg-surface-alt); }

/* No photo: the initials on the person's own hue, as before. */
.about-portrait-initials {
  display: flex;
  align-items: center;
  justify-content: center;
  background: oklch(0.62 0.14 var(--avatar-hue, 292));
  color: var(--bg-base);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: calc(var(--size) * 0.3);
}

/* Hovering a person (or their portrait) brings their portrait forward.
   :has() does the linking with no script; where it isn't supported the
   portraits simply don't react. */
.about-duo:has(.about-person--a:hover, .about-portrait--a:hover) .about-portrait--a,
.about-duo:has(.about-person--b:hover, .about-portrait--b:hover) .about-portrait--b {
  z-index: 2;
  transform: scale(1.06);
  box-shadow: 0 0 0 4px oklch(0.62 0.14 var(--avatar-hue, 292) / 35%), 0 0 36px oklch(0.62 0.14 var(--avatar-hue, 292) / 50%);
}

.about-duo:has(.about-person--a:hover, .about-portrait--a:hover) .about-portrait--b,
.about-duo:has(.about-person--b:hover, .about-portrait--b:hover) .about-portrait--a {
  transform: scale(0.95);
  filter: saturate(0.6) brightness(0.75);
}

/* An open portrait holds the forward look, and the other one stays back,
   whatever the pointer is doing. */
.about-portrait-slot.is-open .about-portrait {
  z-index: 2;
  transform: scale(1.06);
  box-shadow: 0 0 0 4px oklch(0.62 0.14 var(--avatar-hue, 292) / 35%), 0 0 36px oklch(0.62 0.14 var(--avatar-hue, 292) / 50%);
  filter: none;
}

.about-portraits:has(.is-open) .about-portrait-slot:not(.is-open) .about-portrait {
  transform: scale(0.95);
  filter: saturate(0.6) brightness(0.75);
}

/* ---------- Orbit: the social buttons around a clicked portrait ----------
   Each item starts at the portrait's centre, then rides out along its own
   --angle to --r, just outside the ring, one after another. The rotate
   pair keeps the button itself upright. Transform and opacity only. */
.about-orbit {
  /* Clear of the portrait at its opened size, not its resting size: the
     1.06 scale plus the 4px halo ring reach 0.53 x size + 4px, and the
     button's own half-width is 22px. This leaves about 12px between the
     ring and the button. */
  --r: calc(var(--size) * 0.53 + 38px);
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 4;
  width: 0;
  height: 0;
}

.about-orbit[hidden] { display: none; }

.about-orbit-item {
  position: absolute;
  left: -22px;
  top: -22px;
  opacity: 0;
  transform: rotate(var(--angle)) translateX(0) rotate(calc(-1 * var(--angle))) scale(0.3);
  transition:
    transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) calc(var(--i) * 45ms),
    opacity 0.25s ease calc(var(--i) * 45ms);
}

.about-portrait-slot.is-open .about-orbit-item {
  opacity: 1;
  transform: rotate(var(--angle)) translateX(var(--r)) rotate(calc(-1 * var(--angle))) scale(1);
}

/* Closing tucks them back in all together, quickly. */
.about-portrait-slot:not(.is-open) .about-orbit-item {
  transition: transform 0.25s ease-in, opacity 0.2s ease;
}

/* Once the orbits are live, the copy of the buttons under each bio (the
   no-JavaScript fallback) steps aside, and the portraits get room above
   for the buttons that fan upward. */
.about.has-orbits .about-person .about-socials { display: none; }
.about.has-orbits .about-portraits { padding-top: 4.25rem; }

.about-portraits-hint {
  position: absolute;
  left: 50%;
  bottom: -1.4rem;
  translate: -50% 0;
  width: max-content;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-faint);
  transition: opacity 0.3s ease;
}

.about-portraits:has(.is-open) .about-portraits-hint { opacity: 0; }

/* ---------- People ---------- */
.about-person {
  display: grid;
  gap: 0.55rem;
  align-content: center;
}

.about-person--a { grid-area: a; text-align: right; justify-items: end; }
.about-person--b { grid-area: b; text-align: left; justify-items: start; }

/* The hover lift uses `translate`, a separate property from the
   slide-in's `transform` ([data-reveal] in style.css), so the two can run
   together without one overriding the other's transition. */
.about-person[data-reveal] {
  transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1), translate var(--transition-base);
}
.about-person:hover { translate: 0 -4px; }

.about-badge {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-gold);
}

.about-name { font-size: clamp(1.6rem, 1.4vw + 1rem, 2rem); font-weight: 600; letter-spacing: -0.02em; }
.about-role { color: var(--accent-secondary); font-size: 0.9rem; font-weight: 600; }
.about-bio { color: var(--text-muted); font-size: 0.98rem; line-height: 1.65; max-width: 34ch; margin-top: 0.35rem; }
.about-socials { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-top: 0.9rem; }
.about-person--a .about-socials { justify-content: flex-end; }

/* ---------- Responsive ---------- */
@media (max-width: 900px) {
  .about-duo {
    grid-template-columns: 1fr;
    grid-template-areas: "portraits" "a" "b";
    justify-items: center;
    gap: 2.5rem;
  }

  .about-person--a,
  .about-person--b { text-align: center; justify-items: center; }
  .about-person--a .about-socials { justify-content: center; }
  .about-portraits { --size: clamp(8rem, 36vw, 10rem); }
}

/* Reduced motion: the buttons simply appear in place. */
@media (prefers-reduced-motion: reduce) {
  .about-person:hover { translate: none; }
  .about-orbit-item,
  .about-portrait-slot:not(.is-open) .about-orbit-item {
    transition: none;
    transform: rotate(var(--angle)) translateX(var(--r)) rotate(calc(-1 * var(--angle)));
  }
}


/* ===== assets/css/components/about-story.css ===== */
/* =========================================================
   About: the company story, under the founders (about.php)

   Without JavaScript: a plain note, the four beats two by two and the
   sign-off under them.

   With it (.is-staged, from about-story.js): the note becomes a pinned
   scroll story. Eight finished texture sheets and one blank one fill a
   stage pinned beside the beats, and each beat moves them into a new
   formation:
     0 stack   the studio's pile of finished sheets
     1 yours   one lifts out, turns to face you, and catches the light
     2 fan     all face up, nothing hidden
     3 ring    every character different
     4 tower   the pile grows, and a blank sheet lands on top
     5 invite  the tower sinks back and the blank sheet comes to the
               front with the pitch on it; it opens the commission form
   Every card keeps one transform function list in every state, so the
   browser interpolates each part (move, tilt, turn, size) instead of
   collapsing them into a matrix. Transform and opacity only; the
   canvases are painted once, and the idle drift only runs while the
   stage is on screen. Everything pins below the sticky nav. The stage's
   own rules are in about-story-stage.css; this file is the note and the
   beats.
   ========================================================= */
.about-story {
  position: relative;
  display: grid;
  margin-top: clamp(4.5rem, 9vw, 7rem);
  padding-top: clamp(3.5rem, 7vw, 5.5rem);
}

/* One chapter break between the duo and the note: a hairline that fades
   out at both ends, so it reads as a pause rather than a box edge. */
.about-story::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--border-strong) 20%, var(--border-strong) 80%, transparent);
}

.about-story-beats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(2.5rem, 4.5vw, 3.5rem); }
.about-beat { display: grid; align-content: start; gap: 0.55rem; }
.about-beat-label { font-size: 0.88rem; font-weight: 500; color: var(--text-faint); }
.about-beat-title {
  font-size: clamp(1.25rem, 0.7vw + 1rem, 1.5rem);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.02em;
  max-width: 28ch;
  text-wrap: balance;
}
.about-beat-body { color: var(--text-muted); font-size: 1.02rem; line-height: 1.72; max-width: 52ch; }

.about-story-close {
  grid-column: 1 / -1;
  display: grid;
  gap: 0.4rem;
  justify-items: start;
  padding-top: clamp(0.5rem, 2vw, 1rem);
}
.about-story-close h3 { font-size: 1.15rem; font-weight: 600; letter-spacing: -0.015em; }
.about-story-close p { color: var(--text-muted); max-width: 44ch; }
.about-story-close .btn { margin-top: 1.1rem; }

.about-story-stage,
.story-tail { display: none; }
.story-mark { display: block; height: 0; }

/* =========================================================
   Staged
   ========================================================= */
.about-story.is-staged {
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  grid-template-areas: "stage beats";
  column-gap: clamp(1.5rem, 4vw, 4rem);
  row-gap: 0;
  timeline-scope: --story;
}

/* Split headings: each word rides up out of its own mask once its beat
   is reached, one after another. The full sentence sits beside them for
   screen readers; the split copy is hidden from them. */
.about-story.is-staged .sw {
  display: inline-block;
  overflow: clip;
  vertical-align: top;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}
.about-story.is-staged .sw > span {
  display: inline-block;
  transform: translateY(108%);
  transition: transform 0.95s var(--ease) calc(var(--w) * 34ms);
}
.about-story.is-staged .is-seen .sw > span { transform: none; }

/* ---------- Beats: pinned, one at a time, both ways ----------
   Every beat is a full-height panel pinned below the nav. The next one
   scrolls up toward it unseen and swaps in once it is pinned in the same
   spot, so the text never moves with the scroll. Scrolling down, the old
   beat fades out upward and the new one rises in from below; scrolling
   up, the reverse. Waiting beats rest below their spot and finished ones
   above it, so the direction follows from where each is coming from. */
.about-story.is-staged .about-story-beats {
  grid-area: beats;
  display: block;
  position: relative;
  padding-left: clamp(1.25rem, 2.2vw, 2.25rem);
}

.about-story.is-staged .about-beat,
.about-story.is-staged .about-story-close {
  position: sticky;
  top: var(--nav-height);
  min-height: calc(100vh - var(--nav-height));
  display: grid;
  align-content: center;
  opacity: 0;
  translate: 0 48px;
  pointer-events: none;
  transition: opacity 0.4s ease, translate 0.7s var(--ease);
}
.about-story.is-staged .about-beat { gap: 1.1rem; }
.about-story.is-staged .about-story-close { gap: 0.6rem; padding-top: 0; }
.about-story.is-staged .is-active { opacity: 1; translate: 0 0; pointer-events: auto; transition-delay: 0.12s, 0.12s; }
.about-story.is-staged .is-past { opacity: 0; translate: 0 -48px; }

/* Before the first swap nothing is pinned yet, so the first beat shows as
   it scrolls in, in its place. */
.about-story-beats[data-state="0"] .about-beat:first-of-type { opacity: 1; translate: 0 0; transition: none; }

/* Room after the sign-off, so it and the blank sheet hold still for a
   moment before the section scrolls away. */
.about-story.is-staged .story-tail { display: block; height: 60vh; }

.about-story.is-staged .about-beat-label {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  color: var(--text-muted);
}
.about-story.is-staged .about-beat-label::before {
  content: '';
  width: 2.5rem;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.8s var(--ease) 0.1s;
}
.about-story.is-staged .about-beat.is-active .about-beat-label::before { transform: none; }

.about-story.is-staged .about-beat-title {
  font-size: clamp(2rem, 1.9vw + 1.1rem, 3.1rem);
  line-height: 1.08;
  letter-spacing: -0.03em;
  max-width: 16ch;
}

.about-story.is-staged .about-beat-body {
  font-size: clamp(1.05rem, 0.35vw + 0.95rem, 1.2rem);
  line-height: 1.7;
  max-width: 40ch;
  opacity: 0;
  translate: 0 14px;
  transition: opacity 0.7s ease 0.45s, translate 0.9s var(--ease) 0.45s;
}
.about-story.is-staged .about-beat.is-seen .about-beat-body { opacity: 1; translate: 0 0; }

.about-story.is-staged .about-story-close h3 { font-size: clamp(1.6rem, 1.2vw + 1rem, 2.2rem); letter-spacing: -0.025em; }
.about-story.is-staged .about-story-close p { font-size: 1.1rem; }

/* ---------- Phones ---------- */
@media (max-width: 900px) {
  .about-story-beats { grid-template-columns: 1fr; }

  /* The beats pin just under the sheet band (about-story-stage.css). */
  .about-story.is-staged { grid-template-columns: 1fr; grid-template-areas: "stage" "beats"; }

  .about-story.is-staged .about-beat,
  .about-story.is-staged .about-story-close {
    top: calc(var(--nav-height) + 42vh);
    top: calc(var(--nav-height) + 42svh);
    min-height: calc(58vh - var(--nav-height));
    min-height: calc(58svh - var(--nav-height));
    align-content: start;
    padding-top: 2vh;
  }
  .about-story.is-staged .story-tail { height: 40vh; }

  .about-story.is-staged .about-beat-title { font-size: clamp(1.8rem, 7vw, 2.5rem); max-width: 18ch; }
}

/* Reduced motion: the story still changes formation and beat, it just
   cuts instead of moving. */
@media (prefers-reduced-motion: reduce) {
  .about-beat-label::before, .about-beat, .about-story-close { transition: none !important; }
  .about-story.is-staged .sw > span { transform: none; transition: none; }
  .about-story.is-staged .about-beat-body { opacity: 1; translate: 0 0; transition: none; }
}


/* ===== assets/css/components/about-story-stage.css ===== */
/* =========================================================
   About story: the 3D sheet stage (about-story.css has the note and the
   beats, and the overview of the formations)

   Pinned beside the beats once about-story.js arms the story. Eight
   finished texture sheets and one blank one change formation with the
   beat showing, then the blank sheet comes to the front as the
   commission button. Transform and opacity only; the canvases are
   painted once.
   ========================================================= */
/* ---------- Stage ---------- */
.about-story.is-staged .about-story-stage {
  --card: clamp(13rem, min(28vw, 40vh), 25rem);
  grid-area: stage;
  position: sticky;
  top: var(--nav-height);
  height: calc(100vh - var(--nav-height));
  align-self: start;
  display: block;
}

.story-glow {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--card) * 2.4);
  aspect-ratio: 1;
  border-radius: 50%;
  translate: -50% -50%;
  pointer-events: none;
  transition: opacity 1s ease;
}
.story-glow--violet { background: radial-gradient(closest-side, color-mix(in oklab, var(--accent-primary) 40%, transparent), transparent); }
.story-glow--cyan { background: radial-gradient(closest-side, color-mix(in oklab, var(--accent-secondary) 24%, transparent), transparent); opacity: 0; }
[data-state="1"] .story-glow--cyan { opacity: 0.55; }
[data-state="4"] .story-glow--cyan,
[data-state="5"] .story-glow--cyan { opacity: 1; }

.story-yaw { position: absolute; inset: 0; perspective: 1600px; }

/* The mouse tilt is written straight onto .story-scene's transform by the
   script, eased frame by frame, so it has no CSS transition to fight. */
.story-scene-wrap,
.story-scene,
.story-group {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

.story-group {
  --grx: 0deg;
  --gry: 0deg;
  transform: rotateX(var(--grx)) rotateY(var(--gry));
  transition: transform 1.2s var(--ease);
}

.story-card {
  --x: 0px;
  --y: calc(var(--i) * 5px - 20px);
  --z: calc(var(--i) * -10px);
  --rx: 58deg;
  --ry: 0deg;
  --rz: -32deg;
  --s: 1;
  --order: var(--i);
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--card);
  height: var(--card);
  margin: calc(var(--card) / -2) 0 0 calc(var(--card) / -2);
  border-radius: var(--radius-sm);
  transform: translate3d(var(--x), var(--y), var(--z)) rotateX(var(--rx)) rotateY(var(--ry)) rotateZ(var(--rz)) scale(var(--s));
  will-change: transform;
  transition:
    transform 1.1s var(--ease) calc(var(--order) * 50ms),
    opacity 0.6s ease calc(var(--order) * 50ms);
  /* The idle drift rides on the card's own `translate`, a separate
     property from the formation's `transform`, so the two never cancel. It moves the whole composited card, painted
     once, so nothing redraws while it drifts. Each card has its own
     length and starting point, so the pile never bobs in step. */
  animation: story-drift calc(7s + var(--i) * 0.35s) ease-in-out calc(var(--i) * -1.1s) infinite;
  animation-play-state: paused;
}
.about-story-stage.is-onscreen .story-card { animation-play-state: running; }
@keyframes story-drift {
  0%, 100% { translate: 0 0; }
  50% { translate: 4px -9px; }
}

/* The sheet's face: the paint, its corners and its glow. Static. */
.story-face {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  box-shadow: var(--shadow-glow-violet);
  transition: box-shadow 0.6s ease;
}
.story-face canvas { display: block; width: 100%; height: 100%; }

/* A band of light that crosses a sheet when it takes the spotlight. */
.story-face::after {
  content: '';
  position: absolute;
  inset: -10%;
  background: linear-gradient(105deg, transparent 38%, color-mix(in oklab, var(--text-primary) 30%, transparent) 50%, transparent 62%);
  transform: translateX(-130%);
  pointer-events: none;
}
@keyframes story-sheen {
  from { transform: translateX(-130%); }
  to { transform: translateX(130%); }
}

/* The blank sheet waits out of sight above the stack until the end. */
.story-card--next {
  --y: calc(var(--card) * -1.6);
  --z: 0px;
  opacity: 0;
  transition:
    transform 1.1s var(--ease) calc(var(--order) * 50ms),
    opacity 0.6s ease calc(var(--order) * 50ms),
    scale 0.45s var(--ease);
}

/* 1: yours. */
[data-state="1"] .story-card {
  --y: calc(var(--card) * 0.5 + var(--i) * 5px);
  --z: calc(-320px - var(--i) * 10px);
  --rx: 66deg;
  opacity: 0.35;
}
[data-state="1"] .story-card--yours { --y: calc(var(--card) * -0.16); --z: 140px; --rx: 9deg; --ry: -17deg; --rz: 3deg; --s: 1.16; opacity: 1; }
[data-state="1"] .story-card--yours .story-face { box-shadow: var(--shadow-glow-cyan); }
[data-state="1"] .story-card--yours .story-face::after { animation: story-sheen 1.5s var(--ease) 0.75s both; }

/* 2: fan, like a hand of cards laid open. */
[data-state="2"] .story-card {
  --a: calc(var(--f) * 8.5deg);
  --x: calc(sin(var(--a)) * var(--card) * 1.3);
  --y: calc((1 - cos(var(--a))) * var(--card) * 1.3 - var(--card) * 0.12);
  --z: calc(var(--fd) * -22px);
  --rx: 12deg;
  --rz: var(--a);
  --s: 0.7;
  --order: var(--fd);
  opacity: 1;
}

/* 3: ring, seen from a little above. */
[data-state="3"] .story-group { --grx: -15deg; --gry: -12deg; }
[data-state="3"] .story-card {
  --th: calc(var(--i) * 45deg);
  --x: calc(sin(var(--th)) * var(--card) * 0.86);
  --y: 0px;
  --z: calc(cos(var(--th)) * var(--card) * 0.86 - var(--card) * 0.35);
  --rx: 0deg;
  --ry: var(--th);
  --rz: 0deg;
  --s: 0.6;
  opacity: 1;
}

/* The blank sheet stays hidden above through 1 to 3. After those rules on
   purpose: each of them also matches it as a .story-card. */
[data-state="1"] .story-card--next,
[data-state="2"] .story-card--next,
[data-state="3"] .story-card--next { --x: 0px; --y: calc(var(--card) * -1.6); --z: 0px; --rx: 58deg; --ry: 0deg; --rz: -32deg; --s: 1; opacity: 0; }

/* 4: tower, built from the bottom, with the blank sheet dropping on top. */
[data-state="4"] .story-card { --y: calc(var(--card) * 0.62 - var(--t) * var(--card) * 0.13); --z: 0px; --s: 0.86; --order: var(--t); opacity: 1; }
/* The blank sheet lands last, once the tower under it has built (and, coming
   back up from the invitation, once it has risen back into place), so its
   turn never cuts through the sheets below it. */
[data-state="4"] .story-card--next { --y: calc(var(--card) * -0.58); --z: 30px; --order: 12; opacity: 1; }
[data-state="4"] .story-card--next .story-face { box-shadow: 0 0 0 1px var(--accent-secondary), var(--shadow-glow-cyan); }
[data-state="4"] .story-card--next .story-face::after { animation: story-sheen 1.5s var(--ease) 1.1s both; }

/* 5: the invitation. The tower sinks back into the dark and the blank
   sheet comes down from the top to the front, turning to face you. */
[data-state="5"] .story-group { --grx: 0deg; --gry: 0deg; }
/* The tower sinks away top first, and the blank sheet only starts its turn
   once the sheets it was lying on are well back; turning upright on the
   spot, its lower edge sweeps down through where they stood. */
[data-state="5"] .story-card { --y: calc(var(--card) * 0.95 - var(--t) * var(--card) * 0.13); --z: -440px; --s: 0.86; --order: calc(7 - var(--t)); opacity: 0.16; pointer-events: none; }
[data-state="5"] .story-card--next { --y: calc(var(--card) * -0.06); --z: 150px; --rx: 4deg; --ry: 0deg; --rz: 0deg; --s: 1.2; --order: 7; opacity: 1; pointer-events: auto; }
[data-state="5"] .story-card--next .story-face { box-shadow: 0 0 0 1px var(--accent-secondary), var(--shadow-glow-cyan); }

/* The pitch, printed on the blank sheet. Sized in container units so it
   scales with the sheet, from a phone's band to a desktop stage. */
.story-card--next .story-face { container-type: inline-size; }
.story-ghost { position: absolute; inset: 0; opacity: 0; transition: opacity 0.7s ease; }
.story-invite {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  gap: 2.2cqi;
  padding: 8cqi;
  background: linear-gradient(180deg, transparent 26%, color-mix(in oklab, var(--bg-base) 66%, transparent) 56%, color-mix(in oklab, var(--bg-base) 93%, transparent));
  color: var(--text-primary);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease;
}
[data-state="5"] .story-invite { opacity: 1; pointer-events: auto; transition-delay: 0.95s; }
.story-invite-kicker { font-family: var(--font-mono); font-size: 3.7cqi; letter-spacing: 0.02em; color: var(--accent-secondary); }
.story-invite-title { font-family: var(--font-heading); font-size: 9.4cqi; font-weight: 600; line-height: 1.04; letter-spacing: -0.03em; text-wrap: balance; }
.story-invite-body { font-size: 4.4cqi; line-height: 1.5; color: var(--text-muted); max-width: 30ch; }
/* Painted with the primary button's own tokens (.btn-primary in
   style.css): the same gradient, border and glow. It is a label inside
   the sheet's link, so it cannot take the class itself; the sheet's hover
   and press drive it below, the way they drive the rest of the sheet. The
   padding gives the border its pixel back, so the pill keeps its size. */
.story-invite-cta {
  display: inline-flex;
  align-items: center;
  gap: 2cqi;
  margin-top: 2.4cqi;
  padding: calc(3.2cqi - 1px) calc(5.6cqi - 1px);
  border: 1px solid var(--border-cta);
  border-radius: var(--radius-pill);
  background-color: var(--accent-primary);
  background-image: var(--gradient-cta);
  background-size: 220% 100%;
  background-position: 0% 50%;
  box-shadow: var(--shadow-cta);
  font-size: 4.3cqi;
  font-weight: 600;
  transition: translate 0.3s var(--ease), background-position 0.7s var(--ease), box-shadow var(--transition-base);
}
.story-invite-cta .icon { transition: translate 0.3s var(--ease); }

/* Hover: the sheet leans in, a ghost of paint shows through the islands
   (what it becomes), and a light crosses it. */
[data-state="5"] .story-card--next:hover { scale: 1.03; }
[data-state="5"] .story-card--next:hover .story-ghost { opacity: 0.34; }
[data-state="5"] .story-card--next:hover .story-invite-cta { translate: 0 -2px; background-position: 100% 50%; box-shadow: var(--shadow-cta-hover); }
[data-state="5"] .story-card--next:hover .story-invite-cta .icon { translate: 3px 0; }
[data-state="5"] .story-card--next:hover .story-face::after { animation: story-sheen 1.2s var(--ease) both; }
/* The press wash goes over the gradient, as it does on .btn-primary. */
.story-card--next:active .story-invite-cta { background-image: linear-gradient(var(--press-tint), var(--press-tint)), var(--gradient-cta); }

@container (max-width: 15rem) {
  .story-invite { padding: 7cqi; gap: 3cqi; }
  .story-invite-body { display: none; }
  .story-invite-title { font-size: 11.5cqi; }
  .story-invite-kicker { font-size: 5.2cqi; }
  .story-invite-cta { font-size: 6.2cqi; padding: calc(4cqi - 1px) calc(7cqi - 1px); }
}

/* One line near the foot of the stage saying what you are looking at. */
.story-caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 7vh;
  height: 1.5em;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--text-muted);
}
.story-caption span {
  position: absolute;
  inset: 0;
  opacity: 0;
  translate: 0 8px;
  transition: opacity 0.4s ease, translate 0.5s var(--ease);
}
[data-state="0"] [data-caption="0"],
[data-state="1"] [data-caption="1"],
[data-state="2"] [data-caption="2"],
[data-state="3"] [data-caption="3"],
[data-state="4"] [data-caption="4"] { opacity: 1; translate: 0 0; transition-delay: 0.3s; }

/* The reading line, beside the text, filling a step per beat. It rides on
   the pinned stage (across the column gap, at the text column's left
   edge), so it leaves exactly when the story does; the script fades it
   out as Contact comes up. */
.story-rail {
  position: absolute;
  top: 20%;
  right: calc(-1 * clamp(1.5rem, 4vw, 4rem));
  width: 2px;
  height: 60%;
  border-radius: 2px;
  background: var(--border-subtle);
  overflow: hidden;
  transition: opacity 0.35s linear;
}
.story-rail::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, var(--accent-primary), var(--accent-secondary));
  transform-origin: top;
  transform: scaleY(var(--progress, 0));
  transition: transform 1s var(--ease);
}

/* A slow turn of the whole stage across the story, tied to the scroll
   where the browser supports scroll-linked animation. */
@supports (animation-timeline: view()) {
  .about-story.is-staged .about-story-beats { view-timeline: --story block; }
  .about-story.is-staged .story-scene-wrap { animation: story-yaw linear both; animation-timeline: --story; }
}
@keyframes story-yaw {
  from { transform: rotateY(-12deg); }
  to { transform: rotateY(12deg); }
}

/* ---------- Phones: the stage pins under the nav as a band, edge to
   edge, with the rail beside the beats below it ---------- */
@media (max-width: 900px) {
  .about-story.is-staged .about-story-stage {
    --card: min(38vw, 19vh);
    --card: min(38vw, 19svh);
    height: 42vh;
    height: 42svh;
    z-index: 2;
    margin-inline: calc(-1 * clamp(1.25rem, 4vw, 2.5rem));
    background: linear-gradient(to bottom, var(--bg-base) 78%, transparent);
  }
  .story-caption { bottom: 4vh; font-size: 0.78rem; }  .story-rail {
    top: calc(100% + 2vh);
    right: auto;
    left: clamp(1.25rem, 4vw, 2.5rem);
    height: calc(52vh - var(--nav-height));
    height: calc(52svh - var(--nav-height));
  }}

/* Reduced motion: the sheets still change formation, they just cut
   instead of moving. */
@media (prefers-reduced-motion: reduce) {
  .story-card, .story-group, .story-glow, .story-caption span, .story-rail::after,
  .story-face, .story-ghost, .story-invite, .story-invite-cta, .story-invite-cta .icon { transition: none !important; }
  .story-card, .story-face::after, .story-scene-wrap { animation: none !important; }
  [data-state="5"] .story-card--next:hover { scale: 1; }
}

/* ===== assets/css/components/social-buttons.css ===== */
/* =========================================================
   Social buttons (About and Contact)

   Hover or keyboard focus: a ring draws itself around the button, the
   icon hops, and the platform's name pops up above it. Press: four paw
   beans pop out in a paw shape (the "boop", social-buttons.js adds
   .is-booped). Markup comes from social_button() in
   includes/social-links.php.

   Cost: the hop, label and beans are transform/opacity. The ring is the
   one repaint, a 52px conic gradient redrawn for half a second while a
   registered --sweep angle eases from 0 to 360deg, and only on hover.
   ========================================================= */
@property --sweep {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.social-btn {
  --sweep: 0deg;
  position: relative;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border-radius: 50%;
  background-color: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  color: var(--text-muted);
  font-size: 1.05rem;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease, --sweep 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

/* The ring: a conic stroke masked down to 2px, drawn round to --sweep. */
.social-btn::before {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: conic-gradient(from -90deg, var(--accent-secondary) var(--sweep), transparent var(--sweep));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), black calc(100% - 2px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 2px), black calc(100% - 2px));
  pointer-events: none;
}

.social-btn .icon {
  vertical-align: 0;
  transition: transform 0.4s cubic-bezier(0.34, 1.7, 0.64, 1);
}

.social-btn:hover,
.social-btn:focus-visible {
  --sweep: 360deg;
  color: var(--accent-secondary);
  border-color: transparent;
  background-color: var(--bg-surface-alt);
}

/* The ring is the focus indicator, so the default outline steps aside. */
.social-btn:focus-visible { outline: none; }

.social-btn:hover .icon,
.social-btn:focus-visible .icon { transform: translateY(-3px) rotate(-8deg) scale(1.12); }

.social-btn:active .icon {
  transform: scale(0.88);
  transition-duration: 0.08s;
}

.social-btn-label {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  translate: -50% 6px;
  opacity: 0;
  padding: 0.25rem 0.55rem;
  border-radius: var(--radius-sm);
  background-color: var(--bg-surface-alt);
  border: 1px solid var(--border-subtle);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--text-primary);
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 0.2s ease, translate 0.3s cubic-bezier(0.34, 1.7, 0.64, 1);
}

.social-btn:hover .social-btn-label,
.social-btn:focus-visible .social-btn-label {
  opacity: 1;
  translate: -50% 0;
}

/* ---------- The boop ---------- */
.social-btn-burst { position: absolute; inset: 0; pointer-events: none; }

.social-btn-burst i {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 8px;
  height: 10px;
  margin: -5px 0 0 -4px;
  border-radius: 50%;
  background: var(--accent-secondary);
  opacity: 0;
}

.social-btn-burst i:nth-child(1) { --x: -19px; --y: -16px; --r: -30deg; }
.social-btn-burst i:nth-child(2) { --x: -7px; --y: -27px; --r: -10deg; }
.social-btn-burst i:nth-child(3) { --x: 7px; --y: -27px; --r: 10deg; }
.social-btn-burst i:nth-child(4) { --x: 19px; --y: -16px; --r: 30deg; }

.social-btn.is-booped .social-btn-burst i { animation: social-bean 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; }

@keyframes social-bean {
  0% { opacity: 0; transform: translate(0, 0) scale(0.3) rotate(var(--r)); }
  30% { opacity: 1; transform: translate(var(--x), var(--y)) scale(1.1) rotate(var(--r)); }
  100% { opacity: 0; transform: translate(calc(var(--x) * 1.35), calc(var(--y) * 1.35)) scale(0.6) rotate(var(--r)); }
}

/* Reduced motion: colour and the finished ring only. The global rule in
   style.css already shortens transitions; these remove the movement. */
@media (prefers-reduced-motion: reduce) {
  .social-btn:hover .icon,
  .social-btn:focus-visible .icon,
  .social-btn:active .icon { transform: none; }
  .social-btn-label { translate: -50% 0; }
  .social-btn.is-booped .social-btn-burst i { animation: none; }
}


/* ===== assets/css/components/contact.css ===== */
/* =========================================================
   Contact: the postcard

   Message side (topic chips and the ruled draft) and address side (To,
   Subject, send), with the sample-texture stamp stuck over the card's
   top edge. Markup and drafts: includes/partials/contact.php. Topic
   switching, the stamp and Copy: contact-postcard.js. The social
   buttons are social-buttons.css, shared with About.

   Cost: switching topics is opacity/transform on stacked drafts; the
   stamp canvas is repainted once per switch; the postmark "thunk" is a
   one-off transform animation. Nothing runs while idle.
   ========================================================= */
.contact {
  isolation: isolate;
  padding-block: clamp(3rem, 6vw, 5.5rem) clamp(3rem, 5vw, 4.5rem);
}

.contact::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--gradient-hero);
  transform: rotate(180deg);
  z-index: -1;
  pointer-events: none;
}

.contact-head { max-width: 40rem; margin-bottom: clamp(2.25rem, 5vw, 3.25rem); }

.contact-title {
  font-size: clamp(2rem, 3.2vw + 1rem, 3rem);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1.08;
  text-wrap: balance;
  margin-bottom: 1rem;
}

.contact-subtitle {
  color: var(--text-muted);
  font-size: clamp(1rem, 0.5vw + 0.9rem, 1.125rem);
  line-height: 1.7;
  max-width: 34rem;
}

/* ---------- The card ----------
   Its variables live on the card itself, not the section, because the
   careers page uses the same card (includes/partials/postcard.php). */
.postcard {
  --postcard-line: 2rem;
  --postcard-rule: color-mix(in oklch, var(--accent-primary) 16%, transparent);
  --postcard-cyan-edge: color-mix(in oklch, var(--accent-secondary) 60%, transparent);
  --postcard-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --postcard-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --postcard-inset: clamp(1.25rem, 3vw, 2.5rem);
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) 1px minmax(0, 1fr);
  gap: clamp(1.5rem, 3.5vw, 3rem);
  padding: clamp(1.5rem, 3.2vw, 2.75rem);
  border-radius: var(--radius-lg);
  border: 1px solid transparent;
  background-image:
    radial-gradient(70% 55% at 0% 0%, color-mix(in oklch, var(--accent-secondary) 6%, transparent), transparent 70%),
    linear-gradient(var(--bg-surface), var(--bg-surface)),
    linear-gradient(140deg, color-mix(in oklch, var(--accent-secondary) 50%, transparent), color-mix(in oklch, var(--accent-primary) 22%, transparent) 45%, color-mix(in oklch, var(--accent-primary) 40%, transparent));
  background-origin: padding-box, padding-box, border-box;
  background-clip: padding-box, padding-box, border-box;
  box-shadow: var(--shadow-glow-violet);
}

.postcard-divider {
  background: linear-gradient(to bottom, transparent, var(--border-strong) 12%, var(--border-strong) 88%, transparent);
}

/* ---------- Message side ---------- */
.postcard-message { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }

/* No global [hidden] rule, and the chips only work with the script. */
.postcard-topics[hidden],
.postcard-copy[hidden] { display: none; }

.postcard-topics { border: 0; margin: 0; padding: 0; min-width: 0; }

.postcard-topics legend {
  padding: 0;
  margin-bottom: 0.6rem;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--text-muted);
}

.postcard-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.postcard-chip { position: relative; }

.postcard-chip input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}

.postcard-chip span {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 38px;
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background: transparent;
  color: var(--text-muted);
  font-size: 0.9rem;
  font-weight: 500;
  line-height: 1.3;
  transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
}

.postcard-chip .icon {
  font-size: 0.8em;
  width: 0;
  opacity: 0;
  transition: width 0.25s var(--postcard-ease), opacity 0.2s ease;
}

.postcard-chip input:hover + span { color: var(--text-primary); border-color: var(--border-strong); }

.postcard-chip input:checked + span {
  color: var(--text-primary);
  background: var(--bg-surface-alt);
  border-color: var(--postcard-cyan-edge);
}

.postcard-chip input:checked + span .icon { width: 0.8em; opacity: 1; color: var(--accent-secondary); }
.postcard-chip input:focus-visible + span { outline: 2px solid var(--accent-secondary); outline-offset: 2px; }

/* The draft, ruled like a postcard. Every line is one --postcard-line
   tall, so the rules stay under the words whatever wraps. All drafts
   share one grid cell and the tallest sets the height. */
.postcard-letters {
  display: grid;
  font-size: 1.05rem;
  line-height: var(--postcard-line);
  /* One line-tall tile with a 1px rule at its foot, repeated. Firefox
     drew a repeating-linear-gradient with calc() stops as thick bands. */
  background: linear-gradient(to bottom, transparent calc(100% - 1px), var(--postcard-rule) 0) 0 0 / 100% var(--postcard-line) repeat-y;
}

.postcard-letter {
  grid-area: 1 / 1;
  display: grid;
  gap: var(--postcard-line);
  align-content: start;
  visibility: hidden;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.35s var(--postcard-ease), visibility 0s linear 0.35s;
}

.postcard-letter.is-active {
  visibility: visible;
  opacity: 1;
  transform: none;
  transition: opacity 0.3s ease 0.08s, transform 0.45s var(--postcard-ease) 0.08s, visibility 0s;
}

.contact-blank {
  font-family: var(--font-mono);
  font-size: 0.8em;
  font-weight: 500;
  line-height: 1; /* a mono inline box at the full line height would push its line off the rules */
  color: var(--text-muted);
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
  background: color-mix(in oklch, var(--accent-secondary) 7%, transparent);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

.contact-blank::before { content: '['; }
.contact-blank::after { content: ']'; }

/* ---------- Address side ---------- */
.postcard-address {
  display: flex;
  flex-direction: column;
  gap: 1.35rem;
  min-width: 0;
  padding-top: clamp(8.5rem, 12vw, 10rem); /* the stamp's corner */
}

.postcard-field {
  display: grid;
  gap: 0.2rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--postcard-rule);
}

.postcard-label { font-size: 0.8rem; font-weight: 500; color: var(--text-muted); }

.postcard-to {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.postcard-email {
  font-family: var(--font-heading);
  font-size: clamp(1.15rem, 1vw + 0.85rem, 1.4rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  overflow-wrap: anywhere;
  user-select: all;
}

.postcard-copy {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 32px;
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius-sm);
  background: transparent;
  border: 1px solid var(--border-strong);
  color: var(--text-muted);
  font-size: 0.8rem;
  font-weight: 600;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.postcard-copy:hover,
.postcard-copy.is-copied { color: var(--accent-secondary); border-color: var(--postcard-cyan-edge); }
.postcard-copy:focus-visible { outline: 2px solid var(--accent-secondary); outline-offset: 2px; }

.postcard-subjects { display: grid; }

.postcard-subject {
  grid-area: 1 / 1;
  font-weight: 500;
  visibility: hidden;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.2s ease, transform 0.35s var(--postcard-ease), visibility 0s linear 0.35s;
}

.postcard-subject.is-active {
  visibility: visible;
  opacity: 1;
  transform: none;
  transition: opacity 0.3s ease 0.05s, transform 0.4s var(--postcard-ease) 0.05s, visibility 0s;
}

.postcard-send { margin-top: auto; display: grid; justify-items: start; gap: 0.65rem; }
.postcard-send .btn { padding: 1rem 2rem; font-size: 1rem; }
.postcard-send .btn .icon { transition: transform 0.4s var(--postcard-spring); }

.postcard-send .btn:hover .icon,
.postcard-send .btn:focus-visible .icon { transform: translate(3px, -3px) rotate(-6deg); }

.postcard-hint { font-size: 0.85rem; line-height: 1.5; color: var(--text-muted); max-width: 30ch; }

/* ---------- Stamp and postmark ----------
   Two layers: the outer one carries the tilt and the glow, the inner
   one the perforated mask. On one element the mask would cut the glow
   off, since a mask applies after filters. */
.postcard-stamp,
.postcard-postmark { visibility: hidden; }

.postcard.is-live .postcard-stamp,
.postcard.is-live .postcard-postmark { visibility: visible; }

.postcard-stamp {
  position: absolute;
  top: -1.1rem;
  right: var(--postcard-inset);
  width: 118px;
  rotate: 4deg;
  filter: drop-shadow(0 14px 22px color-mix(in oklch, var(--accent-primary) 45%, transparent));
  z-index: 2;
}

/* Perforated edge: a solid middle plus a grid of punched holes, where
   only the holes along the border survive the union. */
.postcard-stamp-paper {
  padding: 7px;
  background: var(--text-primary);
  -webkit-mask:
    linear-gradient(#000 0 0) center / calc(100% - 10px) calc(100% - 10px) no-repeat,
    radial-gradient(circle 3.4px at 5px 5px, transparent 97%, #000) -5px -5px / 10px 10px;
  mask:
    linear-gradient(#000 0 0) center / calc(100% - 10px) calc(100% - 10px) no-repeat,
    radial-gradient(circle 3.4px at 5px 5px, transparent 97%, #000) -5px -5px / 10px 10px;
}

/* 4:5 on the art itself, matching the crop in contact-postcard.js. */
.postcard-stamp canvas { display: block; width: 100%; aspect-ratio: 4 / 5; background: var(--bg-base); }

.postcard-postmark {
  position: absolute;
  top: 2rem;
  right: calc(var(--postcard-inset) + 108px);
  width: 96px;
  height: 96px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 2px solid color-mix(in oklch, var(--text-muted) 70%, transparent);
  box-shadow: inset 0 0 0 5px transparent, inset 0 0 0 6px color-mix(in oklch, var(--text-muted) 45%, transparent);
  color: var(--text-muted);
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.56rem;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.04em;
  rotate: -14deg;
  z-index: 3;
  pointer-events: none;
  /* Light ink over the dark card, dark ink where it crosses the white
     stamp paper, like a real postmark. Blends within .contact's
     isolated group only. */
  mix-blend-mode: difference;
}

.postcard-postmark b { display: block; font-size: 0.64rem; letter-spacing: 0.05em; color: var(--text-primary); }

.postcard-postmark.is-stamping { animation: postcard-thunk 0.5s var(--postcard-spring); }
.postcard-stamp.is-stamping { animation: postcard-press 0.5s var(--postcard-ease); }

@keyframes postcard-thunk {
  0% { opacity: 0; transform: scale(1.45); }
  55% { opacity: 1; transform: scale(0.94); }
  100% { opacity: 1; transform: scale(1); }
}

@keyframes postcard-press {
  0%, 30% { transform: none; }
  45% { transform: scale(0.95); }
  100% { transform: none; }
}

/* ---------- Under the card ---------- */
.contact-after {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem 2rem;
  margin-top: clamp(1.75rem, 3vw, 2.25rem);
}

.contact-socials,
.contact-book { display: flex; flex-wrap: wrap; align-items: center; gap: 0.9rem; }
.contact-socials-row { display: flex; gap: 0.7rem; }

.contact-socials > span,
.contact-book > span { font-size: 0.9rem; color: var(--text-muted); }

/* ---------- Narrow: one column, stamp tucked into the corner ---------- */
@media (max-width: 820px) {
  .postcard {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.75rem;
    padding: 1.5rem 1.25rem 1.75rem;
  }

  .postcard-divider {
    height: 1px;
    background: repeating-linear-gradient(to right, var(--border-strong) 0 6px, transparent 6px 12px);
  }

  .postcard-stamp { width: 78px; top: -0.9rem; right: 1rem; }
  .postcard-stamp-paper { padding: 5px; }
  .postcard-postmark { width: 72px; height: 72px; top: 2.4rem; right: 4.9rem; font-size: 0.44rem; }
  .postcard-postmark b { font-size: 0.5rem; }

  /* Room for the stamp above the chips; without the script there are
     no chips and no stamp, so no room either. */
  .postcard.is-live .postcard-message { padding-top: 5.25rem; }
  .postcard-address { padding-top: 0; }

  .postcard-send { justify-items: stretch; }
  .postcard-send .btn { width: 100%; }
  .postcard-hint { max-width: none; }

  .contact-after { flex-direction: column; align-items: flex-start; }
  .contact-book { flex-direction: column; align-items: flex-start; gap: 0.75rem; }
}

@media (prefers-reduced-motion: reduce) {
  .postcard-letter,
  .postcard-letter.is-active,
  .postcard-subject,
  .postcard-subject.is-active { transform: none; transition: none; }

  .postcard-postmark.is-stamping,
  .postcard-stamp.is-stamping { animation: none; }

  .postcard-send .btn:hover .icon,
  .postcard-send .btn:focus-visible .icon { transform: none; }
}


/* ===== assets/css/components/footer.css ===== */
/* =========================================================
   Footer
   ========================================================= */
.site-footer { border-top: 1px solid var(--border-subtle); }

/* Section links repeated at the bottom of the page - see the comment in
   includes/partials/footer.php. */
.footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.75rem;
  padding-top: 2rem;
  font-size: 0.85rem;
}

.footer-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--text-muted);
  transition: color var(--transition-fast);
}

.footer-nav a:hover,
.footer-nav a[aria-current="page"] { color: var(--accent-secondary); }

.footer-nav + .footer-inner { border-top: 1px solid var(--border-subtle); margin-top: 1.5rem; }

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding-block: 2rem;
  font-size: 0.8rem;
  color: var(--text-faint);
}

.footer-tools { font-family: var(--font-mono); }

.footer-link { color: var(--text-faint); transition: color var(--transition-fast); }
.footer-link:hover { color: var(--accent-secondary); }

/* The back-to-top button is a real primary button (.btn .btn-primary in
   style.css): the fill, border, glow, hover slide, focus ring and press
   wash all come from that rule, so it can never fall out of step with
   it again. This only makes it a fixed circle and shows or hides it.

   .btn brings its own padding and a smaller font size; both are reset so
   the circle stays 46px with a 16px chevron (the cookie notice's cut-out
   in cookie-consent-modal.css depends on that width). It is hidden with
   visibility as well as opacity, so it takes no keyboard focus while it
   is invisible; the delay keeps it visible for the fade-out. */
.back-to-top {
  position: fixed;
  bottom: 1.75rem;
  right: 1.75rem;
  width: 46px;
  height: 46px;
  padding: 0;
  border-radius: 50%;
  font-size: 1rem;
  opacity: 0;
  visibility: hidden;
  transform: translateY(12px);
  pointer-events: none;
  transition: opacity var(--transition-base), transform var(--transition-base), box-shadow var(--transition-base), background-position 0.7s cubic-bezier(0.16, 1, 0.3, 1), visibility 0s linear 0.3s;
  z-index: var(--z-back-to-top);
}

.back-to-top.visible { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; transition-delay: 0s; }

/* .visible above outranks .btn:hover, so the lift is restated here. */
.back-to-top.visible:hover { transform: translateY(-3px); }

/* =========================================================
   Responsive
   ========================================================= */
@media (max-width: 640px) {
  .footer-inner { flex-direction: column; align-items: flex-start; }
}

/* WCAG 2.2 AA 2.5.8 wants 24x24 minimum. These sit at 13px/1.6, which
   is 20px tall, and they are the only route to the legal documents from
   most pages. */
.footer-link {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

.footer-link[aria-current="page"] { color: var(--accent-secondary); }


/* ===== assets/css/components/commission-modal.css ===== */
/* =========================================================
   Commission Modal
   The shell: dialog, section tabs, summary rail and the sent screen.
   Choice cards, the artist picker and the image drop live in
   commission-picker.css.
   ========================================================= */
.commission-modal-backdrop {
  position: fixed;
  inset: 0;
  background: oklch(0.13 0.035 280 / 70%);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-base);
  z-index: var(--z-modal-backdrop);
}

.commission-modal-backdrop.is-open { opacity: 1; pointer-events: auto; }

/* Clicks outside the shell fall through to the backdrop, which closes. */
.commission-modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: clamp(1rem, 4vw, 2.5rem);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--transition-base), visibility 0s linear var(--transition-base);
}

.commission-modal.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--transition-base), visibility 0s linear;
}

.cm-shell {
  position: relative;
  width: min(1000px, 100%);
  height: min(744px, 100%);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-glow-violet);
  pointer-events: auto;
  transform: translateY(16px) scale(0.98);
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}

.commission-modal.is-open .cm-shell { transform: none; }

.cm-form { display: grid; grid-template-columns: minmax(0, 1fr) 316px; height: 100%; }
.cm-form[hidden] { display: none; }

.cm-close {
  position: absolute;
  top: 18px;
  right: 18px;
  z-index: 3;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border-subtle);
  background: var(--bg-base);
  color: var(--text-muted);
  font-size: 0.95rem;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}

.cm-close:hover { border-color: color-mix(in oklab, var(--accent-secondary) 60%, transparent); color: var(--accent-secondary); }

/* ---------- Main column ---------- */
.cm-main { position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0; }

/* Fades the bottom edge so a scrollable section reads as one. */
.cm-main::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 36px;
  background: linear-gradient(to bottom, transparent, var(--bg-surface));
  pointer-events: none;
}

.cm-head { padding: 28px 32px 0; }

.cm-title {
  margin: 0;
  font-weight: 600;
  font-size: 1.6rem;
  line-height: 1.15;
  letter-spacing: -0.025em;
  text-wrap: balance;
}

.cm-sub { margin: 6px 0 0; max-width: 54ch; color: var(--text-muted); font-size: 0.93rem; line-height: 1.55; }

.cm-steps {
  display: flex;
  gap: 6px;
  margin-top: 18px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border-subtle);
}

.cm-step {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 6px 6px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-muted);
  font-size: 0.88rem;
  font-weight: 500;
  transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast);
}

.cm-step:hover { color: var(--text-primary); }

.cm-step[aria-current="step"] {
  color: var(--text-primary);
  background: color-mix(in oklab, var(--accent-primary) 22%, transparent);
  border-color: var(--border-strong);
}

.cm-step-mark {
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  line-height: 1;
  color: var(--text-muted);
}

.cm-step-mark .icon { font-size: 0.62rem; }
.cm-step[aria-current="step"] .cm-step-mark { border-color: var(--accent-secondary); color: var(--accent-secondary); }
.cm-step.is-done .cm-step-mark { background: var(--accent-secondary); border-color: var(--accent-secondary); color: var(--bg-base); }
.cm-step.has-error .cm-step-mark { background: transparent; border-color: var(--accent-error); color: var(--accent-error); }

.cm-alert {
  margin: 14px 32px 0;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--accent-error);
  background: color-mix(in oklab, var(--accent-error) 12%, transparent);
  border: 1px solid color-mix(in oklab, var(--accent-error) 32%, transparent);
}

.cm-alert[hidden] { display: none; }

.cm-panes {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding: 22px 32px 0;
}

.cm-panes::-webkit-scrollbar { display: none; }

/* Both paddings set: style.css gives every <section> page-level padding-block. */
.cm-pane { display: flex; flex-direction: column; gap: 26px; padding-block: 0 40px; }
.cm-pane[hidden] { display: none; }
.cm-pane.is-entering { animation: cm-pane-in 0.34s cubic-bezier(0.16, 1, 0.3, 1) both; }

@keyframes cm-pane-in {
  from { opacity: 0; transform: translateX(calc(var(--cm-dir, 1) * 14px)); }
  to { opacity: 1; transform: none; }
}

/* ---------- Summary rail ---------- */
.cm-rail {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding: 28px 24px 24px;
  background: var(--bg-base);
  border-left: 1px solid var(--border-subtle);
}

.cm-rail::-webkit-scrollbar { display: none; }

.cm-rail-body { display: flex; flex-direction: column; gap: 16px; }

.cm-rail-title {
  margin: 0;
  min-height: 40px;
  padding-right: 44px;
  display: flex;
  align-items: center;
  font-size: 0.98rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.cm-price {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  width: 100%;
  padding: 14px 16px;
  text-align: left;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-subtle);
  background: var(--bg-surface);
  color: var(--text-primary);
  transition: border-color var(--transition-fast);
}

.cm-price:hover { border-color: var(--border-strong); }

.cm-price-value {
  font-family: var(--font-mono);
  font-size: 1.45rem;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.cm-price-value.is-empty { font-size: 1rem; color: var(--text-muted); }
.cm-price-name { font-size: 0.84rem; color: var(--text-muted); }

.cm-sum { margin: 0; }

.cm-sum-row {
  display: grid;
  grid-template-columns: 78px minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  padding: 9px 0;
  border-top: 1px solid var(--border-subtle);
}

.cm-sum dt { font-size: 0.78rem; color: var(--text-faint); }
.cm-sum dd { margin: 0; min-width: 0; }

.cm-sum-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 26px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: none;
  text-align: left;
  font-size: 0.87rem;
  color: var(--text-primary);
}

.cm-sum-btn:hover .cm-sum-val { text-decoration: underline; text-decoration-color: color-mix(in oklab, var(--accent-secondary) 60%, transparent); text-underline-offset: 3px; }

.cm-sum-btn.is-empty { color: var(--text-faint); }
.cm-sum-btn.is-missing { color: var(--accent-error); }
.cm-sum-val { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.cm-sum-av { width: 26px; height: 26px; flex-shrink: 0; border-radius: 6px; overflow: hidden; background: var(--bg-surface-alt); }
.cm-sum-av .cm-face { margin: 0; border-radius: 0; }
.cm-sum-av .cm-initials { font-size: 0.62rem; }

.cm-changed { animation: cm-value-in 0.32s cubic-bezier(0.16, 1, 0.3, 1); }

@keyframes cm-value-in { from { opacity: 0.25; transform: translateY(3px); } to { opacity: 1; transform: none; } }

.cm-rail-note { margin: 0; font-size: 0.78rem; line-height: 1.55; color: var(--text-faint); }
.cm-compact { display: none; }

.cm-rail-actions { margin-top: auto; display: flex; align-items: center; gap: 10px; }

/* Both actions share one size and the utility radius, so they read as a
   pair. The pill stays for page-level calls to action. */
.commission-modal .btn,
.cm-back {
  height: 44px;
  padding: 0 1.1rem;
  border-radius: var(--radius-sm);
  font-size: 0.92rem;
}

.commission-modal .btn:hover { transform: none; }

.cm-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-subtle);
  background: transparent;
  color: var(--text-muted);
  font-weight: 500;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.cm-back:hover { color: var(--text-primary); border-color: var(--border-strong); }
.cm-next { flex: 1; }
.cm-next.is-loading .icon { opacity: 0; }
/* No global [hidden] rule, and an SVG ignores the attribute on its own. */
.cm-next .icon[hidden] { display: none; }

/* ---------- Shared form fields ----------
   The .cf-* rules below are used by the sign-in, report, review, chat,
   claim and reset-password forms too, which all load this file. */
.cf-field { display: flex; flex-direction: column; gap: 0.5rem; }

.cf-field label {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--text-primary);
}

.cf-optional { color: var(--text-faint); font-weight: 500; }

.cf-hint {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--text-faint);
}

/* The solid option background matters: the select's own background is
   translucent, and a browser that can't use it draws the list on white. */
.cf-field select option {
  background: var(--bg-surface);
  color: var(--text-primary);
}

.cf-field input[type="text"],
.cf-field input[type="email"],
.cf-field input[type="password"],
.cf-field select,
.cf-field textarea {
  width: 100%;
  background: oklch(0.13 0.035 280 / 55%);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: 0.8rem 1rem;
  font-family: var(--font-body);
  font-size: 0.95rem;
  color: var(--text-primary);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.cf-field input[type="text"]::placeholder,
.cf-field input[type="email"]::placeholder,
.cf-field input[type="password"]::placeholder,
.cf-field textarea::placeholder { color: var(--text-faint); }

.cf-field input[type="text"]:focus-visible,
.cf-field input[type="email"]:focus-visible,
.cf-field input[type="password"]:focus-visible,
.cf-field select:focus-visible,
.cf-field textarea:focus-visible {
  outline: none;
  border-color: oklch(0.85 0.15 202 / 60%);
  box-shadow: 0 0 0 3px oklch(0.85 0.15 202 / 18%);
}

.cf-field select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23c4c2e0' stroke-width='1.6'%3E%3Cpath d='M5 7.5l5 5 5-5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.9rem center;
  background-size: 16px;
  padding-right: 2.5rem;
}

.cf-field textarea { resize: vertical; min-height: 120px; }

.cf-field.has-error input[type="text"],
.cf-field.has-error input[type="email"],
.cf-field.has-error input[type="password"],
.cf-field.has-error select,
.cf-field.has-error textarea {
  border-color: var(--accent-error);
}

.cf-error {
  display: none;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--accent-error);
}

.cf-field.has-error .cf-error { display: block; }

.cf-form-error {
  font-size: 0.875rem;
  color: var(--accent-error);
  background: color-mix(in oklab, var(--accent-error) 12%, transparent);
  border: 1px solid color-mix(in oklab, var(--accent-error) 30%, transparent);
  border-radius: var(--radius-md);
  padding: 0.75rem 1rem;
  margin: 0;
}

.cf-honeypot {
  position: absolute;
  left: -9999px;
  top: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.btn-submit { position: relative; }

.btn-submit .btn-spinner {
  display: none;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid oklch(0.96 0.02 250 / 35%);
  border-top-color: var(--text-primary);
}

.btn-submit.is-loading .btn-label { opacity: 0; }
.btn-submit.is-loading .btn-spinner {
  display: block;
  position: absolute;
  inset: 0;
  margin: auto;
  animation: cf-spin 0.7s linear infinite;
}

@keyframes cf-spin { to { transform: rotate(360deg); } }

/* ---------- Phones: a full-height sheet, the summary becomes a bottom bar ---------- */
@media (max-width: 760px) {
  .commission-modal { padding: 0; place-items: stretch; }
  .cm-shell { width: 100%; height: 100%; border-radius: 0; }
  .cm-form { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; }
  .cm-close { top: 12px; right: 12px; }
  .cm-head { padding: 16px 64px 0 16px; }
  .cm-title { font-size: 1.3rem; }
  .cm-sub { font-size: 0.86rem; }
  .cm-steps { margin-top: 14px; margin-right: -48px; gap: 4px; }
  .cm-step { padding: 5px 9px 5px 5px; font-size: 0.82rem; gap: 6px; }
  .cm-alert { margin: 12px 16px 0; }
  .cm-panes { padding: 18px 16px 0; }

  .cm-rail {
    flex-direction: row;
    align-items: center;
    gap: 12px;
    overflow: visible;
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
    border-left: 0;
    border-top: 1px solid var(--border-subtle);
  }

  .cm-rail-body { display: none; }
  .cm-compact { display: flex; flex-direction: column; min-width: 0; flex: 1; }
  .cm-compact-value { font-family: var(--font-mono); font-size: 1rem; font-weight: 600; white-space: nowrap; }
  .cm-compact-name { font-size: 0.76rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .cm-rail-actions { margin-top: 0; flex-shrink: 0; gap: 8px; }
  .cm-next { flex: none; }
  .commission-modal .btn, .cm-back { padding: 0 0.95rem; }
}

/* Phones and touch devices: a modal backdrop blur forces a readback of
   everything painted behind it. Dropped in favour of a more opaque
   scrim, which reads the same once the ambient layer behind it is
   already static (see ambient-background.css). */
@media (max-width: 900px), (hover: none) {
  .commission-modal-backdrop {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: oklch(0.13 0.035 280 / 88%);
  }
}


/* ===== assets/css/components/commission-picker.css ===== */
/* =========================================================
   Commission Modal: the choices
   Choice cards (type and package), base chips, the artist picker, the
   details fields, the reference image drop and the consent boxes. The
   shell they sit in is commission-modal.css.

   Every choice is a real radio inside a label. The radio is visually
   hidden and the card after it takes the checked and focus states.
   ========================================================= */
.cm-group { border: 0; margin: 0; padding: 0; min-width: 0; }

.cm-legend {
  padding: 0;
  margin-bottom: 12px;
  font-family: var(--font-heading);
  font-size: 1.04rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.012em;
  color: var(--text-primary);
}

.cm-optional {
  margin-left: 6px;
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--text-faint);
}

.cm-group-hint { margin: -6px 0 12px; max-width: 60ch; font-size: 0.84rem; line-height: 1.5; color: var(--text-muted); }

.cm-error { display: none; font-size: 0.82rem; color: var(--accent-error); }
.has-error > .cm-error, .has-error .cm-error { display: block; }
.cm-group > .cm-error { margin-top: 8px; }

/* ---------- Commission line ----------
   Texturing or Blender, shown only while both can be booked
   (commission-line.js). Two options in one track at the utility scale: the
   chosen one lifts a surface step and takes the cyan hairline. */
.cm-line {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.35rem;
  padding: 0.3rem;
  border-radius: calc(var(--radius-sm) + 0.3rem);
  border: 1px solid var(--border-subtle);
  background-color: color-mix(in oklab, var(--bg-surface) 70%, transparent);
}

.cm-line-opt {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  height: 100%;
  padding: 0.55rem 0.85rem;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  color: var(--text-muted);
  transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}

.cm-line-name { font-size: 0.92rem; font-weight: 600; }
.cm-line-sub { font-size: 0.78rem; line-height: 1.35; color: var(--text-faint); }
.cm-choice:hover .cm-line-opt { color: var(--text-primary); }

.cm-line input:checked + .cm-line-opt {
  background-color: var(--bg-surface-alt);
  border-color: color-mix(in oklab, var(--accent-secondary) 55%, transparent);
  color: var(--text-primary);
}

.cm-line input:checked + .cm-line-opt .cm-line-sub { color: var(--text-muted); }
.cm-line input:focus-visible + .cm-line-opt { outline: 2px solid var(--accent-secondary); outline-offset: 2px; }
[data-field="commission_line"].has-error .cm-line { border-color: color-mix(in oklab, var(--accent-error) 55%, transparent); }

/* ---------- Choice cards ---------- */
.cm-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
/* Blender has four kinds of job. Four across leaves each card too narrow
   for its title beside the summary rail, so they sit two by two. The class
   is set by commission-line.js; phones are at the end. */
.cm-grid--four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cm-choice { position: relative; display: block; cursor: pointer; }
/* A card of the commission line that is not chosen. */
.cm-choice[hidden] { display: none; }

.cm-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
  height: 100%;
  padding: 14px 16px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-subtle);
  background: color-mix(in oklab, var(--bg-base) 50%, transparent);
  transition: border-color var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast);
}

.cm-choice:hover .cm-card { border-color: var(--border-strong); }

.cm-choice input:checked + .cm-card {
  border-color: color-mix(in oklab, var(--accent-secondary) 62%, transparent);
  background: color-mix(in oklab, var(--accent-secondary) 7%, var(--bg-surface));
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--accent-secondary) 38%, transparent);
}

.cm-choice input:focus-visible + .cm-card,
.cm-choice input:focus-visible + .cm-chip {
  outline: 2px solid var(--accent-secondary);
  outline-offset: 2px;
}

[data-field="commission_type"].has-error .cm-card { border-color: color-mix(in oklab, var(--accent-error) 55%, transparent); }

.cm-tick {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent-secondary);
  color: var(--bg-base);
  font-size: 0.6rem;
  opacity: 0;
  transform: scale(0.6);
  transition: opacity var(--transition-fast), transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

.cm-choice input:checked + .cm-card .cm-tick { opacity: 1; transform: none; }

.cm-choice-title { padding-right: 24px; font-size: 0.93rem; font-weight: 600; }
.cm-choice-desc { font-size: 0.8rem; line-height: 1.45; color: var(--text-muted); }

.cm-tier-name {
  padding-right: 24px;
  font-family: var(--font-heading);
  font-size: 0.98rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.cm-tier-price {
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: 0.92rem;
  font-weight: 500;
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
}

.cm-tilde { margin-left: 1px; font-weight: 500; color: var(--text-faint); }

.cm-tier-spec {
  font-family: var(--font-mono);
  font-size: 0.64rem;
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.cm-tier-unsure .cm-tier-price { font-family: var(--font-body); font-size: 0.8rem; font-weight: 400; line-height: 1.45; color: var(--text-muted); }

/* ---------- Base chips ---------- */
.cm-chips { display: flex; flex-wrap: wrap; gap: 8px; }

.cm-chip {
  display: block;
  padding: 8px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background: color-mix(in oklab, var(--bg-base) 50%, transparent);
  font-size: 0.88rem;
  color: var(--text-muted);
  transition: border-color var(--transition-fast), color var(--transition-fast), background-color var(--transition-fast);
}

.cm-choice:hover .cm-chip { border-color: var(--border-strong); color: var(--text-primary); }

.cm-choice input:checked + .cm-chip {
  border-color: color-mix(in oklab, var(--accent-secondary) 62%, transparent);
  background: color-mix(in oklab, var(--accent-secondary) 9%, transparent);
  color: var(--text-primary);
}

/* Gold marks who can work on the chosen base, and only that, across the form. */
.cm-owners { margin-top: 12px; font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500; line-height: 1.5; color: var(--accent-gold); }
.cm-owners.is-none { color: var(--text-faint); }
.cm-owners[hidden] { display: none; }

/* ---------- Artist picker ---------- */
.cm-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; }

.cm-filter-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background: transparent;
  font-size: 0.82rem;
  color: var(--text-muted);
  transition: color var(--transition-fast), border-color var(--transition-fast), background-color var(--transition-fast);
}

.cm-filter-btn:hover { color: var(--text-primary); border-color: var(--border-strong); }

.cm-filter-btn[aria-pressed="true"] {
  color: var(--text-primary);
  border-color: color-mix(in oklab, var(--accent-secondary) 62%, transparent);
  background: color-mix(in oklab, var(--accent-secondary) 9%, transparent);
}

.cm-filter-btn[data-filter="owns"] { color: var(--accent-gold); }

.cm-filter-btn[data-filter="owns"][aria-pressed="true"] {
  border-color: color-mix(in oklab, var(--accent-gold) 60%, transparent);
  background: color-mix(in oklab, var(--accent-gold) 10%, transparent);
}

.cm-filter-btn[hidden] { display: none; }
.cm-count { font-family: var(--font-mono); font-size: 0.72rem; font-weight: 500; line-height: 1; color: var(--text-faint); }

.cm-search { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; font-size: 0.82rem; color: var(--text-muted); }

.cm-search input {
  width: 150px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background: color-mix(in oklab, var(--bg-base) 50%, transparent);
  font-family: var(--font-body);
  font-size: 0.85rem;
  color: var(--text-primary);
}

.cm-search input:focus-visible,
.cm-input:focus-visible {
  outline: none;
  border-color: color-mix(in oklab, var(--accent-secondary) 62%, transparent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent-secondary) 18%, transparent);
}

.cm-artists { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 10px; }
.cm-artist[hidden] { display: none; }
.cm-artist .cm-card { padding: 8px 8px 12px; gap: 0; }

.cm-face {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  margin-bottom: 10px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg-surface-alt);
  transition: box-shadow var(--transition-fast);
}

/* The "Any artist" tile holds one set of faces per commission line. */
.cm-face[hidden] { display: none; }
.cm-face img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Same fallback as the homepage artist cards (artist-deck.css), on the artist's hue. */
.cm-initials {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-heading);
  font-size: 1.6rem;
  font-weight: 600;
  line-height: 1;
  color: var(--text-primary);
  background: radial-gradient(120% 120% at 50% 18%, oklch(0.62 0.14 var(--avatar-hue, 292) / 55%) 0%, var(--bg-surface-alt) 72%);
}

.cm-mosaic { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; background: var(--bg-base); }
.cm-mosaic--one { grid-template-columns: 1fr; }
.cm-mosaic .cm-face { margin: 0; border-radius: 0; }
.cm-mosaic .cm-initials { font-size: 0.9rem; }

/* A line's own set of faces fills its square with two or three as well: two
   stand side by side at full height, a third runs along the bottom. A
   restricted line often has that few open artists. The one set of a form
   with a single line carries no data-line and keeps the look it has. */
.cm-mosaic[data-line] > .cm-face:first-child:nth-last-child(2),
.cm-mosaic[data-line] > .cm-face:nth-child(2):last-child { aspect-ratio: 1 / 2; align-self: stretch; }
.cm-mosaic[data-line] > .cm-face:nth-child(3):last-child { grid-column: 1 / -1; aspect-ratio: 2 / 1; }

.cm-artist-name { padding-inline: 4px; font-size: 0.9rem; font-weight: 600; line-height: 1.25; }
.cm-artist-role { padding-inline: 4px; font-size: 0.75rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* "First free Blender artist" is longer than a tile is wide, so the line
   that changes with the commission line may wrap. */
.cm-artist-role[data-line-text] { white-space: normal; line-height: 1.35; }

.cm-status { display: flex; align-items: center; gap: 6px; margin-top: 7px; padding-inline: 4px; font-size: 0.72rem; color: var(--text-muted); }
.cm-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: 50%; background: var(--text-faint); }
.cm-status--available .cm-dot { background: var(--accent-secondary); box-shadow: 0 0 8px var(--accent-secondary); }
.cm-status--at_capacity .cm-dot { background: var(--accent-gold-alt); }

.cm-slots { margin-top: 3px; padding-inline: 4px; font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; line-height: 1.3; color: var(--text-faint); }
.cm-owns { margin-top: 5px; padding-inline: 4px; font-family: var(--font-mono); font-size: 0.68rem; font-weight: 600; line-height: 1.3; color: var(--accent-gold); }
.cm-owns[hidden] { display: none; }
.cm-artist.owns .cm-face { box-shadow: 0 0 0 2px var(--accent-gold); }

/* A full queue is the only thing that locks an artist. */
.cm-artist.is-full { cursor: not-allowed; }
.cm-artist.is-full .cm-card { opacity: 0.55; }
.cm-artist.is-full .cm-face img,
.cm-artist.is-full .cm-initials { filter: grayscale(0.85); }
.cm-artist.is-full:hover .cm-card { border-color: var(--border-subtle); }

.cm-empty {
  grid-column: 1 / -1;
  padding: 18px;
  border-radius: var(--radius-md);
  border: 1px dashed var(--border-subtle);
  font-size: 0.88rem;
  color: var(--text-muted);
}

.cm-empty[hidden] { display: none; }

.cm-linkbtn {
  padding: 0;
  border: 0;
  background: none;
  font-weight: 600;
  color: var(--accent-secondary);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.cm-artist-note { margin-top: 14px; font-size: 0.84rem; line-height: 1.5; color: var(--text-muted); }
.cm-artist-note[hidden] { display: none; }

.cm-profile-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 14px;
  font-size: 0.84rem;
  font-weight: 600;
  color: var(--accent-secondary);
  text-decoration: none;
}

.cm-profile-link:hover { text-decoration: underline; }
.cm-profile-link .icon { font-size: 0.72rem; }
.cm-profile-link[hidden] { display: none; }

/* ---------- Details ---------- */
.cm-field { display: flex; flex-direction: column; gap: 7px; }
.cm-label-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.cm-label { font-size: 0.9rem; font-weight: 600; color: var(--text-primary); }
.cm-counter { font-family: var(--font-mono); font-size: 0.72rem; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; color: var(--text-faint); }

.cm-input {
  width: 100%;
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background: color-mix(in oklab, var(--bg-base) 50%, transparent);
  font-family: var(--font-body);
  font-size: 0.95rem;
  color: var(--text-primary);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.cm-input::placeholder { color: var(--text-faint); }
textarea.cm-input { min-height: 132px; resize: vertical; line-height: 1.55; }
.cm-hint { font-size: 0.8rem; line-height: 1.45; color: var(--text-faint); }
.has-error .cm-input { border-color: var(--accent-error); }

/* The file input covers the whole zone, so a click opens the picker and a
   drop lands on it natively. */
.cm-drop {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 20px 16px;
  text-align: center;
  border-radius: var(--radius-md);
  border: 1px dashed var(--border-strong);
  transition: border-color var(--transition-fast), background-color var(--transition-fast);
}

.cm-drop:hover,
.cm-drop.is-over {
  border-color: color-mix(in oklab, var(--accent-secondary) 62%, transparent);
  background: color-mix(in oklab, var(--accent-secondary) 6%, transparent);
}

.cm-drop:focus-within { outline: 2px solid var(--accent-secondary); outline-offset: 2px; }
.cm-drop input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.cm-drop.is-full { opacity: 0.55; }
.cm-drop-main { font-size: 0.92rem; color: var(--text-primary); }
.cm-drop-main span { font-weight: 600; color: var(--accent-secondary); text-decoration: underline; text-underline-offset: 3px; }

.cm-thumbs { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 12px; }
.cm-thumbs:empty { display: none; }
.cm-thumb { position: relative; width: 92px; }
.cm-thumb img { display: block; width: 92px; height: 92px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border-subtle); }
.cm-thumb-name { display: block; margin-top: 5px; font-family: var(--font-mono); font-size: 0.66rem; line-height: 1.3; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.cm-thumb-x {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: var(--bg-surface-alt);
  color: var(--text-primary);
  font-size: 0.6rem;
}

.cm-thumb-x:hover { border-color: color-mix(in oklab, var(--accent-secondary) 62%, transparent); color: var(--accent-secondary); }

.cm-consents {
  margin: 0;
  padding: 14px 16px 16px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  display: grid;
  gap: 12px;
}

.cm-consents legend { padding: 0 6px; margin-left: -6px; font-family: var(--font-heading); font-size: 0.92rem; font-weight: 600; }
.cm-check { display: flex; align-items: flex-start; gap: 11px; cursor: pointer; }

/* 24px: the WCAG 2.2 AA target floor these required boxes already met. */
.cm-check input { flex-shrink: 0; width: 24px; height: 24px; margin: 0; accent-color: var(--accent-primary); cursor: pointer; }
.cm-check span { font-size: 0.8rem; line-height: 1.55; color: var(--text-muted); }
.cm-check a { color: var(--accent-secondary); text-decoration: underline; }
.cm-check a:hover { color: var(--text-primary); }
.has-error .cm-check input { outline: 2px solid var(--accent-error); outline-offset: 2px; }

@media (max-width: 760px) {
  .cm-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cm-grid--types { grid-template-columns: minmax(0, 1fr); }
  .cm-artists { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 8px; }
  .cm-search { margin-left: 0; width: 100%; }
  .cm-search input { flex: 1; width: auto; }
}

/* Four kinds of job stay two by two on phones as well (hence after the
   block above, which sets the type cards one across). */
@media (max-width: 760px) {
  .cm-grid--four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


/* ===== assets/css/components/commission-sent.css ===== */
/* =========================================================
   Commission Modal: the sent screen
   Shown in place of the form once a request goes through: who has it,
   the order number, and what happens next. The shell is
   commission-modal.css; .cm-face comes from commission-picker.css.
   ========================================================= */
.cm-done {
  height: 100%;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.cm-done::-webkit-scrollbar { display: none; }
.cm-done[hidden] { display: none; }

.cm-done-inner { width: min(500px, 100%); display: flex; flex-direction: column; align-items: center; text-align: center; }

.cm-done-face { position: relative; width: 104px; margin-bottom: 20px; animation: cm-pop 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; }
.cm-done-face > .cm-face { margin: 0; border-radius: 18px; box-shadow: var(--shadow-glow-violet); }

.cm-done-badge {
  position: absolute;
  right: -8px;
  bottom: -8px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--bg-surface);
  color: var(--accent-secondary);
  font-size: 1.6rem;
}

@keyframes cm-pop {
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 1; transform: none; }
}

.cm-done-title { margin: 0; font-size: 1.7rem; font-weight: 600; line-height: 1.15; letter-spacing: -0.025em; }
.cm-done-title:focus { outline: none; }
.cm-done-msg { margin: 8px 0 22px; max-width: 46ch; color: var(--text-muted); line-height: 1.6; }

.cm-code {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 14px 12px 18px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-subtle);
  background: var(--bg-base);
  text-align: left;
}

.cm-code-label { display: block; font-size: 0.76rem; color: var(--text-faint); }

.cm-code-value {
  font-family: var(--font-mono);
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--accent-secondary);
}

.cm-copy {
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
  background: transparent;
  font-size: 0.82rem;
  color: var(--text-muted);
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.cm-copy:hover { color: var(--text-primary); border-color: color-mix(in oklab, var(--accent-secondary) 60%, transparent); }

.cm-after-title { width: 100%; margin: 22px 0 0; text-align: left; font-size: 0.92rem; font-weight: 600; }

.cm-after {
  width: 100%;
  margin: 12px 0 26px;
  padding: 0;
  list-style: none;
  counter-reset: cm-after;
  display: grid;
  gap: 12px;
  text-align: left;
}

.cm-after li {
  counter-increment: cm-after;
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: 12px;
  align-items: start;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--text-muted);
}

.cm-after li::before {
  content: counter(cm-after);
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1;
  color: var(--text-primary);
}

.cm-done-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }



/* ===== assets/css/components/commission-auth-modal.css ===== */
/* =========================================================
   Commission Auth Modal (sign-in required prompt)
   ========================================================= */
.commission-auth-modal-backdrop {
  position: fixed;
  inset: 0;
  background: oklch(0.13 0.035 280 / 70%);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-base);
  z-index: var(--z-modal-backdrop);
}

.commission-auth-modal-backdrop.is-open { opacity: 1; pointer-events: auto; }

.commission-auth-modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(1rem, 4vw, 2.5rem);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--transition-base), visibility 0s linear var(--transition-base);
}

.commission-auth-modal.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--transition-base), visibility 0s linear;
}

.commission-auth-modal-panel {
  position: relative;
  width: 100%;
  max-width: 480px;
  max-height: min(88vh, 780px);
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-glow-violet), var(--shadow-card);
  padding: clamp(1.75rem, 4vw, 2.75rem);
  transform: translateY(24px) scale(0.96);
  transition: transform var(--transition-base);
}

.commission-auth-modal-panel::-webkit-scrollbar { display: none; }

/* Only the panel takes clicks. The full-screen wrapper stays click-through,
   so a click beside the panel reaches the backdrop, which closes the modal. */
.commission-auth-modal.is-open .commission-auth-modal-panel { pointer-events: auto; transform: none; }

.commission-auth-modal-close {
  position: absolute;
  top: 1.25rem;
  right: 1.25rem;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border-subtle);
  background: transparent;
  color: var(--text-muted);
  font-size: 1rem;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}

.commission-auth-modal-close:hover {
  border-color: oklch(0.85 0.15 202 / 60%);
  color: var(--accent-secondary);
}

.commission-auth-modal-title {
  font-size: clamp(1.4rem, 1.5vw + 1rem, 1.75rem);
  margin-top: 1rem;
  margin-bottom: 0.5rem;
}

.commission-auth-modal-subtitle {
  color: var(--text-muted);
  font-size: 0.95rem;
  margin-bottom: 1.75rem;
}

.commission-auth-modal-providers {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-bottom: 1.5rem;
}

.commission-auth-modal-providers .btn { width: 100%; }

.commission-auth-modal-actions {
  display: flex;
  justify-content: center;
}

/* Phones and touch devices: a modal backdrop blur forces a readback of
   everything painted behind it. Dropped in favour of a more opaque
   scrim, which reads the same once the ambient layer behind it is
   already static (see ambient-background.css). */
@media (max-width: 900px), (hover: none) {
  .commission-auth-modal-backdrop {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: oklch(0.13 0.035 280 / 88%);
  }
}

/* Says what the brief will ask for, so the account step reads as part of
   a process rather than as a toll gate. */
.commission-auth-modal-preview {
  margin-top: 0.85rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: oklch(0.13 0.035 280 / 55%);
  color: var(--text-muted);
  font-size: 0.85rem;
  line-height: 1.6;
}


/* ===== assets/css/components/account-modal.css ===== */
/* =========================================================
   Account Modal (sign in / create account)
   ========================================================= */
.account-modal-backdrop {
  position: fixed;
  inset: 0;
  background: oklch(0.13 0.035 280 / 70%);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-base);
  z-index: var(--z-modal-backdrop);
}

.account-modal-backdrop.is-open { opacity: 1; pointer-events: auto; }

/* Deliberately opacity/visibility, never display:none - Turnstile renders
   implicitly into .cf-turnstile via its data-sitekey attribute with no
   explicit turnstile.render() call, which requires the container to have
   real layout dimensions at all times. Switching this to display:none
   later would silently break that. */
.account-modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(1rem, 4vw, 2.5rem);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--transition-base), visibility 0s linear var(--transition-base);
}

.account-modal.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--transition-base), visibility 0s linear;
}

.account-modal-panel {
  position: relative;
  width: 100%;
  max-width: 440px;
  max-height: min(88vh, 780px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-glow-violet), var(--shadow-card);
  padding: clamp(1.75rem, 4vw, 2.75rem);
  transform: translateY(24px) scale(0.96);
  transition: transform var(--transition-base);
}


/* Only the panel takes clicks. The full-screen wrapper stays click-through,
   so a click beside the panel reaches the backdrop, which closes the modal. */
.account-modal.is-open .account-modal-panel { pointer-events: auto; transform: none; }

/* The register tab (three fields plus the Turnstile widget) is taller than
   a 900px-high window, and this panel hides its scrollbar by design, so the
   submit button used to sit below the fold with nothing at all to say so.

   Same shape as .chat-modal-panel: the panel clips and an inner region
   scrolls, which lets the fade below be painted on the panel itself. On a
   scrolling panel the fade would scroll away with the content. */
.account-modal-body {
  flex: 1;
  min-height: 0;
  max-height: 100%;
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.account-modal-body::-webkit-scrollbar { display: none; }

/* Overflow cue in place of the scrollbar this panel deliberately hides.
   Sits inside the panel's rounded corners and ignores pointer events so
   it never intercepts a click on the submit button behind it. */
.account-modal-panel::after {
  content: "";
  position: absolute;
  left: 1px;
  right: 1px;
  bottom: 1px;
  height: 2.5rem;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  background: linear-gradient(to top, var(--bg-surface), oklch(0.19 0.055 285 / 0%));
  pointer-events: none;
}

.account-modal-close {
  position: absolute;
  top: 1.25rem;
  right: 1.25rem;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border-subtle);
  background: transparent;
  color: var(--text-muted);
  font-size: 1rem;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}

.account-modal-close:hover {
  border-color: oklch(0.85 0.15 202 / 60%);
  color: var(--accent-secondary);
}

.account-modal-title {
  font-size: clamp(1.4rem, 1.5vw + 1rem, 1.75rem);
  margin-top: 1rem;
  margin-bottom: 1.25rem;
}

.account-modal-tabs {
  display: flex;
  gap: 0.4rem;
  padding: 0.3rem;
  margin-bottom: 1.5rem;
  background: oklch(0.13 0.035 280 / 55%);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.account-modal-tab {
  flex: 1;
  padding: 0.55rem 0.75rem;
  border-radius: calc(var(--radius-md) - 0.3rem);
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 0.9rem;
  font-weight: 600;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.account-modal-tab.is-active {
  background: var(--accent-primary);
  color: var(--text-primary);
}

.account-form { display: flex; flex-direction: column; gap: 1.25rem; }

/* No global [hidden] rule, and both of these set display. */
.account-form[hidden],
.account-oauth[hidden] { display: none; }

/* Google / Discord (partials/oauth-providers.php), plus "Sign up with
   email" on the sign-up tab. */
.account-oauth {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.account-oauth .btn { width: 100%; }

/* "or with email" between the providers and the sign-in form. */
#account-panel-login .auth-or { margin: 1.1rem 0.25rem 1.25rem; }

.account-signup-back { align-self: center; }

.account-switch-line {
  margin: 1.25rem 0 0;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.85rem;
}

/* Why the sign-up tab opened by itself (account-modal.php). */
.account-modal-notice {
  margin: 0 0 1.25rem;
  padding: 0.75rem 1rem;
  border-radius: var(--radius-md);
  background: color-mix(in oklab, var(--accent-secondary) 8%, transparent);
  border: 1px solid color-mix(in oklab, var(--accent-secondary) 35%, transparent);
  color: var(--text-primary);
  font-size: 0.875rem;
}

/* The Terms and Privacy box, last in the sign-up form. Same look as the
   commission form's .cm-check. */
.auth-consent {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.65rem 0.8rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--bg-base) 55%, transparent);
  cursor: pointer;
  transition: border-color var(--transition-fast);
}

.auth-consent:has(input:checked) {
  border-color: color-mix(in oklab, var(--accent-secondary) 40%, transparent);
}

.auth-consent input {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin: 1px 0 0;
  accent-color: var(--accent-primary);
  cursor: pointer;
}

/* font-weight: .cf-field label sets 600 on the label. */
.auth-consent span {
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.5;
  color: var(--text-muted);
}

.auth-consent a {
  color: var(--accent-secondary);
  text-decoration: underline;
}

.auth-consent a:hover { color: var(--text-primary); }

.cf-field.has-error .auth-consent { border-color: var(--accent-error); }

.account-modal-link-btn {
  background: transparent;
  border: none;
  /* 24px floor per WCAG 2.2 AA 2.5.8. These are standalone controls
     under a field, not links inside a sentence, so the inline
     exception does not cover them. */
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0;
  color: var(--accent-secondary);
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: underline;
}

/* Sits inside the password .cf-field, under the input, so it reads as
   attached to that field rather than as a third form control. */
.account-forgot-trigger {
  align-self: flex-start;
  font-size: 0.8rem;
}

.account-forgot-lead {
  color: var(--text-muted);
  font-size: 0.9rem;
  margin: 0;
}

.account-resend-prompt {
  padding: 0.75rem 1rem;
  background: oklch(0.19 0.055 285 / 50%);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  font-size: 0.85rem;
  color: var(--text-muted);
}

.account-resend-prompt p { margin: 0 0 0.4rem; }

.account-modal-success {
  text-align: center;
  padding: 1rem 0;
}

.account-modal-success .icon {
  font-size: 2.25rem;
  color: var(--accent-secondary);
  margin-bottom: 0.75rem;
}

.account-modal-success h3 { margin: 0 0 0.5rem; }

.account-modal-success p {
  color: var(--text-muted);
  margin: 0 0 1.25rem;
}

.cf-turnstile { margin: 0.25rem 0; }

.account-status-banner {
  max-width: 640px;
  margin: 1.5rem auto 0;
  padding: 0.9rem 1.25rem;
  border-radius: var(--radius-md);
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  background: oklch(0.19 0.055 285 / 50%);
}

.account-status-banner p { margin: 0; }

.account-status-banner--success { border: 1px solid color-mix(in oklab, var(--accent-secondary) 45%, transparent); }
.account-status-banner--error { border: 1px solid color-mix(in oklab, var(--accent-error) 45%, transparent); }

/* =========================================================
   Standalone auth page (reset-password.php)

   A full page rather than a modal, because it's the landing
   target of a link in an email. Reuses .account-form and the
   .cf-* form primitives so it stays in step with the modal's
   fields; only the page chrome around them is new.
   ========================================================= */
.auth-page {
  min-height: calc(100vh - var(--nav-height));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(2rem, 6vw, 4rem) 1rem;
}

.auth-page-card {
  width: 100%;
  max-width: 440px;
  padding: clamp(1.75rem, 4vw, 2.5rem);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-glow-violet);
}

.auth-page-title {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.auth-page-lead {
  color: var(--text-muted);
  font-size: 0.9rem;
  margin-bottom: 1.75rem;
}

/* Phones and touch devices: a modal backdrop blur forces a readback of
   everything painted behind it. Dropped in favour of a more opaque
   scrim, which reads the same once the ambient layer behind it is
   already static (see ambient-background.css). */
@media (max-width: 900px), (hover: none) {
  .account-modal-backdrop {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: oklch(0.13 0.035 280 / 88%);
  }
}

/* Password reveal. The wrapper is relative so the button can sit inside
   the field's right edge; the input keeps room for it so long values do
   not run underneath. */
.cf-password-wrap { position: relative; display: block; }

.cf-password-wrap input { padding-right: 4.25rem; }

.cf-password-toggle {
  position: absolute;
  top: 50%;
  right: 0.5rem;
  transform: translateY(-50%);
  min-height: 24px;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-sm);
  /* .btn is not in play here, and there is no global button reset, so
     both of these have to be explicit or the browser's own chrome shows
     through. */
  background: transparent;
  border: 1px solid var(--border-subtle);
  color: var(--text-muted);
  font-size: 0.75rem;
  font-weight: 600;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}

.cf-password-toggle:hover {
  border-color: oklch(0.85 0.15 202 / 60%);
  color: var(--accent-secondary);
}

.cf-password-toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px oklch(0.85 0.15 202 / 30%);
}


/* ===== assets/css/components/terms-modal.css ===== */
/* =========================================================
   Terms & Conditions Modal
   ========================================================= */
.terms-modal-backdrop {
  position: fixed;
  inset: 0;
  background: oklch(0.13 0.035 280 / 70%);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-base);
  z-index: var(--z-modal-legal-backdrop);
}

.terms-modal-backdrop.is-open { opacity: 1; pointer-events: auto; }

.terms-modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal-legal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(1rem, 4vw, 2.5rem);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--transition-base), visibility 0s linear var(--transition-base);
}

.terms-modal.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--transition-base), visibility 0s linear;
}

.terms-modal-panel {
  position: relative;
  width: 100%;
  max-width: 720px;
  max-height: min(88vh, 820px);
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-glow-violet), var(--shadow-card);
  padding: clamp(1.75rem, 4vw, 2.75rem);
  transform: translateY(24px) scale(0.96);
  transition: transform var(--transition-base);
}

/* Only the panel takes clicks. The full-screen wrapper stays click-through,
   so a click beside the panel reaches the backdrop, which closes the modal. */
.terms-modal.is-open .terms-modal-panel { pointer-events: auto; transform: none; }

.terms-modal-panel::-webkit-scrollbar { display: none; }

.terms-modal-close {
  position: absolute;
  top: 1.25rem;
  right: 1.25rem;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border-subtle);
  background: transparent;
  color: var(--text-muted);
  font-size: 1rem;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}

.terms-modal-close:hover {
  border-color: oklch(0.85 0.15 202 / 60%);
  color: var(--accent-secondary);
}

.terms-modal-title {
  font-size: clamp(1.4rem, 1.5vw + 1rem, 1.75rem);
  margin-top: 1rem;
  margin-bottom: 1.5rem;
}

/* ---------- Long-form legal content ---------- */
.terms-content h2 {
  font-size: 1.05rem;
  font-weight: 700;
  margin-top: 1.75rem;
  margin-bottom: 0.65rem;
}

.terms-content h2:first-child { margin-top: 0; }

.terms-content p {
  color: var(--text-muted);
  font-size: 0.9rem;
  line-height: 1.7;
}

.terms-content ul {
  margin-top: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

.terms-content li {
  position: relative;
  padding-left: 1.1rem;
  color: var(--text-muted);
  font-size: 0.9rem;
  line-height: 1.7;
}

.terms-content li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent-sky);
}

.terms-content li strong { color: var(--text-primary); }

.terms-content code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  background: oklch(0.13 0.035 280 / 55%);
  border: 1px solid var(--border-subtle);
  border-radius: 4px;
  padding: 0.1rem 0.35rem;
}

/* Numbered clauses (<p class="clause"><span class="clause-no">2.1</span>).
   The number hangs in its own column so every clause's text lines up, the
   way contracts are set. */
.terms-content p.clause {
  position: relative;
  padding-left: 2.6rem;
}

.terms-content p.clause + p.clause { margin-top: 0.6rem; }

.terms-content .clause-no {
  position: absolute;
  left: 0;
  top: 0.2em;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}

/* Links in the prose (contact addresses, the supervisory authority)
   must read as links, not as more text. */
.terms-content a {
  color: var(--accent-secondary);
  text-decoration: underline;
}

.terms-content a:hover { color: var(--text-primary); }

/* A list that belongs to the clause above it sits under that clause's text. */
.terms-content ul.clause-list { padding-left: 2.6rem; }

.terms-consent-note {
  margin-top: 2rem;
  color: var(--text-primary) !important;
  font-size: 0.85rem !important;
  font-style: italic;
  background: oklch(0.85 0.15 202 / 8%);
  border: 1px solid oklch(0.85 0.15 202 / 25%);
  border-radius: var(--radius-md);
  padding: 1rem 1.25rem;
}

/* Phones and touch devices: a modal backdrop blur forces a readback of
   everything painted behind it. Dropped in favour of a more opaque
   scrim, which reads the same once the ambient layer behind it is
   already static (see ambient-background.css). */
@media (max-width: 900px), (hover: none) {
  .terms-modal-backdrop {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: oklch(0.13 0.035 280 / 88%);
  }
}

/* ---------- Shared with the standalone pages ---------- */

/* Sits between the dialog title and the prose. Machine-readable via
   <time>, and the same date seo_page_json_ld() reports as dateModified. */
.legal-updated {
  color: var(--text-faint);
  font-size: 0.8rem;
  margin-bottom: 1.5rem;
}

/* Escape hatch out of the modal, for someone reading this mid-form who
   wants to keep what they have typed. */
.legal-full-link {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  margin-top: 1.75rem;
  color: var(--accent-secondary);
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: underline;
}


/* ===== assets/css/components/cookie-consent-modal.css ===== */
/* =========================================================
   Cookie notice

   A bottom banner, not a dialog - see the comment at the top of
   includes/partials/cookie-consent-modal.php for why it stopped being
   one. It must never cover content or trap focus: it sits below every
   overlay in the stacking scale and above nothing but the page.
   ========================================================= */
.cookie-banner {
  position: fixed;
  left: 1rem;
  right: 1rem;
  bottom: 1rem;
  margin-inline: auto;
  max-width: 44rem;
  z-index: var(--z-back-to-top);

  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.9rem 1.1rem;

  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-glow-violet);
}

.cookie-banner[hidden] { display: none; }

.cookie-banner-text {
  flex: 1;
  color: var(--text-muted);
  font-size: 0.85rem;
  line-height: 1.6;
}

.cookie-banner-text a {
  color: var(--accent-secondary);
  text-decoration: underline;
}

/* .btn declares a border but no background, so a variant that does not
   set one falls through to the browser's default button chrome. */
.cookie-banner-dismiss {
  flex-shrink: 0;
  background: transparent;
  padding: 0.6rem 1.25rem;
  font-size: 0.85rem;
}

/* The back-to-top button lives in the same corner. Clear it, rather than
   letting the two overlap on the one viewport width where both show. */
@media (min-width: 641px) {
  .cookie-banner { right: 5.5rem; }
}

@media (max-width: 640px) {
  .cookie-banner {
    flex-direction: column;
    align-items: stretch;
    gap: 0.85rem;
    text-align: center;
  }
}


/* ===== assets/css/components/auth-nav.css ===== */
/* =========================================================
   Auth nav (sign in / welcome)
   ========================================================= */
.auth-nav {
  position: relative;
  display: inline-flex;
}

/* Guests: Sign in + Sign up, side by side. */
.auth-nav-guest {
  align-items: center;
  gap: 0.5rem;
}

.user-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.9rem 0.4rem 0.4rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  background: oklch(0.19 0.055 285 / 50%);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 0.875rem;
  font-weight: 600;
  white-space: nowrap;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

.user-chip:hover,
.user-chip[aria-expanded="true"] {
  border-color: oklch(0.85 0.15 202 / 45%);
  background: var(--bg-surface-alt);
}

.user-chip-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--gradient-primary);
  color: var(--text-primary);
  font-weight: 700;
  font-size: 0.85rem;
  flex-shrink: 0;
}

.user-chip-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.user-chip-caret {
  font-size: 0.65rem;
  color: var(--text-faint);
  transition: transform var(--transition-fast);
}

.user-chip[aria-expanded="true"] .user-chip-caret { transform: rotate(180deg); }

.user-chip-mobile {
  width: 100%;
  padding: 0.7rem 1.15rem 0.7rem 0.7rem;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  background: oklch(0.19 0.055 285 / 50%);
  box-shadow: var(--shadow-glow-violet);
  font-size: 0.95rem;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
  animation: navGroupIn 0.32s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.user-chip-mobile:hover,
.user-chip-mobile:focus-visible {
  border-color: oklch(0.85 0.15 202 / 45%);
  box-shadow: var(--shadow-glow-cyan);
}

.user-chip-mobile .user-chip-avatar {
  position: relative;
  width: 38px;
  height: 38px;
  font-size: 1rem;
}

/* A gradient ring peeking out 2px behind the avatar, since the pill shape
   already borrows the desktop chip's radius-pill: the mobile identity chip
   should read as more than another rectangular card in the stack below. */
.user-chip-mobile .user-chip-avatar::before {
  content: "";
  position: absolute;
  inset: -2px;
  z-index: -1;
  border-radius: 50%;
  background: var(--gradient-primary);
}

.user-chip-mobile-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.user-chip-mobile .user-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.user-chip-mobile-sub {
  font-size: 0.75rem;
  color: var(--text-faint);
}

.user-chip-mobile-caret {
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--text-faint);
}

.auth-dropdown {
  position: absolute;
  top: calc(100% + 0.75rem);
  right: 0;
  min-width: 220px;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.5rem;
  background: oklch(0.13 0.035 280 / 96%);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  z-index: var(--z-dropdown);
}

.auth-dropdown[hidden] {
  display: none;
}

.auth-provider-link {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.65rem 0.75rem;
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--text-primary);
  /* Harmless on the existing <a> tags (browsers give anchors no default
     chrome), but required now that this class is also used on a <button>
     (the "Continue with email" option) - this codebase has no global
     button background/border reset. */
  background: transparent;
  border: none;
  width: 100%;
  text-align: left;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.auth-provider-link:hover {
  background: oklch(0.85 0.15 202 / 10%);
  color: var(--accent-secondary);
}

.auth-provider-link .icon {
  width: 1.1em;
  text-align: center;
}

/* The "or with email" rule on the account modal's sign-in tab. */
.auth-or {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.15rem 0.25rem;
  color: var(--text-faint);
  font-size: 0.75rem;
}

.auth-or::before,
.auth-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-subtle);
}

/* =========================================================
   Notification bell
   ========================================================= */
.notif-bell {
  position: relative;
  display: inline-flex;
}

.bell-trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--border-subtle);
  border-radius: 50%;
  background: oklch(0.19 0.055 285 / 50%);
  color: var(--text-muted);
  font-size: 1rem;
  transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
}

.bell-trigger:hover,
.bell-trigger[aria-expanded="true"] {
  border-color: oklch(0.85 0.15 202 / 45%);
  background: var(--bg-surface-alt);
  color: var(--text-primary);
}

.bell-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 0.3rem;
  border-radius: var(--radius-pill);
  background: var(--accent-error);
  box-shadow: 0 0 0 2px var(--bg-base);
  color: var(--text-primary);
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1;
}

.bell-dropdown {
  position: absolute;
  top: calc(100% + 0.75rem);
  right: 0;
  width: 320px;
  max-width: min(320px, calc(100vw - 2rem));
  display: flex;
  flex-direction: column;
  background: oklch(0.13 0.035 280 / 96%);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  z-index: var(--z-dropdown);
}

.bell-dropdown[hidden] {
  display: none;
}

.bell-dropdown-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 0.9rem;
  border-bottom: 1px solid var(--border-subtle);
  font-family: var(--font-heading);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-primary);
}

.bell-dropdown-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 0.35rem;
  border-radius: var(--radius-pill);
  background: var(--accent-primary);
  color: var(--text-primary);
  font-size: 0.7rem;
  font-weight: 700;
}

.bell-dropdown-list {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.4rem;
  max-height: 320px;
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.bell-dropdown-list::-webkit-scrollbar { display: none; }

.bell-dropdown-item {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.65rem 0.7rem;
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  transition: background var(--transition-fast);
}

.bell-dropdown-item:hover { background: oklch(0.85 0.15 202 / 10%); }

.bell-dropdown-item-title { font-size: 0.85rem; font-weight: 600; }
.bell-dropdown-item-meta { font-size: 0.75rem; color: var(--text-muted); }

.bell-dropdown-empty {
  padding: 1rem 0.9rem;
  font-size: 0.85rem;
  color: var(--text-muted);
}


