@import url('fonts/fonts.css');   /* self-hosted; Google Fonts is blocked in mainland China */

:root{
  --bg:#ede5d6;
  --panel:#fffdf8;
  --card:#fffdfa;
  --card-warm:#fbf5ea;
  --ink:#232120;
  --ink-dim:#77716a;
  --gold:#9b8a61;
  --gold-soft:#d8ccb2;
  --seal:#9c5a4d;
  --line:#e4ded3;
  --shadow:0 24px 60px -34px rgba(83,70,50,.45);
  /* Gold is used both as an accent (borders, dots) and as a text colour. As a
     text colour on --bg it only reaches 2.7:1, so body copy uses --gold-ink
     (5.4:1, clears WCAG AA) and --gold stays for accents. */
  --gold-ink:#6b5a34;
  --radar-fill:rgba(155,138,97,0.22);
  /* Fixed quiz geometry: the question box and each option box keep a constant
     height so an option never shifts position when the option text or the
     question text is a different length — including across a zh/en toggle,
     where English wraps to more lines. Sized from the tallest real string at
     this breakpoint (see the narrow override in the media query below). */
  --q-text-h:296px;
  --option-h:148px;
  --font-display:'Fraunces', Georgia, serif;
  --font-mono:'Space Mono', ui-monospace, monospace;
  /* Inter is unicode-range'd to latin, so every CJK glyph falls past it. The
     CJK faces are named explicitly rather than left to `sans-serif`: on iOS
     -apple-system already resolves to PingFang SC, but on Android WeChat the
     first three entries all miss and the generic fallback is whatever the
     vendor shipped. No CJK webfont is downloaded — a subsetted Chinese face is
     a multi-megabyte download, and these are installed on every target device. */
  --font-body:'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI",
              "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0;}
*:focus-visible{outline:2px solid var(--gold);outline-offset:2px;}
html,body{height:100%;}
/* Kills pull-to-refresh and the rubber-band overscroll inside the WeChat and
   Xiaohongshu webviews. `contain` rather than `none` so scroll chaining stops
   at the page while an internal scroller (the biography slide) still works. */
html{overscroll-behavior-y:contain;}
body{
  overscroll-behavior-y:contain;
  -webkit-text-size-adjust:100%;   /* WeChat's webview otherwise inflates text */
  background:
    radial-gradient(circle at 50% 2%, rgba(255,255,255,.72) 0%, rgba(255,255,255,0) 38%),
    linear-gradient(180deg,#f7f4ee 0%, var(--bg) 34%, #e2d8c6 100%);
  background-color:var(--bg);
  color:var(--ink);
  font-family:var(--font-body);
  min-height:100vh;
  display:flex;
  justify-content:center;
}
body::before{
  content:"";position:fixed;inset:0;pointer-events:none;z-index:0;opacity:.45;
  background-image:radial-gradient(rgba(108,94,72,.06) 1px, transparent 1px);
  background-size:4px 4px;
}
.app{
  width:100%;max-width:480px;min-height:100vh;
  /* dvh tracks the collapsing browser chrome; the vh line above stays as the
     fallback for webviews that don't know the unit. */
  min-height:100dvh;
  padding:26px 20px 60px;display:flex;flex-direction:column;position:relative;z-index:1;
  /* Notch and home-indicator. Added to the existing padding rather than
     replacing it, so the base rhythm is unchanged on a device with no insets. */
  padding-top:calc(26px + env(safe-area-inset-top));
  padding-bottom:calc(60px + env(safe-area-inset-bottom));
  padding-left:calc(20px + env(safe-area-inset-left));
  padding-right:calc(20px + env(safe-area-inset-right));
}
.screen{
  display:none;flex-direction:column;flex:1;animation:fade .45s cubic-bezier(.2,.7,.3,1);
  background:linear-gradient(180deg,rgba(255,255,255,.92),rgba(255,252,246,.86));
  border:1px solid rgba(255,255,255,.72);
  border-radius:30px;
  box-shadow:var(--shadow);
  padding:30px 26px;
}
.screen.active{display:flex;}
@keyframes fade{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:translateY(0);}}
@media (prefers-reduced-motion: reduce){
  .screen{animation:none;}
  .spinner{animation-duration:2s;}
}

.eyebrow{
  font-family:var(--font-mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--gold);text-align:left;margin-bottom:18px;font-weight:700;
}
h1{
  font-family:var(--font-display);font-size:34px;line-height:1.16;text-align:left;
  font-weight:700;letter-spacing:0;margin-bottom:18px;
}
.sub{text-align:left;color:var(--ink-dim);font-size:17px;line-height:1.8;margin-bottom:28px;}
.cta{
  margin-top:auto;background:#252321;color:#fffaf0;border:none;border-radius:16px;
  padding:17px 20px;font-size:16px;font-family:var(--font-body);font-weight:600;
  cursor:pointer;width:100%;letter-spacing:.01em;box-shadow:0 16px 30px -18px rgba(35,33,32,.75);
}
.cta:disabled{background:#c9c1b4;color:#fffaf0;cursor:not-allowed;box-shadow:none;}
.cta:active{transform:scale(.98);}
.hero-spacer{flex:1;min-height:20px;}

.teaser-row{display:flex;gap:12px;overflow-x:auto;padding:4px 2px 10px;-webkit-overflow-scrolling:touch;}
.teaser-row::-webkit-scrollbar{display:none;}
.teaser-card{
  flex:0 0 128px;height:172px;border-radius:18px;
  background:linear-gradient(160deg,#fffefa,#f4ecd9);border:1px solid var(--line);
  position:relative;overflow:hidden;display:flex;align-items:flex-end;padding:12px;
  box-shadow:0 14px 34px -28px rgba(83,70,50,.55);
}
/* Real portrait, blurred: the match is in the archive, just not revealed yet.
   Was a blurred emoji before portraits existed. */
/* Shown plainly. These are the archive's best-known faces and obscuring them
   sells nothing. `contain`, not `cover` — the whole portrait stays in frame
   instead of being cropped to fill the box. */
.teaser-card .blur-fig{
  position:absolute;inset:0;width:100%;height:100%;object-fit:contain;
  filter:none;opacity:1;transform:none;
}
.teaser-card .teaser-label{
  position:relative;z-index:1;font-family:var(--font-mono);font-size:11px;color:var(--ink-dim);
  background:rgba(255,253,246,.9);padding:3px 7px;border-radius:7px;backdrop-filter:blur(2px);
}

.gate-input{
  width:100%;background:var(--card);border:1px solid var(--gold-soft);color:var(--ink);
  border-radius:18px;padding:17px;font-size:18px;letter-spacing:.08em;text-align:center;
  font-family:var(--font-mono);margin-bottom:12px;text-transform:uppercase;
  box-shadow:0 16px 34px -30px rgba(83,70,50,.55);
}
.gate-input::placeholder{color:var(--ink-dim);letter-spacing:.06em;opacity:.62;}
.gate-error{color:#a45a4e;font-size:13px;text-align:center;min-height:18px;margin-bottom:8px;font-family:var(--font-body);}
.gate-note{color:var(--ink-dim);font-size:15px;line-height:1.7;text-align:left;margin-bottom:22px;}

.step-label{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--gold);margin-bottom:16px;font-weight:700;}
.pref-note{color:var(--ink-dim);font-size:17px;line-height:1.8;margin-bottom:28px;}
.pref-options{display:flex;flex-direction:column;gap:16px;}
.pref-card{
  text-align:left;background:var(--card);border:1px solid var(--line);border-radius:22px;
  padding:24px 28px;cursor:pointer;transition:border-color .15s, background .15s, box-shadow .15s;
  box-shadow:0 16px 34px -30px rgba(83,70,50,.55);
}
.pref-card.selected{border-color:var(--gold-soft);background:var(--card-warm);box-shadow:0 18px 36px -28px rgba(83,70,50,.7);}
.pref-title{font-family:var(--font-display);font-size:22px;font-weight:600;margin-bottom:12px;}
.pref-desc{font-size:16px;line-height:1.6;color:var(--ink-dim);}

.progress-wrap{display:flex;gap:6px;margin-bottom:26px;}
.progress-seg{flex:1;height:3px;border-radius:3px;background:#ddd4c5;overflow:hidden;}
.progress-seg .fill{height:100%;width:0;background:var(--gold);transition:width .3s;}
/* Fixed height, not min-height: the options below must start at the same y on
   every question regardless of how many lines the question wraps to. */
.q-text{
  font-family:var(--font-display);font-size:29px;font-weight:700;line-height:1.38;
  height:var(--q-text-h);margin-bottom:24px;overflow:hidden;
}
.options{display:flex;flex-direction:column;gap:14px;}
.option{
  display:flex;align-items:center;gap:14px;text-align:left;background:var(--card);border:1px solid var(--line);color:var(--ink);
  border-radius:20px;padding:18px 20px;font-size:16px;line-height:1.55;cursor:pointer;
  transition:border-color .15s, background .15s, box-shadow .15s;
  box-shadow:0 14px 30px -28px rgba(83,70,50,.5);
  /* Fixed box: a long option must not make its own row taller (which would
     push every row below it down). Text centres in the leftover space. */
  height:var(--option-h);overflow:hidden;
}
.option:active{border-color:var(--gold-soft);background:var(--card-warm);}
/* The A/B/C/D badge. Fixed 32px square that never reflows: the letters are
   language-neutral (see OPTION_LABELS in app.js), so the badge is the same
   width in zh and en, and it stays centred both ways inside its own circle
   and vertically within the fixed-height option row. */
.option-label{
  flex:0 0 32px;align-self:center;display:inline-flex;align-items:center;justify-content:center;
  width:32px;height:32px;border-radius:50%;border:1px solid rgba(155,138,97,.2);
  color:#786b4d;font-family:var(--font-mono);font-size:13px;font-weight:700;line-height:1;
  text-align:center;box-shadow:inset 0 1px 0 rgba(255,255,255,.85);
}
.option:nth-child(1) .option-label{background:#f8efdc;color:#806d48;}
.option:nth-child(2) .option-label{background:#f1ead9;color:#6f7159;}
.option:nth-child(3) .option-label{background:#f5e9df;color:#80625a;}
.option:nth-child(4) .option-label{background:#ece7db;color:#68665a;}
.option-text{flex:1;min-width:0;}

.loading-screen{align-items:center;justify-content:center;text-align:center;gap:18px;}
.spinner{width:42px;height:42px;border-radius:50%;border:3px solid #ded4c5;border-top-color:var(--gold);animation:spin 1s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}
.loading-text{color:var(--ink-dim);font-size:14px;font-family:var(--font-mono);letter-spacing:.02em;}

.archetype-eyebrow{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--gold);text-align:left;margin-bottom:8px;font-weight:700;}
.archetype-title{font-family:var(--font-display);font-size:30px;font-weight:700;text-align:left;margin-bottom:12px;}
.archetype-desc{font-size:15px;color:var(--ink-dim);line-height:1.7;text-align:left;margin-bottom:18px;padding:0 4px 0 0;}
.dim-tags{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:24px;}
.dim-tag{font-family:var(--font-mono);font-size:11px;border:1px solid var(--gold-soft);color:var(--gold);background:#fffaf1;padding:5px 11px;border-radius:100px;}

.radar-card{border:1px solid var(--line);border-radius:22px;background:var(--card);padding:14px 6px 6px;margin-bottom:20px;box-shadow:0 16px 34px -30px rgba(83,70,50,.55);}
.radar-card-title{font-family:var(--font-mono);font-size:11px;color:var(--ink-dim);text-align:center;margin-bottom:2px;letter-spacing:.02em;}

.fig-card{
  border-radius:22px;border:1px solid var(--line);
  background:linear-gradient(165deg,#fffefa,#f5ecdc);
  padding:24px 18px;text-align:center;margin-bottom:10px;position:relative;
  box-shadow:0 16px 34px -30px rgba(83,70,50,.55);
}
.fig-emoji{font-size:50px;display:block;margin-bottom:8px;}
/* Must out-specify the display above, or the attribute is ignored and the
   fallback shows next to the portrait it is supposed to replace. */
.fig-emoji[hidden]{display:none;}
.fig-name{font-family:var(--font-display);font-size:20px;font-weight:600;margin-bottom:3px;}
.fig-nickname{
  font-family:var(--font-display);font-style:italic;font-size:15px;
  color:var(--gold-ink);margin:-1px 0 5px;
}
.fig-nickname[hidden]{display:none;}
.fig-era{font-family:var(--font-mono);font-size:11px;color:var(--ink-dim);margin-bottom:10px;}
.fig-trait-note{
  font-family:var(--font-display);font-style:italic;font-size:14px;color:var(--ink);line-height:1.55;
  border-left:2px solid var(--gold-soft);padding:2px 0 2px 12px;margin:4px 0 14px;text-align:left;
}
.fig-facts{list-style:none;text-align:left;margin-bottom:14px;}
.fig-facts li{font-size:12.5px;color:var(--ink-dim);line-height:1.6;margin-bottom:8px;padding-left:14px;position:relative;}
.fig-facts li::before{content:"-";position:absolute;left:0;color:var(--gold);}
.fig-disclaimer{font-size:11px;color:var(--ink-dim);line-height:1.55;padding:0 6px;opacity:.85;}

.price{font-size:13px;color:var(--ink-dim);text-align:center;margin:16px 0 12px;}
.price b{color:var(--gold);font-family:var(--font-mono);font-size:14px;}
.share-row{display:flex;gap:10px;margin-top:14px;}
.share-btn{
  flex:1;background:var(--card);border:1px solid var(--line);color:var(--ink);
  border-radius:16px;padding:12px;font-size:14px;font-weight:600;cursor:pointer;font-family:var(--font-body);
}
.fine-print{text-align:center;color:var(--ink-dim);font-size:11.5px;margin-top:16px;line-height:1.55;opacity:.75;}

.stamp-wrap{display:flex;justify-content:center;margin:6px 0 18px;}
.stamp{
  position:relative;display:inline-flex;align-items:center;justify-content:center;
  width:104px;height:104px;border-radius:50%;
  border:2px solid var(--seal);color:var(--seal);
  transform:rotate(-9deg);
  font-family:var(--font-mono);font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  text-align:center;line-height:1.3;padding:8px;
  mix-blend-mode:normal;opacity:0;animation:stamp-in .5s cubic-bezier(.2,.9,.3,1.3) .1s forwards;
}
.stamp::before{
  content:"";position:absolute;inset:-6px;border-radius:50%;border:1px solid var(--seal);opacity:.5;
}
@keyframes stamp-in{
  from{opacity:0;transform:rotate(-9deg) scale(1.7);}
  to{opacity:.88;transform:rotate(-9deg) scale(1);}
}
@media (prefers-reduced-motion: reduce){
  .stamp{animation:none;opacity:.88;}
}

@media (max-width:380px){
  /* Less horizontal room here, so the same strings wrap to more lines and the
     fixed boxes have to be taller. Heights stay constant within the
     breakpoint, which is what keeps the options in place on a given device. */
  :root{--q-text-h:296px;--option-h:172px;}
  .app{
    padding:16px 12px 40px;
    padding-top:calc(16px + env(safe-area-inset-top));
    padding-bottom:calc(40px + env(safe-area-inset-bottom));
    padding-left:calc(12px + env(safe-area-inset-left));
    padding-right:calc(12px + env(safe-area-inset-right));
  }
  .screen{border-radius:26px;padding:26px 20px;}
  h1{font-size:30px;}
  .pref-card{padding:22px 20px;}
}

@media (max-width:359px){
  /* Below ~356px the longest French question wraps to six lines instead of
     five. Split out rather than folded into the 380px rule so 360-380px phones
     don't carry 40px of dead space for a case they never hit. The boundary sits
     at 359 rather than the measured 356 to leave margin for copy edits. */
  :root{--q-text-h:336px;}
}

@media (min-width:600px){
  /* Tablet and small-laptop widths, still above the two-column layer at 1000px:
     the same strings wrap to far fewer lines here, so the reserved block can
     shrink well below the phone value. */
  :root{--q-text-h:216px;}
}

/* ---------- portraits / achievements / kinship / language toggle ----------
   Carried forward from the frontend-redesign work on main. The redesign branch
   this CSS was merged with predates these features, so these blocks are ported
   here and re-tuned for the light palette above (the branch inverted the theme:
   --gold-soft is now a pale tint, unreadable as a text color on --bg). */
/* Top-right controls. A flex row rather than two absolutely-positioned
   buttons, so the home icon can't drift when the language pill changes width
   between EN and 中. */
.top-controls{
  position:absolute;top:20px;right:18px;z-index:5;
  display:flex;align-items:center;gap:8px;
}
.lang-toggle{
  font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.08em;
  color:var(--gold);background:rgba(255,253,248,.85);
  border:1px solid var(--line);border-radius:999px;
  padding:5px 11px;min-width:38px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;transition:border-color .2s,color .2s;
}
.lang-toggle:hover{border-color:var(--gold);color:var(--ink);}

.icon-btn{
  display:inline-flex;align-items:center;justify-content:center;
  width:28px;height:28px;border-radius:999px;
  color:var(--gold);background:rgba(255,253,248,.85);
  border:1px solid var(--line);cursor:pointer;
  -webkit-tap-highlight-color:transparent;transition:border-color .2s,color .2s;
}
.icon-btn:hover{border-color:var(--gold);color:var(--ink);}
.icon-btn[hidden]{display:none;}
/* Armed state: one tap from discarding a part-finished run. Wax-seal red is
   used nowhere else, so it reads as "this one is different". */
.icon-btn.confirming{
  color:var(--seal);border-color:var(--seal);background:rgba(156,90,77,.10);
}

/* Epoch + domain chips. Deliberately quiet — they're filing labels on a
   dossier, not a feature. Uses --gold-ink because 11px on the card gradient
   needs the AA-safe gold, not the accent one. */
.fig-tags{
  display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin:-4px 0 12px;
}
.fig-tags[hidden]{display:none;}
.fig-tag{
  font-family:var(--font-mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--gold-ink);background:var(--card-warm);border:1px solid var(--line);
  border-radius:100px;padding:3px 9px;white-space:nowrap;
}

.fig-portrait{
  display:block;width:132px;aspect-ratio:4/5;object-fit:cover;
  margin:0 auto 10px;border-radius:12px;border:1px solid var(--line);
}
.fig-portrait[hidden]{display:none;}

.fig-achievements-title{
  font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--gold-ink);text-align:left;margin:4px 0 8px;
}
.fig-achievements-title[hidden]{display:none;}
.fig-achievements{list-style:none;text-align:left;margin-bottom:14px;counter-reset:ach;}
.fig-achievements li{
  font-size:13px;color:var(--ink);line-height:1.6;margin-bottom:9px;
  padding-left:22px;position:relative;counter-increment:ach;
}
.fig-achievements li::before{
  content:counter(ach);position:absolute;left:0;top:1px;
  width:15px;height:15px;border-radius:50%;font-size:9px;font-weight:700;
  display:flex;align-items:center;justify-content:center;
  /* Dark-on-pale, not cream-on-gold: at 9px the badge needs all the contrast
     it can get (this way is ~11:1 rather than 3.3:1). */
  color:#241e12;background:var(--gold-soft);font-family:var(--font-mono);
}

.fig-match-note{
  font-size:13px;color:var(--ink);line-height:1.6;text-align:left;
  background:var(--card-warm);border:1px solid var(--line);border-radius:10px;
  padding:10px 12px;margin:0 0 12px;
}
.fig-match-note[hidden]{display:none;}

.fig-encouragement{
  font-family:var(--font-display);font-size:15px;line-height:1.55;color:var(--gold-ink);
  text-align:center;padding:14px 16px;margin-bottom:10px;
}
.fig-encouragement[hidden]{display:none;}

/* ============================================================================
   LAPTOP LAYOUT  (>= 1000px)
   ----------------------------------------------------------------------------
   One codebase, one set of tokens, one HTML — mobile and laptop differ only
   here. Everything below is layout; no new colours, type families or copy.
   A dev tunes the two geometry tokens at the top of this block and nothing
   else needs touching.

   The shape is a two-leaf dossier spread: the left leaf states the case, the
   right leaf is where you act on it. Gate and Reading stay single-column on
   purpose — a clearance box and a spinner are single acts, and splitting them
   would be worse, not wider.

   Note this makes the fixed-option-position rule STRONGER, not weaker: on a
   laptop the question sits in its own column, so question length cannot move
   the options at all. Only --option-h governs them.
   ========================================================================== */
@media (min-width:1000px){
  :root{
    --q-text-h:auto;   /* question has its own column now — no reserved block */
    --option-h:104px;  /* re-derived for the wider column, larger type and French */
  }

  .app{max-width:1500px;padding:40px clamp(28px,4vw,64px) 64px;}
  .screen{
    padding:clamp(36px,3.4vw,56px);
    border-radius:34px;
    column-gap:clamp(34px,4vw,76px);
  }
  h1{font-size:clamp(36px,3.2vw,50px);}
  .sub,.pref-note{font-size:18px;}
  .q-text{font-size:clamp(32px,2.8vw,44px);overflow:visible;}

  /* ---- Home: the pitch, and the archive it draws from ---- */
  #screen-hero.active{
    display:grid;
    grid-template-columns:minmax(0,1fr) minmax(0,.9fr);
    grid-template-areas:
      "eyebrow teasers"
      "title   teasers"
      "sub     teasers"
      "cta     teasers";
    grid-template-rows:auto auto auto auto;
    /* Centre the left stack against the portrait shelf rather than pushing the
       CTA to the floor — a bottom-anchored button is a phone pattern. */
    align-content:center;
  }
  #screen-hero .eyebrow{grid-area:eyebrow;}
  #screen-hero h1{grid-area:title;}
  #screen-hero .sub{grid-area:sub;}
  #screen-hero .hero-spacer{grid-area:gap;}
  #screen-hero .cta{grid-area:cta;margin-top:0;}
  /* Portraits become a vertical shelf rather than a scrolling strip. */
  /* Three across in their true 4:5 crop. Stacked full-width they became wide
     letterboxes that cut the faces off. */
  #screen-hero .teaser-row{
    grid-area:teasers;display:grid;grid-template-columns:repeat(3,1fr);gap:12px;
    overflow:visible;align-content:center;align-self:center;padding:0;
  }
  #screen-hero .teaser-card{flex:none;width:100%;height:auto;aspect-ratio:4/5;}
  #screen-hero .hero-spacer{display:none;}
  #screen-hero .cta{max-width:420px;}

  /* ---- Preference: three cards across, which is what the width is for ---- */
  #screen-gender .pref-options{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;}
  #screen-gender .pref-card{display:flex;flex-direction:column;justify-content:flex-start;}
  #screen-gender .cta{max-width:340px;align-self:center;}

  /* ---- Quiz: question left, answers right ---- */
  #screen-quiz.active{
    display:grid;
    grid-template-columns:minmax(0,1fr) minmax(0,1.06fr);
    grid-template-areas:
      "progress progress"
      "question options";
    grid-template-rows:auto 1fr;
    align-content:start;
  }
  #screen-quiz .progress-wrap{grid-area:progress;margin-bottom:34px;}
  /* Progress pins to the top; the question/answer pair centres in whatever
     height is left, so a short question doesn't leave the card bottom-heavy. */
  #screen-quiz .q-text{grid-area:question;align-self:center;margin-bottom:0;padding-right:8px;}
  #screen-quiz .options{grid-area:options;align-self:center;}

  /* ---- Gate and Reading: single column, centred, just roomier ---- */
  #screen-gate.active,#screen-loading.active{align-items:center;}
  #screen-gate .eyebrow,#screen-gate h1,#screen-gate .gate-note,
  #screen-gate .gate-input,#screen-gate .gate-error,#screen-gate .cta{
    width:100%;max-width:560px;
  }
  #screen-gate h1{text-align:center;}
  #screen-gate .gate-note{text-align:center;}

  /* ---- Dossier: the reading on the left, the figure on the right ---- */
  #screen-result .result-content{
    display:grid;
    /* The figure leaf holds two stages side by side now, so it takes the larger
       share. The reading (title, description, radar) is comfortable narrower;
       two portraits squeezed into .92fr were not. */
    grid-template-columns:minmax(0,.8fr) minmax(0,1.35fr);
    column-gap:clamp(24px,2.4vw,44px);
    grid-template-areas:
      "eyebrow figure"
      "title   figure"
      "desc    figure"
      "tags    figure"
      "radar   figure";
    grid-template-rows:auto auto auto auto 1fr;
    align-items:stretch;
  }
  #screen-result .archetype-eyebrow{grid-area:eyebrow;}
  #screen-result .archetype-title{grid-area:title;font-size:clamp(34px,3vw,46px);}
  #screen-result .archetype-desc{grid-area:desc;font-size:17px;}
  #screen-result .dim-tags{grid-area:tags;justify-content:flex-start;}
  #screen-result .radar-card{grid-area:radar;margin-bottom:0;}
  /* ---- Dossier fits the viewport: no scrolling on a laptop ----
     The screen is a fixed three-row stack (stamp / content / actions) and the
     content row is the only thing that flexes, so the carousel and the radar
     share whatever height is left instead of pushing the actions off-screen. */
  #screen-result.active{
    display:flex;flex-direction:column;
    height:calc(100vh - 104px);   /* .app padding top+bottom */
    overflow:hidden;
  }
  #screen-result .stamp-wrap{flex:0 0 auto;margin-bottom:6px;transform:scale(.78);}
  #screen-result .result-content{flex:1;min-height:0;}
  #screen-result .radar-card{min-height:0;overflow:hidden;display:flex;flex-direction:column;}
  #screen-result #radar-container{
    flex:1;min-height:0;display:flex;align-items:center;justify-content:center;
  }
  #screen-result #radar-container svg{max-height:100%;width:auto;max-width:100%;}
  #screen-result .archetype-desc{
    display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;
  }
  /* The pair owns the right leaf top to bottom; each slide scrolls inside
     itself if its own content is long, so the page never does. */
  #screen-result .fig-pair{
    grid-area:figure;height:100%;min-height:0;
    grid-template-columns:1fr 1fr;gap:clamp(14px,1.4vw,24px);
  }
  /* One figure — a one-character pool, or a gender filter that leaves one —
     shouldn't stretch to double width. */
  #screen-result .fig-pair[data-count="1"]{grid-template-columns:minmax(0,.5fr);justify-content:center;}
  #screen-result .fig-unit{height:100%;}
  #screen-result .fig-stage{
    flex:1;min-height:0;margin-bottom:0;
    /* No aspect-ratio here, unlike the single-stage layout it replaces: two
       4:5 boxes side by side in the same leaf would each be half as wide and
       so half as tall, leaving the row half empty. Fill the height instead and
       let the portrait crop. */
    width:100%;max-width:100%;
  }
  #screen-result .fig-slide{overflow-y:auto;}
  #screen-result .fig-slide[data-slide="portrait"]{aspect-ratio:auto;overflow:hidden;}
  #screen-result .share-row{
    flex:0 0 auto;max-width:620px;margin:14px auto 0;width:100%;
  }
}

