/* ============================================================================
 * The Atelier — public artist portfolio (2026-08-04, ATL-P0…P5)
 * ============================================================================
 *
 * This file replaces apps/profile/styles.css as the source of this page's
 * chrome. The Atelier used to render the LOGGED-IN Profile dashboard to
 * strangers: labelled admin fields, artwork at ~6% of the visible area,
 * and a bio wall above the work. Guide §0 says the opposite — "the artwork
 * is the color. The chrome is monochrome ink on warm paper."
 *
 * THREE RULES THIS FILE OBEYS, all load-bearing:
 *
 * 1. TOKENS ONLY — no raw hex, anywhere. Every colour is a --yeja-* token
 *    from apps/shared/design-tokens.css, which defines each one THREE
 *    times (:root, [data-theme="dark"], [data-theme="high-contrast"]).
 *    That is why this file contains ZERO [data-theme] selectors: all four
 *    theme modes (incl. system) come free from the token layer. If a rule
 *    here ever needs a [data-theme] override, that is the signal it is
 *    reading a non-themable value — fix the token, not the selector.
 *    `npm run lint:css` enforces the no-raw-hex half.
 *
 * 2. SCOPED to .atelier-* — guide §8's cascade-collision warning. This
 *    page also loads apps/profile/styles.css (for the lightbox), so an
 *    unscoped rule here would leak into that component and vice versa.
 *
 * 3. .pf-page SURVIVES. apps/profile/app.js:9771's pfThemeTargetEl() is
 *    literally querySelector('.pf-page') and writes each artist's custom
 *    theme as INLINE STYLES on that element, plus @scope (.pf-page) CSS.
 *    The page root carries BOTH .atelier and .pf-page. Removing .pf-page
 *    silently kills every per-artist custom theme with no error.
 */

@import url("/apps/shared/design-tokens.css");

/* ── §1. Base — and THE dark-mode fix ──────────────────────────────────
 *
 * `background` is the whole bug. apps/profile/styles.css:76 sets only
 * `color: var(--ink)`; inside the shell, apps/platform/styles.css:86
 * paints `background: var(--paper)`. This standalone page never loads the
 * shell stylesheet, so in dark mode ink flipped to white over an
 * unpainted (white) ground: measured contrast 1.00:1 on h1 and every h2 —
 * the artist's own name invisible. Verified fix: 1.00 → 19.44:1.
 *
 * This works because index.html loads atelier.css LAST. */
body {
  margin: 0;
  background: var(--yeja-canvas);
  color: var(--yeja-text-primary);
  font-family: var(--font-family-base);
  font-size: 15.5px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* ── Neutralising .pf-page's dashboard layout ─────────────────────────
 *
 * The page root MUST keep class="pf-page" (see the header note — it is
 * the selector apps/profile/app.js writes custom themes onto). But that
 * class also carries the LOGGED-IN dashboard's page geometry from
 * apps/profile/styles.css:1379 — `max-width: 900px`, plus .prof-page's
 * 32px/24px padding and 32px flex gap.
 *
 * Measured live before this rule: the Atelier rendered 900px wide inside
 * a 1440px viewport with the hero band letterboxed to match. So the class
 * is kept for the theme contract and its LAYOUT is overridden here. This
 * is why atelier.css must load after apps/profile/styles.css. */
.atelier.pf-page {
  max-width: none;
  width: 100%;
  margin: 0;
  padding: 0;
  display: block;
  gap: 0;

  /* THE ARTIST'S CANVAS IS PAINTED HERE, NOT ON body (2026-08-26).
   *
   * body above also sets `background: var(--yeja-canvas)`, and that line
   * can never see an artist's chosen colour: applyPortfolioThemeTokens()
   * writes the theme as INLINE STYLES on .pf-page, and a custom property
   * set on an element inherits DOWNWARD only. body is .pf-page's parent,
   * so it reads the platform default forever.
   *
   * Caught by a screenshot, not by a computed-style read — the probe
   * confirmed --yeja-canvas was #F4E9D8 on .pf-page and everything looked
   * correct, while the image showed a cream navbar sitting on a
   * near-white page. The token was right and the paint was wrong.
   *
   * min-height keeps the colour covering the viewport on a short page,
   * which is what body's rule was doing for it. body's own declaration
   * stays as the ground beneath — it is what paints during load, before
   * the theme fetch resolves. */
  min-height: 100vh;
  background-color: var(--yeja-canvas);
  /* Same inheritance story as the background: body's `color` resolves the
   * PLATFORM ink because it sits above the element the theme is written
   * on. Restating it here means text that inherits from the page root
   * gets the artist's ink. */
  color: var(--yeja-text-primary);
}

.atelier *,
.atelier *::before,
.atelier *::after { box-sizing: border-box; }

.atelier img { max-width: 100%; display: block; }

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

/* Guide §6: EVERY interactive element gets the Cobalt ring, 2px at 2px
 * offset. Deliberately a long selector list rather than `a, button` —
 * the page has cards acting as buttons, a theme segmented control, and
 * form fields in the inquiry panel, and each must be reachable.
 *
 * --atelier-focus, NOT --yeja-cobalt directly. A per-artist portfolio
 * theme remaps --yeja-cobalt on .pf-page (measured live: one artist's
 * theme sets it to #FFB300 amber), and the focus ring is an ACCESSIBILITY
 * affordance, not a brand surface — it must stay the same recognisable
 * colour on every Atelier regardless of the artist's palette, and must
 * keep its contrast against arbitrary artwork. Guide §1 names Cobalt as
 * the keyboard-focus colour specifically, separate from its themable
 * roles.
 *
 * SUPERSEDED 2026-08-27 (task 0.3): this used to capture --yeja-cobalt on
 * :root, on the reasoning that "artist themes never write to :root". That
 * held for the PORTFOLIO path — applyPortfolioThemeTokens targets .pf-page —
 * but NOT for apps/shared/platform-theme.js, which writes inline custom
 * properties on document.documentElement, and documentElement IS :root. So
 * the escape hatch closed the moment a theme was applied in the shell.
 *
 * --yeja-focus (design-tokens.css) is now the platform-wide answer: a token
 * platform-theme.js is forbidden to write, asserted by a parity test. The
 * alias stays so the selector list below reads the same, but it no longer
 * depends on an invariant that was only half true. */
:root { --atelier-focus: var(--yeja-focus); }

.atelier a:focus-visible,
.atelier button:focus-visible,
.atelier [tabindex]:focus-visible,
.atelier summary:focus-visible,
.atelier input:focus-visible,
.atelier select:focus-visible,
.atelier textarea:focus-visible {
  outline: 2px solid var(--atelier-focus);
  outline-offset: 2px;
  border-radius: var(--radius-control);
}

/* ── §2. Motion (guide §4) ─────────────────────────────────────────────
 * The reduced-motion block is MANDATORY, not optional, and must kill the
 * skeleton shimmer too — an infinite background sweep is exactly the kind
 * of decorative motion the media query exists for. */
.atelier {
  --atelier-motion: 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  --atelier-entrance: 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  .atelier *,
  .atelier *::before,
  .atelier *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .atelier .atelier-reveal { opacity: 1; transform: none; }
}

/* Entrance: sections fade up as they enter. Identity state under reduced
 * motion is handled above (opacity 1, no transform) rather than by
 * disabling the observer, so content is never left invisible. */
.atelier-reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--atelier-entrance), transform var(--atelier-entrance);
}
.atelier-reveal.is-visible { opacity: 1; transform: none; }

/* ── §3. Typography ───────────────────────────────────────────────────
 *
 * --font-family-display is the OPT-IN serif (ATL-P5). It resolves to
 * Inter unless the artist picked a display face in the Portfolio Style
 * editor, so the guide-sanctioned faces stay the default and no new
 * platform typeface is imposed.
 *
 * Note the deliberate split: a custom portfolio theme overwrites
 * --font-family-base (apps/profile/app.js:9805) but NOT
 * --font-family-display. The artist's chosen body face applies while the
 * Atelier's display voice stays constant. Do not "fix" this later. */
.atelier {
  --font-family-display: var(--font-family-base);
}
.atelier[data-display-face="serif"] {
  --font-family-display: "Playfair Display", Georgia, "Times New Roman", serif;
}

.atelier-display {
  font-family: var(--font-family-display);
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.08;
  text-wrap: balance;
}

/* Guide §2: the mono metadata voice — "use it wherever small text
 * describes DATA ABOUT content rather than content itself." Prices,
 * dimensions, years, counts. tabular-nums so columns of numbers align. */
