/* Shared case-study styles. Extracted from the duplicated inline <style>
   blocks of studio-ai / category-2.0 during the Astro migration.

   No :root ramp here. This file used to define its own --gray-* / --mint set,
   but CaseStudy.astro always loads paper.css AFTER it, at equal specificity, so
   every one of those declarations was dead. The palette is single-sourced in
   paper.css — if you need a colour here, use a token, don't redefine one. */

    * { box-sizing: border-box; }

    body {
      margin: 0;
      background-color: var(--gray-900);
      font-family: var(--pp-body);
      color: var(--gray-300);
      line-height: 1.65;
      -webkit-font-smoothing: antialiased;
    }

    .wrap {
      max-width: 720px;
      margin: 0 auto;
      padding: 32px 24px 96px;
    }

    /* Back link — layout only. Colour, underline, hover and icon size are THE
       LINK's, in paper.css, shared with every other text link on the site. */
    .back {
      display: inline-flex;
      align-items: center;
      font-size: 14px;
      padding: 8px 4px;
    }

    /* top bar: back link left, agent copy action right */
    .topbar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
      flex-wrap: wrap;
    }
    .case-study-copy-button { margin-left: auto; }
    .case-study-copy-button .tabler-icon { width: 16px; height: 16px; }

    /* Hero */
    .hero { margin-top: 16px; }
    /* ONE size for every case study. This was 46 with a .hero--compact modifier
       that dropped a single page to 36; the modifier is gone and 36 is the
       size, because a per-page opt-in is how five pages ended up opening at two
       different scales. If a page ever needs its own, add the modifier back
       rather than editing this. */
    h1 {
      font-size: 36px;
      line-height: 1.05;
      font-weight: 700;
      color: var(--gray-50);
      margin: 0 0 16px;
      letter-spacing: -0.02em;
    }
    .subtitle {
      font-size: 13px;
      color: var(--gray-300);
      margin: 0 0 28px;
      font-weight: 400;
    }

    .meta {
      display: flex;
      flex-wrap: wrap;
      gap: 28px;
      padding: 20px 0;
      border-top: 1px solid var(--gray-700);
      border-bottom: 1px solid var(--gray-700);
    }
    .meta div { font-size: 13px; }
    .meta dt {
      color: var(--gray-400); /* was gray-500: AA fail */
      text-transform: uppercase;
      letter-spacing: 0.06em;
      font-size: 10px;
      margin-bottom: 4px;
    }
    .meta dd { margin: 0; color: var(--gray-100); font-weight: 500; }
    .product-logos { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-top: 40px; }
    .product-logos .brand-logo { height: 31px; width: auto; }
    /* Marks that ship as white-on-transparent, so they can sit on their own
       product's dark UI. On the light paper they would be invisible, so flip
       them to ink. invert() leaves alpha alone, so only the glyph changes and
       the transparent ground stays transparent. Dark mode wants the source
       colour back. */
    .product-logos .brand-logo.is-inverted { filter: invert(1); }
    [data-theme="dark"] .product-logos .brand-logo.is-inverted { filter: none; }
    .project-wordmark {
      font-family: var(--pp-display);
      font-weight: 700;
      color: var(--gray-100);
    }
    .project-wordmark { font-size: 16px; }

    /* Section rhythm */
    section { margin-top: 48px; }
    h2 {
      font-size: 13px;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: var(--mint);
      font-weight: 600;
      margin: 0 0 16px;
    }
    section p { font-size: 13px; margin: 0 0 18px; }
    /* Sub-heading inside a section (Forest: the benchmark under the reviewer).
       Body-sized and weighted, not another eyebrow, so the section keeps one h2. */
    section > h3 { font-size: 15px; font-weight: 600; color: var(--gray-100); margin: 40px 0 12px; letter-spacing: -0.01em; }
    strong { color: var(--gray-100); font-weight: 600; }

    /* Highlight cards (At a glance / Impact) */
    /* Surface comes from the .edge-card material in paper.css (radius included,
       via --card-radius); this is structure only. */
    .card {
      padding: 28px 32px;
      margin-top: 48px;
    }
    .card h2 { margin-top: 0; }
    .card p:last-child { margin-bottom: 0; }

    /* ── PROSE BULLETS — one marker, one rule, every case study ────────────
       .outcomes (At a glance / Impact) and ul.did (role bullets) are the same
       object: a short list of statements inside the reading column. They used
       to disagree about it — .outcomes carried NO marker on the theory that its
       bold lead-in was entry point enough, while ul.did drew a 7px accent dot
       at 22px of indent — so two lists a few sections apart read as two
       different components. They share this rule now, and a third prose list
       joins by adding its selector here rather than inventing a marker.

       The marker is a literal hyphen: no shape to re-ink per surface, no
       geometry to re-tune when the type size moves, and it survives a copy-
       paste of the rendered text. It inherits the item's colour rather than
       taking the accent, so the marker never outweighs the sentence.

       NOT included: .fx-msgs. That is a mock of the reviewer's console output,
       and its "x" markers are part of what is being simulated, not decoration
       this rule gets to standardise. */
    .outcomes,
    ul.did {
      list-style: none;
      padding-left: 0;
      margin: 0;
    }
    .outcomes li,
    ul.did li {
      position: relative;
      padding-left: 16px;
      margin-bottom: 12px;
      font-size: 13px;
    }
    .outcomes li:last-child,
    ul.did li:last-child { margin-bottom: 0; }
    /* Prose resumes after the list at the same 18px paragraphs keep between themselves. */
    ul.did + p { margin-top: 18px; }
    .outcomes li::before,
    ul.did li::before {
      content: "-";
      position: absolute;
      left: 0;
      top: 0;
    }
    .outcomes strong { color: #fff; }

    /* ── THE BOARD — a live Storybook story, scrubbed by the story itself ───
       The story is an infinite board that handles its own drag, so the ONLY
       job here is to give it a window and zoom it out. No overlay: an earlier
       pass put a transparent sheet on top to read the drag in the parent, and
       it swallowed the pointer the story needed, which is what turned infinite
       scrubbing into a pan that stopped at the frame's edge.

       --board-zoom is the whole zoom control. An iframe's internal viewport is
       its CSS size, so the frame is sized to 1/zoom of the window and scaled
       back down by zoom, then fits the window exactly. More board, same box.

       Think in AREA, not in the number: the viewport grows by 1/zoom on BOTH
       axes, so visible area goes as (1/zoom)^2. That is why the values look
       arbitrary — each is the reciprocal square root of an area target:
         0.577 = 1/sqrt(3)  ->  3x the window's area   (desktop, current)
         0.5   = 1/2        ->  4x                     (small screens)
         0.7                ->  ~2x                    (where this started)
       To pick a new one, take 1/sqrt(area you want).

       Below about 0.4 (6x) the component labels stop being readable and the
       board reads as coloured shapes rather than a component library. That is
       a design floor, not a technical one.

       transform-origin: 0 0 so the scale pins to the top-left corner; the
       default 50% 50% would shrink the frame away from the window's edges and
       leave a gap on all four sides.

       Hit testing goes through the transform, so the story still receives
       correctly mapped pointer coordinates and its own drag is unaffected. */
    .board-view {
      --board-zoom: 0.577;
      position: relative;
      overflow: hidden;
      height: 520px;
      border: 1px solid var(--gray-700);
      background: var(--pp-card);
    }
    .board-frame {
      position: absolute;
      top: 0;
      left: 0;
      width: calc(100% / var(--board-zoom));
      height: calc(100% / var(--board-zoom));
      transform: scale(var(--board-zoom));
      transform-origin: 0 0;
      border: 0;
      display: block;
    }
    /* The loader sits over the frame on the card surface until case-study.js
       adds .is-ready, then fades and drops out of hit testing (visibility, not
       display, so the fade can play). pointer-events: none throughout, so it
       can never swallow the drag the story needs — the lesson of the old
       overlay sheet, applied to the one layer that is allowed to exist here.
       The tiles stand in for component cards and pulse in a stagger that
       walks across the grid, an echo of the homepage canvas assembling
       itself. Tokens only, so dark mode inherits. */
    .board-loader {
      position: absolute;
      inset: 0;
      z-index: 1;
      display: grid;
      place-content: center;
      justify-items: center;
      gap: 16px;
      background: var(--pp-card);
      pointer-events: none;
      transition: opacity 0.45s ease, visibility 0.45s;
    }
    .board-view.is-ready .board-loader { opacity: 0; visibility: hidden; }
    .board-loader-grid {
      display: grid;
      grid-template-columns: 72px 48px 60px;
      gap: 10px;
    }
    .board-loader-grid i {
      display: block;
      height: 40px;
      border-radius: 6px;
      background: var(--gray-800);
      animation: board-tile 1.4s ease-in-out infinite;
    }
    .board-loader-grid i:nth-child(2) { animation-delay: 0.12s; }
    .board-loader-grid i:nth-child(3) { animation-delay: 0.24s; }
    .board-loader-grid i:nth-child(4) { animation-delay: 0.36s; height: 28px; }
    .board-loader-grid i:nth-child(5) { animation-delay: 0.48s; height: 28px; }
    .board-loader-grid i:nth-child(6) { animation-delay: 0.6s; height: 28px; }
    .board-loader-label {
      font-family: var(--pp-mono);
      font-size: 11px;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      color: var(--gray-400);
    }
    @keyframes board-tile { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
    @media (prefers-reduced-motion: reduce) {
      .board-loader { transition: none; }
      .board-loader-grid i { animation: none; opacity: 0.7; }
    }
    /* The board is the one figure with no expand button (case-study.js only
       adds one for img/video), so its caption carries the affordance instead. */
    .board figcaption { margin-top: 12px; }
    @media (max-width: 600px) {
      /* Shorter window, and zoomed out further: a phone-width slice of the
         board shows too little to read as a board at all. */
      .board-view { height: 380px; --board-zoom: 0.5; }
    }

    /* Media block: image, gif, or video with a caption */
    figure.media { margin: 40px 0 0; }
    /* a figure leading a section sits above the heading, with a gap before it */
    section > figure.media:first-child { margin-top: 0; margin-bottom: 32px; }
    /* A caption with no media above it: a note in the media voice, sitting
       under the figure it comments on. It was `style="margin-top:16px"` written
       inline on two pages before <Media> with no src replaced them.
       It must sit AFTER figure.media and match its specificity: as a bare
       `.media--note` (0,1,0) it lost to `figure.media` (0,1,1) and the note
       took the full 40px gap, silently undoing what the inline style did. */
    figure.media--note { margin-top: 16px; }
    figure.media > img,
    figure.media > video {
      display: block;
      width: 100%;
      height: auto;
      /* Square corners: screenshots and screen recordings carry their own UI
         chrome, and a radius here rounds off THAT rather than the frame. */
      border-radius: 0;
      border: 1px solid var(--gray-700);
      background: rgba(255, 255, 255, 0.03);
    }
    figure.media figcaption {
      margin: 12px 0 0;
      font-size: 12px;
      line-height: 1.5;
      color: var(--gray-400); /* was gray-500: AA fail */
    }
    /* Row of media side by side (e.g. the marketing set) */
    .media-row {
      display: flex;
      flex-wrap: wrap;
      gap: 16px;
      margin: 40px 0 0;
    }
    .media-row figure.media { flex: 1 1 200px; margin: 0; }

    /* Expand-to-view affordance + lightbox (added by JS) */
    figure.media { position: relative; }
    .media-expand {
      position: absolute;
      top: 12px;
      right: 12px;
      width: 36px;
      height: 36px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border: none;
      border-radius: 8px;
      background: rgba(15, 23, 42, 0.6);
      color: #fff;
      cursor: pointer;
      opacity: 0;
      transition: opacity 0.15s ease, background 0.15s ease;
    }
    figure.media:hover .media-expand,
    .media-expand:focus-visible { opacity: 1; }
    /* touch has no hover — keep the affordance visible so the work is reachable */
    @media (hover: none) { .media-expand { opacity: 1; } }
    .media-expand:hover { background: rgba(15, 23, 42, 0.92); }
    .media-expand svg { width: 18px; height: 18px; }
    /* native <dialog>: showModal() centers it and provides ::backdrop; a
       dialog is display:none until open, so no [hidden] rule needed */
    .lightbox {
      max-width: 95vw;
      max-height: 92vh;
      padding: 24px;
      border: none;
      background: transparent;
      overflow: visible;
    }
    .lightbox::backdrop { background: rgba(2, 6, 23, 0.92); }
    .lightbox-body {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 14px;
      max-width: 95vw;
    }
    .lightbox img,
    .lightbox video {
      max-width: 95vw;
      max-height: 82vh;
      width: auto;
      height: auto;
      /* square, to match the same asset in the page flow */
      border-radius: 0;
    }
    .lightbox-caption {
      margin: 0;
      max-width: 760px;
      color: var(--gray-300);
      font-size: 13px;
      line-height: 1.5;
      text-align: center;
    }
    .lightbox-close {
      position: fixed;
      top: 18px;
      right: 22px;
      width: 44px;
      height: 44px;
      border: none;
      border-radius: 10px;
      background: rgba(255, 255, 255, 0.12);
      color: #fff;
      font-size: 23px;
      line-height: 1;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }
    .lightbox-close:hover { background: rgba(255, 255, 255, 0.24); }
    .lightbox-close .tabler-icon { width: 24px; height: 24px; }

    /* Reference links: title + diagonal "opens in new tab" arrow */
    .ref-links { list-style: none; padding: 0; margin: 0; }
    /* 14px, a step up from the 13px body around it: these are standalone links
       you are meant to find, not prose. Links that sit INSIDE a sentence
       (a.inline) deliberately keep inheriting, so they never out-size the
       sentence carrying them. */
    .ref-links li { margin-bottom: 16px; font-size: 14px; }
    .ref-links li:last-child { margin-bottom: 0; }
    /* Layout only — see .back above; THE LINK in paper.css paints these. */
    .ref-links a {
      display: inline-flex;
      align-items: center;
      padding: 2px 4px;
    }
    /* The one flourish that survives the swipe removal: the "opens in a new
       tab" arrow steps out along its own diagonal on hover. */
    .ref-arrow { transition: transform 0.15s ease; }
    .ref-links a:hover .ref-arrow { transform: translate(2px, -2px); }

    a.inline { padding: 2px; }

    footer {
      margin-top: 64px;
      padding-top: 28px;
      display: flex;
      flex-wrap: wrap;
      gap: 20px;
      align-items: center;
      justify-content: space-between;
      font-size: 13px;
      color: var(--gray-400); /* was gray-500: AA fail */
    }

    @media (max-width: 600px) {
      h1 { font-size: 30px; }
      .subtitle { font-size: 13px; }
      .card { padding: 22px; }
      .wrap { padding: 24px 18px 72px; }
      /* stat callouts stack full-width so the wide mono figures/labels wrap
         inside the card instead of spilling past its edge. .stat-row prefix
         keeps these winning over the base .stat-* rules defined later in the file. */
      .stat-row .stat { flex-basis: 100%; min-width: 0; }
      .stat-row .stat-figure { font-size: clamp(27px, 8.1vw, 36px); overflow-wrap: break-word; }
      .stat-row .stat-label { overflow-wrap: break-word; }
    }

/* ── Next project row ────────────────────────────────────────────────────────
   Same ruled ledger row as the homepage. Case-study pages do not load
   styles.css, so the small structural subset lives here. */
.next-project { margin: 72px 0 0; }
.next-project-label {
  display: block;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--gray-400);
  margin-bottom: 14px;
}
.work-row {
  box-sizing: border-box;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 24px;
  padding: 20px 0;
  border-block: 1px solid var(--gray-700);
  text-decoration: none;
  text-align: left;
  cursor: pointer;
}
.row-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.row-title { font-size: 18px; font-weight: 600; line-height: 1.25; color: var(--gray-50); }
.row-desc { font-size: 13px; line-height: 1.45; color: var(--gray-400); }
.row-arrow { font-size: 18px; line-height: 1; color: var(--gray-400); }
.work-row:hover .row-title { text-decoration: underline; text-underline-offset: 3px; }
.work-row:focus-visible { outline: 2px solid var(--blue-400); outline-offset: 2px; }

@media (max-width: 600px) {
  .work-row { gap: 16px; }
  .row-title { font-size: 16px; }
  .row-desc { font-size: 12px; }
}

.contact-cta {
  margin: 40px 0 0;
  padding: 40px 0 8px;
  text-align: center;
}
.contact-cta-lead {
  font-size: clamp(27px, 4.5vw, 39px);
  line-height: 1.05;
  font-weight: 700;
  color: var(--gray-50);
  margin: 0 0 8px;
  letter-spacing: -0.02em;
}
.contact-cta-line { font-size: 15px; color: var(--gray-300); margin: 0 0 24px; }
.contact-cta-btn {
  display: inline-block;
  padding: 12px 22px;
  border-radius: 10px;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  background: var(--mint);
  color: var(--gray-900);
  transition: transform 0.15s ease;
}
.contact-cta-btn:hover { transform: translateY(-2px); }

/* ── Stat callout: one or two headline numbers as designed moments ──────────
   used inside .card.impact — figure oversized, label small mono */
.stat-row {
  display: flex;
  flex-wrap: wrap;
  gap: 32px 48px;
  margin: 4px 0 24px;
}
.stat { flex: 0 0 auto; }
.stat-figure {
  display: block;
  font-size: clamp(36px, 5.4vw, 50px);
  line-height: 1;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--gray-50);
  letter-spacing: -0.02em;
}
.stat-label {
  display: block;
  margin-top: 8px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--gray-400);
}