/* ============================================================================
   UNIVERSE THEMES
   ----------------------------------------------------------------------------
   A theme is a TOKEN OVERRIDE and nothing else — same HTML, same JS, same
   class names, same layout. app.js stamps data-universe on <html>, and the
   block below re-points the variables everything already reads. Adding a third
   universe means adding one of these blocks; no component needs to know.
   ========================================================================== */

/* ---- france: Ancien Régime archive ----
   Bleu de France and gold leaf on parchment, sanguine reserved for the seal.
   Display face becomes Cormorant Garamond — Claude Garamond cut his types in
   16th-century Paris, so it is period-correct rather than decorative. */
:root[data-universe="france"]{
  --bg:#F2EADA;          /* parchment */
  --panel:#FFFDF6;
  --card:#FDFAF3;
  --card-warm:#F6EEDD;
  --ink:#1F1B16;
  --ink-dim:#6B6459;
  --gold:#B99653;        /* or / gold leaf — accents */
  --gold-soft:#E2D5B6;
  --gold-ink:#6E5626;    /* 5.8:1 on parchment — gold used as *text* */
  --seal:#8C3A2E;        /* sanguine, stamps only */
  --line:#E2D8C3;
  --bleu:#1B3A6B;        /* bleu de France — 9.4:1 on parchment */
  --radar-fill:rgba(27,58,107,0.14);
  --font-display:'Cormorant Garamond', 'Fraunces', Georgia, serif;
}
/* Engraved guilloche instead of the dot grid — the line-work of French state
   documents and Banque de France notes, at an opacity you feel more than see. */