.atelier-mono {
  font-family: var(--font-family-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12.5px;
  letter-spacing: 0.02em;
}

/* ── §4. Layout rails ─────────────────────────────────────────────────
 * Text sits on a narrow rail; artwork breaks out wider. Guide §3's 24px
 * minimum page padding on every viewport. */
/* EDGE TO EDGE (2026-08-04, user instruction: "the width of the page
 * should match the entire browser. make it elastic, edge to edge").
 *
 * The rail no longer caps at 1180px — it spans the full viewport and only
 * keeps the guide §3 minimum 24px gutter, growing to 40px on large
 * screens so content never runs into the window edge. Every section, the
 * nav, the hero overlay and the footer share this one rail, so they stay
 * aligned with each other at any width.
 *
 * Text blocks still cap their own measure (.atelier-bio at 62ch,
 * .atelier-entry-desc likewise) — elastic layout must not mean 200-
 * character lines. It is the CONTAINER that is elastic; readable measure
 * is a property of the text. */
.atelier-rail {
  width: 100%;
  max-width: none;
  margin-inline: auto;
  padding-inline: clamp(24px, 3.2vw, 40px);
}
.atelier-rail--narrow { max-width: 720px; }

.atelier-section { padding-block: clamp(48px, 7vw, 88px); }
.atelier-section + .atelier-section { padding-top: 0; }

.atelier-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 28px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--yeja-border);
}
.atelier-section-title {
  font-family: var(--font-family-display);
  font-size: clamp(1.35rem, 2.4vw, 1.9rem);
  font-weight: 400;
  letter-spacing: -0.01em;
  margin: 0;
}
.atelier-section-count { color: var(--yeja-text-muted); flex-shrink: 0; }

/* ── §5. Skip link ────────────────────────────────────────────────────
 * The page had ZERO anchors before this rebuild; with a sticky nav a skip
 * link is genuinely load-bearing for keyboard users. */
.atelier-skip {
  position: absolute;
  inset-inline-start: -9999px;
  top: 8px;
  z-index: 100;
  padding: 10px 18px;
  background: var(--yeja-text-primary);
  color: var(--yeja-canvas);
  border-radius: var(--radius-pill);
  font-weight: 500;
}
/* MUST be the logical property, matching the `inset-inline-start: -9999px`
 * above. With `left: 24px` here the two never cascaded against each other —
 * they are different properties — so the logical one kept winning and the
 * link stayed at x = -9999 even while focused. Measured: focused
 * getBoundingClientRect().x was -9999 before this fix. The skip link is the
 * page's only "bypass repeated content" mechanism (WCAG 2.4.1), so it had
 * never once worked for a keyboard user on the public atelier.
 *
 * It also has to stay logical for RTL: `fa` flips this surface, and a
 * physical `left` would drop the link on the wrong edge there. */
.atelier-skip:focus { inset-inline-start: 24px; }

/* ── §6. Sticky nav ───────────────────────────────────────────────────
 * Modelled on apps/landing/public/landing.css's .mkt-topbar. The
 * translucent fill + blur keeps artwork visible as it scrolls under. */
.atelier-nav {
  position: sticky;
  top: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--yeja-canvas) 88%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--yeja-border);
}
.atelier-nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding-block: 14px;
}
.atelier-brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-family-display);
  font-size: 17px;
  letter-spacing: -0.01em;
  flex-shrink: 0;
}
.atelier-brand:hover { opacity: 0.75; }
/* The real Yeja brand mark, ported verbatim from apps/platform/styles.css's
 * .brand-mark — the same geometry apps/landing, /collect, /galleries,
 * /artists, /pricing and the rest already use as .brand-ring: a
 * Crimson-bordered circle with three colour blobs faked by offset
 * box-shadows in a TRIANGULAR arrangement (Cobalt right, Amber lower-left,
 * Mint upper-right).
 *
 * This previously carried its own invented offsets — cobalt/amber at ±6px on
 * the horizontal and mint straight down — which put the blobs on CARDINAL
 * points and, with amber and cobalt mirrored, actually painted amber twice.
 * That is a different silhouette from the mark every other public page
 * shows, and the whole point of the mark is that it is recognisably the same
 * object everywhere.
 *
 * Motion comes from the shared .yeja-brand-animated class in
 * design-tokens.css (hover spin + scale pulse, with its own
 * prefers-reduced-motion guard), NOT from a keyframe redeclared here — see
 * that rule's history for why the animation is centralised while each app's
 * size/border/box-shadow stays local. */
/* The real Yeja brand mark — geometry ported verbatim from
 * apps/platform/styles.css's .brand-mark, the same silhouette
 * apps/landing, /collect, /galleries, /artists and /pricing already render
 * as .brand-ring: a Crimson-bordered circle with three colour blobs faked
 * by offset box-shadows in a TRIANGULAR arrangement — Cobalt right, Amber
 * lower-left, Mint upper-right.
 *
 * This file previously invented its own offsets (cobalt/amber at ±6px on
 * the horizontal, mint straight down), which put the blobs on CARDINAL
 * points — a different object from the mark every other public page shows.
 *
 * THE COLOURS COME FROM THE LOGO RAMP, NOT THE ACCENT TOKENS, and that is
 * load-bearing: applyPortfolioThemeTokens() (apps/profile/app.js) re-points
 * --yeja-cobalt / --yeja-mint / --yeja-amber to the artist's theme on
 * .pf-page, this mark's own ancestor. Measured live before the fix, cobalt
 * resolved to #FFB300 and mint to #E0A030 here, so the mark painted with
 * two distinct colours instead of four. See --yeja-logo-* in
 * design-tokens.css.
 *
 * Motion comes from the shared .yeja-brand-animated class (hover spin +
 * scale pulse, with its own prefers-reduced-motion guard) — not a keyframe
 * redeclared here. */
/* 56px, the mark's reference size — the same box the front page's
 * .brand-ring and the shell's .brand-mark use. Was 20px/0.6px, carried over
 * from the shell's old pre-hummingbird value; see the note on .brand-mark in
 * apps/platform/styles.css for why that size does not survive the bird.
 * 1.75px is the ramp's matching stroke (.yeja-logo-56, components.css). */
.atelier-brand-mark {
  width: 56px; height: 56px;
  flex: 0 0 56px;
  display: block;
}
.atelier-brand-mark > .yeja-brand-logo { width: 56px; height: 56px; --yeja-logo-stroke: 1.75px; }

.atelier-nav-links {
  display: flex;
  align-items: center;
  gap: 26px;
  list-style: none;
  margin: 0;
  padding: 0;
  min-width: 0;
}
/* Guide §2 says no all-caps UI text outside micro-badges, but the comp is
 * explicitly an editorial gallery masthead. Compromise: the DOM text stays
 * Title Case (so screen readers and the Title Case rule are satisfied) and
 * only the PRESENTATION is uppercased, with tracking to keep it legible. */
.atelier-nav-link {
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.11em;
  color: var(--yeja-text-muted);
  white-space: nowrap;
  transition: color var(--atelier-motion);
}
.atelier-nav-link:hover { color: var(--yeja-text-primary); }
/* Guide §6: current state is weight, not colour alone. */
.atelier-nav-link[aria-current="true"] {
  color: var(--yeja-text-primary);
  font-weight: 700;
}
.atelier-nav-right { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }

/* The shared theme control (apps/shared/theme.js) renders a visible
 * "Theme" label and a "System: Light" status line alongside its four
 * buttons. Both make sense in a settings panel; in a compact editorial
 * masthead they read as stray text (they showed as an unexplained
 * divider-like artifact top-right). Clipped rather than display:none so
 * the control's own aria-label and the status live region still reach a
 * screen reader — the state is announced, just not painted. */