/* ── Head-to-head comparison table (Forest vs Astryx) ───────────────────────
   built from the shared tokens so paper.css retunes it like everything else.
   Wrapper scrolls on narrow screens so the page body never scrolls sideways. */
.compare { margin: 24px 0; overflow-x: auto; }
.compare table { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 1.45; }
.compare th, .compare td {
  text-align: left;
  padding: 12px 14px;
  border-bottom: 1px solid var(--gray-700);
  vertical-align: top;
}
.compare thead th {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--gray-400);
  font-weight: 600;
  white-space: nowrap;
}
.compare tbody th { font-weight: 600; color: var(--gray-100); min-width: 150px; }
.compare td { color: var(--gray-200); }
/* Forest is the subject — give its column a touch more weight */
.compare .c-forest { color: var(--gray-50); font-weight: 500; }
.compare tbody tr:last-child th, .compare tbody tr:last-child td { border-bottom: none; }

/* Media slot not shot yet — clearly marks where an asset will drop in */
.media-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 240px;
  border-radius: 16px;
  border: 1px dashed var(--gray-700);
  background: rgba(255, 255, 255, 0.02);
  color: var(--gray-400);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  padding: 24px;
}

/* ── Forest case study: live demos (.fx) ─────────────────────────────────────
   Ad hoc, interactive stand-ins for what used to be screenshots and clips. They
   sit in a figure.media so they inherit its rhythm and the scroll reveal, but
   have no img/video, so the lightbox leaves them alone. Behavior: case-study.js.

   The enforcement switch is the site's own switch hardware (.theme-toggle-track
   + .theme-toggle-knob from paper.css). Its on/hover/active states are scoped
   to .lamp and .tliaa-switch upstream, so .fx-switch carries its own copies,
   same values. Keep them scoped: an unscoped knob rule slides every switch. */
