/* ==========================================================================
   Genva marketing website — styles
   Self-hosted type: Google Sans Flex (the app's own face, SIL OFL) + IBM Plex Mono.
   Design tokens mirror genva-app: lib/app.dart, splash_screen.dart.
   ========================================================================== */

@font-face {
  font-family: "Google Sans Flex";
  src: url("/fonts/GoogleSansFlex-Regular.ttf") format("truetype");
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: "Google Sans Flex";
  src: url("/fonts/GoogleSansFlex-Medium.ttf") format("truetype");
  font-weight: 500; font-display: swap;
}
@font-face {
  font-family: "Google Sans Flex";
  src: url("/fonts/GoogleSansFlex-Bold.ttf") format("truetype");
  font-weight: 600 700; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("/fonts/IBMPlexMono-Regular.woff2") format("woff2");
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("/fonts/IBMPlexMono-Medium.woff2") format("woff2");
  font-weight: 500; font-display: swap;
}

    /* ===== Genva design tokens (lib/app.dart, splash_screen.dart) ===== */
    :root {
      --bg:        #060910;
      --bg-deep:   #030508;
      --ink:       #ffffff;
      --ink-2:     #c8cdd8;
      --ink-3:     #8b909e;
      --ink-4:     #5b6070;
      --pink:      #fe0299;
      --amber:     #f7f062;
      --grad:      linear-gradient(115deg, #fe0299 0%, #ff5fb6 44%, #f7f062 100%);
      /* the app's own create-button texture (/assets/images/create_button_bg.webp) —
         every primary pill on the page paints with this, so it lives here */
      --pill-bg:   #ee2fa4 url("/assets/img/create-button-bg.webp") center / 100% 100% no-repeat;
      --glass:     rgba(255,255,255,.06);
      --glass-2:   rgba(255,255,255,.10);
      --rim:       rgba(255,255,255,.14);
      --rim-soft:  rgba(255,255,255,.08);
      --r-pill:    999px;
      --font: "Google Sans Flex", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      --mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
    }
    /* The beam's sweep angle (see .pb-beam) — genva-app's `uAngle`, which its
       shader spins from 0 to 1. A registered property, because a plain custom
       property is a string and CSS cannot interpolate a string; registering
       it as an <angle> is what makes it animatable, and `inherits` is what
       lets one animation on the box drive every layer's gradient at once. */
    @property --pb-angle {
      syntax: "<angle>";
      inherits: true;
      initial-value: 0deg;
    }

    /* A gated page holds its own paint until js/services/gate.js has decided
       who you are — see the note on <html> in video/image/music//design.
       `visibility`, not `display`: layout still happens, so the page is ready
       to show the instant the session lands rather than reflowing into view.
       The ground stays painted, so this reads as the page loading, which it
       is. Belt and braces with the click intercept in site.js: that one keeps
       you from ever arriving here without a session, this one covers a typed
       URL, a bookmark and a stale tab. */
    html.is-gating body { visibility: hidden; }

    * { box-sizing: border-box; }
    html, body { margin: 0; }
    /* The clip lives on the scroller, NOT on <body>. The fan's side cards are
       transformed past the viewport edge and have to be cut off — but an
       `overflow-x: hidden` on body makes body a scroll container, and iOS
       Safari then refuses to stick `position: sticky` descendants to the
       viewport: the whole top bar scrolls away with the page. On <html>, which
       is already the scrollport, the clip costs nothing. */
    html { overflow-x: hidden; }
    body {
      font-family: var(--font);
      background: var(--bg);
      color: var(--ink-2);
      -webkit-font-smoothing: antialiased;
    }

    /* ambient magenta floor-glow + faint amber top-right — from splash.webp */
    .bgfx {
      position: fixed; inset: 0; z-index: 0; pointer-events: none;
      background:
        radial-gradient(135% 95% at 50% 122%, rgba(254,2,153,.15) 0%, rgba(254,2,153,0) 56%),
        radial-gradient(70% 50% at 90% -12%, rgba(247,240,98,.05) 0%, rgba(247,240,98,0) 55%),
        /* the one background the whole site paints */
        var(--bg);
      animation: breathe 9s ease-in-out infinite;
    }
    .bgfx::after {
      content: ""; position: absolute; inset: 0;
      background-image: radial-gradient(rgba(255,255,255,.5) .5px, transparent .5px);
      background-size: 3px 3px; opacity: .4; mix-blend-mode: overlay;
    }
    @keyframes breathe { 50% { opacity: .82; } }

    .stage { position: relative; z-index: 1; }
    .wrap { max-width: 1200px; margin: 0 auto; padding: 0 28px; }

    /* ===== top promo strip ===== */
    .promo {
      position: relative; z-index: 60;
      display: flex; align-items: center; justify-content: center; gap: 12px;
      padding: 9px 46px 9px 20px; font-size: 13px; color: var(--ink-2);
      background: linear-gradient(90deg, rgba(254,2,153,.14), rgba(247,240,98,.09));
      border-bottom: 1px solid var(--rim-soft);
    }
    .promo b { color: #fff; font-weight: 600; }
    .promo a { color: #fff; font-weight: 600; text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.45); }
    .promo .x {
      position: absolute; right: 16px; top: 50%; transform: translateY(-50%);
      background: none; border: 0; color: var(--ink-3); font-size: 18px; line-height: 1; cursor: pointer;
    }
    .promo .x:hover { color: #fff; }
    .promo.hidden { display: none; }
    @media (max-width: 640px) { .promo { font-size: 11.5px; padding: 8px 38px 8px 14px; gap: 8px; } }

    /* ===== nav ===== */
    header {
      position: sticky; top: 0; z-index: 50;
      backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
      background: linear-gradient(180deg, rgba(6,9,16,.82), rgba(6,9,16,.4));
      border-bottom: 1px solid var(--rim-soft);
    }
    header .row { display: flex; align-items: center; justify-content: space-between; height: 72px; }
    .logo img { height: 32px; display: block; }

    .nav { display: flex; align-items: center; gap: 4px; }
    .navitem { position: relative; }
    .navitem > a, .navitem > button {
      display: inline-flex; align-items: center; gap: 6px;
      padding: 8px 14px; border: 0; background: none; cursor: pointer;
      color: var(--ink-3); font-family: var(--font); font-size: 14.5px; font-weight: 500;
      border-radius: 10px; text-decoration: none;
      transition: color .16s, background .16s;
    }
    .navitem > a:hover, .navitem:hover > button, .navitem.open > button { color: var(--ink); background: rgba(255,255,255,.05); }
    .navitem > a.is-current { color: var(--ink); }
    .navitem .chev {
      width: 7px; height: 7px; border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
      transform: rotate(45deg); margin-top: -3px; opacity: .65; transition: transform .2s;
    }
    .navitem.open .chev { transform: rotate(-135deg); margin-top: 2px; }

    .megamenu {
      position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
      z-index: 70;
      padding-top: 12px;                        /* invisible hover bridge */
      opacity: 0; visibility: hidden; pointer-events: none;
      transition: opacity .18s ease;
    }
    .navitem.open .megamenu { opacity: 1; visibility: visible; pointer-events: auto; }
    .mm-panel {
      display: grid; gap: 6px 32px; padding: 22px 26px;
      background: #10131c;
      border: 1px solid var(--rim-soft); border-radius: 18px;
      box-shadow: 0 30px 80px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.06);
    }
    .mm-panel.cols-4 { grid-template-columns: repeat(4, minmax(138px, 1fr)); }
    .mm-panel.cols-3 { grid-template-columns: repeat(3, minmax(150px, 1fr)); }
    .mm-col h4 {
      font-family: var(--mono); font-size: 10px; letter-spacing: .15em; text-transform: uppercase;
      color: var(--ink-4); margin: 0 0 10px;
    }
    .mm-col a {
      display: block; color: var(--ink-2); font-size: 13.5px; padding: 6px 0; text-decoration: none;
      transition: color .14s;
    }
    .mm-col a:hover { color: #fff; }

    /* the right-hand cluster: sign-in + CTA + (under 980px) the menu button */
    .row-end { display: flex; align-items: center; gap: 10px; }
    .signin-link {
      display: inline-flex; align-items: center; height: 40px; padding: 0 14px;
      border-radius: var(--r-pill); text-decoration: none;
      color: var(--ink-2); font-size: 14px; font-weight: 500;
      transition: color .16s, background .16s;
    }
    .signin-link:hover { color: var(--ink); background: rgba(255,255,255,.05); }
    /* [hidden] is only the UA stylesheet's display:none — the rule above wins
       over it, so hiding the link has to be said explicitly. */
    .signin-link[hidden] { display: none; }
    /* Sign out, to the right of the Pro badge or the coin pill. Quieter than
       either — it is the thing nobody came here to press. */
    .signout-link {
      display: inline-flex; align-items: center; height: 40px; padding: 0 14px;
      border-radius: var(--r-pill); cursor: pointer;
      background: transparent; border: 1px solid var(--rim);
      color: var(--ink-2); font-family: inherit; font-size: 14px; font-weight: 500;
      transition: color .16s, background .16s, border-color .16s;
    }
    .signout-link:hover { color: var(--ink); background: rgba(255,255,255,.05); border-color: rgba(255,255,255,.22); }
    .signout-link[hidden] { display: none; }
    .signout-link:disabled { opacity: .6; cursor: default; }
    /* Below 980px the bar has no room; the drawer carries it instead. */
    @media (max-width: 980px) { .signout-link { display: none; } }
    /* Signed in: the initial stands in for the whole email, which never fits. */
    .signin-link .avatar {
      display: inline-flex; align-items: center; justify-content: center;
      width: 26px; height: 26px; margin-right: 8px; border-radius: 50%;
      background: var(--pill-bg); color: #fff; font-size: 12px; font-weight: 700;
      text-shadow: 0 1px 2px rgba(0,0,0,.25);
    }
    @media (max-width: 640px) {
      /* the drawer carries both, and 375px has room for neither */
      .signin-link { display: none; }
    }

    .burger {
      display: none;                    /* desktop has the real nav */
      flex-direction: column; justify-content: center; gap: 4px;
      width: 40px; height: 40px; padding: 0 10px;
      background: var(--glass); border: 1px solid var(--rim-soft);
      border-radius: 12px; cursor: pointer;
    }
    .burger span {
      display: block; height: 1.6px; background: var(--ink); border-radius: 2px;
      transition: transform .2s ease, opacity .15s ease;
    }
    /* three bars fold into a cross: the middle one fades, the outer two meet */
    .burger.is-open span:nth-child(1) { transform: translateY(5.6px) rotate(45deg); }
    .burger.is-open span:nth-child(2) { opacity: 0; }
    .burger.is-open span:nth-child(3) { transform: translateY(-5.6px) rotate(-45deg); }

    @media (max-width: 980px) {
      .nav { display: none; }
      .burger { display: flex; }
    }

    /* ===== mobile drawer (built from `.nav` by js/site.js) ===== */
    /* z-index sits under the header (50) and the promo strip (60), and above
       the hero (1) and the showcase panel (2) — see js/site.js for why the
       header has to stay on top. */
    .drawer {
      position: fixed; inset: 0; z-index: 45;
      background: rgba(3,5,8,.6);
      opacity: 0; visibility: hidden;
      transition: opacity .2s ease;
    }
    .drawer.open { opacity: 1; visibility: visible; }
    .drawer-panel {
      position: absolute; inset: 0 0 auto; max-height: 100%; overflow-y: auto;
      -webkit-overflow-scrolling: touch;
      display: flex; flex-direction: column; gap: 4px;
      /* padding-top is set in js/site.js from the header's real height */
      padding: 100px 20px calc(28px + env(safe-area-inset-bottom));
      background: #0a0e17;
      border-bottom: 1px solid var(--rim-soft);
      box-shadow: 0 30px 80px rgba(0,0,0,.6);
      transform: translateY(-10px);
      transition: transform .24s cubic-bezier(.4,0,.2,1);
    }
    /* the header (and its close button) stay on top of the sheet */
    .drawer.open .drawer-panel { transform: none; }

    .dgroup { border-bottom: 1px solid var(--rim-soft); }
    .dgroup > summary {
      display: flex; align-items: center; justify-content: space-between;
      padding: 15px 2px; cursor: pointer; list-style: none;
      color: var(--ink); font-size: 17px; font-weight: 600;
    }
    .dgroup > summary::-webkit-details-marker { display: none; }
    .dgroup > summary::after {
      content: ""; width: 8px; height: 8px; margin-right: 4px;
      border-right: 1.8px solid var(--ink-3); border-bottom: 1.8px solid var(--ink-3);
      transform: rotate(45deg) translateY(-2px); transition: transform .2s ease;
    }
    .dgroup[open] > summary::after { transform: rotate(-135deg) translateY(-2px); }
    .dcol { padding: 2px 2px 14px; }
    .dcol h4 {
      font-family: var(--mono); font-size: 9.5px; letter-spacing: .15em; text-transform: uppercase;
      color: var(--ink-4); margin: 8px 0 4px;
    }
    .dcol a {
      display: block; padding: 9px 0; color: var(--ink-2); font-size: 15px; text-decoration: none;
    }
    /* The drawer's Sign out is a `.dlink` like the rest, but a button, so it
       needs the parts a button does not inherit. */
    .dlink-out {
      width: 100%; text-align: left; background: none; border: 0;
      font-family: inherit; cursor: pointer; color: var(--ink-2);
    }
    .dlink[hidden] { display: none; }
    .dlink {
      display: block; padding: 15px 2px; color: var(--ink); font-size: 17px; font-weight: 600;
      text-decoration: none; border-bottom: 1px solid var(--rim-soft);
    }
    .dlink[aria-current] { color: var(--pink); }
    .dlink[hidden] { display: none; }
    .dcta { margin-top: 20px; width: 100%; }

    /* Nothing behind the sheet is visible, so freezing the scroller costs no
       layout — and it keeps the page still while the menu is open. */
    .nav-open, .nav-open body { overflow: hidden; }
    /* The takeover's own lock, deliberately not `nav-open`. It opens on top of
       the template dialog and outlives it — the dialog closing underneath
       would otherwise take the shared lock with it and leave the page
       scrolling behind a full-screen overlay. */
    .gp-open, .gp-open body { overflow: hidden; }
    /* shared gradient-pill treatment (nav CTA + hero primary) — uses the app's
       own create-button background, see --pill-bg. */
    .gpill {
      background: var(--pill-bg);
      border: 0;
      color: #ffffff;
      text-shadow: 0 1px 2px rgba(0,0,0,.22);
      box-shadow: 0 10px 26px -10px rgba(0,0,0,.55), 0 2px 12px rgba(254,2,153,.14);
      transition: transform .18s ease, box-shadow .22s ease, filter .2s ease;
    }
    .gpill:hover {
      transform: translateY(-2px);
      filter: brightness(1.05);
      box-shadow: 0 16px 34px -10px rgba(0,0,0,.6), 0 4px 16px rgba(254,2,153,.18);
    }
    .gpill:active { transform: translateY(0); }

    .navcta {
      display: inline-flex; align-items: center; height: 40px; padding: 0 20px;
      border-radius: var(--r-pill); font-size: 14px; font-weight: 600; text-decoration: none;
    }
    /* ===== hero copy ===== */
    /* whole hero — headline, fan and caption — is centred in the viewport so
       the caption always sits directly under the cards, whatever the height */
    .hero {
      text-align: center;
      /* the card width drives both the fan height and each card — and is
         capped by BOTH viewport width and height, so the whole hero
         (headline + fan + caption) fits on screen at any desktop size. */
      --card-w: min(52vw, 640px, 62svh);
      /* The hero scrolls away with the page like any other section — the
         showcase below simply follows it up. Nothing sticks, nothing is
         covered. At rest the hero ends exactly at the fold. */
      position: relative; z-index: 1;
      /* 72px header + the promo strip, which is two lines on a phone and gone
         once dismissed — js/site.js measures it into --promo-h. */
      min-height: calc(100svh - 72px - var(--promo-h, 34px));
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      padding: clamp(10px, 1.8vh, 22px) 0;
    }
    /* Scrolled past: the fan's videos are paused in JS (see main.js) — there
       is no reason to composite four 3D-transformed layers nobody can see. */
    .hero-idle .mcard { will-change: auto; }
    .hero h1 {
      font-family: var(--font); font-weight: 600;
      font-size: min(clamp(34px, 5.4vw, 72px), 8.4svh); line-height: 1.03;
      letter-spacing: -0.038em; margin: 0 auto clamp(8px, 1.4vh, 12px); color: var(--ink);
      text-wrap: balance;
    }
    .hero h1 .g {
      background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
    }
    .hero .pills { display: flex; gap: 12px; justify-content: center; margin-top: clamp(14px, 2vh, 22px); }
    .btn {
      display: inline-flex; align-items: center; justify-content: center;
      height: 48px; padding: 0 24px; border-radius: var(--r-pill);
      font-family: inherit; font-size: 15px; font-weight: 600; text-decoration: none;
      white-space: nowrap;             /* "Get started free" is not three lines */
      border: 1px solid transparent; cursor: pointer; transition: transform .16s, box-shadow .16s, border-color .16s;
    }
    /* .btn-grad inherits the .gpill treatment — and drops .btn's 1px border so
       it reads exactly like the nav "Get the app" pill (no edge seam). */
    .btn.gpill { border: 0; }
    .btn-glass { background: var(--glass-2); color: var(--ink); border-color: var(--rim); backdrop-filter: blur(12px); transition: transform .16s, border-color .16s, background .16s; }
    .btn-glass:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.3); background: rgba(255,255,255,.14); }

    /* ===== coverflow ===== */
    .flow {
      position: relative;
      width: 100%;
      /* exactly the centre card's height + a little breathing room, so the
         caption always hugs the cards */
      height: calc(var(--card-w) * 0.5625 + 12px);
      /* The headline and its two pills read as their own thing, not as a label
         on the fan, so they get real air underneath. The hero centres its whole
         stack, so widening this gap is what lifts the copy: it does not move
         the copy on its own, it pushes the two halves apart.
         The card's svh cap pays for it (66 -> 62 above). On a short laptop the
         fan is the tallest thing here and the hero has to end exactly at the
         fold — buying the gap without giving the space back somewhere would
         push the caption off it. */
      margin-top: clamp(32px, 7.5vh, 88px);
      perspective: 1700px;
      perspective-origin: 50% 44%;
      cursor: pointer;               /* inherits down to the cards / video */
    }
    .track { position: absolute; inset: 0; transform-style: preserve-3d; }

    .mcard {
      --w: var(--card-w);
      position: absolute; left: 50%; top: 0;
      width: var(--w); aspect-ratio: 16 / 9;
      margin-left: calc(var(--w) / -2);
      border-radius: 20px; overflow: hidden; cursor: pointer;
      background: linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.02)), #0b0f1a;
      border: 1px solid var(--rim);
      transition: transform .85s cubic-bezier(.4,0,.2,1), opacity .85s cubic-bezier(.4,0,.2,1), filter .85s ease;
      will-change: transform, opacity;
      transform:
        translateX(var(--x, 0))
        translateY(var(--y, 0))
        translateZ(var(--z, 0))
        rotateY(var(--ry, 0))
        scale(var(--s, 1));
      opacity: var(--o, 1);
      filter: blur(var(--blur, 0)) brightness(var(--br, 1));
      z-index: var(--zi, 1);
      box-shadow: 0 40px 90px rgba(0,0,0,.5);
    }
    /* diagonal glass rim (GlassRimPainter equivalent) */
    .mcard::after {
      content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1px;
      background: linear-gradient(135deg, rgba(255,255,255,.42), rgba(255,255,255,.05) 50%, rgba(254,2,153,.16));
      -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
      -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none;
    }
    .mcard .mvid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; background: #0b0f1a; display: block;
    }

    /* tap feedback — a quick light bloom over the card the user clicked */
    .mcard .tapfx {
      position: absolute; inset: 0; z-index: 3; pointer-events: none; opacity: 0;
      background: radial-gradient(65% 65% at 50% 50%, rgba(255,255,255,.5), rgba(255,255,255,0) 72%);
      mix-blend-mode: screen;
    }
    .mcard.tap .tapfx { animation: tapfade .5s cubic-bezier(.2,.7,.2,1); }
    @keyframes tapfade { 0% { opacity: .38; } 100% { opacity: 0; } }

    /* hover state — a shadow-like scrim on whichever card the pointer is over
       (JS-driven, since 3D-transformed cards don't fire :hover reliably) */
    .mcard .hoverfx {
      position: absolute; inset: 0; z-index: 2; pointer-events: none; opacity: 0;
      background: radial-gradient(120% 120% at 50% 46%, rgba(0,0,0,.14), rgba(0,0,0,.34) 100%);
      transition: opacity .22s ease;
    }
    .mcard.hover .hoverfx { opacity: 1; }
    /* the hovered card also drops slightly into shadow */
    .mcard.hover { filter: blur(var(--blur, 0)) brightness(.93) !important; }

    /* The prompt box — the concept, said out loud on the card: the video is the
       output, and the box is the input that would have produced it. OpenArt
       puts a "Direct this" pill on its genre cards; this goes a step further
       and shows the prompt itself, per card, from data-prompt in the markup.

       On by default for the centred card rather than revealed on hover: an
       affordance that only exists once you point at it cannot carry a concept
       to somebody who is merely reading the page. The side cards keep it
       hidden — at 88% scale and 27deg of rotation a line of 13px text is
       unreadable noise, not information. */
    .mcard .mprompt {
      position: absolute; z-index: 4;
      /* Narrow and tall rather than a wide strip: a composer shape reads as
         something you type into, where a full-width bar reads as a caption.
         Centred by transform, so the width can change without touching the
         insets. */
      left: 50%; bottom: 14px;
      /* Sized in px, not a percentage of the card. The prompt is a fixed
         string at a fixed 13px, so what it needs is a fixed number of pixels
         (248 for the longest of the three, plus the caret, the gap, the Create
         chip and the padding = 372). The card, though, is
         min(52vw, 640px, 66svh) — so the same text needs 58% of a wide card
         and 81% of a short-window one, and any single percentage clips
         somewhere. max-width keeps it inside the card when the window gets too
         short for even that; past there the run scrolls and the caret stays in
         view, which is the honest fallback rather than a hidden tail. */
      width: 384px; max-width: calc(100% - 24px);
      /* One row: text on the left, Create on the right. The column layout
         stacked them and cost a whole extra row of height for nothing the
         shape was not already saying. */
      display: flex; align-items: center; gap: 10px;
      padding: 7px 7px 7px 14px;
      border: 1px solid var(--rim);
      border-radius: 16px;
      /* Flat, and no backdrop-filter on purpose. These cards are 3D
         transformed and their own transition animates `filter`, so a live blur
         surface here would be re-sampled through that for the whole 850ms of
         every rotation — the one place on this page that cannot afford it.

         Nearly opaque, and that is not a style choice: this box lies over
         whatever frame the video happens to be on. At .72 the background lifts
         toward the artwork, and on a bright frame (the interior shots go almost
         white) placeholder-grey text on it fell to roughly 1:1 — invisible. At
         .92 the worst case still clears 4.5:1, and a real input is solid
         anyway. */
      background: rgba(11, 15, 26, .92);
      cursor: pointer; text-align: left;
      opacity: 0; transform: translateX(-50%) translateY(6px);
      pointer-events: none;
      transition: opacity .28s ease, transform .28s ease, border-color .2s ease;
    }
    .mcard.is-center .mprompt { opacity: 1; transform: translateX(-50%); pointer-events: auto; }
    .mcard.is-center.hover .mprompt { border-color: rgba(255,255,255,.28); }
    /* Pointer on the box: it lifts and firms up. Real :hover rather than the
       JS-driven .mcard.hover class — that class exists because side cards are
       rotated 27deg and do not hit-test reliably, but the centred card carries
       no rotation at all, so the pointer lands on this button honestly.
       translateX(-50%) has to be repeated: it is what centres the box, and a
       transform declaration replaces rather than adds to the previous one.
       -3px matches the lift .gpill and .btn-glass already use on this site. */
    .mcard.is-center .mprompt:hover {
      transform: translateX(-50%) translateY(-3px);
      border-color: rgba(255,255,255,.38);
      background: rgba(11, 15, 26, .96);
      box-shadow: 0 14px 30px -12px rgba(0,0,0,.6);
    }
    .mcard.is-center .mprompt:hover .mprompt-go { filter: brightness(1.08); }

    .mprompt-text {
      font-family: var(--font); font-size: 13px; line-height: 1.45;
      /* --ink-2, not the dimmer --ink-3 a placeholder would normally take: it
         still reads as unentered text next to the white "Create" label, but
         survives a bright frame underneath. */
      color: var(--ink-2);
      /* Exactly one line, and it must never become two: the text arrives a
         character at a time, so a wrap mid-sentence would shove the Create
         button down while you are reading it. `nowrap` guarantees the height,
         and the overflow scrolls instead — main.js keeps the caret in view the
         way a real input does, rather than clipping it off the end. */
      height: 1.45em;
      /* flex-1 with min-width:0 is what lets the run be narrower than its own
         text — without the zero minimum a flex item refuses to shrink past its
         content and the box would widen instead of scrolling. */
      flex: 1; min-width: 0;
      white-space: nowrap; overflow: hidden;
    }
    /* The caret rides in the text flow, immediately after the last character
       typed. steps(1) rather than a fade: a caret that eases is a glow. */
    .mprompt-caret {
      display: inline-block; width: 1.5px; height: 1.05em;
      margin-left: 1.5px; vertical-align: -2px;
      background: var(--pink);
      animation: mcaret 1.05s steps(1, end) infinite;
    }
    @keyframes mcaret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

    .mprompt-go {
      flex: none; display: inline-flex; align-items: center; gap: 6px;
      height: 30px; padding: 0 13px; border-radius: var(--r-pill);
      background: var(--pill-bg); color: #ffffff;
      font-family: var(--font); font-size: 12.5px; font-weight: 600;
      text-shadow: 0 1px 2px rgba(0,0,0,.22);
      box-shadow: 0 6px 16px -6px rgba(0,0,0,.5);
      transition: filter .2s ease;
    }
    .mprompt-go .spk { width: 14px; height: 14px; object-fit: contain; }
    .mcard.is-center.hover .mprompt-go { filter: brightness(1.06); }

    /* No blinking caret for anyone who asked for less motion; the line is
       written out in full by the script in that case, so it has nothing left
       to point at. */
    @media (prefers-reduced-motion: reduce) {
      .mprompt-caret { display: none; }
    }

    /* connecting light beams behind the centre card */
    .beam {
      position: absolute; top: 34%; height: 10px; width: 22%;
      filter: blur(3px); z-index: 0; opacity: .4;
    }
    .beam.l { left: 18%; background: linear-gradient(90deg, transparent, rgba(254,2,153,.26)); transform: rotate(7deg); }
    .beam.r { right: 18%; background: linear-gradient(270deg, transparent, rgba(247,240,98,.22)); transform: rotate(-7deg); }

    .caption {
      position: relative; z-index: 6; text-align: center; margin-top: 12px;
    }
    .caption .c-title {
      display: block; color: var(--ink); font-size: 15px; font-weight: 600; letter-spacing: -0.01em;
    }
    .caption .c-sub {
      display: block; color: var(--ink-3); font-size: 13.5px; margin-top: 3px;
    }

    /* ===== showcase panel ===== */
    /* An ordinary section in the flow: it follows the hero up the page on a
       normal scroll. No sheet, no rounded lip, no cast shadow — and no
       background of its own, so the page's one backdrop shows through. */
    .panel {
      position: relative; z-index: 2;
      padding: clamp(54px, 7vh, 96px) 0 clamp(64px, 8vh, 112px);
      isolation: isolate;
    }
    .wrap-wide { max-width: 1560px; }

    /* ===== template detail dialog (/js/templatedialog.js) ===== */
    /* The app's Template Detail screen, laid out for a desktop the way glam.ai
       lays its own out: the clip on the left, everything you would fill in on
       the right. Sits at 90 — above the header (50), the promo strip (60) and
       the mega-menus (70), and deliberately below the sign-in dialog (95),
       which is what the Use Effect button hands you to.
       Same `visibility`/`opacity`/`translateZ(0)` treatment as .authdlg, for
       the same reason: the hero's 3D-transformed fan is a composited layer and
       would otherwise draw straight through a fixed overlay. */
    .tdlg {
      position: fixed; inset: 0; z-index: 90;
      transform: translateZ(0);
      visibility: hidden; pointer-events: none;
      display: flex; align-items: center; justify-content: center;
      padding: 24px;
      background: rgba(3, 5, 8, .74);
      opacity: 0; transition: opacity .2s ease;
    }
    .tdlg.is-open { opacity: 1; visibility: visible; pointer-events: auto; }

    .tdlg-card {
      display: grid; grid-template-columns: minmax(0, 1fr) 400px;
      width: 100%; max-width: 1180px; height: min(88svh, 820px);
      overflow: hidden;
      background: #0c1018;
      border: 1px solid var(--rim-soft); border-radius: 26px;
      box-shadow: 0 40px 100px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
      transform: translateY(10px) scale(.985);
      transition: transform .22s cubic-bezier(.4,0,.2,1);
    }
    .tdlg.is-open .tdlg-card { transform: none; }

    /* ---- the stage ---- */
    .tdlg-stage { position: relative; overflow: hidden; background: #05070c; }
    /* A 9:16 clip in a landscape box leaves two thirds of the stage empty, so
       the poster fills it blurred — the same trick the card art plays behind a
       portrait video everywhere else. The still, never the video: a live
       backdrop-filter over playing footage is the one thing this page cannot
       afford (see the sign-in dialog's flat scrim). Scaled up because a blur
       samples past its own edges and would otherwise fade to nothing there. */
    .tdlg-blur {
      position: absolute; inset: 0;
      background: center / cover no-repeat;
      filter: blur(34px) saturate(1.25) brightness(.62);
      transform: scale(1.18);
    }
    .tdlg-poster, .tdlg-vid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: contain; display: block;
    }
    /* The poster holds the frame until the clip can paint over it, so the
       stage is never empty and never flashes black between the two. */
    .tdlg-vid[hidden] { display: none; }
    .tdlg-sound {
      position: absolute; top: 16px; right: 16px; z-index: 2;
      display: inline-flex; align-items: center; justify-content: center;
      width: 40px; height: 40px; padding: 0;
      color: #fff; background: rgba(10,12,20,.55);
      border: 1px solid var(--rim); border-radius: 50%;
      cursor: pointer; transition: background .16s;
    }
    .tdlg-sound:hover { background: rgba(10,12,20,.8); }
    .tdlg-sound svg { width: 20px; height: 20px; }
    .tdlg-sound[hidden] { display: none; }

    /* ---- the panel ---- */
    .tdlg-panel {
      display: flex; flex-direction: column; min-height: 0;
      padding: 22px 22px 16px;
      border-left: 1px solid var(--rim-soft);
      background: #0c1018;
    }
    .tdlg-head { display: flex; align-items: flex-start; gap: 12px; }
    /* the app's badge pill: white 16% on a white 8% rim */
    .tdlg-badge {
      display: inline-flex; align-items: center; gap: 4px;
      padding: 6px 10px 6px 8px; border-radius: var(--r-pill);
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.08);
      color: #fff; font-size: 12px; font-weight: 500; line-height: 16px;
    }
    .tdlg-badge img { width: 16px; height: 16px; display: block; }
    .tdlg-badge[hidden] { display: none; }
    .tdlg-x {
      margin-left: auto; flex: 0 0 auto;
      display: inline-flex; align-items: center; justify-content: center;
      width: 32px; height: 32px; padding: 0;
      color: var(--ink-3); background: transparent;
      border: 0; border-radius: 50%; cursor: pointer;
      transition: color .16s, background .16s;
    }
    .tdlg-x:hover { color: var(--ink); background: rgba(255,255,255,.06); }
    .tdlg-x svg { width: 18px; height: 18px; }

    .tdlg-title {
      margin: 12px 0 0; color: #fff;
      font-size: 24px; line-height: 30px; font-weight: 500; letter-spacing: -.01em;
    }
    .tdlg-sub {
      margin: 4px 0 0; color: var(--ink-2); font-size: 15px; line-height: 22px;
    }
    .tdlg-sub[hidden] { display: none; }

    /* Scrolls on its own, not with the panel: the price and the button are the
       two things that must never end up below the fold, and a three-slot
       template on a laptop is taller than the dialog. */
    .tdlg-slots {
      flex: 1 1 auto; min-height: 0; overflow-y: auto;
      display: flex; flex-direction: column; gap: 14px;
      margin-top: 18px; padding-right: 4px;
      scrollbar-width: thin;
    }
    .tdlg-slots[hidden] { display: none; }
    .tdlg-field-label {
      display: block; margin-bottom: 8px;
      color: #fff; font-size: 15px; line-height: 22px; font-weight: 500;
    }
    /* One treatment for every field the sheet has — the photo slot, the prompt
       area, the product name and its details: white 8% fill on a white 8% rim.
       They differ only in shape, which is exactly how the app draws them.
       The photo slot is a <button> over a hidden file input; the prompt and
       the two product fields are real inputs. */
    .tdlg-drop, .tdlg-text {
      display: flex; width: 100%; padding: 16px 14px;
      background: rgba(255,255,255,.08);
      border: 1px solid rgba(255,255,255,.08); border-radius: 20px;
      color: var(--ink-2); font-family: inherit; font-size: 14px; line-height: 20px;
      text-align: left; cursor: pointer;
      transition: background .16s, border-color .16s;
    }
    .tdlg-drop:hover, .tdlg-text:hover {
      background: rgba(255,255,255,.12); border-color: var(--rim);
    }
    .tdlg-drop {
      position: relative; overflow: hidden;
      flex-direction: column; align-items: center; justify-content: center;
      gap: 8px; min-height: 132px; padding: 18px 12px; text-align: center;
    }
    .tdlg-drop-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; }
    .tdlg-drop-empty[hidden] { display: none; }
    /* A filled slot becomes the photo, in the same box: the layout must not
       jump as three slots are filled one after another.

       Letterboxed over a blur of itself, exactly like the clip on the other
       half of this dialog (.tdlg-stage) — not cropped to fill. The app crops,
       and is right to on a phone where the slot is nearly the photo's own
       shape; this slot is a wide panel and cropping it throws away the top
       and bottom of every portrait photo someone is about to generate from. */
    .tdlg-drop-blur {
      position: absolute; inset: -24px;
      background: center / cover no-repeat;
      filter: blur(26px) saturate(1.15) brightness(.6);
    }
    .tdlg-drop-img {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: contain; object-position: center;
    }
    .tdlg-drop-blur[hidden], .tdlg-drop-img[hidden] { display: none; }
    .tdlg-drop.is-filled { padding: 0; border-color: rgba(255,255,255,.22); }
    .tdlg-drop-x {
      position: absolute; top: 8px; right: 8px; z-index: 2;
      width: 26px; height: 26px; display: grid; place-items: center;
      border-radius: 999px; background: rgba(6,8,14,.72); color: #fff;
      border: 1px solid rgba(255,255,255,.2); font-size: 11px; line-height: 1;
    }
    .tdlg-drop-x:hover { background: rgba(6,8,14,.92); }
    .tdlg-drop-x[hidden] { display: none; }
    .tdlg-glyph { width: 36px; height: 36px; display: block; }
    /* a written answer is longer than one line; the name of a product is not */
    .tdlg-text-tall { align-items: flex-start; min-height: 104px; }
    .tdlg-text-pill { align-items: center; border-radius: var(--r-pill); }
    /* These are real inputs now (see the note in js/widgets/sheets/templatedialog.js), so
       they need the field resets a <button> did not: the browser's own font,
       no resize grip, no focus ring of its own on top of the rim. */
    textarea.tdlg-text, input.tdlg-text {
      display: block; cursor: text; resize: none;
      color: #fff; font-size: 15px; line-height: 22px;
      -webkit-appearance: none; appearance: none; outline: 0;
    }
    textarea.tdlg-text { min-height: 104px; }
    textarea.tdlg-text:focus, input.tdlg-text:focus {
      background: rgba(255,255,255,.12); border-color: rgba(255,255,255,.34);
    }
    /* the app's placeholder grey, and the example line under the name field */
    .tdlg-hint, .tdlg-text::placeholder { color: #7c8088; font-size: 15px; line-height: 22px; }
    .tdlg-example {
      display: block; margin: 4px 0 0 16px;
      color: var(--ink-4); font-size: 12.5px; line-height: 18px;
    }
    /* Why a press did nothing, where the press was — kept for the failures
       that are the backend's rather than the form's (no credits, too many
       jobs already running). A field left empty gets the glow and the toast
       below instead. */
    .tdlg-note {
      flex: 0 0 auto; margin: 10px 2px 0; font-size: 13px; line-height: 19px; color: var(--ink-3);
    }
    .tdlg-note.is-bad { color: #ff9a9a; }
    .tdlg-note[hidden] { display: none; }
    .tdlg-go.is-busy { opacity: .7; cursor: default; }

    /* ---- PromptValidationGlow ----
       genva-app/lib/widgets/sections/prompt_parts.dart. Press Create with a
       field still empty and its rim lights for three seconds, then fades:
       a white 1.5px border at .9, over a thin inward fade that hugs the edge
       rather than washing across the field's whole interior. Two layers, both
       on the wrapper, because a `<textarea>` cannot carry a pseudo-element.

       Deliberately not a red error state and deliberately not a disabled
       button: the app answers a press by pointing at what it still needs. */
    .tdlg-glow { position: relative; }
    .tdlg-glow-rim {
      position: absolute; inset: 0; border-radius: 20px; pointer-events: none;
      border: 1.5px solid rgba(255,255,255,.9);
      box-shadow: inset 0 0 5px 1.75px rgba(255,255,255,.35);
      opacity: 0; transition: opacity .2s ease;
    }
    .tdlg-field.is-missing .tdlg-glow-rim { opacity: 1; }
    /* the pill-shaped one, so the product-name field's rim follows its shape */
    .tdlg-glow:has(> .tdlg-text-pill) .tdlg-glow-rim { border-radius: var(--r-pill); }

    /* ---- Recent ----
       `_RecentSection`. Three across at the designed size, the rest scrolling
       in beside them rather than being unreachable past the third. */
    .tdlg-recent { display: flex; flex-direction: column; gap: 12px; }
    .tdlg-recent[hidden] { display: none; }
    .tdlg-recent-title { font-size: 17px; line-height: 24px; font-weight: 500; color: #fff; }
    .tdlg-recent-row {
      display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 24px) / 3);
      gap: 12px; overflow-x: auto; padding-bottom: 4px;
      scrollbar-width: thin; overscroll-behavior-x: contain;
    }
    .tdlg-recent-item {
      position: relative; aspect-ratio: 1 / 1; padding: 0; overflow: hidden;
      border-radius: 20px; cursor: pointer;
      border: 1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.04);
      transition: border-color .16s, transform .16s;
    }
    .tdlg-recent-item:hover { border-color: rgba(255,255,255,.28); transform: translateY(-1px); }
    .tdlg-recent-item[hidden] { display: none; }
    .tdlg-recent-img { width: 100%; height: 100%; object-fit: cover; display: block; }

    /* ---- the tray: the app's BottomCta, one for one ---- */
    .tdlg-tray {
      flex: 0 0 auto; margin-top: 16px;
      padding: 6px 2px 2px;
      background: rgba(233, 199, 140, .12);
      border-radius: 16px 16px 28px 28px;
    }
    .tdlg-coins {
      display: flex; align-items: center; justify-content: center; gap: 4px;
      padding: 6px 0;
      color: var(--ink-2); font-size: 14px; line-height: 20px; font-weight: 500;
    }
    .tdlg-coins img { width: 18px; height: 18px; display: block; }
    .tdlg-coins[hidden] { display: none; }
    .tdlg-go {
      display: flex; align-items: center; justify-content: center; gap: 6px;
      width: 100%; height: 56px; padding: 0;
      background: var(--pill-bg); border: 0; border-radius: 26px;
      color: #fff; font-family: inherit; font-size: 18px; font-weight: 500;
      text-shadow: 0 1px 2px rgba(0,0,0,.22);
      cursor: pointer; transition: filter .16s;
    }
    .tdlg-go:hover { filter: brightness(1.06); }
    .tdlg-go .spk { width: 22px; height: 22px; display: block; }

    /* One column below the split: the stage keeps a fixed slice of the height
       and the panel takes the rest, so the button stays on screen instead of
       being pushed off by a full-height clip. */
    @media (max-width: 900px) {
      .tdlg { padding: 0; }
      .tdlg-card {
        grid-template-columns: 1fr; grid-template-rows: 42svh minmax(0, 1fr);
        max-width: none; height: 100svh; border-radius: 0; border: 0;
      }
      .tdlg-panel { border-left: 0; border-top: 1px solid var(--rim-soft); }
      .tdlg-title { font-size: 21px; line-height: 27px; }
      .tdlg-drop { min-height: 112px; }
      .tdlg-go { height: 50px; font-size: 16px; }
    }


    /* ===== Edit Home Design =====
       genva-app/lib/screens/edit_home_design_screen.dart, in the template
       detail's own card — the same 1180x88svh two-column plate, because the
       user asked for the same size and because the split is what makes the
       flow work on a desktop: the photo holds the left half for all four
       steps, and the steps take the right. On a phone the app has to scroll
       the photo away to show a grid; here it does not have to. */
    .dwz {
      position: fixed; inset: 0; z-index: 92;
      transform: translateZ(0);
      display: flex; align-items: center; justify-content: center; padding: 24px;
      background: rgba(3, 5, 8, .74);
      visibility: hidden; pointer-events: none; opacity: 0;
      transition: opacity .2s ease;
    }
    .dwz.is-open { opacity: 1; visibility: visible; pointer-events: auto; }
    .dwz-card {
      display: grid; grid-template-columns: minmax(0, 1fr) 420px;
      width: 100%; max-width: 1180px; height: min(88svh, 820px);
      overflow: hidden; background: #0c1018;
      border: 1px solid var(--rim-soft); border-radius: 26px;
      box-shadow: 0 40px 100px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
      transform: translateY(10px) scale(.985);
      transition: transform .22s cubic-bezier(.4,0,.2,1);
    }
    .dwz.is-open .dwz-card { transform: none; }

    /* ---- the photo, and the examples under it ---- */
    .dwz-stage {
      display: flex; flex-direction: column; gap: 20px; min-height: 0;
      padding: 24px; background: #05070c;
    }
    /* `_PhotoStep`'s well: white 16% at radius 20 with a 12% rim, and the
       app's own PromptValidationGlow when Continue is pressed without one. */
    .dwz-well {
      position: relative; flex: 1 1 auto; min-height: 0; width: 100%;
      display: grid; place-items: center; overflow: hidden; padding: 0;
      border-radius: 20px; cursor: pointer; font-family: inherit;
      background: rgba(255,255,255,.16);
      border: 1px solid rgba(255,255,255,.12);
      transition: border-color .2s ease;
    }
    .dwz-well.is-filled { cursor: default; background: #05070c; }
    .dwz-well.is-over { border-color: var(--amber); }
    .dwz-well-blur {
      position: absolute; inset: -24px;
      background: center / cover no-repeat;
      filter: blur(26px) saturate(1.15) brightness(.6);
    }
    .dwz-well-img {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: contain; object-position: center;
    }
    .dwz-well-blur[hidden], .dwz-well-img[hidden], .dwz-well-x[hidden],
    .dwz-well-empty[hidden] { display: none; }
    .dwz-well-empty {
      position: relative; z-index: 1;
      display: flex; flex-direction: column; align-items: center; gap: 4px;
      text-align: center; padding: 0 24px;
    }
    .dwz-glyph { width: 36px; height: 36px; display: block; }
    .dwz-well-title { margin-top: 4px; font-size: 16px; line-height: 24px; color: #C8CDD8; }
    .dwz-well-sub { font-size: 14px; line-height: 20px; color: rgba(255,255,255,.56); }
    .dwz-well-x {
      position: absolute; top: 12px; right: 12px; z-index: 2;
      width: 32px; height: 32px; display: grid; place-items: center;
      border-radius: 999px; font-size: 13px; color: #fff;
      background: rgba(75,79,88,.88); border: 1px solid rgba(255,255,255,.16);
    }
    .dwz-well-x:hover { background: rgba(75,79,88,1); }
    /* PromptValidationGlow — the same rim the template dialog's fields wear. */
    .dwz-well-rim, .dwz-glow-rim {
      position: absolute; inset: 0; border-radius: 20px; pointer-events: none;
      border: 1.5px solid rgba(255,255,255,.9);
      box-shadow: inset 0 0 5px 1.75px rgba(255,255,255,.35);
      opacity: 0; transition: opacity .2s ease;
    }
    .dwz-well.is-missing .dwz-well-rim,
    .dwz-glow.is-missing .dwz-glow-rim { opacity: 1; }

    /* `_PhotoStep`'s example photos, in step one's own body — three across,
       the rest scrolling in beside them. */
    .dwz-examples { display: grid; gap: 12px; margin-top: 24px; }
    .dwz-examples[hidden] { display: none; }
    .dwz-examples-title { font-size: 17px; line-height: 24px; font-weight: 500; color: #fff; }
    .dwz-examples-row {
      display: grid; grid-auto-flow: column; grid-auto-columns: calc((100% - 24px) / 3);
      gap: 12px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: thin;
    }
    .dwz-example-photo {
      position: relative; aspect-ratio: 1 / 1;
      padding: 0; overflow: hidden; border-radius: 16px; cursor: pointer;
      background: rgba(255,255,255,.04);
      border: 1px solid rgba(255,255,255,.08);
      transition: border-color .16s, transform .16s;
    }
    .dwz-example-photo:hover { transform: translateY(-1px); border-color: rgba(255,255,255,.3); }
    .dwz-example-photo.is-on { border-color: rgba(255,255,255,.88); border-width: 1.5px; }
    .dwz-example-img { width: 100%; height: 100%; object-fit: cover; display: block; }

    /* ---- the panel: nav, stepper, step, button ---- */
    .dwz-panel {
      display: flex; flex-direction: column; min-height: 0;
      padding: 22px 24px 0; border-left: 1px solid var(--rim-soft);
    }
    .dwz-nav { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; }
    .dwz-title {
      flex: 1 1 auto; min-width: 0; margin: 0;
      font-size: 22px; line-height: 30px; font-weight: 500; color: #fff;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .dwz-back, .dwz-x {
      flex: 0 0 auto; width: 34px; height: 34px; display: grid; place-items: center;
      border-radius: 999px; cursor: pointer; color: var(--ink-2);
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.08);
      transition: background .16s, color .16s;
    }
    .dwz-back:hover, .dwz-x:hover { background: rgba(255,255,255,.26); color: #fff; }
    .dwz-back[hidden] { display: none; }
    /* tpl_back.webp is 27x48 — a tall chevron, not a square glyph. Forced
       into a 15x15 box it came out stretched sideways; the app draws it
       `BoxFit.contain` inside its own box, which is height-driven. The ratio
       is stated rather than waited for, so the button does not reflow when
       the glyph finishes loading. */
    .dwz-back img { height: 15px; width: auto; aspect-ratio: 27 / 48; display: block; }
    .dwz-x { background: transparent; border-color: var(--rim-soft); }
    .dwz-x svg { width: 16px; height: 16px; }

    /* The stepper. Four segments at most; the ones past this flow's own
       length collapse to zero width over 320ms rather than being removed,
       which is what makes picking Custom grow a fourth bar in place instead
       of relaying the row. */
    .dwz-stepper { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; margin-top: 12px; }
    .dwz-stepper[hidden] { display: none; }
    .dwz-bar {
      flex: 1 1 0; height: 4px; border-radius: 999px; background: #1C2030;
      transition: flex-grow .32s cubic-bezier(.215,.61,.355,1),
                  margin .32s cubic-bezier(.215,.61,.355,1),
                  background .2s ease, box-shadow .2s ease;
    }
    .dwz-bar.is-off { flex-grow: 0; flex-basis: 0; margin-left: -8px; }
    .dwz-bar.is-done { background: #fff; }
    .dwz-bar.is-now { box-shadow: 0 0 6px .5px rgba(255,255,255,.6); }
    .dwz-count {
      flex: 0 0 auto; width: 34px; text-align: center;
      font-size: 14px; line-height: 20px; font-weight: 500; color: #C8CDD8;
    }

    .dwz-body {
      flex: 1 1 auto; min-height: 0; overflow-y: auto;
      margin: 20px -4px 0; padding: 0 4px; scrollbar-width: thin;
    }
    /* `_StepHeading` — 20/28 medium over 14/20 regular, 8 apart. One rule so
       the four steps cannot drift. */
    .dwz-head-title { margin: 0; font-size: 20px; line-height: 28px; font-weight: 500; color: #fff; }
    .dwz-head-sub { margin: 8px 0 0; font-size: 14px; line-height: 20px; color: #C8CDD8; }
    .dwz-step { display: flex; flex-direction: column; }
    .dwz-step .dwz-head { margin-bottom: 20px; }

    /* `_RoomCard` / `_SelectableGridCard` — two per row, a 140/84 plate at
       radius 12 with its label under it. Once something is picked the rest
       fade back, which is the app's own rule for this grid. */
    .dwz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
    .dwz-card-pick {
      display: flex; flex-direction: column; gap: 8px; padding: 0;
      background: none; border: 0; cursor: pointer; font-family: inherit;
      opacity: 1; transition: opacity .2s ease, transform .12s ease;
    }
    .dwz-grid.is-picked .dwz-card-pick { opacity: .7; }
    .dwz-grid.is-picked .dwz-card-pick.is-on, .dwz-card-pick:hover { opacity: 1; }
    .dwz-card-pick:active { transform: scale(.96); }
    .dwz-pick-art {
      position: relative; aspect-ratio: 140 / 84; overflow: hidden;
      border-radius: 12px; background: #1C2030;
      border: 1px solid rgba(255,255,255,.04);
      transition: border-color .2s ease, box-shadow .2s ease;
    }
    /* Exterior's real photographs sit on the white "background-active" card
       per Figma and shrink inward; interior's stay on the dark one. */
    .dwz-pick-art.is-light { background: #fff; }
    .dwz-card-pick.is-on .dwz-pick-art {
      border: 1.5px solid rgba(255,255,255,.88);
      box-shadow: 0 0 16px rgba(255,255,255,.08);
    }
    .dwz-pick-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
    .dwz-pick-img.is-contain { object-fit: contain; padding: 10px; }
    .dwz-pick-badge { position: absolute; top: 6px; left: 6px; height: 18px; width: auto; }
    .dwz-pick-name {
      font-size: 12px; line-height: 16px; font-weight: 500; color: #fff;
      text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }

    /* ---- the Custom Prompt step ---- */
    .dwz-glow { position: relative; }
    .dwz-prompt {
      display: block; width: 100%; min-height: 160px; resize: none; cursor: text;
      padding: 16px 12px 12px; border-radius: 20px;
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.08);
      color: #fff; font-family: inherit; font-size: 16px; line-height: 24px;
      -webkit-appearance: none; appearance: none; outline: 0;
      transition: background .18s;
    }
    .dwz-prompt:focus { background: rgba(255,255,255,.32); }
    .dwz-prompt::placeholder { color: #7C8088; }
    .dwz-sub-title { margin: 24px 0 16px; font-size: 17px; line-height: 24px; font-weight: 500; color: #fff; }
    /* `_ReadyMadePromptRow` — a full-width white-16% pill, one line, clipped. */
    .dwz-example {
      display: block; width: 100%; margin-bottom: 12px;
      padding: 12px 20px; border-radius: 999px; cursor: pointer;
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.08);
      color: #fff; font-family: inherit; font-size: 14px; line-height: 20px; font-weight: 500;
      text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      transition: background .16s, transform .12s ease;
    }
    .dwz-example:hover { background: rgba(255,255,255,.24); }
    .dwz-example:active { transform: scale(.98); }

    /* The button and nothing behind it — see the note in designwizard.js.
       A little more air under it than the tray gave, since there is no plate
       left to sit inside. */
    .dwz-tray { flex: 0 0 auto; padding: 16px 0 18px; }
    .dwz-tray .tdlg-go { border-radius: 26px; }

    /* One column below the split, like the template dialog: the photo keeps a
       fixed slice of the height and the steps take the rest, so the button
       never gets pushed off. */
    @media (max-width: 980px) {
      .dwz { padding: 0; }
      .dwz-card {
        grid-template-columns: 1fr; grid-template-rows: 34svh minmax(0, 1fr);
        max-width: none; height: 100svh; border-radius: 0; border: 0;
      }
      .dwz-stage { padding: 16px; }
      .dwz-panel { border-left: 0; border-top: 1px solid var(--rim-soft); padding: 16px 18px 0; }
      .dwz-tray { padding: 12px 0 14px; }
    }

    /* ===== sign-in dialog (/js/authdialog.js) ===== */
    /* Outranks everything in `.stage`, where it lives: the promo strip (60), the
       mega-menus (70), the header (50), the drawer (45).
       `translateZ(0)` is not decoration and must not be "cleaned up". The hero's
       fan is 3D transformed, so its cards are composited layers, and a
       composited layer paints over a non-composited fixed element no matter what
       the z-index says — the fan drew straight through this dialog, while
       hit-testing correctly reported the dialog on top (the pixels lied, not the
       DOM). Promoting the overlay too puts it in the same league, and then the
       z-index decides. */
    .authdlg {
      position: fixed; inset: 0; z-index: 95;
      transform: translateZ(0);
      /* `visibility` + `opacity`, never `display: none`. A display:none element
         has no compositor layer at all, so the promotion above only took effect
         a frame or two after it was shown — and those were exactly the frames
         where the fan drew through. Laid out from the start, the layer exists
         before anyone can see the dialog. `pointer-events` is what keeps a
         full-viewport invisible overlay from eating the page's clicks. */
      visibility: hidden; pointer-events: none;
      display: flex; align-items: center; justify-content: center;
      padding: 20px;
      /* Enough scrim to sit the card forward, not enough to black the page out:
         the page you were on stays legible around the edges, which is half the
         point of signing in without leaving it.
         Flat, with no backdrop-filter: a full-viewport blur sampling four
         playing videos every frame is the most expensive thing this page could
         be asked to do. The drawer's scrim is flat for the same reason. */
      background: rgba(4, 6, 11, .62);
      opacity: 0; transition: opacity .2s ease;
    }
    .authdlg.is-open { opacity: 1; visibility: visible; pointer-events: auto; }

    .authdlg-card {
      display: grid; grid-template-columns: 1fr 1fr;
      width: 100%; max-width: 860px; max-height: 100%;
      overflow: hidden;
      background: #0c1018;
      border: 1px solid var(--rim-soft); border-radius: 26px;
      box-shadow: 0 40px 100px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
      transform: translateY(10px) scale(.985);
      transition: transform .24s cubic-bezier(.4,0,.2,1);
    }
    .authdlg.is-open .authdlg-card { transform: none; }

    /* Left half: the product itself, moving. Two columns of real template
       posters drift past behind the pitch — the claim and the evidence in the
       same panel. The painted background stays underneath: it is what shows
       while the posters decode, and all there is on a page that does not carry
       the content snapshot. */
    .authdlg-aside {
      position: relative; overflow: hidden;
      background:
        radial-gradient(120% 90% at 15% 100%, rgba(254,2,153,.26) 0%, rgba(254,2,153,0) 62%),
        radial-gradient(90% 70% at 100% 0%, rgba(247,240,98,.10) 0%, rgba(247,240,98,0) 60%),
        linear-gradient(160deg, #14111f, #0a0d15);
      /* Opaque, deliberately not var(--rim-soft) like every other rim on the
         site. A 8%-white line has whatever is behind it showing through, and
         behind this one is the panel's own magenta floor-glow — so the divider
         read grey at the top and drifted pink toward the bottom. An opaque grey
         is immune to the gradient underneath it, and stays one colour whatever
         that glow is later tuned to. */
      border-right: 1px solid #262b36;
    }
    .authdlg-stream {
      /* Bled to the panel's left and right edges on purpose: the wall is a
         backdrop, and a margin down its sides turns it into a framed picture
         sitting on the panel instead. `gap` still separates the columns from
         each other — that is interior spacing, not an edge. Vertically it
         over-hangs by 10% so the loop's seam is always outside the panel. */
      position: absolute; inset: -10% 0; display: flex; gap: 6px;
    }
    .stream-col {
      flex: 1;
      /* Each column carries its cards twice and slides exactly -50%, so the
         loop has no seam. The spacing is a margin per card rather than a `gap`
         for the same reason: with a gap, one set is not exactly half the column
         and the loop jumps by one gap every pass. */
      animation: streamUp 26s linear infinite;
    }
    /* Three speeds and two directions: columns moving together read as one
       sliding sheet, which is not what this is. */
    .stream-col:nth-child(2) { animation-duration: 34s; animation-direction: reverse; }
    .stream-col:nth-child(3) { animation-duration: 30s; }
    .stream-col img,
    .stream-col video {
      display: block; width: 100%; aspect-ratio: 9 / 16; object-fit: cover;
      margin-bottom: 6px; border-radius: 10px; background: #0b0f1a;
    }
    @keyframes streamUp { to { transform: translateY(-50%); } }

    /* The copy sits over the wall, and this gradient is what keeps it readable
       whatever poster happens to be sliding past underneath. */
    .authdlg-copy {
      position: relative; z-index: 1; height: 100%;
      display: flex; flex-direction: column; justify-content: flex-end; gap: 16px;
      padding: 36px 32px;
      background: linear-gradient(180deg,
        rgba(6,9,16,.30) 0%, rgba(6,9,16,.62) 42%, rgba(6,9,16,.94) 100%);
    }
    .authdlg-logo { width: 100px; height: auto; }
    .authdlg-pitch {
      margin: 0; color: var(--ink); font-size: 25px; font-weight: 600;
      line-height: 1.12; letter-spacing: -0.032em; text-wrap: balance;
    }
    .authdlg-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 10px; }
    .authdlg-list li {
      position: relative; padding-left: 20px;
      color: var(--ink-3); font-size: 13.5px; line-height: 1.45;
    }
    .authdlg-list li::before {
      content: ""; position: absolute; left: 2px; top: 7px;
      width: 6px; height: 6px; border-radius: 50%; background: var(--grad);
    }

    .authdlg-main {
      position: relative;
      padding: 36px 32px 30px;
      overflow-y: auto; -webkit-overflow-scrolling: touch;
    }
    .authdlg-x {
      position: absolute; top: 14px; right: 14px;
      width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
      background: none; border: 0; border-radius: 10px; cursor: pointer;
      color: var(--ink-3); font-size: 22px; line-height: 1;
      transition: color .16s, background .16s;
    }
    .authdlg-x:hover { color: var(--ink); background: rgba(255,255,255,.06); }

    @media (max-width: 760px) {
      /* One column on a phone: the pitch panel is the half that can go, and a
         dialog that scrolls is better than one that does not fit. */
      .authdlg { padding: 0; }
      .authdlg-card {
        grid-template-columns: 1fr; max-width: none; height: 100%;
        border-radius: 0; border: 0;
      }
      .authdlg-aside { display: none; }
      .authdlg-main { padding: 64px 20px calc(32px + env(safe-area-inset-bottom)); }
    }

    /* ===== the paywall (/js/widgets/sheets/paywall.js) =====
       genva-app/lib/screens/paywall_screen.dart in the shape the web
       paywalls have — a plan grid, a billing switch, a tab of credit packs.
       Same layer rules as the sign-in dialog above (promoted, inert until
       `is-open`), one notch BELOW it: a signed-out visitor who picks a plan
       is handed the sign-in dialog, and that has to land on top of this. */
    .pw-open, .pw-open body { overflow: hidden; }
    .pw {
      position: fixed; inset: 0; z-index: 94;
      transform: translateZ(0);
      visibility: hidden; pointer-events: none;
      display: flex; align-items: center; justify-content: center;
      padding: 20px;
      background: rgba(4, 6, 11, .68);
      opacity: 0; transition: opacity .2s ease;
    }
    .pw.is-open { opacity: 1; visibility: visible; pointer-events: auto; }
    /* The app's `_kPageBg` (#030508) lifted to the dialog surface the sign-in
       card set, with the splash's magenta floor-glow under the plans and the
       amber corner up top. The page behind is locked and the whole offer is
       longer than a viewport on purpose, so the dialog scrolls — but on an
       inner `.pw-scroll`, not on this box: a scrollbar on the rounded box
       itself ran straight through its corners. This one clips, that one
       scrolls, and the bar ends where the corner does. */
    .pw-card {
      position: relative; display: flex;
      width: 100%; max-width: 1160px; max-height: 100%;
      overflow: hidden;
      background:
        radial-gradient(90% 60% at 50% 100%, rgba(254,2,153,.14) 0%, rgba(254,2,153,0) 60%),
        radial-gradient(60% 40% at 100% 0%, rgba(247,240,98,.06) 0%, rgba(247,240,98,0) 55%),
        #0c1018;
      border: 1px solid var(--rim-soft); border-radius: 28px;
      box-shadow: 0 40px 100px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
      transform: translateY(10px) scale(.985);
      transition: transform .24s cubic-bezier(.4,0,.2,1);
    }
    .pw-scroll {
      flex: 1 1 auto; min-width: 0; max-height: 100%;
      overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
      padding: 44px 48px 36px;
      scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.14) transparent;
    }
    .pw.is-open .pw-card { transform: none; }
    /* Nothing else is clickable while a checkout is opening, but the card
       keeps its opacity: fading it reads as the dialog closing, and the page
       behind coming through makes the click look like it dismissed the
       paywall. The pressed button carries the wait instead. */
    .pw.is-busy .pw-card { pointer-events: none; }
    .pw-cta.is-loading { cursor: progress; gap: 10px; }
    /* Waiting on /plans: the button is already drawn as it will be, and only
       declines the click. Deliberately not `disabled`, which would grey it
       and put back the flash this exists to remove. */
    .pw-cta.is-waiting { pointer-events: none; }
    .pw-cta-spin {
      flex: 0 0 auto; width: 16px; height: 16px; box-sizing: border-box;
      border-radius: 999px;
      border: 2px solid rgba(255,255,255,.28); border-top-color: #fff;
      animation: gpspin .8s linear infinite;
    }
    /* On the quiet buttons the ring has to read against a pale label. */
    .pw-cta-quiet .pw-cta-spin { border-color: rgba(255,255,255,.18); border-top-color: var(--ink-2); }
    @media (prefers-reduced-motion: reduce) { .pw-cta-spin { animation-duration: 2.4s; } }
    /* `_NavBar`'s close: the app's 40pt white-16% glass pill with the drop
       shadow that keeps the glyph readable over anything. */
    .pw-x {
      position: sticky; top: 0; float: right; z-index: 3;
      margin: -14px -18px 0 0;
      width: 40px; height: 40px; display: grid; place-items: center;
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.12);
      border-radius: 50%; cursor: pointer; color: #fff;
      backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
      box-shadow: 0 2px 12px rgba(0,0,0,.3);
      transition: background .16s, transform .16s;
    }
    .pw-x:hover { background: rgba(255,255,255,.24); transform: scale(1.04); }

    .pw-head {
      display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
      padding-right: 48px;
    }
    .pw-title {
      margin: 0 0 8px; color: var(--ink); font-size: 36px; font-weight: 600;
      line-height: 1.08; letter-spacing: -0.034em; text-wrap: balance;
    }
    .pw-sub { margin: 0; color: var(--ink-3); font-size: 15px; line-height: 1.5; max-width: 520px; }
    .pw-controls { display: flex; align-items: center; flex: 0 0 auto; padding-top: 8px; }

    .pw-billing { display: inline-flex; align-items: center; gap: 10px; }
    .pw-billing[hidden] { display: none; }
    .pw-billing-label { color: var(--ink-3); font-size: 14px; font-weight: 500; transition: color .16s; }
    .pw-billing:not(.is-year) .pw-billing-label:not(.is-year),
    .pw-billing.is-year .pw-billing-label.is-year { color: var(--ink); }
    /* The switch: a 44x24 track, the app's gradient once it is on. */
    /* The rim is an inset shadow, not a border — the same glass rim every
       pill on the page draws. A real border sat inside the gradient once the
       switch was on and left a pale seam round the track. */
    .pw-switch {
      position: relative; width: 44px; height: 24px; padding: 0; cursor: pointer;
      background: rgba(255,255,255,.14); border: 0; border-radius: 999px;
      box-shadow: inset 0 0 0 1px var(--rim);
      transition: background .2s, box-shadow .2s;
      -webkit-appearance: none; appearance: none;
    }
    .pw-switch-knob {
      position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%;
      background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.4);
      transition: transform .2s cubic-bezier(.4,0,.2,1);
    }
    .pw-switch[aria-checked="true"] { background: var(--grad); box-shadow: inset 0 1px 0 rgba(255,255,255,.22); }
    .pw-switch[aria-checked="true"] .pw-switch-knob { transform: translateX(20px); }
    .pw-switch:focus-visible { outline: 2px solid rgba(255,255,255,.5); outline-offset: 2px; }
    .pw-save {
      padding: 4px 10px; border-radius: 999px;
      background: rgba(254,2,153,.16); border: 1px solid rgba(254,2,153,.32);
      color: #ff8ccd; font-size: 12px; font-weight: 600; letter-spacing: .01em; white-space: nowrap;
    }
    .pw-save[hidden] { display: none; }

    .pw-note {
      margin: 20px 0 0; padding: 10px 14px; border-radius: 12px;
      background: rgba(255,196,69,.10); border: 1px solid rgba(255,196,69,.24);
      color: #ffd98a; font-size: 13px; line-height: 1.45;
    }
    .pw-note[hidden] { display: none; }

    .pw-pane { margin-top: 28px; }
    .pw-pane[hidden] { display: none; }

    /* ---- the plan grid ---- */
    /* Three cards — Free, Pro, Ultimate — laid across the whole dialog. At a
       third of the width a card is a column rather than the band the app's
       pair made, so the features run in one column here and only widen back
       to two when a card has the room (below, once the grid itself drops to
       two across). */
    .pw-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
    /* Rows to their content, not an even share of the box. They were an even
       share when the list was four features of roughly one height; it now
       opens with two one-line counts and a two-line footnote, and `1fr`
       stretched each of those to the height of the longest feature. */
    .pw-grid .pw-feats { display: grid; grid-template-columns: minmax(0, 1fr); grid-auto-rows: auto; column-gap: 18px; }
    /* `_PlanCard`: white-8% glass under a `GlassRimPainter` at 0.5, the
       inner glow, radius 20 — and, selected, the rim to 2.2 and the fill to
       24%. The featured card wears the selected state permanently, plus the
       magenta glow the app's Best Value badge implies. */
    .pw-plan {
      position: relative; display: flex; flex-direction: column;
      padding: 22px 24px 20px; border-radius: 22px;
      background: rgba(255,255,255,.05);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.10), inset 0 1px 0 rgba(255,255,255,.06), inset 0 0 24px rgba(255,255,255,.03);
      transition: box-shadow .2s, background .2s, transform .2s;
    }
    .pw-plan:hover { background: rgba(255,255,255,.07); box-shadow: inset 0 0 0 1px rgba(255,255,255,.18), inset 0 1px 0 rgba(255,255,255,.08), inset 0 0 24px rgba(255,255,255,.04); }
    .pw-plan.is-featured {
      background: linear-gradient(180deg, rgba(254,2,153,.10) 0%, rgba(255,255,255,.05) 40%);
      box-shadow: inset 0 0 0 1.5px rgba(254,2,153,.55), inset 0 1px 0 rgba(255,255,255,.10), 0 24px 60px -30px rgba(254,2,153,.55);
    }
    .pw-plan.is-featured:hover { box-shadow: inset 0 0 0 1.5px rgba(254,2,153,.75), inset 0 1px 0 rgba(255,255,255,.12), 0 28px 70px -30px rgba(254,2,153,.65); }
    .pw-plan-free { background: rgba(255,255,255,.03); }
    .pw-plan-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
    .pw-plan-name { margin: 0; color: var(--ink); font-size: 30px; font-weight: 600; letter-spacing: -0.03em; line-height: 1; }
    /* `BestValueBadge` — the gradient pill; on the second tier the same pill
       in the app's teal-ish "powerful" wash so the two are not twins. */
    .pw-badge {
      display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border-radius: 999px;
      background: var(--grad); color: #fff; font-size: 12px; font-weight: 600; letter-spacing: .01em;
      text-shadow: 0 1px 1px rgba(0,0,0,.2); white-space: nowrap;
    }
    /* Two lines reserved, so the price sits on the same line in both cards
       whatever the tagline's length. */
    .pw-plan-tag { margin: 8px 0 0; color: var(--ink-3); font-size: 14px; line-height: 1.45; min-height: calc(2 * 1.45em); }
    .pw-price { display: flex; align-items: baseline; gap: 4px; margin-top: 14px; color: var(--ink); }
    .pw-price-amount { font-size: 40px; font-weight: 600; letter-spacing: -0.04em; line-height: 1; }
    .pw-price-amount.is-missing { color: var(--ink-4); }
    .pw-price-per { font-size: 18px; font-weight: 500; color: var(--ink-2); }
    .pw-price-note { margin: 6px 0 0; color: var(--ink-3); font-size: 13px; }
    .pw-plan .pw-price-note { min-height: 18px; }

    /* `_ContinueButton` — 56px, full width. The featured card's is the
       site's gradient pill; the others the glass one; a state that is not an
       action ("Current plan", "Coming soon") is quiet and flat. */
    .pw-cta {
      display: flex; align-items: center; justify-content: center;
      width: 100%; height: 48px; margin-top: 14px; padding: 0 20px;
      border-radius: 999px; border: 0; cursor: pointer;
      font-family: inherit; font-size: 15px; font-weight: 600; color: #fff;
      text-decoration: none;
    }
    .pw-cta-glass {
      background: rgba(255,255,255,.10); box-shadow: inset 0 0 0 1px var(--rim);
      transition: background .16s, transform .16s, box-shadow .16s;
    }
    .pw-cta-glass:hover { background: rgba(255,255,255,.16); transform: translateY(-2px); box-shadow: inset 0 0 0 1px rgba(255,255,255,.3); }
    .pw-cta-quiet, .pw-cta:disabled {
      background: rgba(255,255,255,.05); box-shadow: inset 0 0 0 1px var(--rim-soft);
      color: var(--ink-3); cursor: default; transform: none;
    }
    .pw-cta-quiet:hover, .pw-cta:disabled:hover { background: rgba(255,255,255,.05); transform: none; box-shadow: inset 0 0 0 1px var(--rim-soft); filter: none; }
    .pw-saved { min-height: 18px; margin: 8px 0 12px; text-align: center; color: #ff8ccd; font-size: 13px; font-weight: 500; }

    /* The allowance box — Picsart's "500 credits/month" panel, in the app's
       darker inset glass. */
    /* The same box on both cards, whatever is in it: it starts on the same
       line because everything above it is fixed height (a two-line tagline
       slot, the price and its note, the button, the "You save" slot with or
       without text), and it ends at the card's foot because it grows to fill
       — and the two cards are the same height. */
    /* Content to the top, not centred: the allowance is the line the two paid
       cards are compared on, and centring a short list against a long one
       drops one card's number below the other's. */
    .pw-credits {
      flex: 1 1 auto; display: flex; flex-direction: column; justify-content: flex-start;
      margin-top: 0; padding: 14px 16px 12px; border-radius: 16px;
      background: rgba(0,0,0,.22); box-shadow: inset 0 0 0 1px rgba(255,255,255,.07);
    }
    .pw-credits-title { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--ink); font-size: 17px; font-weight: 600; }
    .pw-credits-title img { width: 18px; height: 18px; display: block; }
    .pw-credits-per { color: var(--ink-3); font-weight: 400; font-size: 14px; }
    .pw-feats { margin: 0; padding: 0; list-style: none; }
    .pw-credits-title + .pw-feats { margin-top: 12px; }
    .pw-feats li { display: flex; align-items: center; gap: 10px; padding: 5px 0; color: var(--ink-2); font-size: 13.5px; line-height: 1.35; }
    /* What the allowance buys, ahead of the features: the count carries the
       card's own ink so the number reads before the sentence around it. */
    .pw-feats li.pw-buys { color: var(--ink-3); }
    .pw-feats li.pw-buys strong { color: var(--ink); font-weight: 600; }
    /* The tick: `vo_checkmark.svg` in the accent-16% disc the headline's
       feature rows carry (Figma 442:92597), with the glass rim. */
    .pw-tick {
      flex: 0 0 auto; display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%;
      background: rgba(255,69,255,.16); box-shadow: inset 0 0 0 1px rgba(255,255,255,.18);
    }
    .pw-tick img { width: 8px; height: 8px; display: block; }

    /* ---- credit packs ---- */
    /* The subscriber's screen — credit_purchase_screen.dart: the packs are
       the whole pane, under the balance headline the head carries. */
    .pw-packs-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
    .pw-pack {
      position: relative; display: flex; flex-direction: column; align-items: center; text-align: center;
      padding: 24px 22px 22px; border-radius: 22px;
      background: rgba(255,255,255,.05);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.10), inset 0 1px 0 rgba(255,255,255,.06);
      transition: box-shadow .2s, background .2s;
    }
    .pw-pack:hover { background: rgba(255,255,255,.07); box-shadow: inset 0 0 0 1px rgba(255,255,255,.18), inset 0 1px 0 rgba(255,255,255,.08); }
    .pw-pack.is-featured {
      background: linear-gradient(180deg, rgba(255,196,69,.10) 0%, rgba(255,255,255,.05) 45%);
      box-shadow: inset 0 0 0 1.5px rgba(255,196,69,.55), inset 0 1px 0 rgba(255,255,255,.10), 0 24px 60px -30px rgba(255,196,69,.45);
    }
    .pw-pack-head { display: flex; align-items: center; gap: 10px; }
    .pw-pack .pw-badge { position: absolute; top: 16px; right: 16px; background: rgba(255,196,69,.48); }
    /* The coin on its own — no plate, just the amber glow round it. */
    .pw-pack-coin { display: grid; place-items: center; width: 56px; height: 56px; }
    .pw-pack-coin img {
      width: 52px; height: 52px; display: block;
      filter: drop-shadow(0 0 10px rgba(255,196,69,.55)) drop-shadow(0 0 22px rgba(255,196,69,.25));
    }
    .pw-pack-credits { margin: 10px 0 0; color: var(--ink); font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
    .pw-pack-est { margin: 4px 0 0; color: var(--ink-3); font-size: 13px; }
    .pw-pack-price { margin-top: 14px; }
    .pw-pack-price .pw-price-amount { font-size: 34px; }
    .pw-pack .pw-cta { margin-top: 16px; }

    /* ---- the model rows ---- */
    .pw-models { margin-top: 44px; padding-top: 32px; border-top: 1px solid var(--rim-soft); }
    .pw-models-title { margin: 0 0 22px; text-align: center; color: var(--ink); font-size: 28px; font-weight: 600; letter-spacing: -0.03em; text-wrap: balance; }
    /* `_MarqueeRow`: the two rows bleed off both edges of the card, faded out
       under a mask so the chips appear rather than pop in. */
    .pw-marquee {
      overflow: hidden; margin: 0 -48px; padding: 2px 0;
      -webkit-mask: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
              mask: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
    }
    .pw-marquee + .pw-marquee { margin-top: 12px; }
    .pw-marquee-track { display: flex; gap: 12px; width: max-content; will-change: transform; }
    .pw-marquee-track.is-running { animation: pwSlide linear infinite; }
    .pw-marquee.is-rtl .pw-marquee-track.is-running { animation-direction: reverse; }
    @keyframes pwSlide { to { transform: translateX(calc(-50% - 6px)); } }
    @media (prefers-reduced-motion: reduce) { .pw-marquee-track.is-running { animation: none; } }
    /* `_ModelChip`: GlassPill at white-8%, rim 0.3, padding 2/2/16/2, a
       28px round logo on its own backdrop, the label at 14/500. */
    .pw-chip {
      display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto;
      padding: 2px 16px 2px 2px; border-radius: 999px;
      background: rgba(255,255,255,.08); box-shadow: inset 0 0 0 1px rgba(255,255,255,.12);
      color: #fff; font-size: 14px; font-weight: 500; line-height: 20px; white-space: nowrap;
    }
    .pw-chip-logo { width: 28px; height: 28px; border-radius: 50%; overflow: hidden; flex: 0 0 auto; background: #1c2030; box-sizing: border-box; }
    .pw-chip-logo img { width: 100%; height: 100%; display: block; }

    /* ---- foot ---- */
    .pw-foot { margin-top: 30px; text-align: center; }
    /* `_SafetyNote` — the lock and the line. */
    .pw-safe { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 0; color: var(--ink-2); font-size: 14px; }
    .pw-safe svg { flex: 0 0 auto; }
    /* `_BottomLinks` — muted, 4px dots between. */
    .pw-links { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 16px; margin: 12px 0 0; color: var(--ink-4); font-size: 14px; }
    .pw-links a, .pw-link { color: inherit; text-decoration: none; background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
    .pw-links a:hover, .pw-link:hover { color: var(--ink-2); }
    .pw-link[hidden] { display: none; }
    .pw-dot { width: 4px; height: 4px; border-radius: 50%; background: #363c49; }

    @media (max-width: 1000px) {
      .pw-head { flex-direction: column; align-items: stretch; }
      .pw-controls { flex-direction: row; align-items: center; justify-content: space-between; flex-wrap: wrap; }
      /* Two across, Free dropping to the full-width row underneath: the paid
         pair stays side by side, which is the comparison the page is for. */
      .pw-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .pw-grid .pw-feats { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 1fr; }
      .pw-plan-free { order: 3; grid-column: 1 / -1; }
      .pw-plan-free .pw-feats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    }
    @media (max-width: 760px) {
      .pw { padding: 0; }
      .pw-card { max-width: none; height: 100%; border-radius: 0; border: 0; }
      .pw-scroll { padding: 20px 20px calc(28px + env(safe-area-inset-bottom)); }
      .pw-x { margin: 0 0 0 0; }
      .pw-title { font-size: 28px; }
      .pw-grid, .pw-packs-grid { grid-template-columns: 1fr; }
      .pw-grid .pw-feats { grid-template-columns: 1fr; }
      .pw-plan-free { order: 3; grid-column: auto; }
      .pw-plan-free .pw-feats { grid-template-columns: 1fr; }
      .pw-marquee { margin: 0 -20px; }
    }

    /* ===== the special offer (/js/widgets/sheets/offerdialog.js) =====
       special_offer_screen.dart: the hero clip, "Special Offer", the painted
       discount, the countdown, the price block, Claim Offer — with the
       discount under a scratch card. Same layer rules as the paywall, and
       the same one notch under the sign-in dialog. */
    .offer-open, .offer-open body { overflow: hidden; }
    .offer {
      position: fixed; inset: 0; z-index: 94;
      transform: translateZ(0);
      visibility: hidden; pointer-events: none;
      display: flex; align-items: center; justify-content: center;
      padding: 20px;
      background: rgba(4, 6, 11, .72);
      opacity: 0; transition: opacity .2s ease;
    }
    .offer.is-open { opacity: 1; visibility: visible; pointer-events: auto; }
    /* The card's ground is the foil's: the Pro banner's artwork, under
       enough of the page colour that the copy sits on ground — so the
       scratch card reads as cut from the dialog rather than dropped on it. */
    .offer-card {
      position: relative; width: 100%; max-width: 520px; max-height: 100%;
      overflow: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch;
      background:
        linear-gradient(180deg, rgba(6,9,16,.35) 0%, rgba(6,9,16,.8) 45%, rgba(6,9,16,.96) 100%),
        #1a1030 url("/assets/img/pro-upsell-bg.webp") center / cover no-repeat;
      border: 1px solid var(--rim-soft); border-radius: 28px;
      box-shadow: 0 40px 100px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
      transform: translateY(10px) scale(.985);
      transition: transform .24s cubic-bezier(.4,0,.2,1);
      scrollbar-width: none;
    }
    .offer-card::-webkit-scrollbar { display: none; }
    .offer.is-open .offer-card { transform: none; }
    /* Nothing else is clickable while checkout opens, but the card keeps its
       opacity — fading it reads as the dialog closing. The pressed button
       carries the wait, as on the paywall. */
    .offer.is-busy .offer-card { pointer-events: none; }
    .offer-cta.is-loading { cursor: progress; gap: 10px; }
    .offer-cta-spin {
      flex: 0 0 auto; width: 18px; height: 18px; box-sizing: border-box;
      border-radius: 999px;
      border: 2px solid rgba(255,255,255,.28); border-top-color: #fff;
      animation: gpspin .8s linear infinite;
    }
    @media (prefers-reduced-motion: reduce) { .offer-cta-spin { animation-duration: 2.4s; } }

    /* `OfferHero`'s slot: no clip for now, the splash's glow in its place. */
    .offer-hero { display: none; }
    .offer-x {
      position: absolute; top: 16px; left: 16px; z-index: 3;
      width: 40px; height: 40px; display: grid; place-items: center;
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.12);
      border-radius: 50%; cursor: pointer; color: #fff;
      backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
      box-shadow: 0 2px 12px rgba(0,0,0,.3);
      transition: background .16s, transform .16s;
    }
    .offer-x:hover { background: rgba(255,255,255,.24); transform: scale(1.04); }

    /* One fixed-height body for both states, its content centred: before
       the reveal the foil and the line under it sit together in the middle
       of the card; after it the figure, the clock, the price and the button
       fill the same height. The card never changes size, and nothing is
       stranded at the top. */
    /* The card in the middle, a slot above and a slot below, each the
       height of what it holds after the reveal (the clock; the price and
       the button) — so the card does not move when the foil goes. Before
       the reveal the two slots carry the two halves of the instruction. */
    .offer-body { position: relative; z-index: 1; padding: 40px 28px 28px; text-align: center; }
    .offer-above { position: relative; min-height: 118px; margin-bottom: 18px; }
    .offer-below { position: relative; min-height: 150px; margin-top: 18px; }
    .offer-explain {
      position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
      margin: 0; color: var(--ink); font-size: 34px; font-weight: 700; line-height: 1.08;
      letter-spacing: -0.01em; text-transform: uppercase; text-align: center;
    }
    .offer-explain.g { background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
    .offer.is-revealed .offer-explain { display: none; }
    .offer-details { animation: offerDetails .35s ease both; }
    .offer:not(.is-revealed) .offer-details { display: none; }
    @keyframes offerDetails { from { opacity: 0; transform: translateY(6px); } }

    /* ---- the card ---- */
    .offer-ticket {
      position: relative; height: 150px; border-radius: 20px; overflow: hidden;
      user-select: none; -webkit-user-select: none;   /* a scratch that runs on after the foil lifts must not select the figure */
      background: rgba(255,255,255,.05);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.12), inset 0 1px 0 rgba(255,255,255,.06);
    }
    .offer-figure { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
    /* `OfferHeadline`: the figure in the brand gradient, and the same text
       blurred 24 behind it at 55% — the glow the app paints under it. */
    .offer-figure-text, .offer-figure-glow {
      font-size: 64px; font-weight: 700; line-height: 72px; letter-spacing: -0.03em;
      background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
    }
    .offer-figure-glow { position: absolute; top: 50%; left: 0; right: 0; transform: translateY(calc(-50% - 9px)); filter: blur(18px); opacity: .55; pointer-events: none; }
    .offer-figure-text { position: relative; }
    .offer-figure-caption { color: var(--ink); font-size: 14px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; }
    .offer-figure-caption:empty { display: none; }

    /* The foil: the canvas is the scratchable layer, the hint floats above
       it and fades on the first touch. Once revealed the whole layer lifts. */
    .offer-foil { position: absolute; inset: 0; cursor: grab; touch-action: none; transition: opacity .5s ease, transform .5s ease; }
    .offer-foil:active { cursor: grabbing; }
    .offer-foil canvas { display: block; width: 100%; height: 100%; }
    /* The banner's own type: white, on the painted ground. The coin sits in
       the same white-16% disc with the glass rim the banner's ticks use. */
    .offer-foil-hint {
      position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
      color: #fff; font-size: 28px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
      text-shadow: 0 1px 2px rgba(0,0,0,.22);   /* the gradient pill's own label treatment (.gpill) */
      pointer-events: none;
      transition: opacity .25s;
    }
    /* The card itself asks for the scratch — a small shiver every few
       seconds, the word on it staying put. Off once the foil is gone. */
    .offer-ticket { animation: offerShiver 3.2s ease-in-out infinite; transition: box-shadow .5s ease; }
    /* Lit from within while the foil is on: its own two colours bleeding
       out around it. Gone with the foil. */
    .offer:not(.is-revealed) .offer-ticket {
      box-shadow:
        0 0 0 1px rgba(255,255,255,.14),
        0 0 28px rgba(247,38,118,.55),
        0 0 64px rgba(247,38,118,.28),
        0 0 96px rgba(247,240,98,.16);
    }

    .offer.is-revealed .offer-ticket, .offer-ticket:has(.is-touched) { animation: none; }
    /* The shiver sits at the start of the cycle, so the first one lands as
       the dialog settles rather than two seconds in; the rest of the cycle
       is the pause between shivers. */
    @keyframes offerShiver {
      0%, 4% { transform: translateX(0) rotate(0); }
      6% { transform: translateX(-3px) rotate(-.6deg); }
      8% { transform: translateX(3px) rotate(.6deg); }
      10% { transform: translateX(-3px) rotate(-.4deg); }
      12% { transform: translateX(3px) rotate(.4deg); }
      14% { transform: translateX(-2px) rotate(0); }
      16% { transform: translateX(2px); }
      18%, 100% { transform: translateX(0); }
    }
    @media (prefers-reduced-motion: reduce) { .offer-ticket { animation: none; } }
    /* The glass rim every plate on the site wears, over the foil only. */
    .offer-foil::after {
      content: ""; position: absolute; inset: 0; border-radius: 20px; pointer-events: none;
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.14), inset 0 1px 0 rgba(255,255,255,.08);
    }
    .offer-foil-coin img { display: block; width: 22px; height: 22px; }
    .offer-foil.is-touched .offer-foil-hint { opacity: 0; animation: none; }
    .offer.is-revealed .offer-foil { opacity: 0; transform: scale(1.06); pointer-events: none; }

    /* ---- countdown: `OfferCountdown` ---- */
    .offer-count { display: flex; flex-direction: column; align-items: center; gap: 10px; }
    .offer-count-label { color: var(--ink-3); font-size: 13px; }
    .offer-digits { display: flex; align-items: center; gap: 6px; }
    /* `_DigitBox`: 62×76 (the phone size), radius 14, black 24%, a 1.5px
       glass rim and the white-12% inner glow; the digit 40px bold. */
    .offer-digit {
      display: grid; place-items: center; width: 62px; height: 76px; border-radius: 14px;
      background: rgba(0,0,0,.24);
      box-shadow: inset 0 0 0 1.5px rgba(255,255,255,.18), inset 0 0 12px rgba(255,255,255,.12);
      color: #fff; font-size: 40px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums;
    }
    .offer-digit-sep { padding: 0 2px; color: #fff; font-size: 28px; font-weight: 500; line-height: 1; }
    .offer-count.is-over .offer-digit { color: #ff8ccd; }

    /* ---- price ---- */
    .offer-price { display: flex; flex-direction: column; align-items: center; gap: 4px; }
    .offer-price-caption { color: var(--ink-3); font-size: 13px; }
    .offer-price-line { display: flex; align-items: baseline; gap: 10px; }
    .offer-price-was { color: var(--ink-4); font-size: 18px; text-decoration-color: rgba(255,255,255,.35); }
    .offer-price-was[hidden] { display: none; }
    .offer-price-now { color: #fff; font-size: 26px; font-weight: 700; letter-spacing: -0.02em; }
    .offer-price-foot { color: var(--ink-3); font-size: 12.5px; }
    .offer-price-foot:empty { display: none; }

    .offer-cta {
      display: flex; align-items: center; justify-content: center;
      width: 100%; height: 56px; margin-top: 16px; border-radius: 999px; border: 0; cursor: pointer;
      font-family: inherit; font-size: 17px; font-weight: 600; color: #fff;
    }
    .offer-cta-quiet, .offer-cta:disabled {
      background: rgba(255,255,255,.06); box-shadow: inset 0 0 0 1px var(--rim-soft);
      color: var(--ink-3); cursor: default;
    }
    /* ---- the bar: the offer, folded into the corner ---- */
    .offer-bar {
      position: fixed; right: 20px; bottom: 20px; z-index: 93;
      display: flex; align-items: center; gap: 6px;
      padding: 8px 8px 8px 12px; border-radius: 999px;
      background: rgba(12,16,24,.78);
      box-shadow: inset 0 0 0 1px var(--rim), 0 20px 50px -20px rgba(0,0,0,.7), 0 0 40px -10px rgba(247,38,118,.35);
      backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
      transform: translateY(16px); opacity: 0; visibility: hidden; pointer-events: none;
      transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .3s ease, visibility 0s .3s;
    }
    .offer-bar.is-on { transform: none; opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; }
    .offer-bar-main {
      display: flex; align-items: center; gap: 16px;
      padding: 0; background: none; border: 0; cursor: pointer; color: #fff; font-family: inherit; text-align: left;
    }
    /* The figure in its own paint — the same gradient the dialog's "% OFF" wears. */
    .offer-bar-fig {
      font-size: 24px; font-weight: 700; letter-spacing: -0.02em; line-height: 1; padding-left: 6px;
      background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
    }
    .offer-bar-text { display: flex; flex-direction: column; gap: 1px; }
    .offer-bar-label { color: var(--ink-3); font-size: 11.5px; line-height: 1.2; letter-spacing: .02em; text-transform: uppercase; }
    /* `_DigitBox` at a third of its size. */
    .offer-bar-digits { gap: 3px; margin-top: 2px; }
    .offer-bar-digits .offer-digit {
      width: 22px; height: 28px; border-radius: 6px; font-size: 16px;
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.18), inset 0 0 6px rgba(255,255,255,.12);
    }
    .offer-bar-digits .offer-digit-sep { font-size: 14px; padding: 0 1px; }
    .offer-bar-cta {
      display: inline-flex; align-items: center; height: 42px; padding: 0 20px; border-radius: 999px;
      background: var(--pill-bg); font-size: 15px; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,.22);
      box-shadow: 0 2px 12px rgba(254,2,153,.14);
    }
    .offer-bar-main:hover .offer-bar-cta { filter: brightness(1.05); }
    .offer-bar-x {
      width: 34px; height: 34px; display: grid; place-items: center; border-radius: 50%;
      background: none; border: 0; cursor: pointer; color: var(--ink-3);
      transition: color .16s, background .16s;
    }
    .offer-bar-x:hover { color: #fff; background: rgba(255,255,255,.08); }
    @media (max-width: 560px) {
      .offer-bar { right: 12px; left: 12px; bottom: calc(12px + env(safe-area-inset-bottom)); justify-content: space-between; }
      .offer-bar-main { flex: 1; }
      .offer-bar-cta { margin-left: auto; }
    }

    @media (max-width: 560px) {
      .offer { padding: 0; }
      .offer-card { max-width: none; height: 100%; border-radius: 0; border: 0; }
      .offer-body { padding: 72px 20px calc(24px + env(safe-area-inset-bottom)); }
      .offer-figure-text, .offer-figure-glow { font-size: 54px; line-height: 62px; }
    }

    /* ===== purchase success (/js/widgets/sheets/purchasesuccess.js) =====
       purchase_success_screen.dart on `Background/background-card` (#090D18):
       the Pro badge, the check in its green glow, the headline, the coin
       pill, one button. A dialog like the paywall's — the page dims behind a
       card — with the confetti falling over the whole viewport, outside the
       card too. Above the paywall and the offer (it replaces them). */
    .psuccess-open, .psuccess-open body { overflow: hidden; }
    .psuccess {
      position: fixed; inset: 0; z-index: 97;
      transform: translateZ(0);
      visibility: hidden; pointer-events: none;
      display: flex; align-items: center; justify-content: center;
      padding: 20px;
      background: rgba(4, 6, 11, .72);
      opacity: 0; transition: opacity .32s ease;
    }
    .psuccess.is-open { opacity: 1; visibility: visible; pointer-events: auto; }
    .psuccess-card {
      position: relative; width: 100%; max-width: 520px; max-height: 100%;
      overflow-y: auto; -webkit-overflow-scrolling: touch;
      display: flex; flex-direction: column; align-items: center;
      padding: 24px 28px 28px;
      background: #090D18;
      border: 1px solid var(--rim-soft); border-radius: 28px;
      box-shadow: 0 40px 100px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
      transform: translateY(10px) scale(.985);
      transition: transform .24s cubic-bezier(.4,0,.2,1);
      scrollbar-width: none;
    }
    .psuccess-card::-webkit-scrollbar { display: none; }
    .psuccess.is-open .psuccess-card { transform: none; }
    .psuccess-x {
      position: absolute; top: 16px; right: 16px; z-index: 3;
      width: 40px; height: 40px; display: grid; place-items: center;
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.12);
      border-radius: 50%; cursor: pointer; color: #fff;
      backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
      transition: background .16s, transform .16s;
    }
    .psuccess-x:hover { background: rgba(255,255,255,.24); transform: scale(1.04); }
    .psuccess-body {
      position: relative; z-index: 1;
      display: flex; flex-direction: column; align-items: center; text-align: center;
      width: 100%; padding: 24px 0 20px;
    }
    .psuccess-badge { height: 56px; width: 112px; border-radius: 28px; margin-bottom: 32px; }
    .psuccess-mark {
      display: block; width: 120px; height: 120px; margin-bottom: 20px; border-radius: 50%;
      box-shadow: 0 4px 48px rgba(38,247,94,.32);
    }
    .psuccess-mark img { width: 100%; height: 100%; display: block; object-fit: contain; }
    .psuccess-title { margin: 0; color: #fff; font-size: 32px; font-weight: 500; line-height: 40px; letter-spacing: -0.02em; }
    .psuccess-sub { margin: 12px 0 0; color: var(--ink-2); font-size: 18px; line-height: 28px; }
    .psuccess-coins { display: flex; flex-direction: column; align-items: center; margin-top: 28px; }
    .psuccess-every { margin: 0 0 16px; color: var(--ink-2); font-size: 16px; line-height: 24px; }
    /* The header's coin pill, exactly as the header draws it (.coinpill). */
    .psuccess-amount { font-size: 16px; font-weight: 500; line-height: 1.5; }
    .psuccess-use { margin: 16px 0 0; color: var(--ink-2); font-size: 16px; line-height: 24px; }
    .psuccess-note { margin: 16px 0 0; max-width: 400px; color: var(--ink-3); font-size: 14px; line-height: 20px; }
    .psuccess-cta {
      position: relative; z-index: 1; flex: 0 0 auto;
      width: 100%; height: 56px; margin-top: 8px;
      border-radius: 999px; border: 0; cursor: pointer;
      font-family: inherit; font-size: 18px; font-weight: 500; color: #fff;
    }
    /* `Positioned.fill` + `IgnorePointer`, on the viewport rather than the
       card: the burst falls across the dimmed page as well. `slice` is
       `BoxFit.cover`. */
    .psuccess-confetti { position: fixed; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
    .psuccess-confetti svg { width: 100% !important; height: 100% !important; display: block; }
    @media (max-width: 560px) {
      .psuccess { padding: 0; }
      .psuccess-card { max-width: none; height: 100%; border-radius: 0; border: 0; padding: 24px 20px calc(24px + env(safe-area-inset-bottom)); justify-content: center; }
      .psuccess-title { font-size: 30px; line-height: 38px; }
      .psuccess-sub { font-size: 17px; line-height: 26px; }
    }

    /* ===== the info dialog (/js/widgets/sheets/infodialog.js) =====
       info_dialog.dart: the card on #090D18, the orb on its bloom, title,
       body, the refund pill, one button. Same layer as the paywall. */
    .idlg-open, .idlg-open body { overflow: hidden; }
    .idlg {
      position: fixed; inset: 0; z-index: 96;
      transform: translateZ(0);
      visibility: hidden; pointer-events: none;
      display: flex; align-items: center; justify-content: center;
      padding: 20px;
      background: rgba(4, 6, 11, .72);
      opacity: 0; transition: opacity .2s ease;
    }
    .idlg.is-open { opacity: 1; visibility: visible; pointer-events: auto; }
    .idlg-card {
      position: relative; width: 100%; max-width: 400px;
      display: flex; flex-direction: column; align-items: center; text-align: center;
      padding: 56px 24px 24px;
      background: #090D18;
      border: 1px solid var(--rim-soft); border-radius: 28px;
      box-shadow: 0 40px 100px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
      transform: translateY(10px) scale(.985);
      transition: transform .24s cubic-bezier(.4,0,.2,1);
    }
    .idlg.is-open .idlg-card { transform: none; }
    .idlg-x {
      position: absolute; top: 16px; right: 16px;
      width: 36px; height: 36px; display: grid; place-items: center;
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.12);
      border-radius: 50%; cursor: pointer; color: #fff;
      transition: background .16s;
    }
    .idlg-x:hover { background: rgba(255,255,255,.24); }
    /* `_Orb`: 132 across, the bloom at 32% of the kind's colour, blurred 48. */
    .idlg-orb {
      display: block; width: 132px; height: 132px; border-radius: 50%;
      box-shadow: 0 4px 48px rgba(var(--idlg-glow, 247,38,118), .32);
    }
    .idlg-art { width: 100%; height: 100%; display: block; object-fit: contain; }
    .idlg-title { margin: 24px 0 0; color: #fff; font-size: 24px; font-weight: 500; line-height: 30px; letter-spacing: -0.01em; text-wrap: balance; }
    .idlg-body { margin: 16px 0 0; color: #C8CDD8; font-size: 16px; line-height: 24px; }
    .idlg-refund { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 24px 0 0; color: #C8CDD8; font-size: 16px; }
    .idlg-refund[hidden] { display: none; }
    .idlg-cta {
      width: 100%; height: 56px; margin-top: 24px; border-radius: 999px; border: 0; cursor: pointer;
      font-family: inherit; font-size: 18px; font-weight: 500; color: #fff;
    }
    @media (max-width: 480px) {
      .idlg-card { padding: 52px 20px 20px; }
      .idlg-orb { width: 116px; height: 116px; }
      .idlg-title { font-size: 22px; line-height: 28px; }
      .idlg-body { font-size: 15px; line-height: 22px; }
    }

    /* ===== the report dialog (/js/widgets/sheets/reportdialog.js) =====
       report_feedback_sheet.dart: the reasons as rows with their emoji,
       then the words, the consent line and Submit, then the thank-you. */
    .rpt-open, .rpt-open body { overflow: hidden; }
    .rpt, .tkt {
      position: fixed; inset: 0; z-index: 97;
      transform: translateZ(0);
      visibility: hidden; pointer-events: none;
      display: flex; align-items: center; justify-content: center;
      padding: 20px;
      background: rgba(4, 6, 11, .72);
      opacity: 0; transition: opacity .2s ease;
    }
    .rpt.is-open, .tkt.is-open { opacity: 1; visibility: visible; pointer-events: auto; }
    .rpt-card, .tkt-card {
      position: relative; width: 100%; max-width: 440px; max-height: 100%;
      overflow-y: auto; -webkit-overflow-scrolling: touch;
      padding: 28px 24px 24px;
      background: #090D18;
      border: 1px solid var(--rim-soft); border-radius: 28px;
      box-shadow: 0 40px 100px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
      transform: translateY(10px) scale(.985);
      transition: transform .24s cubic-bezier(.4,0,.2,1);
      scrollbar-width: none;
    }
    .rpt-card::-webkit-scrollbar, .tkt-card::-webkit-scrollbar { display: none; }
    .rpt.is-open .rpt-card, .tkt.is-open .tkt-card { transform: none; }
    .rpt-x, .tkt-x {
      position: absolute; top: 14px; right: 14px; z-index: 2;
      width: 34px; height: 34px; display: grid; place-items: center;
      background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.1);
      border-radius: 50%; cursor: pointer; color: #fff;
      transition: background .16s;
    }
    .rpt-x:hover, .tkt-x:hover { background: rgba(255,255,255,.2); }
    .rpt-body { display: flex; flex-direction: column; }
    .rpt-title { margin: 0 32px 0 0; color: #fff; font-size: 22px; font-weight: 600; line-height: 1.2; letter-spacing: -0.01em; }
    .rpt-sub { margin: 8px 0 0; color: var(--ink-3); font-size: 14px; line-height: 1.45; }
    .rpt-list { display: flex; flex-direction: column; gap: 6px; margin-top: 18px; }
    .rpt-reason {
      display: flex; align-items: center; gap: 12px; width: 100%;
      padding: 12px 14px; border-radius: 14px; border: 1px solid var(--rim-soft); cursor: pointer;
      background: rgba(255,255,255,.04); color: #fff; font-family: inherit; font-size: 15px; font-weight: 500; text-align: left;
      transition: background .14s, border-color .14s;
    }
    .rpt-reason:hover { background: rgba(255,255,255,.08); border-color: var(--rim); }
    .rpt-reason.is-on { border-color: rgba(255,255,255,.6); }
    .rpt-emoji { width: 24px; text-align: center; font-size: 18px; line-height: 1; }
    .rpt-text {
      width: 100%; margin-top: 18px; padding: 12px 14px; resize: vertical; min-height: 110px;
      background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft); border-radius: 14px;
      color: #fff; font-family: var(--font); font-size: 15px; line-height: 1.5;
    }
    .rpt-text::placeholder { color: var(--ink-4); }
    .rpt-text:focus { outline: none; border-color: rgba(254,2,153,.55); background: rgba(255,255,255,.06); }
    .rpt-consent { display: flex; align-items: flex-start; gap: 10px; margin-top: 14px; cursor: pointer; }
    .rpt-consent input { margin: 3px 0 0; accent-color: #fe0299; width: 16px; height: 16px; flex: 0 0 auto; }
    .rpt-consent-text { color: var(--ink-3); font-size: 12.5px; line-height: 1.5; }
    .rpt-consent-text a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 2px; }
    .rpt-consent.is-error .rpt-consent-text { color: #ff8ccd; }
    .rpt-error { margin: 6px 0 0; color: #E23A57; font-size: 12.5px; }
    .rpt-error[hidden] { display: none; }
    .rpt-submit { width: 100%; height: 52px; margin-top: 18px; border-radius: 999px; border: 0; cursor: pointer; font-family: inherit; font-size: 16px; font-weight: 600; color: #fff; }
    .rpt-submit:disabled { opacity: .6; cursor: progress; }
    .rpt-back { margin-top: 10px; padding: 10px; background: none; border: 0; cursor: pointer; color: var(--ink-3); font-family: inherit; font-size: 14px; }
    .rpt-back:hover { color: #fff; }
    .rpt-check {
      align-self: center; width: 64px; height: 64px; margin: 8px 0 18px; border-radius: 50%; display: grid; place-items: center;
      background: rgba(38,247,94,.12); color: #26F75E; font-size: 28px; font-weight: 700;
      box-shadow: 0 4px 40px rgba(38,247,94,.25);
    }
    .rpt-check ~ .rpt-title, .rpt-check ~ .rpt-sub { text-align: center; margin-right: 0; }

    /* ===== the ticket dialog (/js/widgets/sheets/ticketdialog.js) =====
       submit_ticket_screen.dart: Subject, Email, Description, a screenshot,
       Submit — and its two outcomes. */
    .tkt-open, .tkt-open body { overflow: hidden; }
    .tkt-title { margin: 0 32px 0 0; color: #fff; font-size: 22px; font-weight: 600; line-height: 1.2; letter-spacing: -0.01em; }
    .tkt-form { display: flex; flex-direction: column; margin-top: 18px; }
    .tkt-form[hidden] { display: none; }
    .tkt-label { margin: 0 0 6px 2px; color: var(--ink-2); font-size: 12.5px; font-weight: 500; }
    .tkt-input {
      width: 100%; height: 46px; padding: 0 14px; margin-bottom: 12px;
      background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft); border-radius: 12px;
      color: var(--ink); font-family: var(--font); font-size: 15px;
      transition: border-color .16s, background .16s;
    }
    .tkt-input::placeholder { color: var(--ink-4); }
    .tkt-input:focus { outline: none; border-color: rgba(254,2,153,.55); background: rgba(255,255,255,.06); }
    .tkt-input.is-error { border-color: rgba(226,58,87,.7); }
    .tkt-input.is-locked { color: var(--ink-2); background: rgba(255,255,255,.07); cursor: default; }
    .tkt-text { height: auto; padding: 12px 14px; resize: vertical; min-height: 120px; line-height: 1.5; }
    .tkt-error { margin: -6px 0 12px 2px; color: #E23A57; font-size: 12.5px; }
    .tkt-error[hidden] { display: none; }
    /* `ticket_attach_label`: a dashed slot; the picked file, a thumb. */
    .tkt-attach {
      display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 52px; padding: 12px;
      border: 1px dashed var(--rim); border-radius: 12px; cursor: pointer;
      color: var(--ink-3); font-size: 13.5px; transition: border-color .16s, color .16s, background .16s;
    }
    .tkt-attach:hover { border-color: rgba(255,255,255,.3); color: var(--ink-2); background: rgba(255,255,255,.03); }
    .tkt-attach[hidden] { display: none; }
    .tkt-preview { display: flex; align-items: center; gap: 12px; padding: 8px; border-radius: 12px; background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft); }
    .tkt-preview[hidden] { display: none; }
    .tkt-preview img { width: 56px; height: 56px; border-radius: 8px; object-fit: cover; display: block; }
    .tkt-preview-x { margin-left: auto; padding: 8px 12px; background: none; border: 1px solid var(--rim-soft); border-radius: 999px; color: var(--ink-2); font-family: inherit; font-size: 13px; cursor: pointer; }
    .tkt-preview-x:hover { color: #fff; border-color: var(--rim); }
    .tkt-submit { width: 100%; height: 52px; margin-top: 18px; border-radius: 999px; border: 0; cursor: pointer; font-family: inherit; font-size: 16px; font-weight: 600; color: #fff; }
    .tkt-submit:disabled { opacity: .6; cursor: progress; }
    .tkt-done { display: flex; flex-direction: column; align-items: center; text-align: center; padding-top: 8px; }
    .tkt-done[hidden] { display: none; }
    .tkt-done-mark { width: 64px; height: 64px; margin: 8px 0 18px; border-radius: 50%; display: grid; place-items: center; font-size: 28px; font-weight: 700; }
    .tkt-done-mark.is-ok { background: rgba(38,247,94,.12); color: #26F75E; box-shadow: 0 4px 40px rgba(38,247,94,.25); }
    .tkt-done-mark.is-bad { background: rgba(226,58,87,.12); color: #E23A57; box-shadow: 0 4px 40px rgba(226,58,87,.25); }
    .tkt-done-title { margin: 0; color: #fff; font-size: 22px; font-weight: 600; }
    .tkt-done-body { margin: 10px 0 0; color: var(--ink-3); font-size: 14px; line-height: 1.5; max-width: 320px; }

    /* ===== /support (/js/screens/support.js) =====
       support_screen.dart: search, chips, rows; the article opens in place. */
    .support .wrap { max-width: 1080px; }
    /* The head: the words on the left, the ticket bar on the right — the
       one action the page has, where you see it first. */
    .support-head { display: flex; align-items: center; justify-content: space-between; gap: 32px; flex-wrap: wrap; }
    .support-head h1 { margin: 0; color: #fff; font-size: clamp(32px, 4vw, 44px); font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; }
    .support-head p { margin: 10px 0 0; color: var(--ink-3); font-size: 16px; line-height: 1.5; max-width: 560px; }
    /* `_SearchField` */
    .support-search {
      position: relative; display: flex; align-items: center; margin-top: 36px;
      height: 56px; padding: 0 18px; border-radius: 16px;
      background: rgba(255,255,255,.05); border: 1px solid var(--rim-soft); color: var(--ink-3);
      transition: border-color .16s, background .16s;
    }
    .support-search:focus-within { border-color: rgba(254,2,153,.55); background: rgba(255,255,255,.07); }
    .support-search input {
      flex: 1 1 auto; height: 100%; margin-left: 10px; padding: 0;
      background: none; border: 0; outline: none; color: #fff; font-family: var(--font); font-size: 15px;
    }
    .support-search input::placeholder { color: var(--ink-4); }
    .support-search input::-webkit-search-cancel-button { filter: invert(1) brightness(.7); }
    /* `_CategoryChips` */
    .support-chips { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
    .support-chip {
      height: 40px; padding: 0 18px; border-radius: 999px; cursor: pointer;
      background: rgba(255,255,255,.05); border: 1px solid var(--rim-soft);
      color: var(--ink-2); font-family: inherit; font-size: 13.5px; font-weight: 500;
      transition: background .14s, color .14s, border-color .14s;
    }
    .support-chip:hover { color: #fff; background: rgba(255,255,255,.09); }
    .support-chip.is-on { color: #fff; background: rgba(255,255,255,.12); border-color: rgba(255,255,255,.6); }
    .support-list { display: flex; flex-direction: column; gap: 12px; margin-top: 28px; }
    /* `_FaqRow`, and the article under it (support_detail_screen.dart). */
    .sa { border-radius: 16px; background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft); overflow: hidden; transition: background .14s, border-color .14s; }
    .sa:hover, .sa.is-open { background: rgba(255,255,255,.06); border-color: var(--rim); }
    .sa-head {
      display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%;
      padding: 20px 24px; background: none; border: 0; cursor: pointer; text-align: left;
      color: #fff; font-family: inherit; font-size: 17px; font-weight: 500; line-height: 1.35;
    }
    .sa-chev { flex: 0 0 auto; color: var(--ink-3); transition: transform .2s; }
    .sa.is-open .sa-chev { transform: rotate(180deg); }
    .sa-body { padding: 0 24px 24px; max-width: 760px; color: var(--ink-2); font-size: 15px; line-height: 1.65; }
    .sa-p { margin: 0 0 12px; }
    .sa-h { margin: 18px 0 8px; color: #fff; font-size: 15px; font-weight: 600; }
    .sa-list { margin: 0 0 12px; padding-left: 22px; }
    .sa-list li { margin: 4px 0; }
    .sa-link { display: inline; padding: 0; background: none; border: 0; cursor: pointer; color: #fff; font: inherit; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: rgba(254,2,153,.7); }
    .sa-link:hover { text-decoration-color: #fe0299; }
    .sa-action {
      display: inline-flex; align-items: center; height: 40px; margin-top: 6px; padding: 0 18px; border-radius: 999px; cursor: pointer;
      background: rgba(255,255,255,.1); border: 1px solid var(--rim); color: #fff; font-family: inherit; font-size: 14px; font-weight: 600;
      transition: background .14s;
    }
    .sa-action:hover { background: rgba(255,255,255,.16); }
    /* `_NoResults` */
    .support-none { padding: 40px 20px; text-align: center; border-radius: 16px; background: rgba(255,255,255,.03); border: 1px dashed var(--rim-soft); }
    .support-none h3 { margin: 0; color: #fff; font-size: 18px; font-weight: 600; }
    .support-none p { margin: 8px 0 0; color: var(--ink-3); font-size: 14px; }
    /* `_BottomHelpBar` */
    .support-help {
      flex: 0 0 auto; display: flex; align-items: center; gap: 18px;
      padding: 14px 14px 14px 22px; border-radius: 999px;
      background: rgba(255,255,255,.05); border: 1px solid var(--rim-soft);
      color: #fff; font-size: 16px; font-weight: 500; white-space: nowrap;
    }
    .support-ticket { height: 44px; }
    @media (max-width: 720px) {
      .support-head { flex-direction: column; align-items: stretch; gap: 20px; }
      .support-help { justify-content: space-between; }
    }

    /* ===== account page (/account) ===== */
    .authpage {
      position: relative; z-index: 2;
      display: flex; justify-content: center;
      padding: clamp(28px, 6vh, 72px) 20px clamp(64px, 10vh, 120px);
    }
    .authcard {
      width: 100%; max-width: 400px;
      background: rgba(16, 19, 28, .72);
      border: 1px solid var(--rim-soft); border-radius: 22px;
      padding: clamp(22px, 4vw, 32px);
      box-shadow: 0 30px 80px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.05);
      backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
    }
    .auth-title {
      margin: 0; color: var(--ink); font-weight: 600; letter-spacing: -0.03em;
      font-size: clamp(24px, 4vw, 30px); line-height: 1.1;
    }
    .auth-sub { margin: 8px 0 0; color: var(--ink-3); font-size: 14px; line-height: 1.5; }

    /* One status line for the whole card: errors, and the handful of moments
       worth a word of reassurance ("we sent another code"). */
    .auth-note {
      margin: 16px 0 0; padding: 10px 12px; border-radius: 12px;
      font-size: 13.5px; line-height: 1.45;
    }
    .auth-note.is-error { background: rgba(254,2,153,.10); border: 1px solid rgba(254,2,153,.34); color: #ffd7ee; }
    .auth-note.is-info  { background: rgba(255,255,255,.06); border: 1px solid var(--rim-soft); color: var(--ink-2); }
    .auth-note[hidden] { display: none; }

    .auth-providers { margin-top: 20px; display: flex; flex-direction: column; gap: 10px; }
    .auth-providers[hidden] { display: none; }
    .gbtn-host { display: flex; justify-content: center; }
    .gbtn-host[hidden] { display: none; }
    .oauth-btn {
      display: flex; align-items: center; justify-content: center; gap: 10px;
      width: 100%; height: 46px; padding: 0 18px; cursor: pointer;
      background: var(--glass-2); border: 1px solid var(--rim); border-radius: var(--r-pill);
      color: var(--ink); font-family: var(--font); font-size: 14.5px; font-weight: 600;
      transition: background .16s, border-color .16s;
    }
    .oauth-btn:hover:not(:disabled) { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.3); }
    /* An unconfigured provider stays visible but honest about it (title says
       why) — a button that looks alive and fails on click is worse. */
    .oauth-btn.is-unavailable { opacity: .38; cursor: not-allowed; }
    .oauth-btn[hidden] { display: none; }
    .oauth-mark { font-size: 15px; font-weight: 700; }

    .auth-or {
      display: flex; align-items: center; gap: 12px; margin: 4px 0;
      font-family: var(--mono); font-size: 10px; letter-spacing: .16em;
      text-transform: uppercase; color: var(--ink-4);
    }
    .auth-or::before, .auth-or::after {
      content: ""; flex: 1; height: 1px; background: var(--rim-soft);
    }

    .field { display: block; margin-top: 16px; }
    .field > span {
      display: block; margin-bottom: 6px;
      font-size: 12.5px; font-weight: 500; color: var(--ink-2);
    }
    .field input {
      width: 100%; height: 46px; padding: 0 14px;
      background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft); border-radius: 12px;
      color: var(--ink); font-family: var(--font); font-size: 15px;
      transition: border-color .16s, background .16s;
    }
    .field input::placeholder { color: var(--ink-4); }
    .field input:focus {
      outline: none; border-color: rgba(254,2,153,.55); background: rgba(255,255,255,.06);
    }
    .field small { display: block; margin-top: 6px; color: var(--ink-4); font-size: 12px; }
    /* A 6-digit code is easier to check at a glance in mono, spaced out. */
    .code-input {
      font-family: var(--mono) !important; font-size: 20px !important;
      letter-spacing: .32em; text-align: center;
    }

    .auth-submit { width: 100%; margin-top: 20px; }
    form.is-busy { opacity: .7; pointer-events: none; }

    .auth-links {
      margin-top: 16px; display: flex; flex-direction: column; gap: 8px;
      /* Centred, not stretched. As flex items these links filled the row, and
         the underline every link carries then ran edge to edge — reading as a
         divider rule between "Forgot your password?" and the line below it
         rather than as an underline on the link. */
      align-items: center;
      font-size: 13.5px; color: var(--ink-3); text-align: center;
    }
    /* The underline here is a text decoration, never a border. As flex items
       these links are blockified, so a `border-bottom` draws across the whole
       row — which is what read as a divider rule under "Forgot your password?".
       A decoration can only ever follow the text. */
    .auth-links a, .linkbtn {
      color: var(--ink); text-decoration: underline;
      text-decoration-color: var(--rim);
      text-decoration-thickness: 1px; text-underline-offset: 3px;
    }
    .auth-links a:hover, .linkbtn:hover:not(:disabled) {
      text-decoration-color: rgba(255,255,255,.55);
    }
    .linkbtn {
      background: none; border: 0; padding: 0; cursor: pointer;
      font-family: var(--font); font-size: 13.5px;
    }
    .linkbtn:disabled { color: var(--ink-4); cursor: default; text-decoration-color: transparent; }

    .auth-legal { margin: 18px 0 0; color: var(--ink-4); font-size: 11.5px; line-height: 1.5; text-align: center; }

    .acct-rows {
      margin: 22px 0 0; display: grid; grid-template-columns: auto 1fr; gap: 10px 16px;
      font-size: 13.5px;
    }
    .acct-rows dt { color: var(--ink-4); }
    .acct-rows dd { margin: 0; color: var(--ink-2); word-break: break-all; }

    /* ===== /home — the app frame =====
       The landing sells Genva; home is Genva with the door open, so it drops
       the marketing page's centred column for the shape a studio tool has: a
       rail of everything you can make, and one feed. Both live inside the same
       chrome (promo strip, header, footer) — this is a different room in the
       same building, not a different building. */
    .app {
      display: grid; grid-template-columns: 248px minmax(0, 1fr);
      gap: 28px; align-items: start;
      max-width: 1560px; margin: 0 auto; padding: 22px 28px 72px;
    }


    /* ===== moving between the surfaces =====
       Home, Video, Image, Music and Home Design are five documents, not five
       tabs, and a hard white-to-dark repaint between them is the one place the
       site stops feeling like the app. This is a cross-document View
       Transition: both documents opt in, the browser snapshots the outgoing
       one, and the two cross-fade.

       The motion is the app's own, and deliberately not invented here — 260ms,
       easeOutCubic in and easeInCubic out, a fade with a 2.5% rise. It is the
       same transition `AnimatedSwitcher` plays between the Edit Home Design
       steps, between the profile's tabs, and between a result's Image and
       Prompt tabs (see edit_home_design_screen.dart's own layoutBuilder note).

       The app itself does NOT animate its tab switch — its shell is an
       IndexedStack, which is instant because every tab is already built and
       still mounted. A website cannot be instant in that way: the next
       document has to be fetched and parsed either way, and the choice is only
       between covering that with something or not.

       A browser without cross-document view transitions ignores all of this
       and navigates the way it always did. */
    @view-transition { navigation: auto; }

    ::view-transition-old(root) {
      animation: gvt-out .26s cubic-bezier(.55,.06,.68,.19) both;
    }
    ::view-transition-new(root) {
      animation: gvt-in .26s cubic-bezier(.215,.61,.355,1) both;
    }
    @keyframes gvt-out { to { opacity: 0; } }
    @keyframes gvt-in { from { opacity: 0; transform: translateY(2.5%); } }

    /* The chrome is the same chrome on both sides, so it holds still while the
       content changes under it — named, which takes it out of the root
       snapshot and gives it its own. That is what makes this read as one app
       changing screens rather than two pages swapping. */
    header { view-transition-name: genva-header; }
    .rail { view-transition-name: genva-rail; }
    .promo { view-transition-name: genva-promo; }

    /* Asked for less motion: still a cross-fade, because the alternative is a
       flash, but nothing moves and it is over in half the time. */
    @media (prefers-reduced-motion: reduce) {
      ::view-transition-old(root) { animation-duration: .12s; }
      ::view-transition-new(root) {
        animation: gvt-fade .12s linear both;
      }
      @keyframes gvt-fade { from { opacity: 0; } }
    }

    /* ---- the rail ---- */
    /* Sticky under the header rather than fixed: fixed would need to know the
       promo strip's height, which is two lines on a phone and gone once
       dismissed — sticky just stops where the flow says. */
    .rail { position: sticky; top: calc(72px + var(--promo-h, 0px) + 16px); }
    .rail-in {
      display: flex; flex-direction: column;
      padding: 14px; border-radius: 22px;
      background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft);
    }
    .rail-home {
      display: flex; align-items: center; gap: 10px;
      padding: 11px 12px; border-radius: 14px; text-decoration: none;
      color: var(--ink); font-size: 15px; font-weight: 600;
      background: var(--glass); border: 1px solid var(--rim-soft);
    }
    .rail-home.is-current { border-color: rgba(254,2,153,.42); }
    /* The app's own filled Home glyph off the bottom nav bar
       (`nav_home_filled.webp`, BottomBarWidget's `activeIcon`) — this row is
       always the current tab's equivalent here, so it wears the active state.
       It replaced a gradient dot, which was the site's own invention. */
    .rail-home-ico { width: 20px; height: 20px; display: block; flex: 0 0 auto; }
    /* Mono, uppercase, wide — a label reads as a label everywhere on this site
       (the same treatment .eyebrow and the mega-menu headings use). */
    .rail-h {
      font-family: var(--mono); font-size: 9.5px; letter-spacing: .16em;
      text-transform: uppercase; color: var(--ink-4);
      margin: 20px 0 8px; padding: 0 4px;
    }
    .rail-tools { display: flex; flex-direction: column; gap: 2px; }
    .rail-tool, .rail-link {
      display: flex; align-items: center; gap: 10px;
      width: 100%; padding: 9px 12px; border-radius: 12px;
      background: transparent; border: 0; cursor: pointer;
      color: var(--ink-2); font-family: inherit; font-size: 14px; text-align: left;
      text-decoration: none; transition: color .16s, background .16s;
    }
    .rail-tool:hover, .rail-link:hover { color: var(--ink); background: rgba(255,255,255,.06); }
    .rail-tool img { width: 20px; height: 20px; display: block; flex: 0 0 auto; }
    .rail-cta { margin-top: 20px; width: 100%; height: 44px; font-size: 14.5px; }

    /* ---- the feed ---- */
    .feed { min-width: 0; }          /* or a row scroller stretches the grid */

    /* ---- the composer ---- */
    .composer { text-align: center; padding: clamp(18px, 4vh, 48px) 0 clamp(24px, 4vh, 44px); }
    .composer h1 {
      font-family: var(--font); font-weight: 600;
      font-size: clamp(28px, 3.4vw, 44px); line-height: 1.06; letter-spacing: -.03em;
      margin: 0; color: var(--ink);
    }
    .composer-sub { margin: 10px 0 0; color: var(--ink-3); font-size: 15.5px; }
    /* openart.ai's footprint, measured rather than guessed: their composer is
       884x176 at a 1172px content column — 75% of the column, wide and low,
       with 30px corners and 16px of padding. Ours takes the same proportion of
       its own column so it lands the same way at any width. What goes inside
       stays the app's: the type, the chips and the pill are Genva's numbers,
       not theirs. */
    /* One geometry, both pages: the composer lands in the same place whichever
       page you reached it from, which is what you expect of a widget that IS
       the same widget. It used to be centred on home and flush left on a
       create surface — the surface then had a 228px hole down its right side
       while the template rows below it ran the full width of the same column,
       and the box read as having slipped rather than as having been placed. */
    .composer-box,
    .surface .pb {
      width: 76%; max-width: 884px;
      margin: clamp(18px, 3vh, 30px) auto 0; text-align: left;
    }
    /* Dressed as an input, deliberately a <button> — the rule the hero's own
       prompt box set. There is nothing here to submit a prompt to. */
    .cbox {
      display: block; width: 100%; max-width: 720px; margin: clamp(18px, 3vh, 28px) auto 0;
      padding: 18px 16px 14px; border-radius: 22px; cursor: pointer;
      background: rgba(255,255,255,.06); border: 1px solid var(--rim);
      font-family: inherit; text-align: left;
      transition: background .16s, border-color .16s;
    }
    .cbox:hover { background: rgba(255,255,255,.09); border-color: rgba(255,255,255,.26); }
    .cbox-line { display: block; color: var(--ink-4); font-size: 16px; line-height: 24px; }
    .cbox-foot {
      display: flex; align-items: center; justify-content: space-between; gap: 12px;
      margin-top: 26px;
    }
    .cbox-mode {
      display: inline-flex; align-items: center; padding: 7px 13px;
      border-radius: var(--r-pill); background: var(--glass); border: 1px solid var(--rim-soft);
      color: var(--ink-2); font-size: 13px; font-weight: 500;
    }
    .cbox-go {
      display: inline-flex; align-items: center; gap: 6px; height: 40px; padding: 0 18px;
      border-radius: var(--r-pill); background: var(--pill-bg);
      color: #fff; font-size: 14.5px; font-weight: 600;
      text-shadow: 0 1px 2px rgba(0,0,0,.22);
    }
    .cbox-go .spk { width: 18px; height: 18px; display: block; }
    .cchips {
      display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
      max-width: 760px; margin: 16px auto 0;
    }
    .cchip {
      padding: 8px 14px; border-radius: var(--r-pill); cursor: pointer;
      background: var(--glass); border: 1px solid var(--rim-soft);
      color: var(--ink-2); font-family: inherit; font-size: 13.5px;
      transition: color .16s, background .16s, border-color .16s;
    }
    .cchip:hover { color: var(--ink); background: var(--glass-2); border-color: var(--rim); }

    /* ---- a feed section ---- */
    .feed-sec { margin-top: clamp(28px, 4vh, 44px); }
    .feed-head {
      display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
      margin-bottom: 14px;
    }
    .feed-head h2 {
      margin: 0; color: var(--ink); font-size: clamp(18px, 1.6vw, 22px);
      font-weight: 600; letter-spacing: -.01em;
    }
    .feed-more {
      flex: 0 0 auto; color: var(--ink-3); font-size: 13.5px; text-decoration: none;
      transition: color .16s;
    }
    .feed-more:hover { color: var(--ink); }

    /* ---- Explore Genva ---- */
    .features { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
    .feature {
      position: relative; display: block; aspect-ratio: 1 / 1; overflow: hidden;
      border-radius: 18px; background: #0b0f1a; border: 1px solid var(--rim-soft);
      text-decoration: none; transition: border-color .2s;
    }
    .feature:hover { border-color: var(--rim); }
    .feature-art, .feature-vid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; display: block;
    }
    .feature-vid { opacity: 0; transition: opacity .3s ease; }
    .feature.is-playing .feature-vid { opacity: 1; }
    .feature::after {
      content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
      background: linear-gradient(180deg, rgba(0,0,0,0) 46%, rgba(0,0,0,.8) 100%);
    }
    .feature-name {
      position: absolute; left: 12px; right: 12px; bottom: 11px; z-index: 2;
      color: #fff; font-size: 14px; font-weight: 600; line-height: 1.25;
    }

    /* ---- quick start ----
       The app's HomeExplore row, and its proportions: a 52px rounded icon box
       over a two-line label, the whole tile 104px tall. All eight across in one
       line, because that is what they are — the eight things Genva makes. A
       two-row grid of the same tiles reads as a menu of features instead. */
    .qs {
      display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 10px;
      margin-bottom: clamp(8px, 1.6vh, 18px);
    }
    /* Under the composer, and the same width as it: the box is the offer,
       this row is the shortcut for when you already know which of the eight
       you came for. */
    .qs-under {
      width: 76%; max-width: 884px;
      margin: clamp(16px, 2.4vh, 26px) auto 0;
    }
    .qs-item {
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      gap: 8px; min-height: 104px; padding: 12px 8px;
      border-radius: 16px; text-decoration: none;
      background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft);
      transition: background .16s, border-color .16s, transform .16s;
    }
    .qs-item:hover {
      background: rgba(255,255,255,.08); border-color: var(--rim);
      transform: translateY(-2px);
    }
    .qs-ico {
      display: flex; align-items: center; justify-content: center;
      width: 42px; height: 42px; flex: 0 0 auto;
      border-radius: 13px; background: var(--glass); border: 1px solid var(--rim-soft);
    }
    .qs-ico img { width: 24px; height: 24px; display: block; }
    /* Two lines' worth of label whether the label needs them or not. Six of
       the eight wrap ("Text to / Image") and two do not ("AI Song", "Lyrics"),
       and with the tile centring its column that difference pushed those two
       icons down a whole line — a row of eight tiles with two icons out of
       step reads as a bug, not as a ragged label. */
    .qs-label {
      min-height: 30px;
      color: var(--ink); font-size: 11.5px; line-height: 15px; font-weight: 500;
      text-align: center;
    }

    /* ---- the prompt container (/js/promptbox.js) ----
       The app's composer, one for one: the chip row, then the prompt area with
       the field, the settings pill and Create. The numbers are its numbers —
       white 8% on white 8% at rest, 20px corners, a 160px floor. */
    .pb { display: block; }
    /* Nine chips on home do not fit any column, so the row scrolls — and says
       so by fading at the edge rather than cutting a chip in half.

       Both edges, and only the side that actually has something behind it.
       The trailing fade alone left the leading edge slicing a chip in two on
       a dead-straight vertical line, which is the one thing this mask exists
       to prevent; and a fade that is on whether or not anything is hidden
       dims the first chip while the row sits at rest and the last one once
       you have reached it. js/widgets/sections/promptbox.js keeps
       --pb-fade-l / --pb-fade-r in step with the scroll offset, so each side
       grows in over its own first 64px of travel.

       The ramp is eased rather than straight: .06 / .3 / .7 across the width
       holds a chip nearly solid for the first stretch and then drops it, so
       it reads as a chip leaving rather than as a wipe passing over it. */
    .pb-chips {
      display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px;
      scrollbar-width: none; scroll-behavior: smooth;
      --pb-fade: 64px;
      --pb-fade-l: 0px;
      --pb-fade-r: 0px;
      -webkit-mask-image: linear-gradient(90deg,
        transparent 0,
        rgba(0,0,0,.06) calc(var(--pb-fade-l) * .25),
        rgba(0,0,0,.3)  calc(var(--pb-fade-l) * .5),
        rgba(0,0,0,.7)  calc(var(--pb-fade-l) * .75),
        #000 var(--pb-fade-l),
        #000 calc(100% - var(--pb-fade-r)),
        rgba(0,0,0,.7)  calc(100% - var(--pb-fade-r) * .75),
        rgba(0,0,0,.3)  calc(100% - var(--pb-fade-r) * .5),
        rgba(0,0,0,.06) calc(100% - var(--pb-fade-r) * .25),
        transparent 100%);
      mask-image: linear-gradient(90deg,
        transparent 0,
        rgba(0,0,0,.06) calc(var(--pb-fade-l) * .25),
        rgba(0,0,0,.3)  calc(var(--pb-fade-l) * .5),
        rgba(0,0,0,.7)  calc(var(--pb-fade-l) * .75),
        #000 var(--pb-fade-l),
        #000 calc(100% - var(--pb-fade-r)),
        rgba(0,0,0,.7)  calc(100% - var(--pb-fade-r) * .75),
        rgba(0,0,0,.3)  calc(100% - var(--pb-fade-r) * .5),
        rgba(0,0,0,.06) calc(100% - var(--pb-fade-r) * .25),
        transparent 100%);
    }
    .pb-chips::-webkit-scrollbar { display: none; }
    /* Active is a filled white pill with a dark glyph well; at rest both invert
       — the app's own two states, not a tint of one. */
    .pb-chip {
      display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
      padding: 2px 12px 2px 2px; border-radius: var(--r-pill); cursor: pointer;
      background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.04);
      color: #fff; font-family: inherit; font-size: 14px; font-weight: 500;
    }
    .pb-chip:hover { background: rgba(255,255,255,.22); }
    .pb-chip.is-on { background: rgba(255,255,255,.72); border-color: rgba(0,0,0,.08); color: #090d18; }
    .pb-chip-ico {
      display: flex; align-items: center; justify-content: center;
      width: 32px; height: 32px; border-radius: 50%;
      background: rgba(255,255,255,.08);
    }
    .pb-chip.is-on .pb-chip-ico { background: rgba(0,0,0,.48); }
    .pb-chip-ico img { width: 18px; height: 18px; display: block; }

    .pb-area {
      position: relative;
      display: flex; flex-direction: column; min-height: 176px;
      margin-top: 18px; padding: 16px; cursor: text;
      background: rgba(255,255,255,.08);
      border: 1px solid rgba(255,255,255,.08); border-radius: 30px;
      transition: background .18s, border-color .18s;
    }

    /* ===== the border beam =====
       genva-app/lib/widgets/common/border_beam.dart and its
       shaders/border_beam.frag, ported — the same beam that rims
       GenerateProcessScreen and the generation dock while a job runs, so the
       two surfaces read as one state.

       What it is NOT is a light travelling round the box. The app does not
       move an object round a track at all: the rim is ALWAYS lit, in nine
       fixed colours, and what moves is a wide soft WINDOW that reveals part
       of it. Half that window sits at full brightness and its edges ramp over
       60-80 degrees, which is why a conic sweep is exactly the right tool
       here — nothing in it is sharp enough for the difference between an
       angle and an arc length to show. A narrow comet on the same sweep is
       what makes that difference glaring, and that is the mistake this
       replaces.

       Three layers, in the painter's own order, and one colour matrix over
       the lot. js/widgets/common/beam.js carries the blob table and the
       gradient stops, because they are the Dart file's numbers and one copy
       of them is the point; the geometry and the compositing are here.

       Two surfaces wear it, the same two the app rims: the composer's prompt
       box, and the generate takeover (.gp-beam, which pins it to the
       viewport at the square corner radius a browser window has). */
    .pb-beam {
      position: absolute; inset: -1px; border-radius: inherit;
      pointer-events: none; overflow: hidden;
      /* the shader's uCM, which is _filterMatrix(0, 1.3, 1.2) */
      filter: brightness(1.3) saturate(1.2);
      animation: pbbeam 1.96s linear infinite;
    }
    @keyframes pbbeam { to { --pb-angle: 360deg; } }
    /* Nothing to light while a dialog covers the page. */
    /* Nothing to light while a dialog covers the page — except the takeover's
       own beam, which is what that dialog just opened. */
    .tdlg-open .pb-beam:not(.gp-beam) { animation-play-state: paused; }

    /* Every layer is the reveal window over a still, coloured rim: the outer
       element carries the window and the inner one the layer's own geometry.
       Nesting them is what multiplies the two, the way the shader's
       `geom * mask` does — and it keeps every mask in here to a single
       compositing trick instead of stacking three of them. */
    .pb-beam-inner, .pb-beam-rim, .pb-beam-bloom, .pb-beam-lit {
      position: absolute; inset: 0; border-radius: inherit;
    }
    .pb-beam-inner, .pb-beam-rim, .pb-beam-bloom {
      -webkit-mask-image: var(--pb-reveal);
              mask-image: var(--pb-reveal);
    }
    /* kind 1 — the inner glow: the blobs at 0.9 size and 0.45 alpha, faded
       out over 28px from every edge, over the white inner rim the shader
       lays under them (0.27 alpha, 10px, quadratic falloff). */
    .pb-beam-inner { opacity: .42; }
    .pb-beam-inner .pb-beam-lit {
      background-image: var(--pb-blobs-inner);
      box-shadow: inset 0 0 10px rgba(255,255,255,.27);
      /* _kInnerEdgeMaskPx. The shader adds the two axes' falloffs and clamps;
         mask layers composite with `add` unless told otherwise, which is the
         same shape — and needs no mask-composite at all. */
      -webkit-mask-image:
        linear-gradient(to bottom, #000, transparent 28px),
        linear-gradient(to top, #000, transparent 28px),
        linear-gradient(to right, #000, transparent 28px),
        linear-gradient(to left, #000, transparent 28px);
      mask-image:
        linear-gradient(to bottom, #000, transparent 28px),
        linear-gradient(to top, #000, transparent 28px),
        linear-gradient(to right, #000, transparent 28px),
        linear-gradient(to left, #000, transparent 28px);
    }
    /* kind 0 — the stroke: the same blobs at full strength on a 1.5px ring,
       with a white core riding the window. */
    .pb-beam-rim { opacity: .26; }
    .pb-beam-rim .pb-beam-lit { background-image: var(--pb-white), var(--pb-blobs); }
    /* And the bloom: the stroke again through a tighter window, blurred. The
       blur has to happen AFTER the ring is cut or there is nothing left to
       bloom, so the ring is the child and the blur sits on the parent —
       Flutter's saveLayer, whose sigma is _kBloomBlur / 2. */
    .pb-beam-bloom { opacity: .24; filter: blur(4px); }
    .pb-beam-bloom .pb-beam-lit { background-image: var(--pb-bloom), var(--pb-blobs); }
    /* the ring the two stroke layers are cut to: paint the whole box, then
       punch the content box out, leaving only the band `padding` is wide */
    .pb-beam-rim .pb-beam-lit, .pb-beam-bloom .pb-beam-lit {
      padding: 1.5px;                        /* kBeamBorderWidth */
      -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
      -webkit-mask-composite: xor;
              mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
    }
    /* The app lifts the fill and the rim once the field has content (24% on
       88%); at rest both sit at 8% and the box recedes. */
    .pb-area.is-live {
      background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.3);
    }
    /* Switching modes changes the box in place, so this is the app's own tab
       transition replayed by hand — profile_screen.dart's AnimatedSwitcher:
       260ms, a fade with a 2.5% slide up, easeOutCubic. */
    .pb-area.is-swap > .pb-field,
    .pb-area.is-swap > .pb-photo,
    .pb-area.is-swap > .pb-foot { animation: pbswap .26s cubic-bezier(.215,.61,.355,1) both; }
    .pb-area.is-swap > .pb-photo { animation-delay: .03s; }
    .pb-area.is-swap > .pb-foot { animation-delay: .05s; }
    @keyframes pbswap {
      from { opacity: 0; transform: translateY(2.5%); }
      to   { opacity: 1; transform: none; }
    }
    /* The chips move with the same clock. */
    .pb-chip { transition: background .26s cubic-bezier(.215,.61,.355,1), color .26s cubic-bezier(.215,.61,.355,1); }
    /* The reference image the mode opens with — imageToVideo and imageToImage
       are the two that lead with a picture rather than with words. A round
       button in the bottom row, the way openart's composer carries its attach
       control: the field keeps the whole box, and the one control that is not
       about words stays off the words. */
    .pb-slot {
      position: relative;
      display: inline-flex; align-items: center; justify-content: center;
      flex: 0 0 auto; width: 42px; height: 42px; padding: 0;
      border-radius: 50%; cursor: pointer; overflow: visible;
      background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.06);
      color: var(--ink-2); font-family: inherit;
      transition: background .16s, color .16s;
    }
    .pb-slot:hover { background: rgba(255,255,255,.16); color: var(--ink); }
    .pb-slot[hidden] { display: none; }
    /* PromptValidationGlow, on the slot and on the box — the rim the design
       wizard's well and the template dialog's fields wear, shaped to each. */
    .pb-slot-rim, .pb-area-rim {
      position: absolute; inset: -1px; pointer-events: none;
      border: 1.5px solid rgba(255,255,255,.9);
      box-shadow: inset 0 0 5px 1.75px rgba(255,255,255,.35);
      opacity: 0; transition: opacity .2s ease;
    }
    .pb-slot-rim { border-radius: 50%; }
    .pb-area-rim { border-radius: 30px; }
    .pb-slot.is-missing .pb-slot-rim, .pb-area.is-missing .pb-area-rim { opacity: 1; }
    .pb-slot-plus { font-size: 22px; line-height: 1; }
    .pb-slot-plus[hidden] { display: none; }
    /* The real input, kept out of sight but not out of the tab order — the
       <label> around it is what you actually see and click. */
    .pb-file {
      position: absolute; width: 1px; height: 1px; opacity: 0;
      inset: 0; margin: auto; cursor: pointer;
    }
    .pb-slot-thumb {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; border-radius: 50%; display: block;
    }
    .pb-slot-thumb[hidden] { display: none; }
    /* The app's own remove button, and its overhang: it pokes above the slot's
       top edge rather than sitting inside and covering the picture. */
    .pb-slot-x {
      position: absolute; top: -5px; right: -5px; z-index: 2;
      display: flex; align-items: center; justify-content: center;
      width: 18px; height: 18px; padding: 0;
      border: 0; border-radius: 50%; cursor: pointer;
      background: rgba(107,114,128,.92); color: #fff;
      font-family: inherit; font-size: 14px; line-height: 1;
    }
    .pb-slot-x:hover { background: rgba(107,114,128,1); }
    .pb-slot-x[hidden] { display: none; }
    /* A real field with the placeholder stacked behind it: the hint types
       itself in and out, which the `placeholder` attribute cannot do. Both
       layers share the same box so the caret lands exactly where the last
       typed character was. */
    .pb-field { position: relative; flex: 1 1 auto; width: 100%; margin-bottom: 12px; }
    .pb-input {
      display: block; width: 100%; min-height: 24px; padding: 0; margin: 0;
      background: none; border: 0; outline: none; resize: none; overflow-y: auto;
      color: var(--ink); caret-color: var(--pink);
      font-family: inherit; font-size: 16px; line-height: 24px;
    }
    /* pre-line: the Lyrics examples are multi-line, and the shape of the
       thing is half of what they show. `pointer-events: none` so the layer
       never comes between you and the field underneath it. */
    .pb-hint {
      position: absolute; inset: 0; pointer-events: none;
      color: #7c8088; font-size: 16px; line-height: 24px; white-space: pre-line;
    }
    /* The caret the typing animation writes behind — it blinks only while the
       placeholder is running, and there is nothing to blink once it is empty. */
    .pb-hint:not(:empty)::after {
      content: ""; display: inline-block; width: 1.5px; height: 17px;
      margin-left: 2px; vertical-align: -3px;
      background: #7c8088; animation: pbcaret 1s steps(1) infinite;
    }
    @keyframes pbcaret { 50% { opacity: 0; } }
    .pb-foot { display: flex; align-items: center; gap: 8px; }
    /* `_DiceButton`. The glyph is static, so the press carries the feedback —
       a soft radial glow at 1.6x the icon fades in behind it while it is held.
       Drawn on an overflowing layer so reaching past the icon never disturbs
       the row's layout, which is what `Clip.none` buys it in the app. */
    .pb-dice {
      position: relative; flex: 0 0 auto; display: grid; place-items: center;
      width: 24px; height: 24px; padding: 0; border: 0; background: none;
      cursor: pointer; overflow: visible;
    }
    .pb-dice[hidden] { display: none; }
    .pb-dice-ico { width: 24px; height: 24px; display: block; position: relative; }
    /* 1.6x the icon, the app's own ratio for this glow. */
    .pb-dice-glow {
      position: absolute; left: 50%; top: 50%;
      width: 38.4px; height: 38.4px; margin: -19.2px 0 0 -19.2px;
      border-radius: 50%; pointer-events: none; opacity: 0;
      background: radial-gradient(circle,
        rgba(255,255,255,.30) 25%, rgba(255,255,255,0) 100%);
      transition: opacity .14s ease-out;
    }
    .pb-dice:active .pb-dice-glow, .pb-dice:focus-visible .pb-dice-glow { opacity: 1; }
    .pb-pill {
      display: inline-flex; align-items: center; gap: 7px; min-width: 0;
      padding: 9px 13px; border-radius: var(--r-pill); cursor: pointer;
      background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.03);
      color: var(--ink-2); font-family: inherit; font-size: 13px;
      white-space: nowrap; overflow: hidden;
    }
    .pb-pill:hover { background: rgba(255,255,255,.15); border-color: rgba(255,255,255,.16); }
    /* The app lifts the pill out of the box's own fill once the form is ready
       (15% on 16%, from 6% on 3%) — at rest both sit at 6% and the pill
       disappears into the background it is drawn on. */
    .pb-pill {
      transition: background .2s, border-color .2s;
    }
    .pb-pill.is-ready {
      background: rgba(255,255,255,.15); border-color: rgba(255,255,255,.16);
    }
    .pb-pill img { width: 16px; height: 16px; display: block; }
    .pb-pill-logo { width: 18px; height: 18px; display: block; border-radius: 5px; }
    .pb-pill-model { color: var(--ink); font-weight: 500; }
    .pb-pill-dot { color: var(--ink-4); }
    /* The Create button has two forms, and which one it wears is the app's own
       condition: a picture AND some words in the image modes, some words
       everywhere else (`_PromptAreaAndTips`'s `isReady`).

       At rest it is a white pill with a dark glyph. Ready, it takes the app's
       own compact create-button texture, white text and the spark — the same
       swap `_CreateButton` makes, down to the two icon files. */
    .pb-create {
      display: inline-flex; align-items: center; gap: 6px; margin-left: auto;
      padding: 10px 14px 10px 11px; border: 0; border-radius: var(--r-pill);
      cursor: pointer; font-family: inherit; font-size: 14px; font-weight: 500;
      background: rgba(255,255,255,.88); color: #090d18;
      transition: background .2s, color .2s, box-shadow .2s;
    }
    .pb-create:hover { background: #fff; }
    .pb-create.is-ready {
      background: #ee2fa4 url("/assets/img/create-button-bg-compact.webp") center / cover no-repeat;
      color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.22);
      box-shadow: 0 6px 18px -8px rgba(254,2,153,.7);
    }
    .pb-create.is-ready:hover { filter: brightness(1.06); }
    .pb-create:disabled { cursor: default; opacity: .72; }
    .pb-create.is-busy { filter: none; }
    /* Create reports on itself, but a refusal needs words: the button can say
       "Starting…" and it cannot say why 402 came back. */
    .pb-note {
      margin: 10px 2px 0; font-size: 13px; line-height: 1.4; color: var(--ink-2);
    }
    .pb-note[hidden] { display: none; }
    .pb-note.is-ok { color: #4EB892; }
    .pb-note.is-bad { color: #ff8080; }
    .pb-create img { width: 20px; height: 20px; display: block; }

    /* Interior, exterior and clean-up have no text field at all — the app's
       spec for them is a photo slot and nothing else, and it is right: there
       is no prompt to write for "redesign this room". */
    .pb.is-photo .pb-field,
    .pb.is-photo .pb-foot,
    .pb.is-photo .pb-slot { display: none; }
    .pb-photo {
      display: none; flex-direction: column; align-items: center; justify-content: center;
      gap: 12px; flex: 1 1 auto; min-height: 150px; width: 100%;
      background: none; border: 0; cursor: pointer; font-family: inherit;
    }
    .pb.is-photo .pb-photo { display: flex; }
    /* No plate behind the glyph: the whole area is already the target, and a
       filled 64px square inside it read as a second, smaller button. What is
       left is a centring box that shrink-wraps the glyph, so the 12px gap to
       the label below is the 12px it says rather than 12 plus the 15 of dead
       space a 64px plate left around a 34px mark. */
    .pb-photo-ico { display: flex; align-items: center; justify-content: center; }
    .pb-photo-glyph { width: 44px; height: 44px; display: block; color: var(--ink-2); }
    .pb-photo-text { color: var(--ink-2); font-size: 15px; }

    /* ---- a create surface (video/image/music//design) ---- */
    .surface { padding: clamp(14px, 3vh, 34px) 0 clamp(18px, 3vh, 30px); }
    .surface h1 {
      margin: 0; color: var(--ink); font-weight: 600;
      font-size: clamp(26px, 3vw, 38px); line-height: 1.08; letter-spacing: -.03em;
    }
    .surface-sub { margin: 8px 0 22px; color: var(--ink-3); font-size: 15.5px; }

    /* ---- a template row ---- */
    /* Scrolls sideways rather than wrapping: a row is one category, and the
       page already has thirteen of them — letting each one wrap to three lines
       would bury the last row under a screenful of the first. */
    .row-scroll {
      display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x proximity;
      padding-bottom: 4px; scrollbar-width: thin;
      /* the row runs to the edge of the feed and fades out, so it reads as
         continuing rather than as ending on a hard cut */
      mask-image: linear-gradient(90deg, #000 calc(100% - 48px), transparent 100%);
    }
    .row-scroll::-webkit-scrollbar { height: 6px; }
    .row-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); border-radius: 3px; }
    .row-card {
      flex: 0 0 auto; width: 172px; scroll-snap-align: start;
    }
    /* The card shape a row asks for, off its layout section's `aspect`. The
       app does the same: a 1:1 section is drawn square (Quick edits, Discover
       music), a 9:16 one is the tall template tile, and the default here stays
       9:16 because that is what every home row is. The square card is a little
       wider than the portrait one so a row of them still fills the rail
       instead of reading as a strip of thumbnails. */
    .feed-sec.is-square .tile { aspect-ratio: 1 / 1; }
    .feed-sec.is-square .row-card { width: 196px; }
    .feed-sec.is-wide .tile { aspect-ratio: 16 / 9; }
    .feed-sec.is-wide .row-card { width: 268px; }

    /* A discover shelf, laid out the way `TemplateShowcaseSection` lays one
       out: a grid rather than a run, at more than twice a template card's
       width. `HomeTemplates` draws 120pt cards you scroll through because a
       template row is the front of a category that continues elsewhere; a
       discover shelf is the whole thing, so it is shown whole.

       auto-fill at 196 lands on five across a full-width feed and folds to two
       on a phone, which is the app's own phone layout. The mask and the snap
       belong to a scroller and come off with it. */
    .feed-sec.is-discover .row-scroll {
      display: grid; grid-template-columns: repeat(auto-fill, minmax(196px, 1fr));
      gap: 16px; overflow: visible; scroll-snap-type: none;
      mask-image: none; -webkit-mask-image: none; padding-bottom: 0;
    }
    .feed-sec.is-discover .row-card { width: auto; scroll-snap-align: none; }

    @media (max-width: 1100px) {
      .features { grid-template-columns: repeat(4, minmax(0, 1fr)); }
      /* Eight across stops fitting long before the rail goes; the row keeps
         its one line and scrolls sideways instead of wrapping into a grid. */
      .qs {
        display: flex; overflow-x: auto; scrollbar-width: none;
        margin-inline: -20px; padding-inline: 20px;
      }
      .qs::-webkit-scrollbar { display: none; }
      .qs-item { flex: 0 0 96px; }
      .composer-box, .qs-under, .surface .pb { width: 100%; max-width: none; }
    }
    @media (max-width: 980px) {
      /* The rail's contents are already in the drawer the burger opens (it is
         built from `.nav`), so below the desktop breakpoint the rail is a
         second copy of a menu that is one tap away. */
      .app { grid-template-columns: minmax(0, 1fr); gap: 0; padding: 8px 20px 56px; }
      .rail { display: none; }
    }
    @media (max-width: 640px) {
      .features { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .composer { padding-top: 8px; }
      .qs-item { flex-basis: 84px; min-height: 96px; }
      .qs-ico { width: 38px; height: 38px; border-radius: 12px; }
      .qs-ico img { width: 22px; height: 22px; }
      /* The box already sits at the app's own numbers; a phone only needs the
         corners back off openart's 30px, which is a desktop radius. */
      .pb-area { border-radius: 22px; padding: 14px 12px 12px; }
      /* 64px is a fifth of the row on a phone; the fade comes back with the
         corners so it stays a hint of an edge rather than a vignette. */
      .pb-chips { --pb-fade: 44px; }
      .pb-hint, .pb-input { font-size: 15px; }
      .cbox-line { font-size: 15px; }
      .row-card { width: 138px; }
      .feed-sec.is-square .row-card { width: 152px; }
      .feed-sec.is-wide .row-card { width: 216px; }
      /* Two across, which is the app's own phone layout for this shelf. */
      .feed-sec.is-discover .row-scroll {
        grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px;
      }
      .row-scroll { mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent 100%); }
    }


    /* ==================================================================
       THE PROFILE  (/js/screens/profile.js)
       `profile_screen.dart` + `profile_content_section.dart`. Your work is the
       page; the account is a row of controls above it. It replaced a
       definition list of email and providers, which is the opposite emphasis.
       ================================================================== */
    .feed-wide { max-width: 1180px; }
    #profile { padding: 8px 0 40px; }

    /* `_ProUpsellBanner`. Its own painted artwork, the wordmark, and the logo
       bleeding off the bottom-right corner — the three files the app ships,
       rather than a gradient of ours pretending to be them. */
    .prof-pro {
      position: relative; display: block; overflow: hidden;
      margin-top: 20px; padding: 24px; border-radius: 24px; text-decoration: none;
      background: #1a1030 url("/assets/img/pro-upsell-bg.webp") center / cover no-repeat;
    }
    /* Positioned(-30, -20) at 110pt: it hangs off the corner on purpose. */
    .prof-pro-logo {
      position: absolute; right: -30px; bottom: -20px; z-index: 0;
      width: 110px; height: 110px; display: block; pointer-events: none;
    }
    .prof-pro-head {
      position: relative; z-index: 1;
      display: flex; align-items: center; gap: 16px; margin-bottom: 16px;
    }
    .prof-pro-mark { height: 30px; width: auto; display: block; }
    /* The one light control on the page — white, #090D18 text. That contrast
       is what makes it read as the offer.

       It needs its own separation rather than the backdrop's. The banner's
       artwork sweeps a near-white ribbon through exactly this corner, and a
       white pill with a black-16% hairline vanished into it. Answering with
       a coloured fill would have cost the thing that makes it read as the
       offer in the first place, so the pill stays white and is lifted off
       the picture instead — by a shadow alone, no outline. The shadow is
       not borrowed from what is behind, so it holds wherever the ribbon
       falls; it is cast tight and deep rather than wide and soft, which is
       what reads as height against a busy picture instead of dissolving
       into it. */
    .prof-pro-cta {
      margin-left: auto; flex: 0 0 auto;
      padding: 8px 16px; border-radius: 999px;
      background: #fff; border: 0;
      color: #090D18; font-size: 14px; line-height: 20px; font-weight: 600;
      box-shadow: 0 2px 4px -1px rgba(0,0,0,.55), 0 10px 22px -6px rgba(0,0,0,.75);
      transition: box-shadow .18s ease, transform .18s ease;
    }
    .prof-pro:hover .prof-pro-cta {
      transform: translateY(-1px);
      box-shadow: 0 3px 6px -1px rgba(0,0,0,.6), 0 16px 30px -6px rgba(0,0,0,.85);
    }
    .prof-pro-list {
      position: relative; z-index: 1; margin: 0; padding: 0; list-style: none;
      display: grid; grid-template-columns: 1fr 1fr; gap: 0 24px;
    }
    .prof-pro-list li {
      display: flex; align-items: center; gap: 10px; padding: 6px 0;
      font-size: 13px; line-height: 19px; font-weight: 500; color: #fff;
    }
    /* `vo_checkmark.svg` in a white-16% disc with the glass rim. */
    .prof-pro-tick {
      flex: 0 0 auto; display: grid; place-items: center;
      width: 20px; height: 20px; border-radius: 50%;
      background: rgba(255,255,255,.16);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.22);
    }
    .prof-pro-tick img { width: 10px; height: 10px; display: block; }
    /* `kCardEdgeGlow`: a 6px frame at #CABECA/35%, blurred 12. */
    .prof-pro-glow {
      position: absolute; inset: 0; z-index: 2; pointer-events: none;
      border-radius: 24px; border: 6px solid rgba(202,190,202,.35);
      filter: blur(12px);
    }

    .prof-tabs { display: flex; gap: 8px; margin-top: 24px; }
    .prof-tab {
      display: inline-flex; align-items: center; gap: 7px;
      height: 40px; padding: 0 16px; border-radius: 999px; cursor: pointer;
      background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.04);
      color: var(--ink-2); font-family: inherit; font-size: 14px; font-weight: 500;
      transition: color .16s, background .16s, border-color .16s;
    }
    .prof-tab:hover { color: var(--ink); background: rgba(255,255,255,.09); }
    .prof-tab.is-on {
      color: #fff; background: rgba(255,255,255,.12); border-color: rgba(255,255,255,.4);
    }
    .prof-tab img { display: block; }
    .prof-tab-ico { display: grid; place-items: center; width: 16px; height: 16px; }
    .prof-tab-glyph { width: 15px; height: auto; display: block; }

    .prof-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
    .prof-chip {
      padding: 7px 14px; border-radius: 999px; cursor: pointer;
      background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.04);
      color: var(--ink-2); font-family: inherit; font-size: 13px;
    }
    .prof-chip:hover { color: var(--ink); background: rgba(255,255,255,.09); }
    .prof-chip.is-on { color: #fff; background: rgba(255,255,255,.12); border-color: rgba(255,255,255,.36); }

    .prof-pane { margin-top: 18px; }
    .prof-loading { padding: 40px 0; color: var(--ink-3); font-size: 14px; }
    .prof-empty {
      display: flex; flex-direction: column; align-items: center; gap: 10px;
      padding: 56px 24px; text-align: center;
      border-radius: 20px; border: 1px dashed var(--rim-soft);
      background: rgba(255,255,255,.02);
    }
    .prof-empty img { opacity: .9; }
    .prof-empty h3 { margin: 4px 0 0; font-size: 17px; font-weight: 600; color: #fff; }
    .prof-empty p { margin: 0; max-width: 46ch; font-size: 14px; line-height: 1.5; color: var(--ink-3); }
    .prof-empty-cta {
      margin-top: 8px; display: inline-flex; align-items: center; height: 40px;
      padding: 0 20px; border: 0; border-radius: 999px; cursor: pointer;
      background: var(--pill-bg); color: #fff; text-decoration: none;
      font-family: inherit; font-size: 14px; font-weight: 500;
    }
    .prof-empty-cta:hover { filter: brightness(1.06); }

    /* ---- the creations grid (`MasonryGridView`) ----
       Masonry, because a creation keeps its own ratio: a 9:16 video and a
       square song in one row of equal boxes would letterbox both. CSS columns
       give the same staggered fill without measuring anything. */
    .cre-grid { column-count: 4; column-gap: 14px; }
    .cre {
      position: relative; display: block; break-inside: avoid;
      width: 100%; margin: 0 0 14px; overflow: hidden;
      border-radius: 16px; background: #12141d; text-decoration: none;
      border: 0; padding: 0; cursor: pointer; font-family: inherit;
      box-shadow: 0 0 0 1px rgba(255,255,255,.08);
    }
    .cre-art, .cre .tile-vid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; display: block;
    }
    .cre .tile-vid { opacity: 0; transition: opacity .3s ease; }
    .cre.is-playing .tile-vid { opacity: 1; }
    .cre-scrim {
      position: absolute; inset: 0; pointer-events: none;
      background: linear-gradient(180deg,
        rgba(0,0,0,.34) 0%, rgba(0,0,0,0) 24%, rgba(0,0,0,0) 46%, rgba(0,0,0,.84) 100%);
    }
    .cre-foot { position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 2; }
    .cre-title {
      display: block; font-size: 13px; font-weight: 600; color: #fff;
      overflow: hidden; text-overflow: ellipsis;
      display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    }
    .cre-sub { display: block; margin-top: 3px; font-size: 11.5px; color: rgba(255,255,255,.66); }
    /* Top right is one slot with two tenants: the countdown while it runs, the
       options button once it has stopped — the app's own call, because a
       generating card has no more button. */
    .cre-eta, .cre-more {
      position: absolute; top: 8px; right: 8px; z-index: 3;
      border-radius: 999px; border: 1px solid rgba(255,255,255,.16);
      background: rgba(10,12,20,.72); color: #fff; backdrop-filter: blur(8px);
    }
    .cre-eta {
      padding: 5px 10px; font-family: var(--mono); font-size: 9.5px;
      letter-spacing: .06em; text-transform: uppercase;
    }
    /* `_MoreButton`: a matte grey glass disc (#3A3C42 at 60% over a real
       blur — grey, not white, is what reads matte) with the shared dots
       stood on end. */
    .cre-more {
      width: 32px; height: 32px; padding: 0; cursor: pointer;
      display: grid; place-items: center;
      background: rgba(58,60,66,.6); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
      border: 1px solid rgba(255,255,255,.18);
      transition: background .16s;
    }
    .cre-more:hover { background: rgba(58,60,66,.85); }
    .cre-more-ico { width: 14px; height: 14px; display: block; object-fit: contain; transform: rotate(90deg); }
    .cre-more[aria-expanded="true"] { background: rgba(58,60,66,.95); }

    /* ---- the options menu (/js/widgets/sheets/workmenu.js) ----
       `showWorkOptionsSheet` as a popover on the ⋯: the composer panel's own
       surface (site ground, rim, shadow), the sheet's own rows. */
    .wm {
      position: fixed; z-index: 96; width: min(320px, calc(100vw - 16px));
      display: none; flex-direction: column;
      border-radius: 20px; overflow: hidden;
      background: var(--bg); border: 1px solid var(--rim);
      box-shadow: 0 28px 70px -20px rgba(0,0,0,.8), 0 2px 10px rgba(0,0,0,.4);
      animation: wmIn .16s ease-out;
    }
    .wm.is-open { display: flex; }
    @keyframes wmIn { from { opacity: 0; transform: translateY(-4px); } }
    /* Figma's "Working Card": the tile, the title / description stack. */
    .wm-head { display: flex; align-items: center; gap: 12px; padding: 14px 14px 12px; flex-wrap: wrap; }
    .wm-tile {
      flex: 0 0 auto; width: 56px; height: 56px; border-radius: 12px; overflow: hidden;
      display: grid; place-items: center; background: rgba(255,255,255,.06);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
    }
    .wm-tile-art { width: 100%; height: 100%; object-fit: cover; display: block; }
    /* `_ErrorDisc`: the failed glyph on its 12% red disc. */
    .wm-tile.is-failed { background: none; box-shadow: none; }
    .wm-disc { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; background: rgba(226,58,87,.12); }
    .wm-disc img { width: 20px; height: 20px; display: block; }
    .wm-text { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
    .wm-title { color: #fff; font-size: 15px; font-weight: 600; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .wm-desc { color: var(--ink-3); font-size: 13px; line-height: 1.35; }
    .wm-desc.is-failed { color: #E23A57; }
    .wm-eta {
      flex: 0 0 auto; padding: 4px 10px; border-radius: 999px;
      background: rgba(255,255,255,.08); box-shadow: inset 0 0 0 1px var(--rim);
      font-family: var(--mono); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-2);
    }
    .wm-rows { display: flex; flex-direction: column; padding: 0 6px 6px; }
    .wm-rows[hidden] { display: none; }
    /* Rows split by the heavier separator (#272C38), items by the hairline. */
    .wm-sep { display: block; height: 1px; margin: 6px 8px; background: #272C38; }
    .wm-row { display: flex; flex-direction: column; }
    .wm-item {
      display: flex; align-items: center; gap: 12px; width: 100%;
      padding: 12px 10px; border: 0; border-radius: 12px; cursor: pointer;
      background: none; color: #fff; font-family: inherit; font-size: 15px; font-weight: 500; line-height: 1.4; text-align: left;
      transition: background .12s;
    }
    .wm-item + .wm-item { box-shadow: inset 0 1px 0 rgba(255,255,255,.04); border-radius: 0 0 12px 12px; }
    .wm-item:hover { background: rgba(255,255,255,.04); }
    .wm-item:disabled { opacity: .5; cursor: default; }
    .wm-item.is-danger { color: #E23A57; }
    .wm-ico { width: 20px; height: 20px; display: block; object-fit: contain; flex: 0 0 auto; }
    .wm-ico.is-mirrored { transform: scaleX(-1); }
    .wm-label { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

    /* ---- View details, in the same popover ---- */
    .wm-details { display: flex; flex-direction: column; padding: 0 14px 14px; }
    .wm-details[hidden] { display: none; }
    .wm-bar { display: flex; align-items: center; gap: 8px; padding: 12px 14px 0; }
    .wm-bar[hidden] { display: none; }
    .wm-bar + .wm-head { padding-top: 10px; }
    .wm-back {
      width: 30px; height: 30px; display: grid; place-items: center; padding: 0;
      background: rgba(255,255,255,.08); border: 1px solid var(--rim-soft); border-radius: 50%;
      color: #fff; cursor: pointer; transition: background .16s;
    }
    .wm-back:hover { background: rgba(255,255,255,.14); }
    .wm-bar-title { color: var(--ink-2); font-size: 13px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; }
    .wm-section { margin: 4px 0 8px 2px; color: #fff; font-size: 15px; font-weight: 500; line-height: 24px; }
    /* `_PromptArea`: grey #7C8088 at 20%, the same for its border, radius 20,
       at least 160 tall, the chip row and Copy along its foot. */
    .wm-prompt {
      display: flex; flex-direction: column; justify-content: space-between; gap: 12px;
      min-height: 160px; padding: 14px 12px 12px; border-radius: 20px;
      background: rgba(124,128,136,.2); border: 1px solid rgba(124,128,136,.2);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.06);
    }
    .wm-prompt-text { margin: 0; color: #fff; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; max-height: 220px; overflow-y: auto; scrollbar-width: thin; }
    .wm-prompt-text.is-empty { color: var(--ink-3); }
    .wm-prompt-row { display: flex; gap: 10px; align-items: flex-start; }
    /* `_SourceImageChip`: the input photo, small, beside the words. */
    .wm-src { flex: 0 0 auto; width: 56px; height: 56px; border-radius: 10px; object-fit: cover; box-shadow: inset 0 0 0 1px rgba(255,255,255,.12); }
    .wm-prompt-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
    .wm-chips { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
    /* `SettingChip`: the composer's own glass chip, read-only here. */
    .wm-chip {
      display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px; border-radius: 999px;
      background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.04);
      color: var(--ink-2); font-size: 12.5px; font-weight: 500; white-space: nowrap;
    }
    .wm-chip-logo { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; display: block; margin-left: -4px; }
    /* `_CopyButton`: white at 88%, the page colour for its word. */
    .wm-copy {
      flex: 0 0 auto; height: 34px; padding: 0 16px; border-radius: 999px; border: 0; cursor: pointer;
      background: rgba(255,255,255,.88); color: #090D18; font-family: inherit; font-size: 13px; font-weight: 600;
      transition: background .16s;
    }
    .wm-copy:hover { background: #fff; }
    /* `_DetailsCard`: label / value rows on the glass rim. */
    .wm-dcard { display: flex; flex-direction: column; border-radius: 16px; background: rgba(255,255,255,.05); box-shadow: inset 0 0 0 1px rgba(255,255,255,.1); padding: 4px 14px; }
    .wm-drow { display: flex; justify-content: space-between; gap: 16px; padding: 10px 0; font-size: 14px; }
    .wm-drow + .wm-drow { box-shadow: inset 0 1px 0 rgba(255,255,255,.06); }
    .wm-drow.is-stacked { flex-direction: column; gap: 4px; }
    .wm-drow-k { color: var(--ink-3); }
    .wm-drow-v { color: #fff; text-align: right; }
    .wm-drow.is-stacked .wm-drow-v { text-align: left; line-height: 1.45; }
    .wm-meta { margin: 10px 2px 0; color: var(--ink-3); font-size: 12.5px; }

    .cre.is-failed { box-shadow: 0 0 0 1px rgba(255,90,90,.36); }
    .cre.is-working { box-shadow: 0 0 0 1px rgba(255,255,255,.18); }
    /* A running job has nothing to show yet, so the card shows the app's own
       answer instead of a blank plate: `_ProcessingCardBody` — its ground,
       its spark, its caption, its vignette.

       The spark is sized off the card's SHORTER side (Figma's 64pt box on a
       183pt-wide card), not off a fixed number: these cards come in every
       output ratio, and a 16:9 one is barely taller than the mark itself at a
       fixed size. `cqmin` is exactly that measurement, which is why this box
       is its own size container. */
    .cre-proc { position: absolute; inset: 0; background: #030508; container-type: size; }
    .cre-proc-in {
      position: absolute; left: 0; right: 0;
      /* _kProcessingSparkAlignment — Alignment(0, -0.22), i.e. 39% down.
         Above the centre because the remaining-time pill hangs off the
         bottom edge and dead centre crowded it. */
      top: 39%; translate: 0 -50%;
      display: flex; flex-direction: column; align-items: center;
    }
    /* The layout box is the still mark's old 64/183 so the caption keeps its
       rhythm; only what the animation paints is scaled past it, which is what
       _kProcessingSparkScale (1.1) tunes. */
    .cre-proc-spark {
      width: calc(100cqmin * 64 / 183); height: calc(100cqmin * 64 / 183);
      display: grid; place-items: center;
    }
    .cre-proc-spark .spark { --spark-mark: calc(100cqmin * 46.08 / 183 * 1.1); }
    .cre-proc-cap {
      margin-top: 10px;                      /* _kProcessingSparkGap */
      padding: 0 8px; max-width: 100%;
      font-size: 12px; line-height: 16px; font-weight: 500; color: #fff;
      text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }


    /* ===== the generating spark =====
       genva-app/lib/screens/generate_process_screen.dart, both halves of it.
       `SparkMark` is the still — a mark, and behind it the same file blown up
       to 70.4/46.08 of the mark's size, blurred by 8.96/46.08 of it, at 32%.
       `SparkLottie` is assets/lottie/generate.json playing the same mark.

       The still paints first and the animation replaces it in place, because
       the composition is 836KB and its player another 168KB — see
       js/widgets/common/spark.js. The element lays out at the MARK's size and
       everything it paints overflows that box, which is what the app's own
       OverflowBox does: generate.json draws a 144-unit mark on a 240-unit
       canvas, so a canvas sized to the layout box would paint the mark at 60%
       of the size every other measurement here assumes. */
    .spark {
      position: relative; flex: 0 0 auto;
      /* Set inline by spark.js when the caller knows the size in pixels; the
         creations card sets --spark-mark itself, in container units. */
      --spark-canvas: calc(var(--spark-mark) * 240 / 144);
      width: var(--spark-mark); height: var(--spark-mark);
    }
    .spark-halo, .spark-mark, .spark-anim {
      position: absolute; left: 50%; top: 50%; translate: -50% -50%;
      pointer-events: none;
    }
    .spark-halo, .spark-mark {
      background: url("/assets/img/generate-spark.webp") center / contain no-repeat;
      transition: opacity .25s ease;
    }
    .spark-mark { width: var(--spark-mark); height: var(--spark-mark); }
    .spark-halo {
      width: calc(var(--spark-mark) * 70.4 / 46.08);
      height: calc(var(--spark-mark) * 70.4 / 46.08);
      opacity: .32; filter: blur(calc(var(--spark-mark) * 8.96 / 46.08));
    }
    .spark-anim {
      width: var(--spark-canvas); height: var(--spark-canvas);
      opacity: 0; transition: opacity .25s ease;
    }
    .spark-anim svg { display: block; }
    .spark.is-live .spark-anim { opacity: 1; }
    .spark.is-live .spark-halo, .spark.is-live .spark-mark { opacity: 0; }

    /* ===== the edge vignette =====
       `EdgeVignette` — four real gradients, one per edge, rather than a
       blurred border or a single radial. A radial would reach a tall screen's
       top and bottom at a different rate than its sides; four linear ones
       fade by the same fraction of each axis whatever the aspect ratio is.

       kVignetteStops / kVignetteOpacities / kVignetteReach / kVignetteIntensity,
       and the pink is kVignettePink. `--gvig-i` and `--gvig-r` are the app's
       own `intensity` / `reach` multipliers: the full-screen takeover takes
       both at 1, a creation card at 0.7 and 2.2, because a small box needs
       more of both to read as the same glow. */
    .gvig { position: absolute; inset: 0; pointer-events: none; overflow: hidden; --gvig-i: 1; --gvig-r: 1; --gvig-c: 227,99,204; }
    .gvig.is-failed { --gvig-c: 227,99,99; }
    .gvig > span { position: absolute; }
    .gvig > .t, .gvig > .b { left: 0; right: 0; height: calc(8% * var(--gvig-r)); }
    .gvig > .l, .gvig > .r { top: 0; bottom: 0; width: calc(8% * var(--gvig-r)); }
    .gvig > .t { top: 0; background: linear-gradient(180deg,
      rgba(var(--gvig-c), calc(.15 * var(--gvig-i))) 0%,
      rgba(var(--gvig-c), calc(.075 * var(--gvig-i))) 35%,
      rgba(var(--gvig-c), calc(.027 * var(--gvig-i))) 70%,
      rgba(var(--gvig-c), 0) 100%); }
    .gvig > .b { bottom: 0; background: linear-gradient(0deg,
      rgba(var(--gvig-c), calc(.15 * var(--gvig-i))) 0%,
      rgba(var(--gvig-c), calc(.075 * var(--gvig-i))) 35%,
      rgba(var(--gvig-c), calc(.027 * var(--gvig-i))) 70%,
      rgba(var(--gvig-c), 0) 100%); }
    .gvig > .l { left: 0; background: linear-gradient(90deg,
      rgba(var(--gvig-c), calc(.15 * var(--gvig-i))) 0%,
      rgba(var(--gvig-c), calc(.075 * var(--gvig-i))) 35%,
      rgba(var(--gvig-c), calc(.027 * var(--gvig-i))) 70%,
      rgba(var(--gvig-c), 0) 100%); }
    .gvig > .r { right: 0; background: linear-gradient(270deg,
      rgba(var(--gvig-c), calc(.15 * var(--gvig-i))) 0%,
      rgba(var(--gvig-c), calc(.075 * var(--gvig-i))) 35%,
      rgba(var(--gvig-c), calc(.027 * var(--gvig-i))) 70%,
      rgba(var(--gvig-c), 0) 100%); }





    /* ===== the result, full screen =====
       genva-app/lib/screens/result_detail_screen.dart, in the discover reel's
       shell — on a desktop the two are the same shape, and the app's own
       Figma treats them as one family.

       The ground is the result screen's own #030508, washed with the colour
       of the thing on it: js/widgets/common/tint.js reads the dominant hue
       out of the result the way `dominantColorOf` does, and this is where it
       lands. Strongest behind the piece and gone by the actions, so the wash
       reads as light coming off the work rather than as a coloured page. */
    .res {
      position: fixed; inset: 0; z-index: 100;
      display: flex; flex-direction: column; align-items: center;
      background: #030508; color: #fff; overflow-y: auto;
      visibility: hidden; opacity: 0; pointer-events: none;
      transition: opacity .22s ease, visibility .22s;
    }
    .res.is-open { visibility: visible; opacity: 1; pointer-events: auto; }
    .res-wash {
      position: fixed; inset: 0; pointer-events: none;
      --res-tint: 120,120,140;
      background:
        radial-gradient(120% 70% at 50% 0%,
          rgba(var(--res-tint), .30) 0%,
          rgba(var(--res-tint), .12) 38%,
          rgba(var(--res-tint), 0) 72%);
      opacity: 0; transition: opacity .5s ease;
    }
    .res.is-tinted .res-wash { opacity: 1; }

    /* `_NavBar` — close, the word, the flag, over its own top-down gradient. */
    .res-nav {
      position: sticky; top: 0; z-index: 3;
      width: 100%; display: grid; grid-template-columns: 40px 1fr 40px;
      align-items: center; gap: 12px; padding: 14px 24px 10px;
      background: linear-gradient(180deg, #060910 0%, rgba(6,9,16,0) 100%);
    }
    .res-nav-title {
      text-align: center; font-size: 20px; line-height: 28px; font-weight: 500;
    }
    .res-nav-btn {
      width: 40px; height: 40px; display: grid; place-items: center;
      border-radius: 999px; cursor: pointer; color: #fff;
      border: 1px solid rgba(255,255,255,.24); background: rgba(255,255,255,.08);
      transition: background .16s;
    }
    .res-nav-btn:hover { background: rgba(255,255,255,.18); }
    .res-nav-btn svg { width: 15px; height: 15px; display: block; }

    .res-col {
      position: relative; z-index: 2;
      width: min(560px, 100%); flex: 1 1 auto;
      display: flex; flex-direction: column; gap: 20px;
      padding: 4px 24px 28px;
    }

    /* `_ResultTabs` — the segmented control above an image's plate. */
    .res-tabs {
      align-self: center; display: inline-flex; gap: 2px; padding: 4px;
      border-radius: 999px;
      background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.04);
    }
    .res-tabs[hidden] { display: none; }
    .res-tab {
      padding: 7px 20px; border: 0; border-radius: 999px; cursor: pointer;
      background: none; color: var(--ink-2); font-family: inherit;
      font-size: 14px; line-height: 20px; font-weight: 500;
      transition: background .18s, color .18s;
    }
    .res-tab.is-on { background: rgba(255,255,255,.12); color: #fff; }

    /* `_Plate` — the card both stages sit in, sized to the job's own ratio
       rather than stretched to fill. No border: it is lifted off the page
       the way the discover reels' cards are (see `.rdlg-stage`), on a deep
       shadow with a hairline of light along the top edge. */
    .res-frame { flex: 0 0 auto; display: grid; place-items: center; }
    .res-frame > * { grid-area: 1 / 1; }
    .res-plate {
      --res-max: 56vh;
      position: relative; width: 100%;
      /* The height budget, applied to the width through the plate's own
         ratio (set by resultsheet.js) so the ratio always holds. */
      max-width: calc(var(--res-max) * var(--res-ratio, 0.5625));
      margin-inline: auto; overflow: hidden;
      border-radius: 16px; background: #060910;
      box-shadow:
        inset 0 1px 0 rgba(255,255,255,.10),
        0 24px 48px -16px rgba(0,0,0,.9),
        0 60px 120px -24px rgba(0,0,0,.75),
        0 0 80px -10px rgba(0,0,0,.5);
    }
    .res-plate[hidden] { display: none; }
    /* A song's plate is its cover and nothing else — square (the ratio is
       set with the plate), filled edge to edge the way the music cards in
       the rows draw it. It used to sit inset on the app's magenta wash,
       which put a frame and a background under it that no other result
       had, and cropped it when the plate's height capped out. */
    .res-stage { position: relative; width: 100%; height: 100%; }
    .res-img, .res-vid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; display: block; background: #060910;
    }
    .res-img[hidden], .res-vid[hidden] { display: none; }
    .res-audio { display: none; }

    /* ---- the player ----
       Hidden until the cursor is over the stage; the play mark alone stays
       while paused so a still frame is not mistaken for a photo. Everything
       is glass, the reel's `_SoundToggle` grey. */
    .res-stage.has-media { cursor: pointer; }
    /* The frame the sheet opens on: no fades, so nothing left over from the
       last result can be seen going. */
    .res-stage.is-settling .res-ctl-play, .res-stage.is-settling .res-ctl-sound, .res-stage.is-settling .res-ctl-bar { transition: none; }
    .res-ctl { position: absolute; inset: 0; z-index: 2; pointer-events: none; display: none; }
    .res-stage.has-media .res-ctl { display: block; }
    .res-ctl-play {
      position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(.92);
      width: 64px; height: 64px; display: grid; place-items: center; padding: 0;
      border: 1px solid rgba(255,255,255,.24); border-radius: 50%;
      background: rgba(107,114,128,.6); color: #fff; cursor: pointer;
      backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
      box-shadow: 0 8px 24px rgba(0,0,0,.35);
      opacity: 0; transition: opacity .2s ease, transform .2s ease, background .2s ease;
      pointer-events: auto;
    }
    .res-ctl-ico { grid-area: 1 / 1; filter: drop-shadow(0 1px 2px rgba(0,0,0,.45)); }
    /* The triangle's own path already sits right of centre in its box —
       the optical correction a play glyph wants — so no nudge on top. */
    .res-stage:not(.is-playing) .res-ctl-play { opacity: 1; transform: translate(-50%, -50%); }
    .res-stage:not(.is-playing) .res-ctl-ico.is-pause { display: none; }
    .res-stage.is-playing .res-ctl-ico.is-play { display: none; }
    .res-stage.is-playing:hover .res-ctl-play { opacity: 1; transform: translate(-50%, -50%); }
    .res-ctl-play:hover { background: rgba(107,114,128,.82); }
    .res-ctl-sound {
      position: absolute; top: 12px; right: 12px;
      display: none; place-items: center; width: 40px; height: 40px; padding: 0;
      border: 1px solid rgba(255,255,255,.24); border-radius: 999px;
      background: rgba(107,114,128,.6); cursor: pointer;
      backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
      opacity: 0; transition: opacity .2s ease, background .2s ease;
      pointer-events: auto;
    }
    .res-stage.is-video .res-ctl-sound { display: grid; }
    .res-stage:hover .res-ctl-sound { opacity: 1; }
    .res-ctl-sound:hover { background: rgba(107,114,128,.82); }
    .res-ctl-sound-ico { width: 20px; height: 20px; display: block; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0,0,0,.45)); }
    /* The progress hairline: 3px along the foot, a wider hit area around it. */
    .res-ctl-bar {
      position: absolute; left: 14px; right: 14px; bottom: 12px; height: 3px;
      border-radius: 999px; background: rgba(255,255,255,.22); cursor: pointer;
      opacity: 0; transition: opacity .2s ease; pointer-events: auto; touch-action: none;
    }
    .res-ctl-bar::before { content: ""; position: absolute; inset: -10px 0; }
    .res-stage:hover .res-ctl-bar, .res-stage:not(.is-playing) .res-ctl-bar { opacity: 1; }
    .res-ctl-fill { position: absolute; inset: 0 auto 0 0; width: 0; border-radius: inherit; background: #fff; box-shadow: 0 0 8px rgba(255,255,255,.5); }
    @media (hover: none) {
      /* No cursor to hover with: the controls stay while anything plays. */
      .res-stage.has-media .res-ctl-play, .res-stage.is-video .res-ctl-sound, .res-stage.has-media .res-ctl-bar { opacity: 1; }
    }


    /* ---- `_HomeDesignStage`: the before/after seam ----
       The uploaded photo against the result, split by a line you can drag.
       The clip is a width on a wrapper rather than a positioned copy, which
       is what keeps the "before" image rendering at the plate's FULL size
       underneath — the app's own note on `_LeftEdgeClip`: a sized box would
       re-fit the picture smaller as the seam swept past it, and the two
       halves would stop lining up. */
    .res-ba {
      position: absolute; inset: 0; overflow: hidden;
      background: #1C2030; cursor: default;
    }
    .res-ba[hidden] { display: none; }
    .res-ba.is-live { cursor: ew-resize; }
    .res-ba-img {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; display: block;
    }
    .res-ba-clip {
      position: absolute; inset: 0 auto 0 0; width: 50%;
      overflow: hidden; pointer-events: none;
    }
    /* The clipped half's own image is sized to the STAGE, not to the clip —
       `100cqw` off the container, so it keeps the same crop as the layer
       underneath however narrow the clip gets. */
    .res-ba { container-type: size; }
    .res-ba-clip .res-ba-img { width: 100cqw; height: 100cqh; }
    .res-ba-seam {
      position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px;
      background: #fff; pointer-events: none;
    }
    /* Figma's two pills: "After" is the bright, dark-on-light one so the eye
       favours the result; "Before" stays the plain glass tint. */
    .res-ba-badge {
      position: absolute; top: 16px; z-index: 2; pointer-events: none;
      padding: 4px 16px; border-radius: 999px;
      font-size: 16px; line-height: 26px; font-weight: 500;
      backdrop-filter: blur(50px); -webkit-backdrop-filter: blur(50px);
    }
    .res-ba-badge.is-before {
      left: 16px; color: #fff;
      background: rgba(58,60,66,.60); border: 1px solid rgba(255,255,255,.18);
    }
    .res-ba-badge.is-after {
      right: 16px; color: #090D18;
      background: rgba(255,255,255,.82); border: 1px solid rgba(255,255,255,.5);
    }
    /* Figma's "Hold" — a glass disc showing both chevrons at once, the
       shorthand a horizontal-resize cursor uses. */
    .res-ba-handle {
      position: absolute; top: 50%; z-index: 3;
      width: 56px; height: 56px; margin: -28px 0 0 -28px; padding: 0;
      display: flex; align-items: center; justify-content: center; gap: 5px;
      border-radius: 999px; color: #fff; cursor: ew-resize;
      background: rgba(58,60,66,.60); border: 1px solid rgba(255,255,255,.24);
      backdrop-filter: blur(50px); -webkit-backdrop-filter: blur(50px);
      box-shadow: 0 2px 14px rgba(0,0,0,.35);
      opacity: 0; transition: opacity .3s ease;
    }
    /* Hidden until the sweep hands the seam over: the app takes drag input
       only once the intro is done, and a handle you cannot move yet is a
       control that lies. */
    .res-ba.is-live .res-ba-handle { opacity: 1; }
    .res-ba-handle:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
    .res-ba.is-dragging .res-ba-handle { background: rgba(58,60,66,.85); }
    .res-ba-chev { width: 9px; display: block; }
    .res-ba-chev svg { width: 100%; height: auto; display: block; }
    .res-ba-chev.is-flip { transform: scaleX(-1); }
    @media (max-width: 640px) {
      .res-ba-badge { top: 12px; padding: 3px 12px; font-size: 14px; line-height: 22px; }
      .res-ba-badge.is-before { left: 12px; }
      .res-ba-badge.is-after { right: 12px; }
      .res-ba-handle { width: 50px; height: 50px; margin: -25px 0 0 -25px; }
    }

    /* `_PromptCard` — the Prompt tab, filling the space the picture would. */
    .res-prompt {
      position: relative; width: 100%; min-height: 220px; max-height: 56vh;
      overflow-y: auto; padding: 20px; border-radius: 16px;
      background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08);
    }
    .res-prompt[hidden] { display: none; }
    .res-prompt-text {
      margin: 0 0 40px; font-size: 15px; line-height: 24px; color: var(--ink-2);
      white-space: pre-wrap;
    }
    .res-prompt-copy {
      position: absolute; right: 16px; bottom: 14px;
      padding: 7px 14px; border-radius: 999px; cursor: pointer;
      border: 1px solid rgba(255,255,255,.16); background: rgba(255,255,255,.10);
      color: #fff; font-family: inherit; font-size: 13px; font-weight: 500;
    }
    .res-prompt-copy:hover { background: rgba(255,255,255,.18); }

    /* `_Info` — what it is on the left, what it cost on the right. */
    .res-info { display: flex; align-items: flex-start; gap: 8px; padding: 0 8px; }
    .res-info-texts { flex: 1 1 auto; min-width: 0; }
    .res-title {
      margin: 0; font-size: 20px; line-height: 30px; font-weight: 500; color: #fff;
      display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
      overflow: hidden;
    }
    .res-sub {
      margin: 0; font-size: 14px; line-height: 20px; color: #C8CDD8;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .res-sub[hidden] { display: none; }
    .res-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
    .res-badges[hidden] { display: none; }
    /* The same chip Discover Music's cards wear — the tag palette at its own
       strength, so a genre reads the same wherever it shows up. */
    .res-badge {
      --res-hue: 236,99,204;
      padding: 5px 8px; border-radius: 999px;
      background: rgba(var(--res-hue), .18);
      border: 1px solid rgba(255,255,255,.14);
      font-size: 12px; line-height: 1.4; font-weight: 500; color: #fff;
    }
    /* `_CoinPill` — "-400" and the coin, in the coin's own amber at 12%. */
    .res-coin {
      flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px;
      padding: 6px 6px 6px 12px; border-radius: 999px;
      background: rgba(255,196,69,.12); border: 1px solid rgba(255,255,255,.04);
      font-size: 16px; line-height: 24px; font-weight: 500; color: #fff;
    }
    .res-coin[hidden] { display: none; }
    .res-coin img { width: 24px; height: 24px; display: block; }

    /* `_Actions` — Regenerate on its own row, then Share and Download. */
    .res-actions { display: flex; flex-direction: column; gap: 16px; }
    .res-again {
      display: flex; align-items: center; justify-content: center; gap: 6px;
      width: 100%; height: 56px; border: 0; border-radius: 28px; cursor: pointer;
      background: var(--pill-bg); color: #fff; font-family: inherit;
      font-size: 18px; line-height: 28px; font-weight: 500;
      text-shadow: 0 1px 2px rgba(0,0,0,.22);
      transition: filter .16s;
    }
    .res-again:hover { filter: brightness(1.06); }
    .res-again[hidden] { display: none; }
    .res-again-ico { width: 22px; height: 22px; display: block; }
    .res-pair { display: flex; gap: 16px; }
    /* `_SubButton` — the two glass pills under Regenerate. Named apart from
       `.res-sub` (the info line) on purpose: they were the same class once
       and the subtitle came out wearing a button. */
    .res-act.is-busy { opacity: .6; cursor: progress; }
    .res-act {
      flex: 1 1 0; display: flex; align-items: center; justify-content: center; gap: 8px;
      padding: 12px 16px; border-radius: 999px; cursor: pointer; text-decoration: none;
      border: 1px solid rgba(255,255,255,.16); background: rgba(255,255,255,.08);
      color: #fff; font-family: inherit; font-size: 16px; line-height: 24px; font-weight: 500;
      transition: background .16s;
    }
    .res-act:hover { background: rgba(255,255,255,.16); }
    .res-act[hidden] { display: none; }
    .res-act-ico { width: 24px; height: 24px; display: grid; place-items: center; color: #fff; }
    .res-act-ico svg { width: 20px; height: 20px; display: block; }

    @media (max-width: 640px) {
      .res-col { padding: 4px 16px 24px; gap: 16px; }
      .res-nav { padding: 12px 16px 8px; }
      .res-plate { --res-max: 46vh; }
      .res-pair { flex-direction: column; }
    }

    /* ===== the header's trailing control =====
       genva-app/lib/widgets/common/app_header.dart. Two states, never both:
       the Pro badge, and the coin pill once there is a balance to show.

       `ProBadge` is a flattened piece of art (288x144, so the pill's width
       follows its height) with a thin highlight travelling round its rim —
       `RotatingRingPainter`: a fixed 1.4px stroke lit by a linear gradient
       whose axis turns once every 7 seconds. Not a conic sweep and not a
       comet: the band is broad and soft (transparent to 5%, full white at
       50%, gone by 95%) and it crosses the whole shape, so opposite sides
       brighten together. */
    .probadge {
      position: relative; display: block; flex: 0 0 auto;
      height: 36px; width: 72px; border-radius: 18px;
      overflow: hidden; text-decoration: none;
      transition: opacity .1s ease-out;
    }
    .probadge:active { opacity: .85; }
    .probadge[hidden] { display: none; }
    .probadge-art { width: 100%; height: 100%; object-fit: contain; display: block; }
    .probadge-ring {
      position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
      padding: 1.4px;                                  /* _ringStrokeWidth */
      background-image: linear-gradient(var(--pro-angle),
        rgba(255,255,255,0) 0%, rgba(255,255,255,0) 5%,
        rgba(255,255,255,.95) 50%,
        rgba(255,255,255,0) 95%, rgba(255,255,255,0) 100%);
      -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
      -webkit-mask-composite: xor;
              mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
      animation: proring 7s linear infinite;
    }
    @property --pro-angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
    @keyframes proring { to { --pro-angle: 360deg; } }
    @media (prefers-reduced-motion: reduce) { .probadge-ring { animation: none; } }

    /* The premium branch's pill: the app's GlassPill filled with 0x7AFFC445
       — the coin's own amber at 48% — the coin, and the balance. */
    .coinpill {
      display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto;
      padding: 6px 12px 6px 6px; border-radius: 999px; text-decoration: none;
      background: rgba(255,196,69,.48);
      border: 1px solid rgba(255,255,255,.16);
      backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
      color: #fff; font-size: 16px; line-height: 1.5; font-weight: 500;
      transition: filter .16s;
    }
    .coinpill:hover { filter: brightness(1.08); }
    .coinpill[hidden] { display: none; }
    .coinpill img { width: 24px; height: 24px; display: block; }

    /* ===== the toast =====
       genva-app/lib/widgets/common/toast_message.dart. The app's dark-glass
       pill, one for one: a 1.2px gradient rim (white .30/.10/.03 at 0/.55/1)
       around a real backdrop blur, tinted with two alpha-blended greys, under
       a soft drop shadow.

       Above every overlay it can be raised over — a validation toast belongs
       on top of the dialog whose field it is talking about. */
    .gtoast-host {
      position: fixed; top: 0; left: 0; right: 0; z-index: 140;
      display: flex; justify-content: center;
      /* The app measures this off the status bar inset. Here the equivalent
         is "clear of the promo strip", which is what a toast fired from a
         full-screen dialog sits over anyway. */
      padding: 10px 20px 0; pointer-events: none;
    }
    .gtoast {
      pointer-events: auto; max-width: 430px; width: max-content;
      padding: 1.2px; border-radius: 999px;
      background: linear-gradient(180deg,
        rgba(255,255,255,.30) 0%, rgba(255,255,255,.10) 55%, rgba(255,255,255,.03) 100%);
      box-shadow: 0 4px 12px rgba(0,0,0,.35);
      /* the app's 220ms enter: a fade with a 30% slide down from above */
      opacity: 0; transform: translateY(-30%);
      transition: opacity .22s ease-out, transform .22s ease-out;
    }
    .gtoast.is-in { opacity: 1; transform: none; }
    .gtoast-in {
      display: flex; align-items: center; gap: 10px;
      padding: 9px 18px 9px 14px; border-radius: 999px;
      /* Color.alphaBlend(white .14 / .08, black .26) over the blur — lighter
         than the nav bar's own tint so more of what is behind bleeds through. */
      background: linear-gradient(180deg, #3b3d44 0%, #34363d 60%);
      backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
    }
    @supports (backdrop-filter: blur(1px)) {
      .gtoast-in { background: linear-gradient(180deg, rgba(59,61,68,.86) 0%, rgba(52,54,61,.86) 60%); }
    }
    /* the tinted badge behind the glyph, with its inner glow */
    .gtoast-badge {
      position: relative; flex: 0 0 auto; width: 34px; height: 34px;
      display: grid; place-items: center; border-radius: 999px;
      --gt-accent: 202,190,202;
      background: rgba(var(--gt-accent), .16);
      border: 1px solid rgba(255,255,255,.04);
      box-shadow: inset 0 0 6px 2px rgba(var(--gt-accent), .28);
    }
    .gtoast-glyph { display: grid; place-items: center; width: 19px; height: 19px; color: #fff; }
    .gtoast-glyph svg { width: 100%; height: 100%; display: block; }
    /* `_TimerRingPainter` — the faint full track, and the bold arc for what
       is left, emptying exactly as the toast dismisses. */
    .gtoast-ring {
      position: absolute; inset: -3px; border-radius: 999px;
      background:
        conic-gradient(rgba(255,255,255,.88) var(--gt-left, 360deg), rgba(255,255,255,.08) 0);
      -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
              mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
      animation: gtring linear forwards;
    }
    @property --gt-left { syntax: "<angle>"; inherits: false; initial-value: 360deg; }
    @keyframes gtring { from { --gt-left: 360deg; } to { --gt-left: 0deg; } }
    .gtoast-title {
      margin: 0; flex: 1 1 auto; min-width: 0;
      font-size: 14px; line-height: 20px; font-weight: 500; color: #fff;
    }
    .gtoast-x {
      flex: 0 0 auto; width: 20px; height: 20px; padding: 3px; cursor: pointer;
      border: 0; background: none; color: #7C8088;
    }
    .gtoast-x:hover { color: #fff; }
    .gtoast-x svg { width: 100%; height: 100%; display: block; }
    @media (prefers-reduced-motion: reduce) {
      .gtoast { transition: opacity .22s ease-out; transform: none; }
      .gtoast-ring { animation: none; }
    }

    /* ===== the generate takeover =====
       genva-app/lib/screens/generate_process_screen.dart. The screen the app
       puts you on the instant Generate is tapped: its ground (#030508), the
       spark, the title/subtitle, the estimate, the notify card, and the pill
       at the bottom that says "Preparing…" until there is something to
       minimize.

       Fixed, never scrollable — the app's own note: "this is a fixed takeover
       screen, not content to page through". The block is anchored to the
       BOTTOM of the content area (Figma's justify-end) with the slack falling
       above the spark, and it scales down as one piece on a short window the
       way FittedBox(scaleDown) does, rather than overflowing. */
    .gp {
      position: fixed; inset: 0; z-index: 120;
      display: flex; flex-direction: column;
      background: #030508; color: #fff;
      visibility: hidden; opacity: 0; pointer-events: none;
      transition: opacity .3s ease, visibility .3s;   /* fadeRoute, 300ms */
    }
    .gp.is-open { visibility: visible; opacity: 1; pointer-events: auto; }
    .gp-beam { inset: 0; border-radius: 0; }
    .gp-vig { z-index: 1; }

    .gp-nav { flex: 0 0 auto; display: flex; justify-content: flex-end; padding: 14px 24px 2px; z-index: 3; }
    /* Nothing to close while the request is still in flight, so the X fades
       out rather than sitting there doing nothing tappable. */
    .gp-x {
      width: 40px; height: 40px; display: grid; place-items: center;
      border-radius: 999px; cursor: pointer; color: #fff;
      border: 1px solid rgba(255,255,255,.24); background: rgba(255,255,255,.08);
      transition: opacity .25s ease, background .18s;
    }
    .gp-x svg { width: 12px; height: 12px; }
    .gp-x:hover { background: rgba(255,255,255,.16); }
    .gp.is-preparing .gp-x { opacity: 0; pointer-events: none; }

    .gp-body {
      flex: 1 1 auto; min-height: 0; z-index: 2;
      display: flex; align-items: flex-end; justify-content: center;
      padding: 32px 24px;
    }
    .gp-block {
      width: min(382px, 100%); display: flex; flex-direction: column; align-items: center;
      text-align: center;
    }
    /* Figma wraps the mark in 28pt of padding, making the icon block 200pt
       tall; the spark itself overflows that box. */
    .gp-spark { padding: 28px; display: grid; place-items: center; }
    .gp-head { margin-top: 32px; }
    .gp-title { margin: 0; font-size: 24px; line-height: 30px; font-weight: 500; color: #fff; }
    .gp-sub { margin: 8px 0 0; font-size: 16px; line-height: 24px; font-weight: 400; color: #C8CDD8; }
    .gp-est { margin-top: 32px; display: flex; flex-direction: column; gap: 8px; }
    .gp-est-label { font-size: 14px; line-height: 20px; color: #C8CDD8; }
    .gp-est-value { font-size: 18px; line-height: 28px; font-weight: 500; color: #fff; }
    /* Figma's notify card is 366 wide inside a 382 column — 8pt narrower on
       each side than its siblings. */
    .gp-notify {
      margin: 48px 8px 0; align-self: stretch;
      padding: 16px; border-radius: 16px; text-align: left;
      background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.10);
    }
    .gp-notify-row { display: flex; align-items: flex-start; gap: 12px; }
    .gp-bell {
      flex: 0 0 auto; width: 36px; height: 36px; display: grid; place-items: center;
      border-radius: 999px; border: 1px solid rgba(255,255,255,.16);
    }
    .gp-bell-img { width: 20px; height: 20px; object-fit: contain; }
    .gp-notify-lines { display: flex; flex-direction: column; padding-top: 6px; }
    .gp-notify-title { font-size: 16px; line-height: 24px; font-weight: 500; color: #fff; }
    .gp-notify-desc { margin-top: 2px; font-size: 14px; line-height: 20px; color: #C8CDD8; }
    /* Where the app offers "Notify me", which needs a push registration this
       site has no way to honour. The true answer takes its place. */
    .gp-notify-foot {
      display: block; margin: 16px 8px 0; padding: 8px 12px; text-align: center;
      border-radius: 999px; background: rgba(255,255,255,.08);
      border: 1px solid rgba(255,255,255,.10);
      font-size: 14px; line-height: 20px; font-weight: 500; color: #fff;
    }

    .gp-cta { flex: 0 0 auto; z-index: 2; padding: 14px 22px 20px; text-align: center; }
    .gp-keep { margin: 0; font-size: 14px; line-height: 20px; color: #7C8088; }
    .gp-pill {
      margin-top: 6px; display: inline-flex; align-items: center; gap: 8px;
      padding: 10px 32px; border-radius: 999px; cursor: pointer;
      font-family: inherit; font-size: 18px; line-height: 28px; font-weight: 500; color: #fff;
      background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.14);
      transition: transform .12s ease, background .18s;
    }
    .gp-pill:hover { background: rgba(255,255,255,.22); }
    .gp-pill:active { transform: scale(.96); }        /* _PressScale */
    .gp.is-preparing .gp-pill { cursor: default; }
    .gp.is-preparing .gp-pill:hover { background: rgba(255,255,255,.16); }
    .gp.is-preparing .gp-pill:active { transform: none; }
    .gp-pill-icon, .gp-pill-spin { width: 24px; height: 24px; display: grid; place-items: center; }
    .gp-pill-icon svg { width: 18px; height: 18px; }
    .gp-pill-spin {
      border-radius: 999px; box-sizing: border-box;
      border: 2px solid rgba(255,255,255,.24); border-top-color: rgba(255,255,255,.7);
      width: 17px; height: 17px; margin: 0 3px;
      animation: gpspin .8s linear infinite;
    }
    @keyframes gpspin { to { rotate: 360deg; } }
    .gp .gp-pill-spin { display: none; }
    .gp.is-preparing .gp-pill-spin { display: block; }
    .gp.is-preparing .gp-pill-icon { display: none; }

    /* Responsive.size's `small` column, and the FittedBox that keeps the
       whole block on a short window instead of letting it overflow. */
    @media (max-height: 780px) {
      .gp-spark { padding: 22px; }
      .gp-head { margin-top: 24px; }
      .gp-title { font-size: 21px; line-height: 27px; }
      .gp-sub { font-size: 14px; line-height: 21px; }
      .gp-est { margin-top: 24px; }
      .gp-notify { margin-top: 32px; }
    }
    @media (max-height: 640px) {
      .gp-body { padding: 18px 20px; }
      .gp-notify { margin-top: 20px; padding: 12px; }
      .gp-est { margin-top: 18px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .gp-beam { animation: none; }
      .gp-pill-spin { animation-duration: 2.4s; }
    }

    @media (max-width: 1100px) { .cre-grid { column-count: 3; } }
    @media (max-width: 780px)  { .cre-grid { column-count: 2; } }
    @media (max-width: 640px) {
      /* The design stacks the four lines into one column on a phone; two
         side by side is the tablet frame's own layout. */
      .prof-pro { padding: 20px; }
      .prof-pro-list { grid-template-columns: 1fr; gap: 0; }
    }

    /* ==================================================================
       "GENVA AI MODELS"  (/js/widgets/sections/models.js)
       `HomeModelPromotion` and its card. The one section on the page that is
       lit from inside — a black wash, an inner glow and the spark artwork
       bleeding off the left edge — because it is an advert for the models
       sitting in among rows that are not.
       ================================================================== */
    .mdl {
      position: relative; overflow: hidden;
      /* The app's own bottom gap is 24; the top one is the site's, because the
         section above this is a `.feed-sec` whose own spacing stops at its
         cards — without it the band starts flush against Explore Genva and
         the two read as one block. */
      margin: 34px 0 24px; padding: 24px 0;
      background: rgba(0,0,0,.08);
    }
    /* `spark_bg.webp` at (-48, centred then lifted 70) — off the left edge on
       purpose, so the band reads as lit from a source outside it. */
    .mdl-bg {
      position: absolute; left: -48px; top: 50%; z-index: 0;
      width: 240px; height: 240px; transform: translateY(calc(-50% - 70px));
      background: url("/assets/img/spark-bg.webp") center / contain no-repeat;
      pointer-events: none;
    }
    /* `InnerGlowPainter(0x3D9092BF)` — a 32px blurred stroke just inside the
       box's own edge. An inset shadow is the same figure: colour, spread and
       blur chosen to land on the painter's numbers. */
    .mdl-glow {
      position: absolute; inset: 0; z-index: 0; pointer-events: none;
      box-shadow: inset 0 0 24px 8px rgba(144,146,191,.24);
    }
    .mdl-in { position: relative; z-index: 1; }
    .mdl-title {
      display: flex; align-items: center; gap: 4px;
      padding: 0 24px; margin-bottom: 12px;
    }
    .mdl-title img { width: 28px; height: 28px; display: block; }
    .mdl-title h2 {
      margin: 0; font-size: 20px; line-height: 28px; font-weight: 500; color: #fff;
    }
    .mdl-scroll {
      display: flex; gap: 12px; padding: 0 24px;
      overflow-x: auto; scrollbar-width: none;
    }
    .mdl-scroll::-webkit-scrollbar { display: none; }

    .mdl-card {
      position: relative; flex: 0 0 auto; overflow: hidden;
      width: 284px; height: 160px; padding: 0; cursor: pointer;
      border: 0; border-radius: 12px; font-family: inherit; text-align: left;
      background: rgba(255,255,255,.08);
    }
    .mdl-art, .mdl-card .tile-vid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; display: block; z-index: 0;
    }
    .mdl-card .tile-vid { opacity: 0; transition: opacity .3s ease; }
    .mdl-card.is-playing .tile-vid { opacity: 1; }
    /* Figma's eased falloff — 17 stops, transparent for the first ~30% and
       darker at every one after, so the photo keeps reading through the
       shadow instead of hitting a solid band behind the title. The stops are
       written by js/widgets/sections/models.js from the design's own table. */
    .mdl-scrim {
      position: absolute; inset: 0; z-index: 1; pointer-events: none;
      background: linear-gradient(180deg,
        rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30.64%, rgba(0,0,0,.0062) 38.81%,
        rgba(0,0,0,.0209) 45.47%, rgba(0,0,0,.0487) 50.83%, rgba(0,0,0,.0819) 55.14%,
        rgba(0,0,0,.1236) 58.65%, rgba(0,0,0,.17) 61.54%, rgba(0,0,0,.2241) 64.09%,
        rgba(0,0,0,.2782) 66.53%, rgba(0,0,0,.3361) 69.10%, rgba(0,0,0,.3972) 71.99%,
        rgba(0,0,0,.4575) 75.50%, rgba(0,0,0,.5177) 79.81%, rgba(0,0,0,.5757) 85.17%,
        rgba(0,0,0,.6305) 91.82%, rgba(0,0,0,.68) 100%);
    }
    .mdl-flag {
      position: absolute; top: 8px; right: 8px; z-index: 3;
      height: 18px; width: auto; display: block;
    }
    .mdl-foot { position: absolute; left: 12px; right: 12px; bottom: 12px; z-index: 3; }
    .mdl-head { display: flex; align-items: center; gap: 6px; }
    /* The plate and the hairline are drawn here rather than baked per asset,
       so a transparent logo still reads on a light video frame. */
    .mdl-logo {
      flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%;
      overflow: hidden; background: rgba(0,0,0,.48);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
    }
    .mdl-logo img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .mdl-name {
      min-width: 0; font-size: 14px; line-height: 20px; font-weight: 500; color: #fff;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .mdl-tags { display: flex; gap: 8px; margin-top: 8px; }
    /* Figma's badge fill: the tag's own hue at 12% over the card's dark
       token (#1C2030), which is what `Color.alphaBlend` composites there. */
    .mdl-tag {
      display: inline-flex; align-items: center; height: 24px; padding: 0 8px;
      border-radius: 999px; font-size: 10px; font-weight: 600; color: #fff;
      background: linear-gradient(rgba(var(--tag-hue),.12), rgba(var(--tag-hue),.12)) #1C2030;
      border: 1px solid rgba(255,255,255,.10);
    }
    /* Figma strokes the card itself, not the fill underneath. */
    .mdl-rim {
      position: absolute; inset: 0; z-index: 4; pointer-events: none;
      border-radius: 12px; border: 1px solid rgba(255,255,255,.08);
    }

    @media (max-width: 640px) {
      .mdl { padding: 20px 0; }
      .mdl-title, .mdl-scroll { padding-inline: 16px; }
      .mdl-title h2 { font-size: 18px; }
      .mdl-card { width: 250px; height: 140px; }
    }

    /* ---- the model detail dialog (`showModelDetailSheet`) ---- */
    .mdlg {
      position: fixed; inset: 0; z-index: 95;
      display: grid; place-items: center; padding: 20px;
      visibility: hidden; opacity: 0; pointer-events: none;
      transition: opacity .2s ease;
    }
    .mdlg.is-open { visibility: visible; opacity: 1; pointer-events: auto; }
    /* The app's own barrier: black at 88%, not the site's lighter scrim. */
    .mdlg-scrim { position: absolute; inset: 0; background: rgba(0,0,0,.88); }
    .mdlg-card {
      position: relative; z-index: 1; width: min(460px, 100%);
      max-height: min(760px, 92vh); display: flex; flex-direction: column;
      border-radius: 24px; overflow: hidden;
      background: var(--bg); border: 1px solid var(--rim);
      box-shadow: 0 30px 80px -20px rgba(0,0,0,.9);
    }
    .mdlg-nav {
      flex: 0 0 auto; position: relative; display: grid; place-items: center;
      padding: 14px 16px;
    }
    .mdlg-nav-title { font-size: 18px; font-weight: 500; color: #fff; }
    .mdlg-x {
      position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
      width: 32px; height: 32px; border-radius: 999px; cursor: pointer;
      border: 1px solid var(--rim); background: rgba(255,255,255,.10);
      color: var(--ink); font-size: 13px; line-height: 1;
    }
    .mdlg-x:hover { background: rgba(255,255,255,.18); }
    .mdlg-body {
      flex: 1 1 auto; min-height: 0; overflow-y: auto;
      padding: 0 20px 4px; scrollbar-width: thin;
    }
    /* `_Preview`: the border is drawn OVER the clipped content — in the same
       decoration as the clip it came out broken where the image met the edge,
       which is the app's own note on this exact box. */
    .mdlg-stage {
      position: relative; width: 100%; aspect-ratio: 16 / 9;
      border-radius: 12px; overflow: hidden; background: #1C2030;
    }
    .mdlg-poster, .mdlg-vid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; display: block;
    }
    .mdlg-poster[hidden], .mdlg-vid[hidden] { display: none; }
    .mdlg-stage-scrim {
      position: absolute; inset: 0; pointer-events: none;
      background: linear-gradient(180deg, rgba(0,0,0,0) 38%, rgba(0,0,0,.88) 100%);
    }
    .mdlg-stage-rim {
      position: absolute; inset: 0; pointer-events: none;
      border-radius: 12px; border: 1px solid rgba(255,255,255,.04);
    }
    .mdlg-name {
      margin: 16px 0 0; font-size: 20px; line-height: 28px; font-weight: 500; color: #fff;
    }
    .mdlg-desc {
      margin: 6px 0 0; font-size: 14px; line-height: 20px; color: #C8CDD8;
    }
    .mdlg-sec { margin-top: 16px; }
    .mdlg-sec[hidden], .mdlg-langs[hidden], .mdlg-badges[hidden] { display: none; }
    .mdlg-label {
      font-size: 12px; line-height: 16px; font-weight: 500; color: #C8CDD8;
      margin-bottom: 8px;
    }
    /* `_LanguageRow` — one bordered row: globe, label, flags, "+N". */
    .mdlg-langs {
      display: flex; align-items: center; gap: 8px; margin-top: 16px;
      padding: 10px 12px; border-radius: 16px;
      background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.04);
    }
    .mdlg-globe { width: 20px; height: 20px; display: block; flex: 0 0 auto; }
    .mdlg-langs-label {
      flex: 1 1 auto; min-width: 0; font-size: 14px; color: #fff;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .mdlg-flags { display: flex; gap: 8px; flex: 0 0 auto; }
    .mdlg-flag {
      display: grid; place-items: center; width: 28px; height: 28px;
      border-radius: 50%; border: 1px solid rgba(255,255,255,.08);
      font-size: 16px; line-height: 1;
    }
    .mdlg-flag.is-more {
      background: rgba(255,255,255,.04); font-size: 11px; color: #fff;
      font-family: var(--mono);
    }
    /* `_Badges` — the promo card's own pills, at the sheet's larger size and
       its 15% hue rather than the card's 12%. */
    .mdlg-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
    .mdlg-badge {
      display: inline-flex; align-items: center; padding: 8px 12px 6px;
      border-radius: 999px; font-size: 10px; line-height: 14px;
      font-weight: 500; color: #fff;
      background: rgba(var(--tag-hue),.15);
      border: 1px solid rgba(255,255,255,.10);
    }
    .mdlg-cta {
      flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
      gap: 6px; margin: 16px 20px calc(16px + env(safe-area-inset-bottom, 0px));
      height: 52px; border: 0; border-radius: 999px; cursor: pointer;
      font-family: inherit; font-size: 17px; font-weight: 500; color: #fff;
      background: var(--pill-bg); text-shadow: 0 1px 2px rgba(0,0,0,.22);
      transition: filter .2s ease;
    }
    .mdlg-cta:hover { filter: brightness(1.06); }
    .mdlg-cta-ico { width: 20px; height: 20px; display: block; }
    .mdlg-cta span {
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }

    @media (max-width: 640px) {
      .mdlg { padding: 12px; }
      .mdlg-card { max-height: 94vh; }
      .mdlg-body { padding-inline: 16px; }
      .mdlg-cta { margin-inline: 16px; height: 48px; font-size: 16px; }
    }

    /* ==================================================================
       THE COMPOSER'S SETTINGS PANEL  (/js/widgets/sections/promptoptions.js)
       The app's video / image / music options sheets, as a popover. A sheet is
       a phone's answer to having one surface; here the composer is a box on a
       page with room beside it. The sections are the sheets' own.
       ================================================================== */
    .po {
      position: fixed; z-index: 96; width: min(420px, calc(100vw - 16px));
      display: none; flex-direction: column; max-height: min(620px, 78vh);
      border-radius: 20px; overflow: hidden;
      /* The site's own ground, not the app's sheet colour. A sheet in the app
         sits on a scrim over the whole screen and can afford its own #090D18;
         this floats on the page with the page still visible around it, and a
         near-but-not-equal grey read as a patch laid over the background
         rather than as part of it. The rim and the shadow are what make it a
         raised surface — the colour does not have to. */
      background: var(--bg); border: 1px solid var(--rim);
      box-shadow: 0 28px 70px -20px rgba(0,0,0,.8), 0 2px 10px rgba(0,0,0,.4);
    }
    .po.is-open { display: flex; }
    .po-body {
      flex: 1 1 auto; min-height: 0; overflow-y: auto;
      padding: 18px 18px 6px; scrollbar-width: thin;
    }
    .po-sec { margin-bottom: 22px; }
    .po-sec:last-child { margin-bottom: 14px; }
    /* `SectionLabel` — 14px, w500, #A0A8B8. */
    .po-label {
      font-size: 13px; font-weight: 500; color: #A0A8B8; margin-bottom: 9px;
    }
    .po-label.is-inline { margin: 0; }
    .po-sub { font-size: 12px; font-weight: 500; color: #C8CDD8; margin: 10px 0 8px; }
    .po-ico { display: block; }

    /* ---- AI Model (`ModelCard`) ---- */
    .po-models { display: flex; flex-direction: column; gap: 10px; }
    .po-model {
      display: block; width: 100%; text-align: left; cursor: pointer;
      padding: 10px 12px; border-radius: 14px;
      background: rgba(255,255,255,.08); border: 1.5px solid rgba(255,255,255,.04);
      color: var(--ink); font-family: inherit;
      transition: border-color .18s ease, box-shadow .18s ease;
    }
    .po-model:hover { border-color: rgba(255,255,255,.24); }
    .po-model.is-on {
      border-color: rgba(255,255,255,.88);
      box-shadow: 0 0 16px rgba(255,255,255,.08);
    }
    .po-model-head { display: flex; align-items: center; gap: 8px; }
    .po-model-logo { width: 30px; height: 30px; border-radius: 999px; object-fit: cover; }
    .po-model-name {
      font-size: 15px; font-weight: 500; color: #fff;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    /* `_SoundPill` — this model can produce audio at all. */
    .po-sound-pill {
      flex: 0 0 auto; font-family: var(--mono); font-size: 9px; letter-spacing: .1em;
      text-transform: uppercase; color: #C8CDD8;
      padding: 3px 7px; border-radius: 999px;
      background: rgba(255,255,255,.06); border: 1px solid var(--rim-soft);
    }
    .po-model-desc {
      margin: 4px 0 0; font-size: 12px; line-height: 1.34;
      color: rgba(255,255,255,.64);
    }
    .po-feats { display: flex; flex-wrap: wrap; gap: 6px; }
    .po-feat {
      display: inline-flex; align-items: center; gap: 4px;
      padding: 5px 6px; border-radius: 999px;
      background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.04);
      font-size: 11px; color: #fff;
    }
    .po-feat-ico { width: 14px; height: 14px; display: block; }
    /* The score the backend sends with a feature, and the tick that stands in
       when it sends none (`ModelScoreBadge` / `ModelCheckBadge`). */
    .po-score {
      font-family: var(--mono); font-size: 10px; color: #C8CDD8;
      padding-left: 2px;
    }
    .po-check {
      display: grid; place-items: center; width: 15px; height: 15px;
      border-radius: 50%; background: rgba(78,184,146,.16);
      border: 1.5px solid #4EB892;
    }
    .po-check-ico { width: 7px; height: 7px; display: block; }

    /* `AudioLanguageRow` — which languages the model can speak. Collapsed it
       is two flags and a +N beside the title; selected it opens to all. */
    .po-langs {
      display: flex; align-items: center; gap: 8px; margin-top: 10px;
    }
    .po-langs.is-open { display: block; }
    .po-langs-title {
      flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px;
      font-size: 12px; font-weight: 500; color: #C8CDD8;
    }
    .po-langs-title img { opacity: .8; }
    .po-langs-title span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .po-lang-chips { display: flex; gap: 6px; flex: 0 0 auto; }
    .po-langs.is-open .po-lang-chips { flex-wrap: wrap; margin-top: 8px; }
    .po-lang {
      display: inline-flex; align-items: center; gap: 4px;
      padding: 2px 8px 2px 6px; border-radius: 999px;
      background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft);
    }
    .po-lang-flag { font-size: 13px; line-height: 1.4; }
    .po-lang-code { font-family: var(--mono); font-size: 9px; color: #fff; }
    .po-lang.is-more {
      font-family: var(--mono); font-size: 9px; color: #fff;
      min-width: 24px; justify-content: center; padding: 4px 6px;
    }

    .po-more {
      display: block; width: 100%; margin-top: 10px; padding: 9px;
      border-radius: 12px; cursor: pointer;
      background: rgba(255,255,255,.04); border: 1px solid var(--rim-soft);
      color: var(--ink-2); font-family: inherit; font-size: 13px;
    }
    .po-more:hover { color: var(--ink); background: rgba(255,255,255,.08); }

    /* ---- Duration (`_DurationSection`) ----
       The bar runs over POSITIONS in the model's own list, not over seconds:
       sparse sets are normal, and every stop has to be a length the server
       will accept. */
    .po-dur-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
    .po-dur-value { font-size: 13px; font-weight: 500; color: #fff; }
    .po-dur { display: flex; align-items: center; gap: 12px; }
    .po-dur-bound { font-size: 14px; font-weight: 500; color: #C8CDD8; flex: 0 0 auto; }
    .po-range {
      flex: 1 1 auto; -webkit-appearance: none; appearance: none;
      height: 18px; background: transparent; cursor: pointer;
    }
    .po-range:disabled { cursor: default; opacity: .5; }
    .po-range::-webkit-slider-runnable-track {
      height: 3px; border-radius: 999px; background: rgba(255,255,255,.16);
    }
    .po-range::-moz-range-track {
      height: 3px; border-radius: 999px; background: rgba(255,255,255,.16);
    }
    .po-range::-webkit-slider-thumb {
      -webkit-appearance: none; appearance: none;
      width: 16px; height: 16px; margin-top: -6.5px;
      border-radius: 50%; background: #fff; border: 0;
      box-shadow: 0 0 0 0 rgba(255,255,255,.12); transition: box-shadow .16s;
    }
    .po-range::-moz-range-thumb {
      width: 16px; height: 16px; border-radius: 50%; background: #fff; border: 0;
    }
    .po-range:not(:disabled):hover::-webkit-slider-thumb {
      box-shadow: 0 0 0 6px rgba(255,255,255,.12);
    }

    /* ---- Sound (`_SoundSection`) ---- */
    .po-switch-row { display: flex; align-items: center; gap: 10px; cursor: pointer; }
    .po-switch { position: absolute; opacity: 0; width: 0; height: 0; }
    .po-switch-track {
      position: relative; flex: 0 0 auto; width: 44px; height: 26px;
      border-radius: 999px; background: #1C2030; transition: background .18s ease;
    }
    .po-switch-track::after {
      content: ""; position: absolute; top: 3px; left: 3px;
      width: 20px; height: 20px; border-radius: 50%; background: #fff;
      transition: transform .18s cubic-bezier(.4,0,.2,1);
    }
    .po-switch:checked + .po-switch-track { background: #4EB892; }
    .po-switch:checked + .po-switch-track::after { transform: translateX(18px); }
    .po-switch:focus-visible + .po-switch-track { outline: 2px solid var(--pink); outline-offset: 2px; }
    .po-switch-text { font-size: 14px; font-weight: 500; color: #fff; }

    /* ---- Ratio (`RatioSection`) ----
       A chip is drawn at its ratio's own proportions and keeps the share of
       the row it would have among all five, so a model offering three of them
       still reads as one descending row instead of three stretched boxes. */
    .po-ratios { display: flex; align-items: flex-end; gap: 8px; }
    .po-ratio {
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      gap: 4px; min-width: 0; cursor: pointer;
      border-radius: 14px; background: rgba(255,255,255,.08);
      border: 1px solid rgba(255,255,255,.04); color: #fff; font-family: inherit;
      transition: border-color .15s ease, box-shadow .15s ease;
    }
    .po-ratio:hover { border-color: rgba(255,255,255,.24); }
    .po-ratio.is-on {
      border-color: rgba(255,255,255,.88);
      box-shadow: 0 0 16px rgba(255,255,255,.08);
    }
    .po-ratio-ico { width: 15px; height: 15px; display: block; }
    .po-ratio-label { font-size: 12px; font-weight: 500; }

    /* ---- Quality (`QualitySection` / `_DynamicQualityChip`) ---- */
    .po-qualities { display: flex; gap: 10px; }
    .po-quality {
      position: relative; min-width: 0; cursor: pointer;
      display: flex; flex-direction: column; align-items: center; gap: 2px;
      padding: 12px 6px; border-radius: 14px;
      background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.04);
      color: #fff; font-family: inherit;
      transition: border-color .15s ease, box-shadow .15s ease;
    }
    .po-quality:hover { border-color: rgba(255,255,255,.24); }
    .po-quality.is-on {
      border-color: rgba(255,255,255,.88);
      box-shadow: 0 0 16px rgba(255,255,255,.08);
    }
    .po-quality-title { font-size: 15px; font-weight: 500; }
    .po-quality-sub { font-size: 11px; color: rgba(255,255,255,.56); }
    .po-pro {
      position: absolute; top: -7px; right: -4px;
      font-family: var(--mono); font-size: 8px; letter-spacing: .08em;
      padding: 2px 5px; border-radius: 999px; color: #0a0c14;
      background: var(--grad);
    }

    /* ---- Style (`_StyleSection`) ----
       Once something is picked every other card fades back, the rule Edit Home
       Design's own style grid follows. */
    .po-styles {
      display: flex; gap: 10px; overflow-x: auto; padding-bottom: 4px;
      scrollbar-width: none;
    }
    .po-styles::-webkit-scrollbar { display: none; }
    .po-style {
      position: relative; flex: 0 0 auto; width: 72px; height: 96px;
      border-radius: 14px; overflow: hidden; cursor: pointer; padding: 0;
      background: rgba(255,255,255,.08); border: 1.5px solid rgba(255,255,255,.04);
      color: #fff; font-family: inherit;
      transition: border-color .15s ease, opacity .18s ease;
    }
    .po-styles.is-picked .po-style { opacity: .5; }
    .po-styles.is-picked .po-style.is-on, .po-style:hover { opacity: 1; }
    .po-style.is-on { border-color: rgba(255,255,255,.88); }
    .po-style-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
    .po-style-name {
      position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
      padding: 14px 4px 5px; font-size: 11px; font-weight: 500;
      background: linear-gradient(180deg, rgba(0,0,0,0), rgba(0,0,0,.82));
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .po-style-badge {
      position: absolute; top: 5px; left: 5px; z-index: 1;
      font-family: var(--mono); font-size: 8px; letter-spacing: .08em;
      padding: 2px 5px; border-radius: 999px; color: #0a0c14; background: var(--grad);
    }

    /* ---- Genre / Mood (`_ChipWrap`) and Vocals / Recording (`_OptionGrid`) ---- */
    .po-chips { display: flex; flex-wrap: wrap; gap: 7px; }
    .po-chip {
      padding: 7px 12px; border-radius: 999px; cursor: pointer;
      background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.04);
      color: var(--ink-2); font-family: inherit; font-size: 13px;
      transition: color .15s, background .15s, border-color .15s;
    }
    .po-chip:hover { color: var(--ink); background: rgba(255,255,255,.1); }
    .po-chip.is-on {
      color: #fff; border-color: rgba(255,255,255,.88);
      background: rgba(255,255,255,.12);
    }
    .po-chip.is-more { color: var(--ink-3); }
    .po-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
    .po-grid-card {
      display: flex; align-items: center; justify-content: center; gap: 8px;
      padding: 13px 10px; border-radius: 14px; cursor: pointer;
      background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.04);
      color: #fff; font-family: inherit; font-size: 14px; font-weight: 500;
      transition: border-color .15s ease, box-shadow .15s ease;
    }
    .po-grid-card:hover { border-color: rgba(255,255,255,.24); }
    .po-grid-card.is-on {
      border-color: rgba(255,255,255,.88);
      box-shadow: 0 0 16px rgba(255,255,255,.08);
    }
    .po-grid-ico { width: 18px; height: 18px; display: block; }

    /* ---- the estimate and Apply (`BottomBar`) ----
       The number is the model's real price table for exactly this
       quality/duration, so it hides rather than guessing when the table does
       not cover the pick. */
    .po-foot {
      flex: 0 0 auto; display: flex; align-items: center; gap: 12px;
      padding: 12px 18px calc(14px + env(safe-area-inset-bottom, 0px));
      border-top: 1px solid var(--rim-soft); background: var(--bg);
    }
    .po-est { display: flex; align-items: center; gap: 6px; margin-right: auto; }
    .po-est-label { font-size: 13px; font-weight: 500; color: #C8CDD8; }
    .po-est-value { font-size: 14px; font-weight: 600; color: #fff; }
    .po-apply {
      margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
      height: 40px; padding: 0 22px; border: 0; border-radius: 999px;
      cursor: pointer; font-family: inherit; font-size: 14px; font-weight: 500;
      color: #fff; background: var(--pill-bg);
      text-shadow: 0 1px 2px rgba(0,0,0,.22);
      transition: filter .2s ease;
    }
    .po-apply:hover { filter: brightness(1.06); }

    @media (max-width: 640px) {
      .po { width: calc(100vw - 16px); max-height: 74vh; }
      .po-body { padding: 16px 14px 4px; }
      .po-foot { padding: 10px 14px calc(12px + env(safe-area-inset-bottom, 0px)); }
    }

    /* ===== discover reels overlay (/js/widgets/sheets/reelsdialog.js) =====
       The app's DiscoverDetailReelsScreen / MusicDetailReelsScreen, on the
       web. #060910 is both screens' scaffold colour; the music tint is
       `_Backdrop` (#241C15 -> #060910, stops 0 / 0.6) and the media reel has
       none, because there its artwork IS the background. */
    .rdlg {
      position: fixed; inset: 0; z-index: 95;
      transform: translateZ(0);
      visibility: hidden; pointer-events: none; opacity: 0;
      background: #060910;
      transition: opacity .2s ease;
    }
    .rdlg.is-open { opacity: 1; visibility: visible; pointer-events: auto; }
    .rdlg-backdrop { position: absolute; inset: 0; }
    .rdlg.is-music .rdlg-backdrop {
      background: linear-gradient(180deg, #241C15 0%, #060910 60%);
    }
    /* The card's own colour over that floor — js/widgets/common/tint.js, the
       app's `dominantColorOf`. A wash rather than a fill: the artwork is
       still the thing to look at, and this is the light coming off it. */
    .rdlg-backdrop::after {
      content: ""; position: absolute; inset: 0;
      --rdlg-tint: 120,120,140;
      background: linear-gradient(180deg,
        rgba(var(--rdlg-tint), .34) 0%,
        rgba(var(--rdlg-tint), .10) 46%,
        rgba(var(--rdlg-tint), 0) 78%);
      opacity: 0; transition: opacity .55s ease;
    }
    .rdlg-backdrop.is-tinted::after { opacity: 1; }

    /* The CTA claims its own row instead of floating over the pager, so the
       pager stops short of it — the app makes the same call, and for the same
       reason: content never has to clear an overlay's height by hand. */
    .rdlg-col {
      position: relative; z-index: 1;
      height: 100%; display: flex; flex-direction: column;
    }
    .rdlg-pager {
      flex: 1 1 auto; min-height: 0;
      overflow-y: auto; scroll-snap-type: y mandatory;
      scrollbar-width: none; outline: none;
      overscroll-behavior: contain;
    }
    .rdlg-pager::-webkit-scrollbar { display: none; }
    .rdlg-page {
      height: 100%; scroll-snap-align: start; scroll-snap-stop: always;
      display: flex; align-items: center; justify-content: center;
      padding: 72px 20px 20px;
    }
    .rdlg-stage {
      position: relative; width: 100%; max-width: 460px; height: 100%;
      display: flex; flex-direction: column; justify-content: flex-end;
      border-radius: 22px; overflow: hidden; background: #05070c;
      /* No frame. A `border` sits against the artwork, so a light hairline
         is invisible on every bright frame and obvious on every dark one.
         The card is lifted off the ground instead: a deep shadow pooled
         below it, a wider softer one under that, a faint darkening all
         round for the frames that go grey, and a hairline of light along
         the top edge — held up in front of the wall, not drawn on it. */
      box-shadow:
        inset 0 1px 0 rgba(255,255,255,.10),
        0 24px 48px -16px rgba(0,0,0,.9),
        0 60px 120px -24px rgba(0,0,0,.75),
        0 0 80px -10px rgba(0,0,0,.5);
      /* The CTA floats over the bottom of the card now, so the title and the
         chips stop above it. Only they move — the artwork keeps the whole
         card, which is the point of the overlay. 16px inset + a 44px button +
         16px of air is what they have to clear. */
      padding-bottom: 76px;
    }
    .rdlg-art { position: absolute; inset: 0; }
    .rdlg-poster, .rdlg-vid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; display: block;
    }
    .rdlg-vid { opacity: 0; transition: opacity .3s ease; }
    .rdlg-vid[src] { opacity: 1; }

    /* A square cover centred over the tint, the way _MusicCard sits — a track
       has cover art, not a full-bleed frame to fill. */
    .rdlg-stage.is-music {
      justify-content: center; gap: 22px; background: none;
      /* The music stage is not a card — it is a transparent column holding a
         square cover over the tint, so the rim belongs on the cover and one
         out here would draw a box around nothing. And because the cover is
         the column's full width, the stage must not clip: its `overflow:
         hidden` was cutting the cover's outset ring off down both sides —
         a frame with a top and a bottom and no sides. */
      box-shadow: none; overflow: visible;
    }
    .rdlg-stage.is-music .rdlg-art {
      position: relative; inset: auto; width: 100%; aspect-ratio: 1 / 1;
      border-radius: 20px; overflow: hidden;
      /* No frame. The cover is lifted off the tint instead: a deep shadow
         pooled well below it, a wider softer one under that, and a hairline
         of light along the top edge — what a sleeve looks like held up in
         front of a dark wall. */
      box-shadow:
        inset 0 1px 0 rgba(255,255,255,.10),
        0 24px 48px -16px rgba(0,0,0,.9),
        0 60px 120px -24px rgba(0,0,0,.75),
        0 0 80px -10px rgba(0,0,0,.5);
    }

    /* `_EdgeScrim`: an eased falloff, not a linear ramp — a straight gradient
       bands visibly against a photo. Only the media reel needs it; the music
       card sits on the tint with nothing to keep legible over artwork. */
    .rdlg-stage:not(.is-music)::after {
      content: ""; position: absolute; inset: 0; pointer-events: none;
      background: linear-gradient(180deg,
        rgba(0,0,0,.72) 0%, rgba(0,0,0,.60) 5%, rgba(0,0,0,.38) 11%,
        rgba(0,0,0,.16) 18%, rgba(0,0,0,0) 27%, rgba(0,0,0,0) 52%,
        rgba(0,0,0,.20) 68%, rgba(0,0,0,.52) 82%, rgba(0,0,0,.86) 100%);
    }

    .rdlg-info { position: relative; z-index: 1; padding: 20px; }
    .rdlg-stage.is-music .rdlg-info { padding: 0; text-align: center; }
    .rdlg-title { margin: 0; font-size: 22px; line-height: 1.2; color: #fff; }
    .rdlg-sub { margin: 6px 0 0; font-size: 14px; color: var(--ink-2); }
    .rdlg-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
    .rdlg-stage.is-music .rdlg-chips { justify-content: center; }
    .rdlg-chip {
      font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
      text-transform: uppercase; color: var(--ink-2);
      padding: 5px 10px; border-radius: 999px;
      background: rgba(255,255,255,.08); border: 1px solid var(--rim-soft);
    }

    .rdlg-nav {
      position: absolute; top: 0; left: 0; right: 0; z-index: 2;
      display: flex; align-items: center; justify-content: space-between;
      gap: 12px; padding: 16px 20px;
    }
    .rdlg-x {
      width: 36px; height: 36px; font-size: 15px; line-height: 1;
      border: 1px solid var(--rim-soft); background: rgba(10,12,20,.62);
      color: var(--ink); cursor: pointer; border-radius: 999px;
      backdrop-filter: blur(8px);
    }
    .rdlg-x:hover { background: rgba(10,12,20,.86); }

    /* `_SoundToggle` (template_detail_screen.dart): on the artwork it controls,
       top right, not out in the page chrome — it was a "SOUND OFF" word pill up
       beside the close button, which is neither the app's placement nor its
       control. The matte grey is that widget's own #6B7280 at 60%, picked there
       because this button sits over unpredictable photography and a translucent
       dark fill disappears into half of it. */
    .rdlg-sound {
      position: absolute; top: 12px; right: 12px; z-index: 3;
      display: grid; place-items: center;
      width: 40px; height: 40px; padding: 0;
      border: 1px solid rgba(255,255,255,.24); border-radius: 999px;
      background: rgba(107,114,128,.6); cursor: pointer;
      backdrop-filter: blur(10px);
      transition: background .2s ease, transform .12s ease;
    }
    .rdlg-sound:hover { background: rgba(107,114,128,.82); }
    .rdlg-sound:active { transform: scale(.94); }
    .rdlg-sound-ico {
      /* `SizedBox.square` + `BoxFit.contain` in the app. The two files are not
         the same shape — sound-off is 72x72 and sound-on 62x45 — so a square
         box without `contain` stretched the speaker vertically. */
      width: 20px; height: 20px; display: block; object-fit: contain;
      /* The same 0 1px 2px black the app hangs under every glyph that sits on
         artwork (`_RailButton`'s label shadow). A white speaker over a bright
         photo needs it; the grey fill alone does not carry it. */
      filter: drop-shadow(0 1px 2px rgba(0,0,0,.45));
    }
    /* The music card is a square cover with rounded corners; the toggle tucks
       inside them rather than sitting on the corner itself. */
    .rdlg-stage.is-music .rdlg-sound { top: 10px; right: 10px; }

    /* `_BottomCta`. It was a small gradient pill painted with `var(--g1)` and
       `var(--g2)` — two tokens this stylesheet has never defined, so the whole
       `background` declaration was invalid and the button rendered as its own
       dark text on the dark reel: present, focusable, and invisible. It is now
       the app's control: a full-width plate wearing the same create-button
       artwork the composer's own Create button does, under the gradient that
       lets the reel run up behind it (#060910, alpha 0 -> 1 at stops 0 / .5). */
    /* Over the pager, not beside it — `DiscoverDetailReelsScreen` puts its
       `_BottomCta` in a `Positioned(bottom: 0)` above the PageView, and the
       artwork runs the full height behind it. As its own flex row it left a
       dead band under the card that read as a container the button was sitting
       in. Nothing paints here: the card's own bottom scrim (`_EdgeScrim`)
       already darkens what the button sits on. */
    .rdlg-ctabar {
      position: absolute; left: 0; right: 0; z-index: 4;
      /* Sat on the column's own bottom, which put it below the card's bottom
         edge — the button crossed the rim. `.rdlg-page`'s bottom padding is
         where the card ends, so that is where this starts; the padding below
         is then the button's inset INSIDE the card. */
      bottom: calc(20px + env(safe-area-inset-bottom, 0px));
      padding: 32px 20px 16px;
      background: none; pointer-events: none;
    }
    /* The app's 56px runs the full width of a phone; the same number under a
       460px stage on a desktop is a slab, so the plate is kept and its scale
       is not — 44px tall and only as wide as it needs to be, centred. */
    .rdlg-cta {
      pointer-events: auto;
      display: flex; align-items: center; justify-content: center; gap: 7px;
      width: 100%; max-width: 300px; margin: 0 auto; height: 44px;
      border: 0; border-radius: 999px; cursor: pointer;
      font-family: inherit; font-size: 15px; font-weight: 500; color: #fff;
      background: var(--pill-bg);
      text-shadow: 0 1px 2px rgba(0,0,0,.22);
      box-shadow: 0 8px 22px -10px rgba(254,2,153,.7);
      transition: filter .2s ease;
    }
    .rdlg-cta:hover { filter: brightness(1.06); }
    .rdlg-cta-ico { width: 18px; height: 18px; display: block; }

    @media (max-width: 640px) {
      .rdlg-page { padding: 64px 12px 12px; }
      .rdlg-title { font-size: 19px; }
      /* `.rdlg-page`'s bottom padding is 12 here, so that is where the card
         ends and where the bar starts. */
      .rdlg-ctabar {
        bottom: calc(12px + env(safe-area-inset-bottom, 0px));
        padding: 28px 16px 14px;
      }
      /* A phone IS the app's own case, so here it takes the app's full width. */
      .rdlg-cta { max-width: 460px; height: 48px; font-size: 16px; }
      .rdlg-cta-ico { width: 20px; height: 20px; }
    }


    /* ---- the action rail (_ActionRail) ----
       Down the right edge over the artwork, at a share of the height rather
       than a fixed offset so it keeps its place against the CTA on any size. */
    .rdlg-rail {
      position: absolute; right: 20px; bottom: 22%; z-index: 2;
      display: flex; flex-direction: column; gap: 16px;
    }
    .rdlg-rail[hidden] { display: none; }
    .rdlg-rail-btn {
      display: flex; flex-direction: column; align-items: center; gap: 4px;
      width: 52px; padding: 8px 0; cursor: pointer;
      border: 0; background: none; color: #fff;
    }
    .rdlg-rail-glyph {
      position: relative;
      display: grid; place-items: center; width: 44px; height: 44px;
      border-radius: 999px; color: #fff;
      background: rgba(10,12,20,.62); border: 1px solid var(--rim-soft);
      backdrop-filter: blur(8px);
      /* _RailButton's press: a shallow squeeze that follows the finger down
         (110ms) and eases back slower than it went (180ms). */
      transition: transform .18s ease-out, background .2s ease;
    }
    .rdlg-rail-btn:hover .rdlg-rail-glyph { background: rgba(10,12,20,.86); }
    .rdlg-rail-btn:active .rdlg-rail-glyph {
      transform: scale(.88); transition-duration: .11s;
    }
    /* Each glyph keeps its own viewBox ratio — none of the three is square
       (26.67x24.43, 26.67x24, 26.67x21.33), so a fixed square box would
       letterbox them to different visual sizes. Width is the shared measure,
       which is what makes them read as one set, and it is the app's call too:
       _LikeRailButton scales the outline against its own reference box rather
       than a fraction of the button. */
    .rdlg-ico { width: 22px; height: auto; display: block; }
    /* The two heart states stack and crossfade over 150ms, the way the app's
       AnimatedSwitcher does. Only the filled one is ever red. */
    .rdlg-rail-glyph .is-filled, .rdlg-rail-glyph .is-outline {
      grid-area: 1 / 1; transition: opacity .15s ease;
    }
    .rdlg-rail-glyph .is-filled { opacity: 0; color: #ff2d55; }
    .rdlg-rail-btn.is-on .rdlg-rail-glyph .is-outline { opacity: 0; }
    .rdlg-rail-btn.is-on .rdlg-rail-glyph .is-filled { opacity: 1; }
    .rdlg-rail-btn.is-on .rdlg-rail-glyph { border-color: rgba(255,45,85,.5); }
    .rdlg-rail-btn.is-done .rdlg-rail-glyph { color: var(--amber); }

    /* The Lottie's burst, rebuilt in CSS: the heart overshoots and settles
       while a ring radiates past the button. Plays only on the way in — an
       unlike drops the class, so nothing animates back out. Its ~900ms is the
       composition's own duration. Kept out of the layout (the ring is a
       pseudo-element on an absolutely placed span) so the rail never shifts. */
    .rdlg-burst {
      position: absolute; inset: 0; border-radius: 999px;
      pointer-events: none;
    }
    .rdlg-rail-btn.is-burst .rdlg-rail-glyph .is-filled {
      animation: rdlg-heart .9s cubic-bezier(.2,.9,.25,1) both;
    }
    .rdlg-rail-btn.is-burst .rdlg-burst::after {
      content: ""; position: absolute; inset: 0; border-radius: 999px;
      border: 2px solid #ff2d55;
      animation: rdlg-ring .9s cubic-bezier(.2,.7,.3,1) both;
    }
    @keyframes rdlg-heart {
      0%   { transform: scale(.2); }
      28%  { transform: scale(1.28); }
      46%  { transform: scale(.94); }
      64%  { transform: scale(1.06); }
      100% { transform: scale(1); }
    }
    @keyframes rdlg-ring {
      0%   { transform: scale(.55); opacity: 0; border-width: 3px; }
      18%  { opacity: .85; }
      100% { transform: scale(1.85); opacity: 0; border-width: 1px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .rdlg-rail-btn.is-burst .rdlg-rail-glyph .is-filled,
      .rdlg-rail-btn.is-burst .rdlg-burst::after { animation: none; }
      .rdlg-rail-btn.is-burst .rdlg-burst::after { display: none; }
    }
    .rdlg-rail-label {
      font-family: var(--mono); font-size: 9px; letter-spacing: .1em;
      text-transform: uppercase; color: var(--ink-2);
    }
    .rdlg-rail-btn.is-done .rdlg-rail-label::after { content: " ✓"; }

    /* ===== quick edits (/js/widgets/sheets/quickeditdialog.js) =====
       QuickEditScreen's shape: the photo claims the room, the effects sit
       under it as settings of the same screen, and the CTA prices whichever
       one is selected. */
    .qed {
      position: fixed; inset: 0; z-index: 96;
      transform: translateZ(0);
      visibility: hidden; pointer-events: none; opacity: 0;
      display: flex; align-items: center; justify-content: center; padding: 24px;
      background: rgba(3, 5, 8, .74);
      transition: opacity .2s ease;
    }
    .qed.is-open { opacity: 1; visibility: visible; pointer-events: auto; }
    /* Three bands, and only the middle one scrolls — the title above, the
       price and the button below, and the photo well between them taking
       whatever height is left. The well is a 1:1 box and the card is capped
       at 90svh, so on any window shorter than about 800px the two used to
       argue and the tray lost: it was pushed out through the bottom of the
       card, corners and all. */
    .qed-card {
      display: flex; flex-direction: column;
      width: 100%; max-width: 460px; max-height: min(90svh, 860px);
      padding: 18px 18px 0; border-radius: 26px; overflow: hidden;
      background: #060910; border: 1px solid var(--rim-soft);
      box-shadow: 0 40px 100px rgba(0,0,0,.6);
      transform: translateY(10px) scale(.985);
      transition: transform .22s cubic-bezier(.4,0,.2,1);
    }
    .qed-body {
      flex: 1 1 auto; min-height: 0; overflow-y: auto;
      display: flex; flex-direction: column; gap: 18px;
      margin: 18px -4px 0; padding: 0 4px;
      scrollbar-width: thin;
    }
    /* Neither band shrinks: a 1:1 box told to flex-shrink keeps painting at
       its own ratio and rides straight over whatever is under it, and a
       `max-height` on it takes the width down with it and leaves a small
       square floating in a wide card. What gives instead is the well's
       RATIO — it stays the card's full width and gets shorter as the window
       does, which is the one measurement here that has any slack in it. */
    .qed-well, .qed-row { flex: 0 0 auto; }
    .qed.is-open .qed-card { transform: none; }
    .qed-head { display: flex; align-items: center; justify-content: space-between; }
    .qed-title { margin: 0; font-size: 17px; color: #fff; }
    .qed-x {
      width: 32px; height: 32px; border-radius: 999px; cursor: pointer;
      border: 1px solid var(--rim-soft); background: transparent; color: var(--ink-2);
    }
    .qed-x:hover { color: var(--ink); background: rgba(255,255,255,.06); }

    /* The photo well: white 16% at radius 20, its rim at 12% — and at 48%
       while a photo is the thing the screen is waiting for. */
    .qed-well { position: relative; display: block; cursor: pointer; }
    .qed-well-inner {
      display: grid; place-items: center; overflow: hidden;
      aspect-ratio: 1 / 1; border-radius: 20px;
      background: rgba(255,255,255,.16);
      border: 1px solid rgba(255,255,255,.48);
      transition: border-color .2s ease;
    }
    .qed.is-ready .qed-well-inner { border-color: rgba(255,255,255,.12); }
    .qed-well.is-over .qed-well-inner { border-color: var(--amber); }
    /* Declared after the base rule, not before it: same specificity, and a
       later plain rule beats an earlier media query. */
    @media (max-height: 860px) { .qed-well-inner { aspect-ratio: 4 / 3; } }
    @media (max-height: 720px) {
      .qed-well-inner { aspect-ratio: 2 / 1; }
      .qed-body { gap: 12px; margin-top: 12px; }
    }
    .qed-photo { width: 100%; height: 100%; object-fit: cover; display: block; }
    .qed-well-note {
      display: flex; flex-direction: column; align-items: center; gap: 8px;
      color: rgba(255,255,255,.72); font-size: 13px;
    }
    .qed-well-glyph {
      display: grid; place-items: center; width: 40px; height: 40px;
      border-radius: 14px; font-size: 20px; background: rgba(255,255,255,.1);
    }
    .qed-file { position: absolute; inset: 0; opacity: 0; cursor: pointer; }

    /* _FeatureCard: #1C2030 at radius 12, unselected sits at 70% with a hair
       rim, selected goes full with a bright 1.5px stroke and a soft glow. */
    .qed-row { display: flex; gap: 12px; }
    .qed-feat {
      flex: 1 1 0; min-width: 0; padding: 0; cursor: pointer;
      background: none; border: 0; color: var(--ink);
      display: flex; flex-direction: column; gap: 7px; align-items: center;
      opacity: .7; transition: opacity .2s ease;
    }
    .qed-feat.is-on { opacity: 1; }
    .qed-feat-art {
      width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block;
      border-radius: 12px; background: #1C2030;
      border: 1px solid rgba(255,255,255,.04);
      transition: border-color .2s ease, box-shadow .2s ease;
    }
    .qed-feat.is-on .qed-feat-art {
      border: 1.5px solid rgba(255,255,255,.88);
      box-shadow: 0 0 16px rgba(255,255,255,.08);
    }
    .qed-feat-name {
      font-size: 11px; line-height: 1.25; text-align: center; color: var(--ink-2);
    }
    .qed-feat.is-on .qed-feat-name { color: var(--ink); }

    /* Quick edits wears the template dialog's BottomCta (.tdlg-tray /
       .tdlg-coins / .tdlg-go) — the same price said the same way on the two
       dialogs that are, from the reader's side, the same decision.

       The one difference is where it sits. In the app this tray is flush
       against the bottom of the sheet, so here it bleeds through the card's
       own 18px padding to the card's edge and its bottom corners are the
       card's (26px), not the sheet's (28px). Floated in the middle of the
       padding it read as a separate object that had come loose. */
    .qed-tray {
      flex: 0 0 auto;
      margin: 18px -18px 0; padding-inline: 8px 8px;
      border-radius: 16px 16px 25px 25px;
    }
    .qed-tray .tdlg-go { border-radius: 24px; }

    @media (max-width: 640px) {
      .qed { padding: 12px; }
      .rdlg-rail { right: 12px; bottom: 24%; }
      .rdlg-rail-glyph { width: 40px; height: 40px; }
    }

    /* ===== standalone sub-page (/templates) ===== */
    /* Borrows .panel-head / .panel-sub / .grid, with its own top padding:
       there is no hero above it to sit under. */
    .page {
      position: relative; z-index: 2;
      padding: clamp(38px, 6vh, 74px) 0 clamp(64px, 9vh, 120px);
    }
    .backlink {
      display: inline-block; margin-bottom: 18px; text-decoration: none;
      font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em;
      text-transform: uppercase; color: var(--ink-4);
      transition: color .16s;
    }
    .backlink:hover { color: var(--ink-2); }

    .panel-head { text-align: center; }
    .panel-head h2 {
      font-family: var(--font); font-weight: 600; color: var(--ink);
      font-size: clamp(30px, 4vw, 54px); line-height: 1.06; letter-spacing: -0.035em;
      margin: 0; text-wrap: balance;
    }
    .panel-head h2 .g {
      background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
    }
    .panel-sub {
      max-width: 56ch; margin: clamp(12px, 1.6vh, 18px) auto 0;
      color: var(--ink-3); font-size: clamp(14px, 1.4vw, 17px); line-height: 1.5;
    }

    /* ===== filter chips ===== */
    .filters {
      display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap;
      margin-top: clamp(24px, 3.4vh, 38px);
    }
    .chips {
      display: flex; gap: 4px; padding: 5px;
      background: var(--glass); border: 1px solid var(--rim-soft); border-radius: var(--r-pill);
    }
    .chip {
      border: 0; background: none; cursor: pointer;
      padding: 8px 16px; border-radius: var(--r-pill);
      font-family: var(--font); font-size: 13.5px; font-weight: 500; color: var(--ink-3);
      transition: color .16s, background .16s;
    }
    .chip:hover { color: var(--ink); }
    .chip.is-on { background: #fff; color: #0a0a12; font-weight: 600; }
    .count {
      font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em;
      text-transform: uppercase; color: var(--ink-4);
    }

    /* ===== showcase grid ===== */
    /* 220px is chosen against `.wrap-wide`'s 1560px ceiling, not by eye: seven
       columns would need a 214px minimum to fit there, so 220 caps the grid at
       six however wide the screen gets, and six is what fills that width at a
       comfortable ~250px per card. Below 1200px the old 176px minimum keeps the
       ladder where it was. */
    .grid {
      display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px;
      margin-top: clamp(22px, 3vh, 34px);
    }
    @media (max-width: 1199px) {
      .grid { grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); }
    }
    /* the whole tile is the link — the pill below is just its hover affordance,
       so a tap anywhere works on touch where there is no hover at all */
    .tile {
      position: relative; display: block; text-decoration: none;
      aspect-ratio: 9 / 16; overflow: hidden; cursor: pointer;
      border-radius: 14px; background: #0b0f1a; border: 1px solid var(--rim-soft);
      transition: border-color .2s ease;
    }
    .tile:hover { border-color: var(--rim); }
    .tile-img, .tile-vid {
      position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block;
    }
    /* the preview fades in over its own poster, so there is never a black frame */
    .tile-vid { opacity: 0; transition: opacity .3s ease; }
    .tile.is-playing .tile-vid { opacity: 1; }
    /* The second layer of a stacked still card. js/gallery.js loads the next
       poster into whichever of the two layers is currently hidden and then
       toggles `is-alt`, so the crossfade always has a decoded frame to reveal
       and the two layers keep swapping roles for as long as you hover. */
    .tile-alt { opacity: 0; transition: opacity .32s ease; }
    .tile.is-alt .tile-alt { opacity: 1; }
    .tile::after {
      content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
      background: linear-gradient(180deg,
        rgba(0,0,0,.42) 0%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 44%, rgba(0,0,0,.82) 100%);
    }
    /* ---- the concept badge (QuickIconBadge) ----
       Top left, the app's own corner, saying what the card MAKES rather than
       which shelf it came off. js/widgets/common/quickicon.js picks the glyph;
       everything here is the circle around it.

       The app draws it as a real blur — FrostedBackdrop(sigma: 50) under a
       #3A3C42 at 60% — and ships an opaque #1C1E24 fallback for the platform
       where that costs too much. This is that platform: one backdrop-filter
       per tile is forty render surfaces re-sampling at once as the grid rises,
       which is the same call `.tile-flag`'s neighbours already made. #2E3036
       is precisely what the app's own fallback resolves to (0.6 * #3A3C42 over
       #1C1E24), so the two look alike rather than merely similar.

       `--qb-inset` is a fraction of the badge's own size, not of the tile's,
       which is why it goes through calc() and not a percentage: percentage
       padding resolves against the CONTAINING block's width, so a 132px tile
       and a 200px one would inset the same glyph differently. */
    .qbadge {
      --qb-size: 30px;
      --qb-inset: .18;                       /* _kIconInset */
      position: absolute; top: 8px; left: 8px; z-index: 2;
      box-sizing: border-box; display: block;
      width: var(--qb-size); height: var(--qb-size); border-radius: 50%;
      padding: calc(var(--qb-size) * var(--qb-inset));
      background: #2E3036; border: 1px solid rgba(255,255,255,.14);
    }
    .qbadge img { width: 100%; height: 100%; display: block; object-fit: contain; }
    /* One inset per artwork family — the three are drawn to different bounding
       boxes, and a single ratio cannot sit them at the same visual size. */
    /* Quick edits pairs its badge with a control of its own, so the app draws
       that one smaller — 24 against the template card's 32. */
    .qbadge.is-edit    { --qb-size: 24px; }
    .qbadge.is-tight   { --qb-inset: .275; } /* fills its canvas edge to edge */
    .qbadge.is-gallery { --qb-inset: .24; }  /* a filled rectangle, not an outline */
    .qbadge.is-svg     { --qb-inset: .2083; }/* 5/24, the ratio that spec draws */
    /* The spark's artwork is bottom-heavy: its alpha-weighted centre sits 7.3%
       of the canvas below the geometric one, so centring the file leaves the
       glyph looking low. A percentage translate is against the glyph's own box,
       which is exactly the `size - inset * 2` the app measures it from. */
    .qbadge.is-tight img { transform: translateY(-7.3%); }

    /* NEW / TRENDING, opposite it. Its own artwork, the app's own corner —
       these two say different things and no longer take turns in one slot. */
    .tile-flag {
      position: absolute; top: 10px; right: 8px; z-index: 2;
      height: 18px; width: auto; display: block;
    }
    .tile-foot {
      position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 2; text-align: left;
      transition: transform .24s cubic-bezier(.4,0,.2,1);
    }
    .tile-sub {
      display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    /* Two lines, which is the app's own `maxLines: 2` on this exact title
       (TemplateShowcaseSection). One line was the site's own tightening, and
       these titles are written long enough to need the second — "Living Room →
       Bohemian" and "Home Office → Custom Prompt" both lost their point to an
       ellipsis. The foot is bottom-anchored, so the second line grows upward
       and the hover lift still clears the CTA pill underneath. */
    .tile-title {
      display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
      overflow: hidden; overflow-wrap: anywhere;
      color: #fff; font-size: 12.5px; font-weight: 600; text-transform: uppercase;
      letter-spacing: .01em; line-height: 1.25;
    }
    .tile-sub {
      color: rgba(255,255,255,.6); font-size: 11px; margin-top: 2px;
      opacity: 0; transform: translateY(4px);
      transition: opacity .22s ease, transform .22s ease;
    }
    .tile-cta {
      position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 3;
      display: inline-flex; align-items: center; justify-content: center; height: 32px;
      /* same treatment as "Get the app" — not the .gpill class itself, whose
         :hover lift would fight this pill's own reveal transform */
      border-radius: var(--r-pill); background: var(--pill-bg); color: #fff;
      font-family: var(--font); font-size: 12.5px; font-weight: 600;
      text-shadow: 0 1px 2px rgba(0,0,0,.22);
      box-shadow: 0 8px 20px -8px rgba(0,0,0,.55), 0 2px 12px rgba(254,2,153,.14);
      opacity: 0; transform: translateY(6px);
      transition: opacity .22s ease, transform .22s ease;
    }
    .tile:hover .tile-foot, .tile:focus-visible .tile-foot { transform: translateY(-38px); }
    .tile:hover .tile-sub,  .tile:focus-visible .tile-sub  { opacity: 1; transform: none; }
    .tile:hover .tile-cta,  .tile:focus-visible .tile-cta  { opacity: 1; transform: none; }
    .tile:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

    /* The grid loads its own second batch, so this footer is not a pager: it
       only turns up once the grid has stopped growing, and it leads out of the
       section (see js/gallery.js). */
    .grid-foot {
      display: flex; flex-direction: column; align-items: center; gap: 12px;
      margin-top: clamp(26px, 3.4vh, 40px);
    }
    .grid-foot[hidden] { display: none; }
    .grid-note {
      font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em;
      text-transform: uppercase; color: var(--ink-4);
    }
    .grid-note[hidden] { display: none; }

    /* ===== touch (nothing reveals on hover, so nothing pretends to) =====
       Every hover affordance is removed rather than pinned open: the fan's
       "Try Effect" would otherwise sit permanently over the artwork, and a tap
       anywhere on a card or a tile already does the same thing the button did.
       `pointermove` fires on a tap too, so the button is taken out with
       `display: none` — not just left un-revealed. */
    /* ===== template wall =====
       deevid.ai's trending section, rebuilt: six staggered portrait columns
       clipped to a fixed height, the copy laid over the top of them, and a
       fade into the sheet at the bottom. The stagger is a symmetric V — the
       middle columns start lowest — which is what opens the gap the headline
       sits in. */
    .tw { position: relative; }

    .tw-wall {
      display: grid; grid-template-columns: repeat(6, 220px); gap: 12px;
      justify-content: center;
      /* Content is taller than this on purpose: the clip is the effect. */
      height: 1360px; overflow: hidden;
    }
    .tw-col {
      display: flex; flex-direction: column;
      /* No `gap`: each card carries its own bottom margin, so one run of cards
         is exactly the loop's travel and the wrap has no seam. With a gap the
         copies would differ by one gap and the wall would jump every pass —
         the same rule the sign-in dialog's wall documents.
         The V-shaped padding this replaced is gone for the same reason: the
         padding is not duplicated, so it would sit inside one copy only. The
         columns are desynchronised with negative delays instead, which is
         also what stops them reading as one sliding sheet. */
      animation: twdrift 44s linear infinite;
    }
    .tw-col:nth-child(2) { animation-duration: 56s; animation-direction: reverse; }
    .tw-col:nth-child(3) { animation-duration: 48s; animation-delay: -13s; }
    .tw-col:nth-child(4) { animation-duration: 60s; animation-direction: reverse; animation-delay: -21s; }
    .tw-col:nth-child(5) { animation-duration: 50s; animation-delay: -7s; }
    .tw-col:nth-child(6) { animation-duration: 64s; animation-direction: reverse; animation-delay: -31s; }
    /* The travel is the height of ONE run of cards, measured in js/main.js and
       written here as --tw-shift. It used to say -50%, which is the same thing
       only while a column holds exactly two runs AND one of them is taller
       than the clip — a silent agreement between the card count in JS, the
       card's 9:16 aspect, the column width and the wall height, the last three
       of which change at five breakpoints. When it stopped holding, a card
       drifted up with nothing behind it. The fallback keeps the wall moving
       for the frame before the measurement lands. */
    @keyframes twdrift { to { transform: translateY(calc(-1 * var(--tw-shift, 50%))); } }

    /* The wall's cards are .tile elements — the same ones the templates page
       renders — so their aspect, radius, scrim and hover reveal all come from
       the .tile rules above. This class only adds what the column needs. */
    /* `flex: 0 0 auto` is load-bearing. A column's cards are taller than the
       column's own box, and a flex item shrinks to fit by default — the aspect
       ratio is only a *suggestion* to the minimum size, and browsers disagree
       about whether it survives. A squashed card makes the run shorter than
       the wall measured it to be, which is a gap at the bottom of the loop. */
    .tw-card { flex: 0 0 auto; margin-bottom: 12px; }   /* not a gap, on purpose */
    /* Reading beats drifting: the pointer stops the column it is over, so a
       card's details can actually be read once they appear. */
    .tw-col:hover { animation-play-state: paused; }

    /* Where this has to part company with deevid: their cards are pale grey,
       so dark copy reads straight off them. Ours are full-colour video, and
       white text over moving footage is unreadable — so the copy carries the
       same gradient scrim the sign-in dialog's does. */
    .tw-copy {
      position: absolute; inset: 0 0 auto; z-index: 3;
      padding: clamp(8px, 2vh, 30px) 0 clamp(56px, 9vh, 128px);
      background: linear-gradient(180deg,
        var(--bg) 0%, rgba(6, 9, 16, .94) 38%, rgba(6, 9, 16, .6) 74%, rgba(6, 9, 16, 0) 100%);
      /* Inert except for the button: the wall behind is decoration and must
         not swallow a click meant for the page. */
      pointer-events: none;
    }
    .tw-copy .btn { pointer-events: auto; }

    .tw-cta {
      margin-top: clamp(18px, 2.6vh, 30px);
      display: flex; flex-direction: column; align-items: center; gap: 11px;
    }

    /* deevid's 300px bottom gradient, aimed at the sheet colour instead of
       white. This is what stops the clip reading as a hard cut. */
    .tw-fade {
      position: absolute; inset-inline: 0; bottom: 0; z-index: 2;
      height: 340px; pointer-events: none;
      background: linear-gradient(180deg, rgba(6, 9, 16, 0) 0%, var(--bg) 84%);
    }

    /* ===== models =====
       Sits directly under the showcase panel and shares the same background,
       so the two read as one continuous page rather than two stacked cards. */
    .models {
      position: relative; z-index: 2;
      padding: clamp(48px, 7vh, 92px) 0 clamp(56px, 8vh, 104px);
      /* The header is sticky and the promo strip above it is measured into
         --promo-h by js/site.js, so an anchor jump has to clear both or the
         heading lands underneath them. */
      scroll-margin-top: calc(72px + var(--promo-h, 0px) + 18px);
    }
    /* Mono, uppercase, wide — the same treatment the mega-menu column headings
       use, so a label reads as a label everywhere on the site. */
    .eyebrow {
      font-family: var(--mono); font-size: 10px; letter-spacing: .18em;
      text-transform: uppercase; color: var(--ink-4);
      margin: 0 0 14px;
    }

    /* The drift, shared by the review rows below: a track carries its cards
       twice and slides exactly -50%, so the instant it wraps the second set is
       sitting where the first one started. The spacing is a margin per card
       and not a `gap` for the same reason — with a gap one set is not exactly
       half the track and the loop jumps by one gap every pass. */
    .mtrack {
      /* stretch, not flex-start: the cards carry different amounts of text and
         a row of ragged-bottomed cards reads as broken rather than organic. */
      display: flex; width: max-content; align-items: stretch;
      animation: mdrift 46s linear infinite;
    }
    @keyframes mdrift { to { transform: translateX(-50%); } }
    .rev-row:hover .mtrack { animation-play-state: paused; }

    /* ===== the page background =====
       One colour, everywhere, on every page — the same --bg the fixed .bgfx
       backdrop and <body> paint, so there is no seam between a section and the
       page it sits on and nothing changes as you scroll. These stay opaque on
       purpose: the ambient floor-glow in .bgfx is anchored to the viewport, so
       letting it through here would drag a magenta band down the page behind
       every section. It belongs to the hero, which is the one thing that does
       not paint over it. */
    .panel, .models, .revs, .foot { background-color: var(--bg); }


    /* ===== models: the burst =====
       The cards start stacked behind the copy and travel out to their own spot
       as the section arrives. Their resting places are --x/--y on each card
       (percentages of the stage); the offset back to the middle is measured in
       js/main.js as --dx/--dy, because a percentage translate here would be a
       percentage of the card and not of the stage. */
    .pm { position: relative; }
    .pm-stage {
      position: relative;
      /* Scales with the viewport rather than sitting at a fixed size: on a
         27-inch screen a fixed stage leaves the cards marooned around a lot of
         empty middle. */
      height: clamp(560px, 52vw, 900px);
      /* One place for the card size, because the positions below have to know
         it: a card is centred on its --x/--y, so its own half has to be kept
         inside the stage or it hangs off the edge. */
      --cw: clamp(152px, 15vw, 340px);
      --ch: calc(var(--cw) * 9 / 16);
    }

    .pm-card {
      position: absolute;
      /* The scatter is clamped so no card can ever cross the stage edge,
         whatever the window. --x/--y are where a card would like to sit; the
         clamp keeps its own half plus a margin inside, which is what stops the
         outermost cards hanging off a wide screen. */
      left: clamp(calc(var(--cw) / 2 + 10px), var(--x), calc(100% - var(--cw) / 2 - 10px));
      top: clamp(calc(var(--ch) / 2 + 10px), var(--y), calc(100% - var(--ch) / 2 - 10px));
      /* Grows with the viewport, and the ceiling is high enough that a 27-inch
         screen does not get postage stamps. */
      width: var(--cw);
      aspect-ratio: 16 / 9;
      border-radius: 16px; overflow: hidden;
      background: #0b0f1a; border: 1px solid var(--rim-soft);
      box-shadow: 0 26px 60px -26px rgba(0, 0, 0, .8);
      transform: translate(-50%, -50%) translate(var(--dx, 0px), var(--dy, 0px)) scale(.62);
      opacity: 0;
      transition: transform .95s cubic-bezier(.16, .84, .24, 1), opacity .55s ease,
                  border-color .2s ease;
      transition-delay: var(--d, 0ms);
    }
    .pm.is-in .pm-card { transform: translate(-50%, -50%) scale(1); opacity: 1; }
    .pm-card:hover { border-color: var(--rim); }

    .pm-art, .pm-vid {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; display: block;
    }
    /* The clip fades up over its own still, so a card that starts playing never
       shows a black frame while the first frame decodes. */
    .pm-vid { opacity: 0; transition: opacity .4s ease; }
    .pm-card.is-playing .pm-vid { opacity: 1; }

    /* No full-bleed scrim here, deliberately. Three of the catalog's promo
       creatives are built on white (DALL-E 3, GPT Image 2 and Nano Banana 2
       are screenshots of a light UI), and a top-and-bottom dark gradient over
       white does not read as a scrim — it reads as two grey bands, which is
       exactly the "broken border" it looked like. The name and the tag carry
       their own solid chips instead, which works over any artwork. */

    .pm-tag {
      position: absolute; top: 9px; left: 9px; z-index: 2;
      padding: 3px 8px; border-radius: var(--r-pill);
      background: rgba(4, 6, 11, .78);
      border: 1px solid rgba(255, 255, 255, .13);
      font-family: var(--mono); font-size: 8.5px; letter-spacing: .12em;
      text-transform: uppercase; color: var(--ink-2);
    }
    .pm-foot {
      position: absolute; left: 9px; bottom: 9px; z-index: 2;
      display: flex; align-items: center; gap: 7px;
      /* Its own chip rather than a band across the card — see above. */
      max-width: calc(100% - 18px);
      padding: 4px 11px 4px 5px; border-radius: var(--r-pill);
      background: rgba(4, 6, 11, .78);
      border: 1px solid rgba(255, 255, 255, .13);
      transition: opacity .18s ease;
    }
    .pm-logo {
      flex: none; width: 22px; height: 22px; border-radius: 6px;
      object-fit: contain; background: rgba(255, 255, 255, .1);
    }
    .pm-name {
      font-family: var(--font); font-weight: 600; font-size: 12.5px; color: #fff;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    /* The description takes the name's place on hover rather than sitting
       beside it — at this card size there is only room for one of them. */
    .pm-desc {
      position: absolute; left: 9px; right: 9px; bottom: 9px; z-index: 3;
      margin: 0; padding: 9px 11px; border-radius: 12px;
      background: rgba(11, 15, 26, .95); border: 1px solid var(--rim);
      font-size: 11.5px; line-height: 1.35; color: var(--ink-2);
      opacity: 0; transform: translateY(6px); pointer-events: none;
      transition: opacity .18s ease, transform .18s ease;
    }
    .pm-card:hover .pm-desc { opacity: 1; transform: none; }
    .pm-card:hover .pm-foot { opacity: 0; }

    /* The copy sits dead centre and the cards ring around it. Inert, so a
       pointer aimed at a card behind the headline still reaches it. */
    .pm-copy {
      position: absolute; inset: 0; z-index: 4;
      display: flex; align-items: center; justify-content: center;
      pointer-events: none;
    }

    /* Below this the ring has nowhere to go without crowding the copy, so the
       cards become a plain two-column grid underneath it and arrive with a
       lift instead of a burst — spreading eight cards across a phone is what
       makes them postage stamps. */
    @media (max-width: 860px) {
      .pm-stage {
        height: auto;
        display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;
        margin-top: clamp(24px, 4vh, 38px);
      }
      .pm-card {
        position: static; width: auto;
        transform: translateY(16px) scale(.98);
        transition-duration: .5s;
      }
      .pm.is-in .pm-card { transform: none; }
      .pm-copy { position: static; pointer-events: auto; }
    }

    /* ===== reviews =====
       Two drifting rows, reusing .mtrack's loop (/js/main.js fills every track
       it finds, so these get the doubling for free). Opposite directions and
       different speeds, for the same reason the model rows have them. */
    .revs {
      position: relative; z-index: 2;
      padding: clamp(44px, 6vh, 84px) 0 clamp(84px, 12vh, 150px);
    }
    /* The copy above needs room to breathe before the rows start sliding
       underneath it; without this the sub-line and the first card touch. */
    .revs .wrap-wide { margin-top: clamp(30px, 4.5vh, 56px); }
    .rev-row {
      overflow: hidden;
      mask-image: linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%);
      -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 5%, #000 95%, transparent 100%);
    }
    .rev-row + .rev-row { margin-top: 12px; }
    .rev-track { animation-duration: 64s; }
    .rev-track-b { animation-duration: 78s; animation-direction: reverse; }

    .rev {
      flex: none; width: 344px; margin: 0 12px 0 0;
      display: flex; flex-direction: column; justify-content: space-between; gap: 16px;
      padding: 20px 22px;
      background: var(--glass); border: 1px solid var(--rim-soft);
      border-radius: 18px;
      transition: border-color .18s ease, background .18s ease;
    }
    .rev:hover { border-color: var(--rim); background: var(--glass-2); }
    .rev blockquote { margin: 0; position: relative; }
    /* The quote mark is decoration, so it is drawn rather than typed — a real
       character here would be read out before the review itself. */
    .rev blockquote::before {
      content: ""; display: block; width: 19px; height: 14px; margin-bottom: 12px;
      background: var(--grad);
      -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19 14'%3E%3Cpath d='M0 14V7.6C0 3.4 2.6.6 6.6 0l.8 2.5C5 3 3.7 4.3 3.6 6.2H7V14H0zm12 0V7.6c0-4.2 2.6-7 6.6-7.6l.8 2.5c-2.4.5-3.7 1.8-3.8 3.7H19V14h-7z' fill='%23000'/%3E%3C/svg%3E") no-repeat center / contain;
      mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19 14'%3E%3Cpath d='M0 14V7.6C0 3.4 2.6.6 6.6 0l.8 2.5C5 3 3.7 4.3 3.6 6.2H7V14H0zm12 0V7.6c0-4.2 2.6-7 6.6-7.6l.8 2.5c-2.4.5-3.7 1.8-3.8 3.7H19V14h-7z' fill='%23000'/%3E%3C/svg%3E") no-repeat center / contain;
    }
    .rev blockquote b {
      display: block; margin-bottom: 7px;
      font-family: var(--font); font-weight: 600; font-size: 15px; color: var(--ink);
      letter-spacing: -0.01em;
    }
    .rev blockquote p {
      margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-3);
    }
    .rev figcaption { display: flex; align-items: center; gap: 10px; }
    .rev-av {
      flex: none; width: 30px; height: 30px; border-radius: 50%;
      background: var(--grad); color: #fff;
      display: flex; align-items: center; justify-content: center;
      font-family: var(--font); font-weight: 600; font-size: 13px;
      text-shadow: 0 1px 2px rgba(0,0,0,.25);
    }
    .rev-who { font-size: 13px; font-weight: 500; color: var(--ink-2); }

    @media (max-width: 640px) {
      .rev { width: 280px; padding: 17px 18px; }
      .rev blockquote p { font-size: 13px; }
    }

    /* ===== footer ===== */
    .foot {
      position: relative; z-index: 2;
      border-top: 1px solid var(--rim-soft);
      padding: clamp(38px, 5vh, 60px) 0 clamp(26px, 3vh, 36px);
    }
    .foot-top {
      display: flex; flex-wrap: wrap; gap: clamp(28px, 4vw, 72px);
      justify-content: space-between;
    }
    .foot-brand img { height: 28px; display: block; }
    .foot-brand p {
      margin: 12px 0 0; max-width: 30ch;
      font-size: 13.5px; line-height: 1.5; color: var(--ink-4);
    }
    .foot-cols { display: flex; flex-wrap: wrap; gap: clamp(28px, 4vw, 68px); }
    .foot-col { display: flex; flex-direction: column; gap: 9px; }
    /* Mono uppercase, the same treatment every other column heading on the
       site gets — the mega-menu's and the models section's. */
    .foot-col h4 {
      margin: 0 0 3px;
      font-family: var(--mono); font-size: 9.5px; letter-spacing: .15em;
      text-transform: uppercase; color: var(--ink-4);
    }
    .foot-col a {
      font-size: 13.5px; color: var(--ink-3); text-decoration: none;
      transition: color .16s ease;
    }
    .foot-col a:hover { color: var(--ink); }
    .foot-bottom {
      display: flex; flex-wrap: wrap; gap: 10px 22px;
      justify-content: space-between; align-items: center;
      margin-top: clamp(30px, 4vh, 48px);
      padding-top: 18px; border-top: 1px solid var(--rim-soft);
      font-size: 12.5px; color: var(--ink-4);
    }

    /* ===== legal pages =====
       Prose ported from genva-app/website, restyled here: the source shipped
       its own light-theme stylesheet and this site is dark, so only the markup
       came across. Measured column, because a 22KB policy set the full width
       of .wrap is unreadable. */
    .legal .wrap { max-width: 820px; }
    .legal h1 {
      margin: 22px 0 6px;
      font-family: var(--font); font-weight: 600; color: var(--ink);
      font-size: clamp(28px, 3.4vw, 42px); line-height: 1.1; letter-spacing: -0.03em;
    }
    .legal .updated {
      margin: 0 0 34px;
      font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
      text-transform: uppercase; color: var(--ink-4);
    }
    .legal h2 {
      margin: 40px 0 12px; padding-top: 22px;
      border-top: 1px solid var(--rim-soft);
      font-family: var(--font); font-weight: 600; color: var(--ink);
      font-size: clamp(18px, 2vw, 23px); line-height: 1.25; letter-spacing: -0.02em;
    }
    .legal h3 {
      margin: 26px 0 8px;
      font-family: var(--font); font-weight: 600; color: var(--ink-2);
      font-size: 15.5px; letter-spacing: -0.01em;
    }
    .legal p, .legal li {
      color: var(--ink-3); font-size: 14.5px; line-height: 1.62;
    }
    .legal p { margin: 0 0 14px; }
    .legal ul { margin: 0 0 16px; padding-left: 20px; }
    .legal li { margin-bottom: 7px; }
    .legal li::marker { color: var(--ink-4); }
    .legal strong { color: var(--ink-2); font-weight: 600; }
    .legal a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }
    .legal a:hover { color: var(--pink); }

    @media (max-width: 640px) {
      .foot-top { flex-direction: column; gap: 26px; }
      .foot-cols { gap: 24px 34px; }
      .legal p, .legal li { font-size: 14px; }
    }

    @media (hover: none) {
      .mcard .hoverfx { display: none; }
      .tile-foot { transform: none !important; }
      .tile-sub, .tile-cta { display: none; }
    }

    /* Six 220px columns need 1380px. Below that the wall drops columns rather
       than shrinking cards — a 9:16 card under ~150px is a thumbnail, not a
       template. The stagger is re-cut for each count so the V still opens in
       the middle. */
    @media (max-width: 1439px) {
      .tw-wall { grid-template-columns: repeat(5, 210px); height: 1240px; }
      .tw-col:nth-child(6) { display: none; }
    }
    @media (max-width: 1100px) {
      .tw-wall { grid-template-columns: repeat(4, 196px); height: 1120px; }
      .tw-col:nth-child(5) { display: none; }
    }
    @media (max-width: 860px) {
      .tw-wall { grid-template-columns: repeat(3, 180px); gap: 10px; height: 960px; }
      .tw-col:nth-child(4) { display: none; }
      .tw-fade { height: 260px; }
    }
    @media (max-width: 640px) {
      /* Two columns and no stagger: on a phone the V just wastes the top of
         the wall, and the copy needs all the room it can get. */
      .tw-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; height: 720px; }
      .tw-col:nth-child(3), .tw-col:nth-child(4), .tw-col:nth-child(5), .tw-col:nth-child(6) { display: none; }
      .tw-copy { padding-bottom: clamp(40px, 7vh, 72px); }
      .tw-fade { height: 200px; }
    }

    /* ===== tablets ===== */
    /* 176px tiles only fit three across a 768px tablet, which reads as three
       big cards rather than a library. Smaller minimum, four columns. */
    @media (max-width: 900px) {
      .grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
    }

    /* ===== phones ===== */
    @media (max-width: 640px) {
      /* 8px back on each side is a whole extra column's worth of tile width */
      .wrap { padding: 0 20px; }

      /* The drawer ends with the same pill, so the bar keeps the logo and the
         menu button only — two competing CTAs in 375px is one too many. */
      .navcta { display: none; }

      /* The fan is the hero's subject; 52vw leaves it a 195px thumbnail. */
      .hero { --card-w: min(78vw, 52svh); }

      /* The prompt box goes with the bar's pill, and for the same reason: the
         hero already ends in two full-width pills here, and a third call to
         action laid over the artwork is one too many. At 78vw the field would
         also be mostly ellipsis, which states the concept worse than the
         caption underneath already does. */
      .mcard .mprompt { display: none; }
      .hero .pills { gap: 10px; }
      .hero .pills .btn { height: 46px; padding: 0 18px; font-size: 14.5px; }

      .panel { padding-top: clamp(40px, 5vh, 54px); }
      .panel-sub { font-size: 14.5px; }

      /* Five chips do not fit across a phone. Rather than wrapping them into a
         second row (which pushes the first tiles off the fold) the strip keeps
         its pill shape and scrolls sideways inside it. */
      .filters { flex-direction: column; gap: 12px; margin-top: 22px; }
      .chips {
        max-width: 100%; overflow-x: auto; scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
      }
      .chips::-webkit-scrollbar { display: none; }
      .chip { flex: 0 0 auto; padding: 8px 14px; }

      .grid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 8px; }
      /* The category words used to be dropped outright here — at 132px they
         were a second line of copy over the artwork. A glyph is not: the app
         keeps its badge at every size and only shrinks it, so this does too. */
      .qbadge { --qb-size: 24px; top: 6px; left: 6px; }
      .qbadge.is-edit { --qb-size: 20px; }
      .tile-flag { height: 15px; top: 8px; right: 6px; }

      .page { padding-top: 26px; }
      .backlink { margin-bottom: 14px; }
    }

    /* very narrow (iPhone SE and friends) — the two hero pills stop fitting
       side by side at about 360px, so below that they stack full-width */
    @media (max-width: 360px) {
      .hero .pills { flex-direction: column; align-items: stretch; }
      .hero .pills .btn { width: 100%; }
    }

    @media (prefers-reduced-motion: reduce) {
      .bgfx { animation: none; }
      /* The beam's sweep is the definition of ambient motion, but the rim it
         sweeps over is lit either way — so the window simply stops where it
         is rather than the whole thing going dark. */
      .pb-beam { animation: none; }
      .mcard { transition: none; }
      .tile-foot, .tile-sub, .tile-cta, .tile-vid, .tile-alt { transition: none; }
      /* the dialog's wall holds still — it is decoration, and this is exactly
         the kind of endless drift the preference is about. The one playing card
         per column is skipped in js/authdialog.js for the same reason. */
      .stream-col { animation: none; }
      /* Review rows hold still and scroll by hand instead. */
      .mtrack { animation: none; }
      /* The wall holds still as well. */
      .tw-col { animation: none; }
      /* No burst: the cards are simply where they belong, straight away. */
      .pm-card { transition: none; transform: translate(-50%, -50%); opacity: 1; }
      .pm.is-in .pm-card { transform: translate(-50%, -50%); }
      /* Review rows hold still as well; they overflow-scroll instead so the
         ones past the edge are still reachable. */
      .rev-row { overflow-x: auto; }
    }