:root[data-universe="france"] body::before{
  opacity:.55;
  background-image:
    repeating-radial-gradient(circle at 50% 18%, transparent 0 17px, rgba(27,58,107,.055) 17px 18px),
    repeating-radial-gradient(circle at 50% 18%, transparent 0 6px,  rgba(185,150,83,.05) 6px 7px);
  background-size:auto;
}
:root[data-universe="france"] body{
  background:
    radial-gradient(ellipse 90% 46% at 50% 0%, rgba(255,255,255,.78), rgba(255,255,255,0) 58%),
    linear-gradient(180deg,#FBF6EB 0%, var(--bg) 38%, #E7DBC4 100%);
}
/* Bleu de France carries the small structural labels; gold stays ornamental. */
:root[data-universe="france"] .eyebrow,
:root[data-universe="france"] .step-label,
:root[data-universe="france"] .archetype-eyebrow{color:var(--bleu);}
:root[data-universe="france"] .fig-trait-note{border-left-color:var(--bleu);}
:root[data-universe="france"] .option-label{border-color:rgba(27,58,107,.22);}
:root[data-universe="france"] .dim-tag{border-color:var(--gold-soft);color:var(--gold-ink);}
:root[data-universe="france"] h1,
:root[data-universe="france"] .archetype-title,
:root[data-universe="france"] .q-text{letter-spacing:-.005em;}

/* ============================================================================
   PINNED ICON BAR
   Icons only, fixed to the viewport so they never travel with the page, and
   never overlap a screen's own content. Each carries a tooltip because an icon
   with no label is a guess.
   ========================================================================== */
.top-controls{
  position:fixed;top:14px;right:14px;z-index:60;
  display:flex;align-items:center;gap:8px;
  padding:6px;border-radius:999px;
  background:color-mix(in srgb, var(--panel) 82%, transparent);
  border:1px solid var(--line);
  backdrop-filter:blur(8px);
  box-shadow:0 10px 26px -18px rgba(60,48,34,.6);
}
.top-controls .lang-toggle,.top-controls .icon-btn{box-shadow:none;background:transparent;border-color:transparent;}
.top-controls .lang-toggle:hover,.top-controls .icon-btn:hover{background:var(--card-warm);border-color:var(--line);}

/* Tooltip. Sits below the bar, fades in, and is driven by data-tip so the text
   is bilingual like everything else. */
.has-tip{position:relative;}
.has-tip::after{
  content:attr(data-tip);
  position:absolute;top:calc(100% + 9px);right:0;
  white-space:nowrap;pointer-events:none;
  font-family:var(--font-body);font-size:11px;letter-spacing:.01em;
  color:var(--panel);background:rgba(35,33,32,.94);
  padding:5px 9px;border-radius:7px;
  opacity:0;transform:translateY(-3px);
  transition:opacity .16s ease, transform .16s ease;
}
.has-tip:hover::after,.has-tip:focus-visible::after{opacity:1;transform:translateY(0);}
@media (hover:none){ .has-tip::after{display:none;} }

/* ============================================================================
   FIGURE CAROUSEL
   Slide 0 is the portrait with no name attached — you meet the face first.
   ========================================================================== */
/* Two stages: stacked on a phone, side by side once there's room (see the
   min-width:1000px block). One figure still renders — the grid just has one
   column of content in it. */
.fig-pair{display:grid;grid-template-columns:1fr;gap:14px;}
/* Each figure is a caption + a stage. The caption is a real row rather than an
   overlay: the biography slide scrolls, so anything floating over the stage
   crosses the prose mid-scroll no matter which corner it's parked in.
   Named .fig-unit, not .fig-card — .fig-card above is the pre-carousel result
   card, unused by any markup now but still carrying a border, background and
   padding that would draw a second frame around every stage. */
.fig-unit{display:flex;flex-direction:column;min-height:0;min-width:0;}

.fig-stage{
  position:relative;display:flex;flex-direction:column;
  border-radius:22px;border:1px solid var(--line);
  background:linear-gradient(165deg,var(--card),var(--card-warm));
  box-shadow:0 16px 34px -30px rgba(83,70,50,.55);
  overflow:hidden;margin-bottom:10px;
}
/* Which of the two this is. Floats over the portrait rather than taking a row
   of its own — slide 0 is meant to be the face and nothing else, and a header
   bar above it would push the portrait down on every slide. */
/* Caption row above each stage: which figure this is, and its carousel dots.
   The dots used to float over the bottom of the slides. That is fine over a
   portrait and wrong over the biography slide, which scrolls — text passes
   under them at every scroll position except the very end. Out here nothing
   overlays the prose, and the row doubles as the card's header. */
.fig-head{
  flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;
  gap:10px;padding:0 4px 6px;
}
.fig-rank{
  font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--gold-ink);
}
/* flex-basis auto, not 0: with `flex:1` and no definite height on the stage
   there is nothing to grow into, so the whole stage collapsed to 2px on mobile
   and the nav buttons were laid out outside the clipped box. */