.fx {
  border: 1px solid var(--gray-700);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.03);
  overflow: hidden;
  font-size: 14px;
}
.fx-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 12px 16px;
  border-bottom: 1px solid var(--gray-700);
}
.fx-file { font-family: var(--pp-mono); font-size: 13px; color: var(--gray-400); }
.fx-switches { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.fx-switch {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: 13px;
  color: var(--pp-ink);
}
.fx-switch .fx-state { font-weight: 600; min-width: 2ch; display: inline-block; }
.fx-switch:hover .theme-toggle-track {
  border-color: var(--pp-accent);
  box-shadow: 3px 3px 0 var(--pp-accent);
  transform: translate(-1px, -1px);
}
.fx-switch:hover .theme-toggle-knob { background: var(--pp-accent); }
.fx-switch:active .theme-toggle-track {
  box-shadow: 1px 1px 0 var(--pp-ink);
  transform: translate(1px, 1px);
}
.fx-switch:focus-visible { outline: 2px solid var(--pp-accent); outline-offset: 4px; border-radius: 14px; }
.fx-switch[aria-checked="true"] .theme-toggle-knob { translate: 28px -50%; }

/* The AI's file. Code is the one place the site uses a monospace face: it is
   quoted source, not site typography. */
.fx-code {
  margin: 0;
  padding: 16px;
  border-bottom: 1px solid var(--gray-700);
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--gray-100);
  tab-size: 2;
}
.fx-code code { font: inherit; }