.atelier-nav [data-yeja-theme-control] .yeja-theme-label,
.atelier-nav [data-yeja-theme-control] [data-theme-status] {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* ── Collapsing theme control (2026-08-05) ────────────────────────────
 * In an editorial masthead a permanently-expanded four-button switcher is
 * the loudest object on the page and competes with the artist's name. Here
 * it collapses to JUST the mode currently in effect and expands to the
 * full set on hover or keyboard focus.
 *
 * CSS-ONLY, and that is the constraint that shapes this rule. The buttons
 * are rendered by the SHARED apps/shared/theme.js, which every other app
 * on the platform also mounts — the shell topbar, the profile editor. This
 * must not change that markup or the other surfaces, so the collapse is
 * driven entirely off the `.is-active` class theme.js already puts on the
 * current mode, scoped to `.atelier-nav`.
 *
 * WIDTH, NOT display:none, for the inactive buttons: display:none would
 * remove them from the tab order, so a keyboard user could never reach the
 * other three modes. They stay focusable at zero width and expand when any
 * of them takes focus (:focus-within below), which is why the transition
 * is on width/opacity rather than a visibility toggle.
 *
 * The whole control keeps its 42px min-height from the shared rule, so
 * collapsing changes only its WIDTH — nothing above or below it shifts. */
.atelier-nav [data-yeja-theme-control] {
  display: flex;
  align-items: center;
  gap: 2px;
  /* Contain the collapsed children so the box shrinks with them. */
  overflow: hidden;
  transition: gap 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Collapsed: every button that is NOT the active one folds to zero width.
 * :not(:hover):not(:focus-within) rather than a separate expanded class —
 * there is no JS here to add one. */
.atelier-nav [data-yeja-theme-control]:not(:hover):not(:focus-within) {
  gap: 0;
}
.atelier-nav [data-yeja-theme-control]:not(:hover):not(:focus-within)
  button:not(.is-active) {
  width: 0;
  min-width: 0;
  padding: 0;
  border-width: 0;
  opacity: 0;
  pointer-events: none;
}

/* The transition lives on the button itself so it plays in BOTH directions
 * — collapsing as well as expanding. Width and opacity only: both are
 * compositor-friendly, and animating them cannot reflow the masthead the
 * way animating margin or padding would. */
.atelier-nav [data-yeja-theme-control] button {
  width: 34px;
  min-width: 34px;
  overflow: hidden;
  transition:
    width 0.22s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.16s ease-in-out,
    background 0.15s ease-in-out,
    color 0.15s ease-in-out;
}

/* The collapsed state shows the CURRENT mode, so it must read as a mode
 * indicator rather than as a selected control — the shared rule paints
 * .is-active solid cobalt, which at rest looks like a live toggle. Quiet
 * it down while collapsed; the moment the control opens, the shared
 * active styling takes over again and shows which mode is set. */
.atelier-nav [data-yeja-theme-control]:not(:hover):not(:focus-within)
  button.is-active {
  background: transparent;
  border-color: transparent;
  color: var(--yeja-text-muted);
}

/* prefers-reduced-motion: the platform-wide net in design-tokens.css
 * already neutralises the duration. Nothing more is needed — the control
 * still opens and closes, it just does so instantly, which is the correct
 * behaviour for this setting (the states themselves stay meaningful). */

@media (max-width: 760px) {
  /* The anchors collapse before the brand and theme control do — on a
   * phone the page is a single scroll, so in-page anchors earn their
   * space least. */
  .atelier-nav-links { display: none; }
}

/* ── §7. Hero band + identity overlay ─────────────────────────────────
 *
 * LOCKED TO 2 INCHES (user instruction, 2026-08-04). CSS `in` is a real
 * absolute unit: 1in == 96px by definition, so 2in == 192px exactly,
 * independent of device DPI or the browser's zoom baseline. Using `in`
 * rather than 192px states the intent in the unit the instruction used.
 *
 * The band is a fixed strip, so it is FULL-BLEED background rather than
 * an <img>: a locked height means no artwork ratio can fit it, and
 * background-size:cover fills every viewport width with no wrapper.
 * The uncropped work stays one click away in the lightbox — guide §3's
 * "never crop" governs the GRID, where forcing a shared ratio destroys
 * the reading of each work. */
.atelier-hero {
  position: relative;
  /* ── BAND HEIGHT IS ARTIST-TUNABLE (2026-08-26) ──
   * Was a fixed 2in for everyone. --atelier-hero-height is written by
   * the renderer from portfolio_hero_height and CLAMPED there to
   * 120-420px, so a value from a crafted draft cannot produce a band
   * taller than the viewport (which would push every work below the fold
   * and read as a broken page). The 2in fallback is the previous
   * behaviour exactly, so an artist who sets nothing sees no change. */
  height: var(--atelier-hero-height, 2in);
  min-height: var(--atelier-hero-height, 2in);
  display: flex;
  align-items: flex-end;    /* identity sits on the floor of the band */
  overflow: hidden;
  background-color: var(--yeja-sidebar);
  background-image: var(--atelier-hero-src);
  background-size: cover;
  /* 38%, not centre. A 2in strip through the MIDDLE of a tall portrait is
   * usually its least informative band — the sitter's torso, a blank sky.
   * Biasing upward catches the part a viewer actually reads (a face, a
   * horizon, the top of a composition). Measured against the live
   * portrait works: centre showed empty ground, this shows the subject. */
  background-position: center 38%;
  border-bottom: 1px solid var(--yeja-border);
}
.atelier-hero--openable { cursor: zoom-in; }

/* No banner image: the identity still needs a home, on plain canvas. */
.atelier-hero--plain {
  background-image: none;
  background-color: var(--yeja-canvas);
}

/* Guide §1 "Scrims over imagery": color-mix on the ramp token, never a
 * raw rgba(), never element opacity (which would dim the caption text
 * too). Bottom-weighted so the artwork stays legible up top while the
 * overlaid name keeps its contrast — this band carries WHITE text over an
 * arbitrary image, so the scrim is the only thing guaranteeing it. */
/* ── Hero video layer (2026-08-05) ────────────────────────────────────
 * A video banner cannot be a background-image — CSS has no such thing — so
 * it is a real element sitting UNDER the scrim and ABOVE the background.
 * The background-image stays exactly as it was and now serves as the poster
 * / fallback layer, which is why a hero whose video never plays (blocked
 * autoplay, reduced motion, an unsupported codec) looks identical to today.
 *
 * object-fit:cover mirrors the background-size:cover it sits on top of, and
 * object-position matches the 38% bias the rule above documents — a 2in strip
 * through the middle of a tall frame is usually its least informative band.
 *
 * pointer-events:none so the hero's own click/keyboard affordance
 * (.atelier-hero--openable opens the lightbox) is never intercepted by the
 * video element, and so no native control surface can appear over the band. */
.atelier-hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 38%;
  pointer-events: none;
  /* NEGATIVE, deliberately. The scrim and .atelier-hero-inner BOTH sit at
   * z-index:1 and rely on source order (the scrim is painted first, the
   * content after). Giving the scrim an explicit index to sit "above" the
   * video would have tied it with the content — and as the later sibling the
   * scrim would then paint OVER the artist's name. z-index:-1 puts the video
   * behind both without touching either, which is why the scrim's own rule
   * stays exactly as it was. The band has overflow:hidden and a
   * background-color, so nothing escapes behind the header. */
  z-index: -1;
}

/* WCAG 2.2.2 pause control for the banner reel. Bottom-RIGHT, because the
 * artist's identity block sits bottom-left and the Verified / Accepting
 * Commissions stamps sit top-right. Small, quiet, and over the scrim so it
 * stays legible against arbitrary footage. */
.atelier-hero-pause {
  position: absolute;
  inset-inline-end: clamp(24px, 3.2vw, 40px);
  bottom: 14px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
  color: var(--yeja-gray-0);
  background: color-mix(in oklch, var(--yeja-gray-1000) 55%, transparent);
  transition: background var(--motion-default, 0.15s ease-in-out);
}
.atelier-hero-pause:hover {
  background: color-mix(in oklch, var(--yeja-gray-1000) 72%, transparent);
}
.atelier-hero-pause .yeja-icon { font-size: 18px; }

/* No video, no control. The button is only appended when a hero reel
 * exists, but reduced motion hides the video — so hide its control too
 * rather than leave a play button over a still image. */
@media (prefers-reduced-motion: reduce) {
  .atelier-hero-pause { display: none; }
}

/* prefers-reduced-motion: the platform-wide net in design-tokens.css zeroes
 * animation/transition duration, but a <video> is neither — playback is
 * paused in JS (wireMediaFrame). This rule is the VISUAL half of the same
 * decision: the poster underneath must stay visible rather than a paused
 * first frame being mistaken for a broken image, so the element is hidden
 * outright and the background-image shows through. */
@media (prefers-reduced-motion: reduce) {
  .atelier-hero-video { display: none; }
}

/* ── SCRIM STRENGTH IS ARTIST-TUNABLE, WITH A FLOOR (2026-08-26) ──
 *
 * The scrim is the ONLY thing guaranteeing the artist's name is legible
 * over an arbitrary image, so this is not a free knob. --atelier-scrim
 * scales the gradient, and the renderer clamps it to 60-140% of the
 * default: an artist can soften it to show more of their image or
 * deepen it over a busy one, but cannot remove it. At the 60% floor the
 * strongest stop is still 49%, which keeps white text legible over the
 * light imagery that motivated these numbers in the first place.
 *
 * calc() inside color-mix percentages, so one variable moves all three
 * stops together and their relationship is preserved. */
.atelier-hero-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    color-mix(in oklch, var(--yeja-gray-1000) calc(82% * var(--atelier-scrim, 1)), transparent) 0%,
    color-mix(in oklch, var(--yeja-gray-1000) calc(46% * var(--atelier-scrim, 1)), transparent) 45%,
    color-mix(in oklch, var(--yeja-gray-1000) calc(14% * var(--atelier-scrim, 1)), transparent) 100%
  );
}
/* A dark preset needs far less scrim — it is already ink-ground. */
.atelier-hero--darkground .atelier-hero-scrim {
  background: linear-gradient(
    to top,
    color-mix(in oklch, var(--yeja-gray-1000) calc(55% * var(--atelier-scrim, 1)), transparent),
    transparent
  );
}

/* A LIGHT preset is a designed pattern, not a photograph: the full
 * photo-strength scrim washed it to flat grey (seen live — "Drift" was
 * indistinguishable from an empty band). It gets a scrim only where the
 * text actually sits, and the text switches to page ink so it stays
 * legible against a pale ground. */
