/* 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/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);
}



/* ===== 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/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/legal-page.css ===== */
/* =========================================================
   Standalone legal pages (/terms, /privacy, /cookies)

   The prose itself is styled by .terms-content in
   components/terms-modal.css, shared with the modal so the two can
   never drift. Only the page chrome around it is new.
   ========================================================= */
.legal-page {
  padding-block: clamp(2.5rem, 6vw, 4.5rem);
}

.legal-page-inner {
  max-width: 48rem;
  margin-inline: auto;
}

.legal-page-title {
  font-size: clamp(1.9rem, 3vw + 1rem, 2.75rem);
  margin-top: 0.75rem;
  margin-bottom: 0.5rem;
}

/* Measure. Legal prose is the longest unbroken text on the site, and the
   modal's 44rem panel was doing this job implicitly. */
.legal-page .terms-content {
  max-width: 42rem;
}

.legal-page .legal-updated {
  margin-bottom: 2.25rem;
}


/* ===== 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/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;
  }
}