.fig-slides{position:relative;flex:1 1 auto;min-height:0;display:flex;}
.fig-slide{display:none;flex:1;min-width:0;flex-direction:column;padding:22px 20px 34px;}
.fig-slide.is-active{display:flex;animation:figIn .34s cubic-bezier(.2,.7,.3,1);}
@keyframes figIn{from{opacity:0;transform:translateX(10px);}to{opacity:1;transform:none;}}
@media (prefers-reduced-motion: reduce){ .fig-slide.is-active{animation:none;} }

/* Portrait slide: image dominates, nothing else on it. */
.fig-slide[data-slide="portrait"]{padding:0;align-items:center;justify-content:center;}
/* The portrait is absolutely positioned, so the slide needs its own intrinsic
   height. 4:5 matches the source crop. The laptop grid gives the stage a real
   height, so the ratio is released there. */
.fig-slide[data-slide="portrait"]{position:relative;aspect-ratio:4/5;}
.fig-slide[data-slide="portrait"] .fig-portrait{
  position:absolute;inset:0;
  width:100%;height:100%;max-width:none;object-fit:cover;object-position:50% 50%;
  border:none;border-radius:0;margin:0;aspect-ratio:auto;
}
.fig-slide[data-slide="portrait"] .fig-emoji{font-size:76px;}