/* What that file renders. The colors in here are the demo's SUBJECT (the mock
   product's own palette: MUI's stock blue, the AI's purple, Forest's near-black
   button), deliberately not site tokens, so they are declared once here and
   nowhere else. The bare mock ignores [data-theme="dark"] on purpose: that is
   the point being made. The Forest mock inverts. */
.fx-render {
  position: relative;
  --fx-purple: #7f56d9;
  --fx-mui-blue: #1976d2;
  --fx-card: #ffffff;
  --fx-card-line: #e2e2df;
  --fx-card-ink: #111111;
  --fx-btn: #111111;
  --fx-btn-ink: #ffffff;
  padding: 20px;
  border-bottom: 1px solid var(--gray-700);
  background: var(--gray-800);
}
[data-theme="dark"] .fx-render {
  --fx-card: #1c1c1b;
  --fx-card-line: #353532;
  --fx-card-ink: #f0f0ef;
  --fx-btn: #f0f0ef;
  --fx-btn-ink: #111111;
}
/* Enforcement on + a violating file: the write never lands. The mock stays
   visible, dimmed, so you can see what was stopped, under a badge in the same
   error palette as the messages. */
.fx-render.is-blocked .fx-mock { opacity: 0.4; filter: saturate(0.6); }
.fx-blocked {
  display: none;
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--fx-err);
  background: var(--fx-err-bg);
  color: var(--fx-err);
  font: 600 12px/1 var(--pp-body);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}