/* A soft horizontal wash behind the identity ONLY — the right two-thirds
 * of the band stay clear so the pattern is actually seen. A full-width
 * vertical gradient (the first attempt) flattened "Drift" to blank grey
 * even at 30%: these patterns are deliberately low-contrast, so any wash
 * over all of them erases them. */
.atelier-hero--patternlight .atelier-hero-scrim {
  background: linear-gradient(
    to right,
    color-mix(in oklch, var(--yeja-gray-0) 78%, transparent) 0%,
    color-mix(in oklch, var(--yeja-gray-0) 55%, transparent) 34%,
    transparent 62%
  );
}
.atelier-hero--patternlight .atelier-hero-name,
.atelier-hero--patternlight .atelier-hero-caption { color: var(--yeja-gray-1000); }
.atelier-hero--patternlight .atelier-hero-headline,
.atelier-hero--patternlight .atelier-hero-workmeta { color: var(--yeja-gray-800); }
.atelier-hero--patternlight .atelier-hero-avatar {
  box-shadow: 0 0 0 2px color-mix(in oklch, var(--yeja-gray-1000) 22%, transparent);
}
.atelier-hero--patternlight .atelier-hero-avatar--initial {
  background: color-mix(in oklch, var(--yeja-gray-0) 88%, transparent);
  color: var(--yeja-gray-1000);
}
.atelier-hero--patternlight .atelier-hero-identity .prof-badge {
  background: color-mix(in oklch, var(--yeja-gray-1000) 88%, transparent);
  color: var(--yeja-gray-0);
}
/* On plain canvas there is no image to scrim, and white text would be
 * invisible — the identity reverts to the page's own ink. */
.atelier-hero--plain .atelier-hero-scrim { display: none; }

.atelier-hero-inner {
  position: relative;       /* above the scrim */
  z-index: 1;
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  padding-bottom: 18px;
}

/* The identity block — bottom-left, per the instruction. */
.atelier-hero-identity {
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;             /* lets the name ellipsize instead of overflow */
}
/* 64 -> 83 (+30%) -> 100px (+20% again, 2026-08-04). */
.atelier-hero-avatar {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--yeja-sidebar);
  /* A ring, not a border: it separates the avatar from whatever image is
   * behind it without changing its size. */
  box-shadow: 0 0 0 2px color-mix(in oklch, var(--yeja-gray-0) 85%, transparent);
  display: block;
}

/* Clickable: opens the avatar full-size in the shared overlay. A real
 * <button> wrapper rather than a click handler on the <img>, so it is
 * keyboard-reachable and announced — the recorded ".click() is not a
 * keypress" class. Transparent chrome so only the avatar itself reads as
 * the control. */
.atelier-hero-avatar-btn {
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
  border-radius: 50%;
  flex-shrink: 0;
  line-height: 0;
  transition: transform var(--atelier-motion);
}
.atelier-hero-avatar-btn:hover { transform: scale(1.04); }
.atelier-hero-avatar--initial {
  display: grid;
  place-items: center;
  /* Scaled with the 100px avatar — 26px in a 100px circle read as a
   * pinhead. */
  font-size: 38px;
  font-weight: 500;
  color: var(--yeja-gray-0);
  background: color-mix(in oklch, var(--yeja-gray-1000) 55%, transparent);
}
.atelier-hero-idtext { min-width: 0; }

/* White on the scrim — theme-invariant, exactly like the scrim itself
 * (guide §1: overlays on imagery are the documented exception to
 * token-driven theming). */
.atelier-hero-name {
  margin: 0;
  font-size: clamp(1.6rem, 3.6vw, 2.6rem);
  line-height: 1.05;
  color: var(--yeja-gray-0);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.atelier-hero-headline {
  margin: 3px 0 0;
  font-size: clamp(0.82rem, 1.1vw, 0.98rem);
  color: var(--yeja-gray-200);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* On the plain (image-less) variant the ink must come from the theme,
 * or white text lands on white canvas — the exact 1.00:1 failure this
 * whole rebuild started from. */
.atelier-hero--plain .atelier-hero-name { color: var(--yeja-text-primary); }
.atelier-hero--plain .atelier-hero-headline { color: var(--yeja-text-muted); }
.atelier-hero--plain .atelier-hero-avatar {
  box-shadow: 0 0 0 1px var(--yeja-border);
}
.atelier-hero--plain .atelier-hero-avatar--initial {
  background: var(--yeja-sidebar);
  color: var(--yeja-text-primary);
}

/* The work's own credit, opposite the identity. */
.atelier-hero-caption {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  text-align: end;
  flex-shrink: 0;
  color: var(--yeja-gray-0);
}
.atelier-hero-workmeta { color: var(--yeja-gray-200); }
.atelier-hero--plain .atelier-hero-caption { color: var(--yeja-text-primary); }

/* The verification badge sits on imagery here, so it cannot rely on the
 * pale success tint it uses elsewhere. */
.atelier-hero-identity .prof-badge {
  flex-shrink: 0;
  background: color-mix(in oklch, var(--yeja-gray-0) 92%, transparent);
  color: var(--yeja-gray-1000);
  border: none;
}
.atelier-hero--plain .atelier-hero-identity .prof-badge {
  background: var(--yeja-sidebar);
  color: var(--yeja-text-primary);
  border: 1px solid var(--yeja-border);
}

@media (max-width: 700px) {
  /* The work credit is the first thing to go — the artist's identity is
   * the point of the band, and at 2in there is no room for both. */
  .atelier-hero-caption { display: none; }
  .atelier-hero-avatar { width: 64px; height: 64px; }
  .atelier-hero-avatar--initial { font-size: 24px; }
}

/* ── §9. Masonry gallery (guide §5) ───────────────────────────────────
 *
 * The engine is apps/art_board/masonry.js — JS position-packing into the
 * shortest column, PRESERVING left-to-right reading order. Guide §5:
 * "never CSS columns (it destroys reading order)". Measured on a
 * column-count prototype: DOM order 1,2,3,4,5,6 read as 1,2,5,3,6,4. For
 * a portfolio the artist's curation order IS the product.
 *
 * ⚠ The gap here MUST equal the `gap:` passed to createMasonry() in
 * atelier.js — the engine computes column widths from its own value, so a
 * mismatch silently misaligns every column. Both are 16px (guide §3). */
.atelier-grid {
  display: flex;
  align-items: flex-start;
  gap: 16px;
}
/* A single work still gets a considered width rather than being blown up
 * across an edge-to-edge rail — a gallery hangs a lone piece at a size,
 * not to fill the wall. Widened from 620px to 760px now that the page is
 * full-bleed.
 *
 * Constrained on the SECTION, not the grid: createMasonry() reads
 * container.clientWidth to compute column widths, and a max-width applied
 * to the container itself lost that race (measured: the card rendered at
 * 1115px anyway). Narrowing the parent means the engine measures the
 * already-correct width. */
.atelier-works--sparse { max-width: 760px; margin-inline: auto; }
.atelier-grid .masonry__col {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}
.atelier-grid .masonry__sentinel { width: 1px; height: 1px; }

.atelier-card {
  display: block;
  width: 100%;
  border: none;
  background: none;
  padding: 0;
  text-align: start;
  font-family: inherit;
  color: inherit;
  cursor: pointer;
}

/* NO aspect-ratio and NO fixed height — this is the fix for the old
 * .pf-pin-thumb { aspect-ratio: 1/1 }, which letterboxed a 1057×1600
 * portrait into a 164px square with 25% dead padding. Guide §3: natural
 * aspect ratio, never crop to force uniformity. */
.atelier-frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-media);
  background: var(--yeja-sidebar);
  border: 1px solid var(--yeja-border);
}
/* 'img, video' (2026-08-05): a video artwork must be sized and faded in by
 * the same rules. Without this the <video> rendered at its INTRINSIC width
 * (1921px for a 1080p reel) and never got the .is-loaded fade, so the card
 * measured 1100px tall inside a 361px column. Measured live, not guessed. */
.atelier-frame img,
.atelier-frame video {
  width: 100%;
  height: auto;
  display: block;
  /* Guide §4: 150ms fade-in once loaded; the skeleton underneath is
   * removed by JS on the load event. */
  opacity: 0;
  transition: opacity 0.15s ease-in-out, transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.atelier-frame img.is-loaded,
.atelier-frame video.is-loaded { opacity: 1; }
.atelier-card:hover .atelier-frame img.is-loaded,
.atelier-card:hover .atelier-frame video.is-loaded { transform: scale(1.02); }

/* The card is a <button>. A video inside it must never take the pointer —
 * otherwise the click lands on the media element instead of opening the
 * lightbox, and a native control surface can appear over the artwork. */
.atelier-frame video { pointer-events: none; }

/* Skeleton shimmer (guide §4), reserving 4:5 per guide §3 so the column
 * height is right before the image's real ratio is known. */
.atelier-skeleton {
  aspect-ratio: 4 / 5;
  background: linear-gradient(
    90deg,
    var(--yeja-sidebar) 25%,
    var(--yeja-hover-primary) 37%,
    var(--yeja-sidebar) 63%
  );
  background-size: 400% 100%;
  animation: atelier-shimmer 1.4s ease-in-out infinite;
}
@keyframes atelier-shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* Meta sits OUTSIDE the frame, unclipped (guide §5 card anatomy). */
.atelier-card-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 2px 0;
}
/* The second meta line sits tight under the first — it is the same
 * record, not a new block. Both children must be allowed to shrink and
 * ellipsize: a long "medium · year" plus a full cm/inch dimension string
 * is easily wider than a 361px column, and without this the text ran out
 * past the frame's right edge (seen live at 1440px). */