.fig-nav{
  position:absolute;top:50%;transform:translateY(-50%);z-index:3;
  width:34px;height:34px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  font-size:20px;line-height:1;padding-bottom:2px;
  color:var(--ink);background:color-mix(in srgb, var(--panel) 88%, transparent);
  border:1px solid var(--line);cursor:pointer;backdrop-filter:blur(5px);
  transition:opacity .18s, background .18s;
}
.fig-nav.prev{left:10px;} .fig-nav.next{right:10px;}
.fig-nav:hover{background:var(--panel);}
.fig-nav:disabled{opacity:.24;cursor:default;}
.fig-dots{display:flex;gap:6px;}
.fig-dots button{
  width:6px;height:6px;padding:0;border-radius:50%;cursor:pointer;
  border:1px solid var(--gold-soft);background:transparent;transition:background .2s;
}
.fig-dots button.is-on{background:var(--gold);border-color:var(--gold);}

/* Share-row tooltips open upward and centred — the bar tooltips drop down, but
   these sit at the bottom of the screen. */
.share-row .has-tip::after{
  top:auto;bottom:calc(100% + 9px);right:50%;transform:translate(50%,3px);
  white-space:normal;max-width:230px;text-align:center;
}
.share-row .has-tip:hover::after,.share-row .has-tip:focus-visible::after{transform:translate(50%,0);}