.fx-render.is-blocked .fx-blocked { display: inline-block; }
.fx-mock h3 { margin: 0 0 12px; }
.fx-mock button { cursor: default; }
.fx-mock-bare {
  background: var(--fx-purple);
  padding: 24px;
  border-radius: 4px;
  font-family: Roboto, system-ui, sans-serif;
}
.fx-mock-bare h3 { color: #fff; font-size: 19px; font-weight: 500; }
.fx-mock-bare button {
  border: none;
  border-radius: 4px;
  padding: 6px 16px;
  background: var(--fx-mui-blue);
  color: #fff;
  font: 500 14px/1.75 Roboto, system-ui, sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.02857em;
  box-shadow: 0 3px 1px -2px rgb(0 0 0 / 0.2), 0 2px 2px 0 rgb(0 0 0 / 0.14), 0 1px 5px 0 rgb(0 0 0 / 0.12);
}
.fx-mock-forest {
  background: var(--fx-card);
  border: 1px solid var(--fx-card-line);
  border-radius: 8px;
  padding: 20px;
}
.fx-mock-forest h3 { color: var(--fx-card-ink); font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.fx-mock-forest button {
  border: none;
  border-radius: 6px;
  padding: 8px 14px;
  background: var(--fx-btn);
  color: var(--fx-btn-ink);
  font: 500 14px/1.3 var(--pp-body);
}

/* What the hook said. */
.fx-out { display: flex; flex-direction: column; gap: 10px; padding: 16px; }
.fx-lead { margin: 0; font-size: 14px; color: var(--gray-100); }
.fx-lead.is-clean { color: var(--fx-ok); font-weight: 600; }
/* The violations read as the error alert the hook's Storybook shows them in:
   MUI's error palette, same as the source of the screenshot they replaced.
   Subject colors again, so local, with a dark pair (5f2120 on fdeded is 9.9:1,
   f4c7c7 on 160b0b is 13:1). */
/* Error and success pairs from MUI's alert palette, the demo's subject.
   Success text: 1b5e20 on paper is 7.4:1, a5d6a7 on the dark paper is 11:1. */
.fx { --fx-err: #5f2120; --fx-err-bg: #fdeded; --fx-ok: #1b5e20; }
[data-theme="dark"] .fx { --fx-err: #f4c7c7; --fx-err-bg: #160b0b; --fx-ok: #a5d6a7; }
.fx-msgs {
  list-style: none;
  margin: 0;
  padding: 12px 14px;
  border-radius: 8px;
  background: var(--fx-err-bg);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.55;
  color: var(--fx-err);
}
.fx-msgs li { position: relative; padding-left: 18px; margin: 0 0 8px; overflow-wrap: anywhere; }
.fx-msgs li:last-child { margin-bottom: 0; }
.fx-msgs li::before { content: "×"; position: absolute; left: 0; top: 0; font-weight: 700; }
.fx-msgs:empty { display: none; }

@media (max-width: 600px) {
  .fx-bar { padding: 10px 14px; }
  .fx-code { padding: 14px; font-size: 12px; }
  .fx-render { padding: 14px; }
}

/* ── Forest demos: tabs, playground, page mock ───────────────────────────────
   Three examples in one .fx frame. The tablist ships hidden and case-study.js
   reveals it, so with no JS example 1 stands alone. */
.fx-tabs {
  display: flex;
  gap: 4px;
  padding: 8px 8px 0;
  border-bottom: 1px solid var(--gray-700);
}
.fx-tabs[hidden] { display: none; }
.fx-tab {
  appearance: none;
  border: none;
  background: none;
  padding: 8px 14px 10px;
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  font: 500 13px/1 var(--pp-body);
  color: var(--gray-400);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.fx-tab:hover { color: var(--pp-ink); }
.fx-tab[aria-selected="true"] { color: var(--pp-ink); border-bottom-color: var(--pp-accent); }
.fx-tab:focus-visible { outline: 2px solid var(--pp-accent); outline-offset: -2px; border-radius: 6px; }

/* Example 2: the editable file. A textarea, so it is a real text field with
   real selection and undo, not a contenteditable imitation. */
.fx-editor {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 16px;
  border: none;
  border-bottom: 1px solid var(--gray-700);
  border-radius: 0;
  background: none;
  color: var(--gray-100);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.6;
  tab-size: 2;
  resize: vertical;
}
.fx-editor:focus-visible { outline: 2px solid var(--pp-accent); outline-offset: -2px; }
/* Visible to screen readers only: the textarea needs a label, the bar above it
   already says the filename to everyone else. */
.fx-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Example 3: the mandatory screen shape, shell omitted. Page surface, titled
   page, then the widget, carded or not. */
.fx-page {
  --fx-page: #ffffff;
  --fx-page-line: #e2e2df;
  --fx-page-ink: #111111;
  --fx-page-dim: #6b6b66;
  --fx-chart-bar: #3f3f3c;
  padding: 18px;
  border-radius: 8px;
  background: var(--fx-page);
  color: var(--fx-page-ink);
}
[data-theme="dark"] .fx-page {
  --fx-page: #1c1c1b;
  --fx-page-line: #353532;
  --fx-page-ink: #f0f0ef;
  --fx-page-dim: #9c9c96;
  --fx-chart-bar: #d8d8d3;
}
.fx-crumbs { margin: 0 0 4px; font-size: 11px; color: var(--fx-page-dim); }
.fx-page-title { margin: 0 0 16px; font-size: 19px; font-weight: 600; letter-spacing: -0.01em; color: var(--fx-page-ink); }
/* The rule itself: with it on, the widget gets the Card. With it off, the chart
   sits bare on the page surface, which is exactly the violation. */
.fx-widget { padding: 0; border: 1px solid transparent; border-radius: 8px; }
.fx-widget.is-carded { padding: 16px; border-color: var(--fx-page-line); }
.fx-chart { display: flex; align-items: flex-end; gap: 18px; height: 150px; }
.fx-col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 8px; }
.fx-col i { display: block; width: 100%; height: calc(var(--v) / 10 * 120px); border-radius: 4px 4px 0 0; background: var(--fx-chart-bar); }
.fx-col em { font-style: normal; font-size: 11px; color: var(--fx-page-dim); }

@media (max-width: 600px) {
  .fx-tabs { padding: 6px 6px 0; }
  .fx-tab { padding: 8px 10px 10px; }
  .fx-editor { padding: 14px; font-size: 12px; }
  .fx-page { padding: 14px; }
  .fx-chart { gap: 12px; }
}

/* A small title over the card, and a caption under it that describes whichever
   example is open (case-study.js swaps it with the tab). The title is the only
   part of this figure the plain-text edition sees, so it carries the framing. */
.fx-title {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.5;
  color: var(--gray-100);
}
.fx-cap[hidden] { display: none; }