.atelier-card-meta--sub {
  padding-top: 3px;
  gap: 10px;
}
.atelier-card-meta--sub .atelier-card-meta-line {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* flex-shrink MUST be reset here. The base .atelier-card-meta-line rule
   * below sets flex-shrink: 0, which wins for these children too — so the
   * ellipsis above could never fire and the line was laid out at its full
   * content width instead. Measured live at 390px: a 155px card rendered a
   * 318px "Acrylic and Gold Pigment on Canvas · 2025" straight across its
   * neighbour. The 2026-08-05 note above about fixing this at 1440px only
   * ever fixed the WIDER case, where the column happened to be roomy enough
   * to hide it. */
  flex-shrink: 1;
}
/* Dimensions are the first thing to go when space is tight — the medium
 * and year identify the work; the exact size is in the lightbox. */
.atelier-card-meta--sub .atelier-card-meta-line:last-child { flex-shrink: 2; }
.atelier-card-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--yeja-text-primary);
  /* Guide §2 truncation: card titles must never wrap to a second line.
   * min-width:0 is required for ellipsis inside a flex child. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.atelier-card-meta-line {
  color: var(--yeja-text-muted);
  white-space: nowrap;
  flex-shrink: 0;
}
.atelier-card-price {
  color: var(--yeja-text-primary);
  font-weight: 500;
  white-space: nowrap;
  flex-shrink: 0;
}
.atelier-card-sold {
  color: var(--yeja-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  flex-shrink: 0;
}

/* ── §10. Exhibitions ─────────────────────────────────────────────────*/
.atelier-timeline { display: flex; flex-direction: column; }
.atelier-entry {
  display: grid;
  grid-template-columns: 128px 1fr;
  gap: 24px;
  padding-block: 20px;
  border-bottom: 1px solid var(--yeja-border);
}
.atelier-entry:last-child { border-bottom: none; }
.atelier-entry-when { color: var(--yeja-text-muted); padding-top: 3px; }
.atelier-entry-title { font-size: 15.5px; font-weight: 500; margin: 0 0 3px; }
.atelier-entry-org { color: var(--yeja-text-muted); margin: 0 0 6px; }
.atelier-entry-desc {
  color: var(--yeja-text-muted);
  margin: 0;
  max-width: 62ch;
  white-space: pre-line;
}
.atelier-entry-kind {
  display: inline-block;
  margin-inline-start: 8px;
  color: var(--yeja-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 10.5px;
}
@media (max-width: 640px) {
  .atelier-entry { grid-template-columns: 1fr; gap: 6px; }
}

/* ── §11. About / Medium & Process ────────────────────────────────────*/
/* Text LEFT, process images RIGHT. The order flipped 2026-08-04 when the
 * avatar moved to the hero: the statement is the substance of this
 * section, so it leads, and the photographs support it. Columns are
 * proportional rather than a fixed 200px sidebar, since the page is now
 * edge-to-edge and a fixed rail would look stranded at 2560px. */
.atelier-about {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: start;
}
/* No process images → no empty second column, so the statement takes the
 * full rail instead of sitting beside a gap that reads as a failed load. */
.atelier-about--noportrait { grid-template-columns: 1fr; }
@media (max-width: 760px) {
  .atelier-about { grid-template-columns: 1fr; }
}

/* Up to 3 artist-chosen process/studio photographs.
 *
 * ART BOARD CARD STANDARDS (2026-08-04 user instruction: "need to follow
 * exact art board preview standards"). Each shot is now a real
 * .masonry__frame-equivalent, matching apps/art_board/styles.css:
 *   - 8px radius (--radius-media), overflow hidden, border-subtle ground
 *   - NATURAL aspect ratio, never forced to a shared one (guide §3)
 *   - skeleton shimmer under it, image fades in over 150ms (guide §4)
 *   - hover lifts the image 1.02, the same figure Art Board's cards use
 * They are clickable and open the same detail view as any other image.
 */
/* One ROW per image (2026-08-05). Each carries its own side, and its note
 * sits OPPOSITE the picture — image right ⇒ text left, and vice versa. */
.atelier-process-rows {
  display: flex;
  flex-direction: column;
  gap: clamp(28px, 4vw, 56px);
}
.atelier-process-row {
  margin: 0;                 /* UA gives <figure> a 40px inline margin */
  display: flex;
  align-items: center;
  gap: clamp(20px, 3vw, 44px);
  --atelier-process-scale: 100%;
}
/* The IMAGE takes the artist's chosen share of the row; the note takes
 * what is left. flex-basis on the button rather than width, so the flex
 * line cannot stretch a deliberately-shrunk image back to full size.
 *
 * `flex-basis`, NOT max-width: a max- cap cannot make a box LARGER than
 * its content, so the upper half of the 20–120% range would silently do
 * nothing — the exact no-op both earlier scale controls hit. */
.atelier-process-row .atelier-process-shot-btn {
  flex: 0 0 var(--atelier-process-scale);
  min-width: 0;              /* a flex item must be allowed to shrink */
}
.atelier-process-row .atelier-process-caption {
  flex: 1 1 auto;
  min-width: 0;
}
/* row-reverse rather than a second set of order/margin rules: the DOM
 * keeps image-then-note in every case, which is the right reading order
 * for a screen reader and for a no-CSS render, while the paint order
 * flips. */
.atelier-process-row[data-side="right"] { flex-direction: row-reverse; }
.atelier-process-row[data-side="left"] { flex-direction: row; }
@media (max-width: 760px) {
  /* Stacked, "opposite" has no meaning — one column, image then note,
   * matching DOM order for both sides. */
  .atelier-process-row,
  .atelier-process-row[data-side="right"],
  .atelier-process-row[data-side="left"] {
    flex-direction: column;
    align-items: stretch;
  }
  .atelier-process-row .atelier-process-shot-btn { flex-basis: auto; }
}
.atelier-process-shot-btn {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
  font: inherit;
}
.atelier-process-frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-media);
  background: var(--yeja-sidebar);
  border: 1px solid var(--yeja-border);
}
.atelier-process-shot {
  width: 100%;
  height: auto;              /* natural ratio — no aspect-ratio lock */
  display: block;
  opacity: 0;
  transition: opacity 0.15s ease-in-out, transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.atelier-process-shot.is-loaded { opacity: 1; }
.atelier-process-shot-btn:hover .atelier-process-shot.is-loaded { transform: scale(1.02); }
.atelier-process-skeleton {
  aspect-ratio: 4 / 5;
  background: linear-gradient(
    90deg,
    var(--yeja-sidebar) 25%,
    var(--yeja-hover-primary) 37%,
    var(--yeja-sidebar) 63%
  );
  background-size: 400% 100%;
  animation: atelier-shimmer 1.4s ease-in-out infinite;
}
/* The [data-count="3"] two-up grid and the .atelier-process-item column
 * that lived here were dropped 2026-08-05: each image is its own full
 * row now (.atelier-process-row above), so there is no shared grid left
 * for a lead image to span. */

.atelier-process-caption {
  font-size: 15px;
  line-height: 1.65;
  color: var(--yeja-text-muted);
  /* The note is the artist's own prose about the piece — left-aligned,
   * not centered, so it reads as writing rather than a label. */
  text-align: start;
  overflow-wrap: break-word;
  /* Beside a picture rather than under it, an unbounded column runs to a
   * ~110-character line at 1440px. Keep it in the readable 45–75 band. */
  max-width: 52ch;
}
/* Artist-authored rich text — the tag list is fixed by the allowlist in
 * apps/shared/rich-text.js, so this styles exactly what can appear. */
.atelier-process-caption > :first-child { margin-top: 0; }
.atelier-process-caption > :last-child { margin-bottom: 0; }
.atelier-process-caption p { margin: 0 0 0.8em; }
.atelier-process-caption ul,
.atelier-process-caption ol { margin: 0 0 0.8em; padding-inline-start: 1.3em; }
.atelier-process-caption li { margin-bottom: 0.3em; }
.atelier-process-caption h3,
.atelier-process-caption h4 {
  font-size: 15px;
  font-weight: 600;
  color: var(--yeja-text-primary);
  margin: 1.2em 0 0.4em;
}
.atelier-process-caption blockquote {
  margin: 0.8em 0;
  padding-inline-start: 14px;
  border-inline-start: 2px solid var(--yeja-border);
  font-style: italic;
}
.atelier-process-caption strong { color: var(--yeja-text-primary); font-weight: 600; }
.atelier-process-caption a { color: var(--yeja-cobalt); text-underline-offset: 2px; }

.atelier-bio {
  font-size: 17px;
  line-height: 1.7;
  color: var(--yeja-text-muted);
  /* Readable measure. The old page ran ~95 characters per line at 590px;
   * 45–75 is the target, and this lands at ~62. */
  max-width: 62ch;
  /* NO white-space: pre-line here any more. The statement became rich
   * text on 2026-08-04, so paragraphs are real <p> elements — preserving
   * the newlines BETWEEN those tags would add a blank line on top of each
   * paragraph's own margin and double the spacing. */
  margin: 0 0 32px;
}

/* Artist-authored rich text. The tag list is fixed by
 * apps/shared/rich-text.js's allowlist, so this styles exactly what can
 * appear and nothing else. */
.atelier-bio > :first-child { margin-top: 0; }
.atelier-bio > :last-child { margin-bottom: 0; }
.atelier-bio p { margin: 0 0 1em; }
.atelier-bio h3,
.atelier-bio h4 {
  /* Sub-headings inside a statement are the artist's structure, not the
   * page's — kept close to body size so they cannot compete with the
   * section title above them. */
  font-size: 17px;
  font-weight: 600;
  color: var(--yeja-text-primary);
  margin: 1.5em 0 0.5em;
}
.atelier-bio ul,
.atelier-bio ol { margin: 0 0 1em; padding-inline-start: 1.4em; }
.atelier-bio li { margin-bottom: 0.4em; }
.atelier-bio blockquote {
  margin: 1em 0;
  padding-inline-start: 16px;
  border-inline-start: 2px solid var(--yeja-border);
  color: var(--yeja-text-muted);
  font-style: italic;
}
.atelier-bio strong { color: var(--yeja-text-primary); font-weight: 600; }
.atelier-bio a {
  color: var(--yeja-cobalt);
  text-underline-offset: 2px;
}
.atelier-tag-group { margin-bottom: 20px; }
.atelier-tag-label {
  display: block;
  margin-bottom: 9px;
  color: var(--yeja-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-size: 10.5px;
}
.atelier-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.atelier-chip {
  padding: 5px 13px;
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-pill);
  font-size: 13px;
  color: var(--yeja-text-primary);
  background: var(--yeja-sidebar);
}

/* ── §12. Conversion zone ─────────────────────────────────────────────*/
.atelier-inquire { text-align: center; }
.atelier-inquire-title {
  font-family: var(--font-family-display);
  font-size: clamp(1.6rem, 3.4vw, 2.4rem);
  font-weight: 400;
  margin: 0 0 12px;
}
.atelier-inquire-copy {
  color: var(--yeja-text-muted);
  max-width: 52ch;
  margin: 0 auto 28px;
}
.atelier-cta-row {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 12px;
}
/* Guide §5 Primary — Ink Pill: solid ink fill, surface-coloured text, no
 * border. ONE of these on the page; everything else is a ghost. */
.atelier-btn-primary {
  background: var(--yeja-text-primary);
  color: var(--yeja-canvas);
  border: none;
  border-radius: var(--radius-pill);
  padding: 13px 30px;
  font-family: inherit;
  font-size: 15.5px;
  font-weight: 500;
  cursor: pointer;
  transition: opacity var(--atelier-motion);
}
.atelier-btn-primary:hover { opacity: 0.85; }
.atelier-btn-ghost {
  background: none;
  color: var(--yeja-text-primary);
  border: 1px solid var(--yeja-text-muted);
  border-radius: var(--radius-pill);
  padding: 12px 24px;
  font-family: inherit;
  font-size: 14px;
  cursor: pointer;
  transition: background var(--atelier-motion), border-color var(--atelier-motion);
}
.atelier-btn-ghost:hover { background: var(--yeja-sidebar); }

.atelier-status-row {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 22px;
}

/* ── The Get In Touch gate, restyled ──────────────────────────────────
 * getInTouchGateHTML() is reused VERBATIM from apps/profile/app.js so the
 * reCAPTCHA-gated inquiry pipeline and every portfolio_sections opt-out
 * keep working. Only its presentation is adjusted here.
 *
 * white-space:nowrap is the fix for a real defect visible on the OLD
 * page at 390px: "Just send a quick inquiry" wrapped to four lines
 * inside its own pill, and the row read as four ragged blocks. */
.atelier-inquire .pf-touch-gate > p {
  color: var(--yeja-text-muted);
  max-width: 52ch;
  margin: 0 auto 26px;
}
.atelier-inquire .prof-form-actions {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
}
.atelier-inquire .prof-form-actions .yeja-pill-btn {
  white-space: nowrap;
  border-radius: var(--radius-pill);
}
/* One primary on the page (guide §5's Ink Pill: solid ink fill,
 * surface-coloured text, no border); the rest are ghosts, so the main
 * action is unambiguous.
 *
 * !important is load-bearing here, not laziness: the shared
 * .yeja-pill-btn in apps/shared/components.css sets its own background
 * and border, and this rule has to beat it from a different stylesheet
 * for markup (getInTouchGateHTML) that is deliberately reused verbatim.
 * Without it the primary lost its fill in dark mode — seen live in
 * production — leaving four visually identical ghost pills. */
.atelier-inquire .prof-form-actions #pf-gate-create-rfp {
  background: var(--yeja-text-primary) !important;
  color: var(--yeja-canvas) !important;
  border: none !important;
}
.atelier-inquire .prof-form-actions #pf-gate-create-rfp:hover { opacity: 0.85; }
/* The remaining three read as secondary: hairline outline, no fill. */
.atelier-inquire .prof-form-actions .yeja-pill-btn-ghost {
  background: transparent !important;
  color: var(--yeja-text-primary) !important;
  border: 1px solid var(--yeja-text-muted) !important;
}
@media (max-width: 560px) {
  /* Full-width stacked pills beat four cramped ones sharing a row. */
  .atelier-inquire .prof-form-actions { flex-direction: column; align-items: stretch; }
  .atelier-inquire .prof-form-actions .yeja-pill-btn { width: 100%; }
}