/* ---------- carousel: filling the space when the text is short ----------
   Slides carry wildly different amounts of text — a two-line trait note and a
   three-item achievement list in the same box. Rather than let short slides
   collapse to the top of a tall stage, content centres vertically (`safe`
   keeps long content from being clipped at the top when it overflows), and the
   figure's own portrait sits behind as a faint ground. */
.fig-slide:not([data-slide="portrait"]){
  justify-content:safe center;
  gap:2px;
  position:relative;
  isolation:isolate;
}
.fig-slide:not([data-slide="portrait"])::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background-image:var(--fig-img);
  background-size:cover;background-position:50% 18%;
  opacity:.10;filter:grayscale(.5) blur(1px);
  -webkit-mask-image:linear-gradient(180deg, rgba(0,0,0,.85), transparent 72%);
          mask-image:linear-gradient(180deg, rgba(0,0,0,.85), transparent 72%);
}
/* Biography (slide 5). The only slide with enough prose to overrun the stage,
   so it is the one place an internal scroll is allowed — the no-page-scroll
   rule for the Dossier is about the page, and a scrollbar inside a box the user
   deliberately stepped into reads as a panel, not as a broken layout. Top-
   aligned rather than centred: long text centred in a tall box has no stable
   first line to return to after scrolling. */