/* ── §13. Footer ──────────────────────────────────────────────────────*/
.atelier-footer {
  border-top: 1px solid var(--yeja-border);
  margin-top: clamp(40px, 6vw, 80px);
  padding-block: 34px 44px;
}
.atelier-footer-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
  color: var(--yeja-text-muted);
}
.atelier-footer-links { display: flex; flex-wrap: wrap; gap: 20px; }
.atelier-footer-links a { transition: color var(--atelier-motion); }
.atelier-footer-links a:hover { color: var(--yeja-text-primary); text-decoration: underline; }

/* ── §14. Empty states (guide §5: centered, quiet, no illustration) ───*/
.atelier-empty {
  text-align: center;
  padding-block: 48px;
  color: var(--yeja-text-muted);
  font-size: 15.5px;
}

/* ── §15. Boot skeleton ───────────────────────────────────────────────
 * Shapes mirror the real layout (nav, hero band, centred identity, 3-col
 * grid) so nothing jumps when the real content replaces it. .pf-skeleton
 * comes from apps/profile/styles.css; these classes only set geometry. */
.atelier-boot { padding-bottom: 64px; }
.atelier-boot-nav {
  height: 57px;
  border-bottom: 1px solid var(--yeja-border);
  margin-bottom: 0;
}
.atelier-boot-hero {
  height: clamp(320px, 58vh, 620px);
  width: 100%;
  border-radius: 0;
}
.atelier-boot-identity {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding-block: clamp(40px, 6vw, 72px) clamp(28px, 4vw, 48px);
}
.atelier-boot-line { height: 18px; border-radius: var(--radius-control); }
.atelier-boot-line--name { width: min(320px, 60vw); height: 46px; }
.atelier-boot-line--sub { width: min(420px, 76vw); }
.atelier-boot-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  max-width: 1180px;
  margin-inline: auto;
  padding-inline: 24px;
}
.atelier-boot-tile { aspect-ratio: 4 / 5; border-radius: var(--radius-media); }
.atelier-boot-tile--tall { aspect-ratio: 3 / 4; }
@media (max-width: 760px) {
  .atelier-boot-grid { grid-template-columns: 1fr; }
  .atelier-boot-tile--tall { display: none; }
}

/* ── §16. Lightbox bridge ─────────────────────────────────────────────
 * The lightbox itself is still rendered by apps/profile/app.js and styled
 * by apps/profile/styles.css (see index.html's comment). Only the pieces
 * that read this page's own type scale are restated here, so the overlay
 * doesn't inherit a mismatched face for its metadata. */
.pf-lightbox-backdrop .pf-story-meta,
.pf-lightbox-backdrop .pf-spec-value {
  font-family: var(--font-family-mono);
  font-variant-numeric: tabular-nums;
}