.fig-slide[data-slide="biography"]{
  justify-content:flex-start;
  overflow-y:auto;
  overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;
  /* The nav buttons are 34px circles pinned at 50% height, and this is the only
     slide whose text reliably reaches them — a short trait note stops well
     short, a full biography does not. Inset the prose so a line can never run
     underneath an arrow. */
  padding-inline:46px;
}
.fig-bio-title{
  font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--gold-ink);text-align:left;margin:2px 0 10px;
  position:sticky;top:0;z-index:1;
  /* Sticky over scrolling prose, so it needs its own ground to sit on. */
  background:linear-gradient(var(--card) 62%, transparent);
  padding-bottom:6px;
}
.fig-bio{
  font-size:14px;line-height:1.75;color:var(--ink);text-align:left;
  white-space:pre-line;   /* blank lines in the source become paragraph breaks */
}
html[lang="zh-Hans"] .fig-bio{font-size:15px;line-height:1.9;}

/* Short slides earn a little more air and a slightly larger measure. */
.fig-slide[data-slide="identity"]{text-align:center;}
.fig-slide[data-slide="identity"] .fig-name{font-size:23px;}
.fig-slide[data-slide="identity"] .fig-trait-note{
  border-left:none;border-top:1px solid var(--gold-soft);
  padding:14px 4px 0;margin:16px 0 0;text-align:center;font-size:15px;
}
.fig-slide[data-slide="identity"] .fig-tags{margin-bottom:2px;}
.fig-slide[data-slide="kinship"]{text-align:center;}
.fig-slide[data-slide="kinship"] .fig-match-note{border:none;background:transparent;font-size:15px;line-height:1.75;}
.fig-slide[data-slide="kinship"] .fig-encouragement{
  border-top:1px solid var(--gold-soft);padding-top:16px;margin-top:6px;
}
.fig-slide[data-slide="achievements"] .fig-achievements{margin-bottom:10px;}

/* ---------- universe backdrop on the Home screen ----------
   Optional. --home-bg is unset by default, so the screen keeps its plain card
   until an image exists; a missing file fails silently rather than breaking
   anything. Sits behind the content at low opacity with a scrim, because text
   sits on top of it. */
#screen-hero{position:relative;isolation:isolate;}
#screen-hero::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  border-radius:inherit;
  background-image:var(--home-bg);
  background-size:cover;background-position:50% 42%;
  opacity:var(--home-bg-opacity, .16);
  filter:grayscale(.25);
  -webkit-mask-image:linear-gradient(200deg, rgba(0,0,0,.15) 0%, rgba(0,0,0,.9) 58%);
          mask-image:linear-gradient(200deg, rgba(0,0,0,.15) 0%, rgba(0,0,0,.9) 58%);
}
:root[data-universe="france"]{
  --home-bg:url("https://pub-01f215c0275b4d5083630833b6a2b41c.r2.dev/scene-france-salon.webp");
  --home-bg-opacity:.18;
}
:root[data-universe="historical-global"]{
  --home-bg:url("https://pub-01f215c0275b4d5083630833b6a2b41c.r2.dev/scene-global-archive.webp");
  --home-bg-opacity:.15;
}

/* ============================================================================
   ROUNDED PORTRAITS + LARGER TEXT
   ========================================================================== */
/* object-fit:contain letterboxes the image inside its box, so the radius has to
   sit on the image itself — the card's own radius doesn't clip anything. */
.teaser-card .blur-fig{border-radius:14px;}
.fig-portrait{border-radius:14px;}
.poster img{border-radius:10px;}

/* Base copy up a step across the board. */
.sub,.pref-note{font-size:18px;}
.option{font-size:17px;}
.q-text{font-size:30px;}
.archetype-desc{font-size:16px;}
.fig-name{font-size:21px;}
.fig-nickname{font-size:16px;}
.fig-era{font-size:12px;}
.fig-trait-note{font-size:15px;}
.fig-achievements li,.fig-facts li,.fig-match-note{font-size:14px;}
.fig-encouragement{font-size:16px;}
.gate-note{font-size:16px;}
.pref-desc{font-size:17px;}

/* Chinese needs more size than Latin at the same nominal px: the glyphs carry
   far more stroke detail in the same em box, so 15px Latin and 15px Hanzi are
   not equally legible. Scoped to the active language via the lang attribute
   applyLang() already sets. */
html[lang="zh-Hans"] .sub,
html[lang="zh-Hans"] .pref-note{font-size:19px;line-height:1.85;}
html[lang="zh-Hans"] .option{font-size:18px;line-height:1.7;}
html[lang="zh-Hans"] .q-text{font-size:32px;line-height:1.5;}
html[lang="zh-Hans"] .archetype-desc{font-size:17px;line-height:1.85;}
html[lang="zh-Hans"] .fig-trait-note{font-size:16px;line-height:1.8;}
html[lang="zh-Hans"] .fig-achievements li,
html[lang="zh-Hans"] .fig-facts li,
html[lang="zh-Hans"] .fig-match-note{font-size:15px;line-height:1.85;}
html[lang="zh-Hans"] .fig-encouragement{font-size:17px;}
html[lang="zh-Hans"] .fig-name{font-size:23px;}
html[lang="zh-Hans"] .gate-note{font-size:17px;}
html[lang="zh-Hans"] .pref-desc{font-size:18px;}
html[lang="zh-Hans"] .dim-tag,
html[lang="zh-Hans"] .fig-tag{font-size:12px;}

/* Laptop question size. This has to live down here, after the blanket
   `.q-text{font-size:30px}` above: that rule is later in the file and equally
   specific, so the clamp inside the min-width:1000px block was being silently
   overridden and the question stayed at phone size in a column twice as wide. */
@media (min-width:1000px){
  .q-text{font-size:clamp(32px,2.8vw,44px);}
  html[lang="zh-Hans"] .q-text{font-size:clamp(34px,3vw,48px);}
}

/* The achievements slide used to sit on a per-domain still life instead of the
   portrait. Dropped: keyed to domain, one image served every figure in it, so
   it never said anything about the person on screen — and at the opacity needed
   to keep the text legible it read as a grey smudge low in the box, away from
   the words it was supposed to be grounding. The slide now inherits the same
   portrait ground as the others, which at least is the figure. The 17
   `domain-*.webp` objects are still in R2 and unused; see docs/slide-images.md. */

/* Dots sit over artwork now, so they need to hold their own. */
.fig-dots button{border-color:var(--gold);box-shadow:0 0 0 2px rgba(255,253,246,.55);}

/* ============================================================================
   H5 layer — mobile webview behaviour (WeChat / Xiaohongshu)
   See docs/wechat-h5.md.
   ========================================================================= */

/* ---- Portrait lock ----
   The short screens become a fixed one-viewport panel: no page scroll, so no
   pull-to-refresh and no rubber-band. `body.screen-locked` is toggled by
   showScreen() in app.js — CSS alone can't tell which screen is up.

   Two screens are deliberately NOT in the locked set, both for the same
   measured reason: they are taller than a phone.

   - The result screen carries the radar, two figure carousels, achievements
     and the share row — ~2.5 viewports at 375x667.
   - The quiz needs about 1000px of content in every language. Measured across
     all 24 questions: the question block alone wants 192px (zh), 248px (en) or
     290px (fr), and the tallest option needs 113-148px, times four, plus 155px
     of chrome. The tallest phone in common use is 932px. There is no viewport
     where it fits, so locking it would mean either clipping option text or
     putting a scrollbar inside a four-item list — both worse than the page
     scroll it would be replacing.

   What the quiz keeps regardless is overscroll-behavior on <body>, which is
   what actually kills pull-to-refresh in the WeChat webview. The lock was
   never what delivered that.

   To make a locked quiz possible, the option copy would have to lose roughly a
   third of its length; see docs/wechat-h5.md. */
@media (max-width:999px){
  body.screen-locked{height:100dvh;overflow:hidden;}
  body.screen-locked .app{height:100dvh;min-height:0;overflow:hidden;}
  body.screen-locked .screen.active{overflow:hidden;min-height:0;}

  /* The gate holds a text input. When the keyboard opens, dvh shrinks and a
     hard clip would put the submit button behind the keyboard, so this one
     scrolls inside itself. */
  body.screen-locked #screen-gate.active{overflow-y:auto;overscroll-behavior:contain;}
}

/* ---- Question transition ----
   renderQuestion() replaces the options wholesale, which previously swapped
   with no transition at all. The incoming question slides up as one unit. */
@keyframes q-advance{
  from{opacity:0;transform:translate3d(0,26px,0);}
  to{opacity:1;transform:translate3d(0,0,0);}
}
@keyframes q-back{
  from{opacity:0;transform:translate3d(0,-22px,0);}
  to{opacity:1;transform:translate3d(0,0,0);}
}
#screen-quiz.q-enter .q-text,
#screen-quiz.q-enter .options{animation:q-advance .34s cubic-bezier(.22,.68,.3,1) both;}
#screen-quiz.q-enter .options{animation-delay:.04s;}
#screen-quiz.q-back .q-text,
#screen-quiz.q-back .options{animation:q-back .3s cubic-bezier(.22,.68,.3,1) both;}

/* ---- Tactile feedback ----
   Only on coarse pointers: a mouse cursor pressing a card down reads as a bug.
   The transform is on the button itself, so it composites without layout. */
@media (hover:none){
  .option{transition:transform .12s ease, border-color .2s, background .2s;}
  .option:active{transform:scale(.975);}
  .cta:active,.share-btn:active,.pref-card:active{transform:scale(.985);}
  .fig-nav:active{transform:scale(.9);}
}
/* Nothing here is load-bearing, so it all goes when motion is not wanted. */
@media (prefers-reduced-motion: reduce){
  #screen-quiz.q-enter .q-text,#screen-quiz.q-enter .options,
  #screen-quiz.q-back .q-text,#screen-quiz.q-back .options{animation:none;}
  .option:active,.cta:active,.share-btn:active,.pref-card:active,.fig-nav:active{transform:none;}
}

/* The figure carousel is swipeable now (see figStage swipe wiring in app.js).
   pan-y keeps vertical page scroll on the result screen while the horizontal
   axis is handled in JS. */
.fig-stage{touch-action:pan-y;}

/* ---- WeChat / in-app browser banner ---- */
.ua-banner{
  position:fixed;left:0;right:0;top:0;z-index:60;
  padding:calc(10px + env(safe-area-inset-top)) 16px 10px;
  background:rgba(35,33,32,.94);color:#f6f1e6;
  font-family:var(--font-body);font-size:13px;line-height:1.5;
  display:flex;align-items:flex-start;gap:12px;
  box-shadow:0 10px 30px -18px rgba(0,0,0,.8);
}
.ua-banner p{flex:1;margin:0;}
.ua-banner strong{color:var(--gold-soft);font-weight:600;}
.ua-banner button{
  flex:0 0 auto;background:none;border:1px solid rgba(246,241,230,.4);color:inherit;
  border-radius:999px;width:26px;height:26px;font-size:15px;line-height:1;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* The banner is fixed, so the panel underneath needs room to clear it. */
body.has-ua-banner .app{padding-top:calc(26px + 58px + env(safe-area-inset-top));}

/* ---- Share sheet (the long-pressable card) ---- */
.share-sheet{
  position:fixed;inset:0;z-index:80;
  background:rgba(28,25,22,.86);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;
  padding:calc(18px + env(safe-area-inset-top)) 18px calc(18px + env(safe-area-inset-bottom));
  overscroll-behavior:contain;
}
.share-sheet img{
  max-width:min(100%,420px);max-height:72dvh;width:auto;height:auto;
  border-radius:14px;box-shadow:0 30px 70px -30px rgba(0,0,0,.9);
  /* Long-press must offer "Save Image" — in WeChat that gesture is the only
     way the card reaches the camera roll. Nothing suppresses the callout today,
     so these are a guard rather than a fix: a later global `user-select:none`
     or `touch-callout:none` for a more app-like feel would silently break the
     one interaction the share sheet exists for. iOS WebKit only; Chrome drops
     the callout declaration from its CSSOM, so it cannot be asserted in a
     desktop-headless test. */
  -webkit-touch-callout:default;user-select:auto;-webkit-user-select:auto;
}
.share-sheet-hint{
  color:#f6f1e6;font-family:var(--font-body);font-size:14px;text-align:center;
  max-width:340px;line-height:1.6;text-wrap:balance;
}
.share-sheet-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;}
.share-sheet-actions button{
  background:rgba(246,241,230,.1);border:1px solid rgba(246,241,230,.42);color:#f6f1e6;
  font-family:var(--font-body);font-size:14px;font-weight:500;
  padding:11px 20px;border-radius:999px;cursor:pointer;-webkit-tap-highlight-color:transparent;
}
.share-sheet-actions button.primary{background:var(--gold);border-color:var(--gold);color:#2a2418;}
.share-sheet-spinner{
  width:34px;height:34px;border:3px solid rgba(246,241,230,.25);border-top-color:#f6f1e6;
  border-radius:50%;animation:spin 1s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg);}}
@media (prefers-reduced-motion: reduce){.share-sheet-spinner{animation-duration:2.4s;}}