/* ── §17. Page wallpaper (2026-08-04) ─────────────────────────────────
 * A tiled, very-low-contrast pattern behind the whole page. Painted on a
 * ::before rather than on .atelier itself so it sits UNDER every section
 * without becoming an ancestor background that child surfaces have to
 * fight — and so removing the class removes the layer entirely.
 *
 * fixed attachment: the wallpaper stays put while content scrolls over
 * it, which is what makes it read as a wall rather than a long banner. */
.atelier.has-bg-pattern::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background-image: var(--atelier-bg-pattern);
  background-repeat: repeat;
  pointer-events: none;
}

/* ── §18. Avatar full-size overlay ────────────────────────────────────*/
.atelier-avatar-overlay {
  display: grid;
  place-items: center;
  padding: clamp(24px, 6vw, 72px);
  /* Guide §5: dim via background alpha, never element opacity (which
   * would dim the portrait itself). */
  background: color-mix(in oklch, var(--yeja-gray-1000) 78%, transparent);
}
/* Round for the portrait, rectangular for process photographs — a studio
 * shot cropped to a circle would lose most of its composition. */
.atelier-avatar-full--rect {
  border-radius: var(--radius-media) !important;
  aspect-ratio: auto !important;
  object-fit: contain !important;
  max-width: min(1100px, 92vw) !important;
}
.atelier-avatar-full {
  max-width: min(520px, 90vw);
  max-height: 80vh;
  width: auto;
  border-radius: 50%;
  object-fit: cover;
  aspect-ratio: 1 / 1;
  box-shadow: 0 24px 64px color-mix(in oklch, var(--yeja-gray-1000) 45%, transparent);
}

/* ── §19. Artist Statement + split Skills (2026-08-04) ────────────────
 * The statement (prose + its own image) and the skills list are now
 * separate sections from Medium & Process — mixing a personal statement
 * with a materials list read as neither. */
.atelier-statement {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: start;
}
.atelier-statement--noimage { grid-template-columns: 1fr; }
/* Which side the statement image sits on (portfolio_statement_side,
 * 2026-08-05). The DOM is text-then-image in both cases — correct
 * reading order for a screen reader and for a no-CSS render — so the
 * flip is done by placing the grid items, not by reordering the markup.
 * Same approach as .atelier-process-row's row-reverse. */
.atelier-statement[data-side="left"] {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
}
.atelier-statement[data-side="left"] > div:first-child { grid-column: 2; grid-row: 1; }
.atelier-statement[data-side="left"] .atelier-statement-btn { grid-column: 1; grid-row: 1; }
/* The image hugs the text column, so the auto margin swaps sides too —
 * left-side images push right, right-side images push left. */
.atelier-statement[data-side="left"] .atelier-statement-btn {
  margin-inline-start: 0;
  margin-inline-end: auto;
}
@media (max-width: 760px) {
  .atelier-statement,
  .atelier-statement[data-side="left"] { grid-template-columns: 1fr; }
  /* Stacked, explicit placement would leave a hole in the first column. */
  .atelier-statement[data-side="left"] > div:first-child,
  .atelier-statement[data-side="left"] .atelier-statement-btn {
    grid-column: 1;
    grid-row: auto;
  }
}
.atelier-statement-btn {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
  font: inherit;
}
.atelier-statement-btn:hover .atelier-process-shot.is-loaded { transform: scale(1.02); }
/* A tall portrait would otherwise run to ~800px beside three lines of
 * statement text and dominate the section. Capped and cropped to a
 * readable band; the uncropped image is one click away in the overlay,
 * the same contract the hero band uses. */
.atelier-statement-btn .atelier-process-frame { max-height: 460px; }
.atelier-statement-btn .atelier-process-shot {
  max-height: 460px;
  object-fit: cover;
}
/* The artist's size knob (portfolio_statement_scale, 20–120%). `width`
 * rather than `max-width`: a max- cap cannot make a block LARGER than its
 * content box, so the shrink half of the range would work and the rest of
 * it would silently do nothing — the exact no-op the process scale hit.
 * margin-inline-start:auto keeps the image against the text column as it
 * narrows, instead of leaving a gap in the middle of the section. */
.atelier-statement-btn {
  width: var(--atelier-statement-scale, 100%);
  margin-inline-start: auto;
}
@media (max-width: 760px) {
  /* Stacked, a scaled-down image floating right reads as a mistake. */
  .atelier-statement-btn { margin-inline-start: 0; }
}

/* .atelier-process-wrap was the profile-wide side switch. Removed
 * 2026-08-05: side is per-image now and lives on .atelier-process-row.
 * Its old note — "at 100% the side choice is a no-op, there is nowhere
 * else to put a full-width block" — no longer applies, because the image
 * now shares its row with the note rather than owning the whole width. */

.atelier-skills-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 24px;
}

/* The banner stamps, top-right (Verified 2026-08-04, Accepting Commissions
 * beside it 2026-08-05 — both user instructions).
 *
 * A flex row that WRAPS: at a narrow viewport two chips plus the hero's own
 * inline padding will not fit on one line, and letting them wrap to a
 * second right-aligned line is better than shrinking the text or letting
 * them run under the identity block. */
.atelier-hero-verified {
  position: absolute;
  top: 14px;
  inset-inline-end: clamp(24px, 3.2vw, 40px);
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
  /* Never cross the midpoint — the artist's name lives bottom-left, and a
   * long "Accepting Commissions" chip on a narrow banner would otherwise
   * reach across the whole band. */
  max-width: min(60%, 320px);
}
.atelier-hero-verified .prof-badge {
  background: color-mix(in oklch, var(--yeja-gray-0) 92%, transparent);
  color: var(--yeja-gray-1000);
  border: none;
}
/* Accepting Commissions deliberately shares Verified's NEUTRAL ink rather
 * than a success green.
 *
 * A mint foreground was tried first and is wrong here for a reason worth
 * recording: applyPortfolioThemeTokens() re-points --yeja-mint (along with
 * --yeja-cobalt and --yeja-amber) to the artist's own theme on .pf-page,
 * this chip's ancestor — the same mapping that repainted the brand mark.
 * Measured live, --yeja-mint resolved to oklch(0.64 0.12 76), i.e. GOLD at
 * hue 76 rather than green at ~145, so the "success" colour was whatever
 * palette the artist happened to pick. Pinning it to the logo ramp would
 * fight the theming, which is legitimate INSIDE the artist's own page.
 *
 * So: no semantic colour. Both stamps are the same near-opaque plate with
 * the same dark ink — they read as one set of platform assertions over
 * arbitrary artwork, and neither can be recoloured out of legibility. */
.atelier-hero-verified .prof-badge-commissions {
  background: color-mix(in oklch, var(--yeja-gray-0) 92%, transparent);
  color: var(--yeja-gray-1000);
  border: none;
}
/* No hero image: there is no artwork to sit over, so both stamps take the
 * page's own surface treatment. One rule covers both — they are styled
 * identically by design (see the commissions chip's note above). */
.atelier-hero--plain .atelier-hero-verified .prof-badge {
  background: var(--yeja-sidebar);
  color: var(--yeja-text-primary);
  border: 1px solid var(--yeja-border);
}

/* ── Support <artist> ─────────────────────────────────────────────────
 * Centered on the Atelier (2026-08-05 user instruction). The markup comes
 * from portfolioSupportHTML() in apps/profile/app.js, which is SHARED with
 * the in-shell dashboard views — so the centering lives here, scoped to
 * .atelier, rather than in that renderer where it would also re-align two
 * dashboard surfaces nobody asked to change.
 *
 * text-align on the section handles the heading and intro; the picker is
 * a flex row, so it needs justify-content of its own — text-align does
 * not position flex children. */
.atelier #pf-support-monthly { text-align: center; }
.atelier #pf-support-monthly .prof-field-hint {
  /* Keep the intro on a readable measure while staying centered. */
  max-width: 56ch;
  margin-inline: auto;
}
.atelier .pf-support-picker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
.atelier .pf-support-amounts {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
}

/* ── Social links rail (2026-08-26) ───────────────────────────────────
 * Emitted by socialLinksHTML() inside getInTouchGateHTML(), so it appears
 * here AND in the in-dashboard preview from one definition.
 *
 * COLOUR: currentColor, deliberately — NOT each platform's brand hex.
 * apps/social/styles.css carries .instagram-bg etc. as raw hex, which
 * predates the token rule and must not be reused here: this rail sits
 * inside .pf-page, where applyPortfolioThemeTokens() repaints the accent
 * tokens per artist. Inheriting the surrounding text colour is what keeps
 * the marks legible in both themes under any artist palette — the same
 * lesson apps/shared/brand-logo.js records for the Yeja mark.
 *
 * Logical properties throughout: `fa` is RTL and this rail must mirror.
 * The glyphs themselves must NOT be mirrored — bidi leaves <svg> alone,
 * which is the behaviour we want for a brand mark. */
.atelier-inquire .pf-social-rail {
  margin-block-start: 32px;
  padding-block-start: 24px;
  border-block-start: 1px solid var(--yeja-border);
}

.pf-social-rail-title {
  margin: 0 0 14px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--yeja-text-muted);
}

.pf-social-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.atelier-inquire .pf-social-list {
  justify-content: center;
}

.pf-social-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-pill);
  color: var(--yeja-text-primary);
  text-decoration: none;
  font-size: 0.875rem;
  line-height: 1;
  transition: border-color 140ms ease, color 140ms ease;
}

.pf-social-link:hover {
  border-color: currentColor;
}

.pf-social-link:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

.pf-social-icon {
  inline-size: 18px;
  block-size: 18px;
  fill: currentColor;
  flex: 0 0 auto;
}

/* The `website` entry has no brand glyph and renders a Material Symbols
 * ligature instead. Any page showing this rail MUST load that font or the
 * ligature name paints as literal text — the failure that already shipped
 * once on the public portfolio. atelier's index.html does load it. */
span.pf-social-icon.yeja-icon {
  font-size: 18px;
}

@media (prefers-reduced-motion: reduce) {
  .pf-social-link {
    transition: none;
  }
}

/* Draft preview banner (2026-08-26).
 *
 * Deliberately loud and deliberately not themeable: an artist's own theme
 * must not be able to make "this is not the live page" quiet or invisible,
 * which is exactly what would happen if it drew from the same tokens the
 * rest of the preview does. Amber is the platform's warning colour.
 *
 * position: sticky rather than fixed — fixed would sit over the page in a
 * full-page screenshot at a single spot, and the whole point is that a
 * screenshot of a preview looks like a preview. */
.atelier-preview-banner {
  position: sticky;
  top: 0;
  z-index: 60;
  padding: var(--space-2, 12px) var(--space-3, 24px);
  background: var(--yeja-amber);
  color: var(--yeja-gray-1000);
  font-size: var(--text-label-size, 12px);
  font-weight: 700;
  text-align: center;
  letter-spacing: 0.02em;
}

/* ── §14. Artist-created sections (2026-08-27) ───────────────────────
 * Rows in atelier_blocks, drawn by apps/portfolio-public/atelier-blocks.js.
 * Every rule below reuses the section chrome already defined above rather
 * than inventing a parallel look: these sections must read as part of the
 * Atelier, not as embedded widgets.
 *
 * Tokens only, per the design guide — no literal hex anywhere, including
 * the image scrim, which uses color-mix over --yeja-gray-1000 (guide §1)
 * rather than rgba(0,0,0,...). */

/* The shared .atelier-section-head is `display:flex;
 * justify-content:space-between`, built for "title ... count". An eyebrow
 * added as a second flex child therefore pushed the heading to the RIGHT
 * edge while the eyebrow sat far left -- every other section title on the
 * page is left-aligned, so the block sections read as broken. Caught in a
 * screenshot; every computed-style check passed.
 *
 * Restacked to a column for block sections only, so the shared rule keeps
 * working everywhere else. */
.atelier-block .atelier-section-head {
  display: block;
}
.atelier-block-eyebrow {
  color: var(--yeja-text-muted);
  margin: 0 0 4px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 11px;
}
.atelier-block-when {
  color: var(--yeja-text-muted);
  display: inline-block;
  margin-bottom: 10px;
}
.atelier-block-prose { max-width: 62ch; }
.atelier-block-prose > :first-child { margin-top: 0; }
.atelier-block-prose > :last-child { margin-bottom: 0; }

/* Capped. Without a max-height a tall portrait fills the whole viewport
 * and buries the copy beneath it -- an announcement is a promo, not a
 * lightbox. object-fit keeps the crop honest at any aspect ratio. */
.atelier-block-image {
  width: 100%;
  max-height: 420px;
  object-fit: cover;
  object-position: center;
  border-radius: var(--radius-media);
  margin-bottom: 16px;
  display: block;
}
@media (max-width: 640px) {
  .atelier-block-image { max-height: 260px; }
}

/* ── The CTA button ──
 * MEASURED AT CONTRAST RATIO 1.0 IN BOTH THEMES before this rule existed:
 * .yeja-pill-btn supplies a background but inherits its text colour, so
 * the label came out the SAME colour as the fill -- ink-on-ink in light,
 * white-on-white in dark. Completely invisible, and every layout metric
 * passed because the element was present and correctly sized. Only the
 * screenshot showed it.
 *
 * Fixed the way the Inquire section already solves it (guide §5's Ink
 * Pill): solid ink fill, canvas-coloured text, no border. Both tokens
 * flip with the theme, so it is legible in each.
 *
 * !important for the same reason documented on .atelier-inquire's rule --
 * it must beat .yeja-pill-btn's own background/border from a different
 * stylesheet. */
.atelier-block-cta { margin: 18px 0 0; }
.atelier-block-cta a.yeja-pill-btn {
  background: var(--yeja-text-primary) !important;
  color: var(--yeja-canvas) !important;
  border: none !important;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}
.atelier-block-cta a.yeja-pill-btn:hover { opacity: 0.85; }
.atelier-block-cta a.yeja-pill-btn:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
}

/* Offers. auto-fit rather than a fixed column count so one offer does not
 * sit in a lonely third of the row and five do not crush. */
.atelier-block-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px;
  margin-top: 20px;
}
.atelier-block-card {
  border: 1px solid var(--yeja-border);
  border-radius: var(--radius-media);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.atelier-block-card-image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
}
.atelier-block-card-body { padding: 16px; display: flex; flex-direction: column; gap: 6px; }
.atelier-block-price { color: var(--yeja-text-muted); margin: 4px 0 0; }

/* Testimonials. A quiet rule rather than a quotation-mark glyph: the
 * latter is decorative, announces nothing, and reads as noise at scale. */
.atelier-block-quotes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 24px;
  margin-top: 20px;
}
.atelier-block-quote {
  margin: 0;
  padding-inline-start: 16px;
  border-inline-start: 2px solid var(--yeja-border);
}
.atelier-block-cite {
  margin-top: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: baseline;
}
.atelier-block-cite-name { font-weight: 500; }
.atelier-block-cite-meta { color: var(--yeja-text-muted); }
.atelier-block-cite-src { color: var(--yeja-text-muted); text-decoration: underline; }

/* FAQ. Native <details> — keyboard accessible and screen-reader announced
 * with no wiring, and it still works if the module fails to load. */
.atelier-block-faqs { margin-top: 12px; }
.atelier-block-faq {
  border-bottom: 1px solid var(--yeja-border);
  padding-block: 14px;
}
.atelier-block-faq:last-child { border-bottom: none; }
.atelier-block-faq-q {
  cursor: pointer;
  font-weight: 500;
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: 12px;
}
.atelier-block-faq-q::-webkit-details-marker { display: none; }
.atelier-block-faq-q::after {
  content: '+';
  color: var(--yeja-text-muted);
  flex-shrink: 0;
}
.atelier-block-faq[open] .atelier-block-faq-q::after { content: '–'; }
/* Focus ring on the actual interactive element, per the guide: 2px cobalt,
 * 2px offset. <summary> is focusable and was easy to miss here. */
.atelier-block-faq-q:focus-visible {
  outline: 2px solid var(--yeja-focus);
  outline-offset: 2px;
  border-radius: var(--radius-control);
}

.atelier-block-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
  margin-top: 20px;
}
.atelier-block-figure { margin: 0; }
.atelier-block-figure img {
  width: 100%;
  height: auto;
  border-radius: var(--radius-media);
  display: block;
}
.atelier-block-figure figcaption {
  color: var(--yeja-text-muted);
  margin-top: 8px;
  font-size: 13px;
}

@media (max-width: 640px) {
  /* One column below the fold width — auto-fit alone still allows two
   * cramped columns on a narrow phone. */
  .atelier-block-grid,
  .atelier-block-quotes,
  .atelier-block-figures { grid-template-columns: 1fr; }
}

/* ── NO-JS FALLBACK: hide the boot skeleton (task 2.6) ──────────────────
 *
 * This rule used to live in a <style> inside the page's <noscript>. It moved
 * here so the Atelier path can enforce `style-src 'self'` — an inline <style>
 * is exactly what that directive blocks, and it was the ONLY thing standing
 * between the current policy and the strict one.
 *
 * It stays load-bearing, not decorative: without it a visitor whose browser or
 * content blocker stopped the script sees the boot skeleton shimmering
 * forever, under the "needs JavaScript" message, which reads as "still
 * loading" rather than "this will never finish".
 *
 * :has() rather than a hash. A hash would have to be recomputed in vercel.json
 * every time the rule's bytes change — including whitespace — and the failure
 * mode is silent: the style stops applying and the skeleton comes back. This
 * selector works because the noscript's children only enter the DOM when
 * scripting is off, so `body:has(noscript > .atelier)` is true exactly when
 * the fallback is showing. Baseline-available in every browser Yeja targets.
 */
body:has(noscript > .atelier) #root .pf-boot { display: none; }
