/* ===================================================================
   The single palette of record. There used to be TWO :root blocks — the
   original gold/parchment/hot-pink theme here, and the flat-cartoon Fonz
   reskin further down — with the second winning by cascade order alone.
   Roughly thirty rules still referenced the old names, two of them
   ("⚡ all" and the control row) still rendered in old-theme wood, and any
   reordering of the file would have resurfaced the rest. One vocabulary now.
   =================================================================== */
  :root{
    /* surfaces */
    --ink:#1c1c1c; --cream:#f7efdc; --cream2:#eee0bf; --cream3:#e4d4ad; --surface:var(--surface);
    --navy:#30333f; --navy2:#22242e;
    /* accents — the BRIGHT values are fills only (buttons, bars, badges), where they
       sit under white or ink. As TEXT on cream they all failed AA, hence the -t twins. */
    --peach:#f4977e; --peachd:#d97a63;
    --fgreen:#7cb063; --fgreend:#5c8f47; --fred:#d94b3f;
    /* text-safe twins. AA (4.5:1) on both --cream and --surface:
       peach 2.98→4.65 · green 3.77→4.76 · red 4.10→5.28 · gold 3.25→4.65 · muted 3.66→5.23 */
    --peach-t:#a55440; --green-t:#47752f; --red-t:#c03b30;
    --gold-t:#8a6508;  --muted-t:#6f6244;
    --accent-t:var(--peach-t);   /* the old --pinkd magenta: accent TEXT, one name */
    /* Reserved for :focus-visible and NOTHING else. A focus ring must never be
       confusable with a state, so it is the one colour no surface is allowed to use. */
    --focus:#ff1493;
    /* la famille doree du premium : le degrade haut->bas, son ombre portee, et la lueur MEGA.
       Elle etait repetee a l identique en 14 endroits sans token, ce qui est la facon dont
       une charte derive : changer le doré demandait 14 retouches. Valeurs inchangees. */
    --gold-hi:#ffe9a8; --gold-lo:#f4c95e; --gold-deep:#8a5a00; --gold-glow:#ffd24a;
    --rail:380px;          /* the roster column; the stage is everything else */

    /* type — "Arial Black" is absent on Android and most Linux, where the stack fell back
       to sans-serif at weight 400 and the whole display layer of the game collapsed. */
    --font-display:"Arial Black","Arial Bold","Helvetica Bold",system-ui,sans-serif;
    --weight-display:900;
    --font-ui:"Trebuchet MS",Verdana,"Segoe UI",sans-serif;
    --font-num:"Courier New",ui-monospace,monospace;
  }
  *{box-sizing:border-box}

  /* ===== LIVE NUMBERS =====================================================
     Every readout that changes while you watch it. In proportional figures a
     "1" is narrower than a "7", so each of these wobbled on every frame — the
     candle count, the DPS line, the HP total, and worst of all the 45-second
     boss timer counting down in 24px display type. Tabular figures are the
     one-line fix; nothing here is monospaced by accident. */
  .resbar .amount,.bres b,.dpsbar,.hdps,.hpnum,.bosstimer,.float,.zoneprog,
  .hirebtn small,.hcstat .hcv,.hcup .hcus,.lbstatv,.lbrow,.w3num,.profrow .pval,
  .docrow .dlvl,.bptxt,.bphrow .bphnum,.tip .tcost,.hrow.ghost .gc,.thumb .tn{
    font-variant-numeric:tabular-nums;
  }
  body{
    margin:0;font-family:var(--font-ui);font-size:15px;
    color:var(--ink);background:var(--cream);
    min-height:100vh;overflow-x:hidden;
  }

  /* ===== gold frame helpers ===== */
/* =================== MOBILE / TOUCH BASELINE ===================
   this game is tapped hard and fast on phones. without these, rapid tapping
   selects the text around the target, long-press opens the iOS save-image
   callout, every tap costs the 300ms double-tap-zoom wait, and an upward
   swipe near the top fires pull-to-refresh, which reloads mid-session. */
html{
  -webkit-text-size-adjust:100%;      /* stop safari resizing text on rotate */
  overscroll-behavior:none;           /* no pull-to-refresh mid-run */
}
body{
  overscroll-behavior:none;
  -webkit-user-select:none;user-select:none;
  -webkit-touch-callout:none;
}
/* but never trap the save code: it exists to be selected and copied */
textarea,input,.selectable{
  -webkit-user-select:text;user-select:text;-webkit-touch-callout:default;
}
/* every interactive control: kill the tap delay and the grey flash */
button,.thumb,.upg,.skill,.mob,.kyle,a{
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;
}
img{-webkit-user-drag:none;user-drag:none;pointer-events:none}
/* images inside controls must not eat the tap */
button img,.skill img,.mob img,.kyle img{pointer-events:none}
/* inline UI icons (currencies, header/button glyphs) — replace the old emoji */
.uii{height:1.15em;width:auto;vertical-align:-0.22em;display:inline-block;object-fit:contain}
.uii.b{height:1.5em;vertical-align:-0.32em}
.docrow .dico{width:40px}
.docrow .dico .uii{height:32px;vertical-align:middle}
/* .dicoi is sized next to the .dico rule that actually wins (see below) — putting it here
   would be dead code, since that later rule caps the cell at 30px. */

.uicandle{width:auto;height:34px;object-fit:contain;vertical-align:middle;filter:drop-shadow(0 1px 2px #0006)}
.uicandle.sm{height:16px;filter:none}
/* progressive disclosure: panels a new player can't act on yet are removed from the layout
   entirely (not just dimmed) until their unlock condition is met — see renderGates() in ui.js */
.gated{display:none!important}
/* visible to a screen reader only — carries the zone announcement (see spawn() in engine.js) */
.skiplink{
  position:fixed;left:8px;top:-100px;z-index:100000;
  background:var(--cream);color:var(--ink);border:3px solid var(--ink);border-radius:10px;
  box-shadow:3px 3px 0 var(--ink);padding:10px 16px;text-decoration:none;
  font-family:var(--font-display);font-weight:var(--weight-display);font-size:14px;
  transition:top .12s ease;
}
.skiplink:focus{top:8px;outline:3px dotted var(--focus);outline-offset:2px}
@media(prefers-reduced-motion:reduce){.skiplink{transition:none}}
/* the eight drawn chrome icons that replaced stock emoji. Each is sized against the text it
   sits with, so swapping art can never change a layout. */
.zico{width:72%;height:72%;object-fit:contain;pointer-events:none;display:block}
/* l'eclair touchait le compteur : la marge vit sur l'icone, pas sur un espace insecable */
.uii.bolt{margin-right:5px}
.buyups .bn{font-variant-numeric:tabular-nums}
.iico{height:1em;width:auto;vertical-align:-0.15em;object-fit:contain}
.lico{height:1.05em;width:auto;vertical-align:-0.18em;object-fit:contain}
/* le bloc valeur et le bloc prix du Pass : meme grammaire, pour qu'on lise
   "ce que j'obtiens" puis "ou va mon argent" sans effort */
.bpvalue,.bpprice{
  background:#fff8;border:2px solid var(--accent-t);border-radius:11px;
  padding:8px 10px;margin:0 0 8px;
}
.bpprice{border-style:dashed}
/* le bloc qui annonce le paiement de la saison : meme grammaire, fond accentue */
.bpseason{background:#fff8;border:2px solid var(--accent-t);border-radius:11px;padding:8px 10px;margin:0 0 8px;
  box-shadow:2px 2px 0 var(--ink)}
.bpvh{
  font-family:var(--font-display);font-weight:var(--weight-display);
  font-size:11px;letter-spacing:.6px;text-transform:uppercase;
  color:var(--muted-t);margin-bottom:5px;
}
.bpvrow{display:flex;gap:8px;align-items:baseline;font-size:12.5px;line-height:1.35;margin:3px 0}
.bpvrow b{flex:0 0 auto;min-width:88px;color:var(--accent-t);font-family:var(--font-num)}
.bpvrow span{flex:1 1 auto;color:var(--ink)}
@media(max-width:940px){
  .bpvrow{flex-direction:column;gap:1px;margin:5px 0}
  .bpvrow b{min-width:0}
}
.sronly{
  position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}
  /* L'art sk-* est un tableau 2.5:1 avec un sujet centre sur une texture parchemin. En
     `contain` il flottait au milieu du bouton blanc et se lisait comme un rectangle perdu
     dans un carre. En `cover` il REMPLIT le bouton : le sujet devient grand et la texture
     redevient un fond voulu au lieu d'un vide. Le rognage se fait sur les bords lateraux,
     ou il n'y a que de la texture.
     Le rayon est porte par l'image et non par le bouton : celui-ci reste en overflow:visible
     pour laisser deborder le badge de cadenas. 11px = 14px de rayon moins 3px de bordure. */
  .skicon{
    width:100%;height:100%;object-fit:cover;border-radius:11px;display:block;
    pointer-events:none;filter:saturate(1.18) contrast(1.06);
  }
  .kyleimg{height:120px;width:auto;display:block}
  .goldframe{
    background:var(--cream);border:3px solid var(--ink);border-radius:14px;
    box-shadow:4px 4px 0 var(--ink);position:relative;
  }
  .goldframe::before,.goldframe::after{
    content:"";position:absolute;width:8px;height:8px;border-radius:50%;
    background:radial-gradient(circle at 35% 30%,var(--gold-hi),var(--gold-deep));top:6px;
  }
  .goldframe::before{left:6px}.goldframe::after{right:6px}

  /* 380px, not 460: the stage is capped by the width of ITS column, so the last 80px of
   island width can only come from here. The roster card is re-proportioned to match —
   portrait 132→110px and the text gutter with it. */
  .layout{display:grid;grid-template-columns:var(--rail) 1fr;min-height:100vh}
  
  /* ---- tap tooltips (bonus descriptions) ---- */
  .tip{
    position:fixed;z-index:380;max-width:270px;display:none;
    background:var(--cream);border:3px solid var(--ink);border-radius:12px;
    box-shadow:4px 4px 0 var(--ink);padding:10px 13px;color:var(--ink);
    font-size:13.5px;line-height:1.4;
  }
  .tip.open{display:block}
  .tip .tname{font-family:var(--font-ui);font-weight:bold;font-size:14.5px;margin-bottom:2px}
  .tip .tico{font-size:17px;margin-right:5px}
  .tip .tdesc{margin:3px 0 7px}
  .tip .tcost{font-family:var(--font-num);font-weight:bold;font-size:13px;margin-bottom:2px}
  .tip .tlock{color:var(--muted-t);font-size:12.5px;font-weight:bold}
  .tip .towned{color:var(--fgreend);font-size:12.5px;font-weight:bold}
  .tip .tcant{color:var(--red-t);font-size:12.5px;font-weight:bold}
  .tip .tbuy{
    display:inline-block;margin-top:7px;padding:7px 15px;cursor:pointer;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:13px;color:var(--ink);
    background:var(--peach);border:3px solid var(--ink);border-radius:9px;box-shadow:3px 3px 0 var(--ink);
  }
  .tip .tbuy:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--ink)}
  .upg.cant{filter:grayscale(.8);opacity:.5}

  
  /* ---- doctrine ---- */
  .rugbtn.doc{background:linear-gradient(var(--gold-hi),var(--gold-lo));color:var(--ink);margin-top:4px}
  .docgrp{font-family:var(--font-ui);font-weight:bold;font-size:12px;letter-spacing:.5px;
    text-transform:uppercase;color:var(--muted-t);margin:12px 2px 6px;padding-bottom:3px;border-bottom:2px dashed #d8c48a}
  .docgrp:first-child{margin-top:0}
  .docrow{display:flex;align-items:center;gap:10px;padding:9px 10px;margin-bottom:8px;
    background:var(--surface);border:3px solid var(--ink);border-radius:12px;box-shadow:3px 3px 0 var(--ink)}
  .docrow .gimg{width:34px;height:34px;object-fit:contain;flex:0 0 auto}
  .docrow .dico{font-size:22px;width:30px;text-align:center}
  /* drawn doctrine art is a square badge, not an inline glyph: an explicit block box, so it
     never inherits .uii's em-relative height from a parent whose font-size changes.
     :has() widens the cell for the art only — .dico is shared with the shop and season-pass
     rows, whose 30px cell must not move. 38px reads properly in an 83px row. */
  .docrow .dico:has(.dicoi){width:38px}
  .docrow .dico .dicoi{width:38px;height:38px;object-fit:contain;display:block;margin:0 auto}
  .docrow .dmid{flex:1;min-width:0}
  .docrow .dname{font-family:var(--font-ui);font-weight:bold;font-size:14px}
  .docrow .ddesc{font-size:12.5px;color:var(--muted-t)}
  .docrow .dlvl{font-family:var(--font-num);font-weight:bold;font-size:12.5px;color:var(--green-t)}
  .docbuy{padding:7px 12px;cursor:pointer;white-space:nowrap;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:12.5px;color:var(--ink);
    background:var(--peach);border:3px solid var(--ink);border-radius:9px;box-shadow:2px 2px 0 var(--ink)}
  .docbuy:disabled{filter:grayscale(.8);opacity:.5;cursor:not-allowed}
  .docbuy:not(:disabled):active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}

  

  /* ---- tap ring: the click acknowledged where it happened ----
     Flat cartoon like everything else: an ink outline, no gradient, no glow. It expands and
     fades in under a third of a second so fast tapping never leaves a trail. Centred on the
     pointer via a -50% translate, so the ring grows out of the exact pixel that was pressed. */
  .tapring{
    position:absolute;z-index:7;pointer-events:none;
    width:52px;height:52px;margin:-26px 0 0 -26px;border-radius:50%;
    border:3px solid var(--ink);
    /* a light rim outside the ink so the mark survives the dark zone skies too — the same
       trick the HUD text uses. Measured sky luma across the five zones: 131 to 204. */
    box-shadow:0 0 0 2px rgba(255,255,255,.5);
    animation:tapring .3s cubic-bezier(.2,.7,.4,1) forwards;
  }
  @keyframes tapring{
    from{transform:scale(.28);opacity:.85}
    to  {transform:scale(1);opacity:0}
  }
  /* a crit gets the brand accent and a touch more room, matching the CRIT! float beside it */
  .tapring.crit{
    width:76px;height:76px;margin:-38px 0 0 -38px;
    border-color:var(--peach);border-width:4px;
    box-shadow:0 0 0 2px rgba(28,28,28,.45);
    animation-duration:.42s;
  }
  @media(prefers-reduced-motion:reduce){.tapring{display:none}}

  /* ---- kill burst (currency particles) ---- */
  .cburst{
    position:absolute;width:7%;max-width:34px;min-width:18px;aspect-ratio:1;object-fit:contain;
    pointer-events:none;z-index:7;will-change:transform,opacity;
    animation:cburst var(--t,.9s) cubic-bezier(.25,.55,.45,1) forwards;
    filter:drop-shadow(1px 2px 0 rgba(28,28,28,.35));
  }
  @keyframes cburst{
    0%{transform:translate(0,0) scale(.5) rotate(0deg);opacity:1}
    38%{transform:translate(calc(var(--dx)*.55),var(--up)) scale(1) rotate(calc(var(--rot)*.5));opacity:1}
    78%{opacity:1}
    100%{transform:translate(var(--dx),var(--dn)) scale(.92) rotate(var(--rot));opacity:0}
  }
  
  /* ---- shrine / missions / combo / primal ---- */
  .docbtns{display:flex;gap:5px}
  .docbtns .rugbtn{flex:1;padding:8px 2px;font-size:10.5px;white-space:nowrap;letter-spacing:0}
  .combotag{
    position:absolute;top:10px;right:12px;z-index:8;pointer-events:none;
    background:var(--peach);border:3px solid var(--ink);border-radius:9px;
    box-shadow:3px 3px 0 var(--ink);padding:4px 9px;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:12.5px;color:var(--ink);
  }
  /* MEGA / GIGA auras. These used to be pure drop-shadows; they are drawn art now
     (aura-mega/aura-giga, cut out with a transparent centre so the sprite sits INSIDE the halo).
     Painted by a ::before on the button rather than the <img>, so the glow can be bigger than
     the sprite without scaling it — :has() picks the variant off the class engine.js already
     writes, so no JS change. The drop-shadow tint stays as the fallback: browsers without
     :has() get exactly what they had before, and on top of the art it reads as rim light. */
  .mobimg.primal{filter:drop-shadow(0 0 14px var(--gold-glow)) drop-shadow(0 0 4px var(--gold-glow)) saturate(1.25)}
  .mobimg.omega{filter:drop-shadow(0 0 16px #7ad0ff) drop-shadow(0 0 6px #b06bff) saturate(1.3)}
  /* Both auras are RINGS with a transparent hole, so the HOLE is what these numbers solve for,
     never the outer edge. Let H be the .mob height (218.8px on a 663px stage):

       sprite    .bossimg is 126% of H = 271px tall, and because the flex line is
                 align-items:flex-end it hangs off the TOP of the box, putting its centre at
                 0.37H rather than 0.5H. Height is the same for every boss; only width varies,
                 from 117px (suit-agent) to 293px (the-black-swan).
       hole      57% of the mega file, 52% of the giga file. So the ring size needed is
                 (widest sprite dimension + margin) / that fraction, which lands at 245% and
                 275%. Both open a ~310px hole: wide enough that no boss touches the flames.

     The previous 195/225% opened only a 245px hole against a 271px sprite, so the ring closed
     over every boss — heads and feet outside, flames crossing the torso. That is the whole bug.

     The ceiling on this is the stage, not taste: there are only 49px above the boss's head, so
     a ring big enough to clear it loses ~50px of flame tips to .battle's overflow:hidden. That
     reads as flames running off the top of the frame, which is fine; a smaller ring cutting
     across the boss is not. */
  .mob:has(.mobimg.primal)::before,
  .mob:has(.mobimg.omega)::before{
    content:"";position:absolute;left:50%;top:37%;z-index:-1;pointer-events:none;
    height:245%;aspect-ratio:1;transform:translate(-50%,-50%);
    background:center/contain no-repeat;
  }
  .mob:has(.mobimg.primal)::before{
    background-image:url(../assets/ui/aura-mega.webp?v=1);
    animation:auraspin 24s linear infinite;
  }
  .mob:has(.mobimg.omega)::before{
    background-image:url(../assets/ui/aura-giga.webp?v=1);
    height:275%;                 /* giga's hole is 52% of its file against mega's 57%; this is
                                    that difference, so both holes match and the giga RING still
                                    reads bigger — 602px of art against 536px */
    animation:auraspin 16s linear infinite reverse,auragiga 1.1s ease-in-out infinite;
  }
  @keyframes auraspin{to{transform:translate(-50%,-50%) rotate(360deg)}}
  @keyframes auragiga{0%,100%{opacity:.82}50%{opacity:1}}
  @media(prefers-reduced-motion:reduce){
    .mob:has(.mobimg.primal)::before,.mob:has(.mobimg.omega)::before{animation:none}
  }
  /* active auto-clicker badge (Shrine clickers) — top-left, out of the combo's way */
  /* second row of the same top-right stack — the top-LEFT is the zone block now */
  .autotag{
    position:absolute;top:52px;right:12px;z-index:8;pointer-events:none;
    background:linear-gradient(#b7f0a8,#8fd67a);border:3px solid var(--ink);border-radius:9px;
    box-shadow:3px 3px 0 var(--ink);padding:4px 9px;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:12.5px;color:var(--ink);
  }

  
  /* ---- pass + profile ---- */
  .bpbar{position:relative;height:26px;border:3px solid var(--ink);border-radius:9px;
    background:var(--surface);box-shadow:3px 3px 0 var(--ink);margin-bottom:10px;overflow:hidden}
  .bpfill{position:absolute;inset:0;width:0;background:linear-gradient(var(--gold-hi),var(--gold-lo))}
  .bptxt{position:relative;display:block;text-align:center;line-height:20px;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:12px;color:var(--ink)}
  .bptier.locked{opacity:.62}
  .bpclaims{display:flex;flex-direction:column;gap:4px}
  .bpclaims .docbuy{font-size:10.5px;padding:5px 8px}
  .bpclaims .docbuy.prem{background:linear-gradient(var(--gold-hi),var(--gold-lo))}
  .profrow{display:flex;align-items:center;gap:9px;padding:7px 4px;border-bottom:2px dashed #d8cdb4;font-size:13.5px}
  .profrow:last-child{border-bottom:none}
  .profrow .pico{width:24px;text-align:center}
  .profrow .plab{flex:1;color:var(--muted-t)}
  .profrow .pval{font-family:var(--font-num);font-weight:bold}
  .xrow{display:flex;gap:8px;margin:12px 0 8px}
  .refrow{display:flex;gap:8px;align-items:stretch;margin-top:6px}
  .refinput{
    flex:1;min-width:0;font-family:var(--font-num);font-size:12px;
    background:var(--cream);color:var(--ink);border:3px solid var(--ink);border-radius:10px;
    padding:8px 10px;box-shadow:inset 2px 2px 0 #0001;
  }
  .refrow .docbuy{flex:0 0 auto}
  .xrow .docbuy{flex:1}
  /* profile: connections card (wallet + X gate) */
  /* padding-bottom reduit : la carte gardait ~40px de vide sous le message de gate */
  .profconn{background:linear-gradient(#fff6e8,#ffeccf);border:3px solid var(--ink);border-radius:12px;padding:10px 11px 11px;margin-bottom:12px}
  .profconn .profgate:last-child{margin-bottom:0}
  .profconn .w3err:empty,.profconn .w3note:empty{display:none}   /* zones de message vides = espace mort */
  .profconn-h{font-family:var(--font-display);font-weight:var(--weight-display);font-size:14px;color:var(--accent-t);margin-bottom:8px}
  .profconn-h small{font-family:inherit;font-weight:normal;color:var(--muted-t);font-size:11px}
  .conrow{display:flex;align-items:center;gap:9px;padding:6px 2px;border-bottom:2px dashed #e0d4ba}
  .conrow .conk{width:92px;font-weight:bold;color:var(--muted-t);font-size:13px}
  .conrow .conv{flex:1;font-family:var(--font-num);font-size:13px;color:var(--muted-t)}
  .conrow .conv.ok{color:var(--green-t)}
  .conrow .conv.ok b{color:var(--green-t)}
  .conrow .docbuy{padding:6px 11px;font-size:12px}
  .profgate{margin-top:9px;font-size:13px;text-align:center;background:#fff8;border:2px dashed var(--accent-t);border-radius:9px;padding:7px 9px;color:var(--peach-t)}
  .profgate.ok{border-color:#3ea55e;background:#eafaef;color:var(--green-t)}
  .profgate b{color:var(--accent-t)}
  .profgate.ok b{color:var(--green-t)}
  .profsubh{margin:14px 0 2px;font-family:var(--font-display);font-weight:var(--weight-display);font-size:12px;letter-spacing:1px;color:var(--muted-t);text-transform:uppercase}
  /* leaderboard: verified X handle + inline link-X button */
  .lbhandle{color:var(--accent-t);font-weight:bold;text-decoration:none}
  .lbhandle:hover{text-decoration:underline}
  .lbxbtn{margin-top:6px;display:inline-block;background:var(--ink);color:#fff;border:0;border-radius:8px;padding:6px 12px;font-family:var(--font-display);font-weight:var(--weight-display);font-size:12px;cursor:pointer}
  .lbxbtn:active{transform:translate(1px,1px)}

  
  .bpbanner{width:100%;display:block;border:3px solid var(--ink);border-radius:12px;
    box-shadow:3px 3px 0 var(--ink);margin-bottom:10px}

  
  /* ---- pass explainer ---- */
  .bpintro,.bpprem{
    background:var(--surface);border:3px solid var(--ink);border-radius:12px;
    box-shadow:3px 3px 0 var(--ink);padding:9px 11px;margin-bottom:9px;font-size:13px;line-height:1.45}
  .bpprem{background:linear-gradient(#fff8e2,#ffeec4)}
  .bpdim{color:var(--muted-t);font-size:12.5px;line-height:1.4}
  .bptothead{font-family:var(--font-display);font-weight:var(--weight-display);font-size:12.5px;margin:2px 0 6px;letter-spacing:.3px}
  .bptotrow{display:flex;gap:8px;align-items:baseline;padding:3px 0;font-size:12.5px}
  .bptotrow .bptotlab{flex:0 0 96px;font-weight:bold}
  .bptotrow.prem .bptotlab{color:var(--gold-t)}
  .bppremhead{font-family:var(--font-display);font-weight:var(--weight-display);font-size:13px;margin-bottom:5px}
  /* premium half-and-half payment */
  .bphalf{margin:8px 0 2px}
  .bphrow{display:flex;justify-content:space-between;align-items:baseline;gap:8px;
    font-size:12px;font-weight:bold;margin-bottom:3px}
  .bphrow .bphnum{font-family:var(--font-num);font-weight:bold;color:var(--muted-t);font-size:11.5px}
  .bphbar{position:relative;height:12px;border:2px solid var(--ink);border-radius:7px;
    background:var(--surface);overflow:hidden;margin-bottom:5px}
  .bphfill{position:absolute;inset:0;width:0;background:linear-gradient(var(--gold-hi),var(--gold-lo));transition:width .3s}
  .bphfill.ok{background:linear-gradient(#bfe6a0,#7fc35a)}
  .bphbtn{width:100%;font-size:11.5px;padding:7px 8px}
  .bphbtn:disabled{opacity:.55;cursor:default;box-shadow:none}

  @media (prefers-reduced-motion: reduce){.cburst{display:none}}

  @media(max-width:940px){.layout{grid-template-columns:1fr}.left{order:2}.battle{order:1}}

  /* =================== MOBILE: two panes, nothing to scroll for ===================
     Beta (Niirwaa): "on devrait pas avoir à scroll vers le bas pour accéder aux héros",
     "tu vois même pas tes candles". So on a phone the page stops being one long column:
     the resource bar is pinned at the top of BOTH panes, a bottom nav switches between
     the battle and the roster, and neither pane scrolls into the other. */
  .mnav{display:none}
  @media(max-width:940px){
    .mnav{
      display:flex;position:fixed;left:0;right:0;bottom:0;z-index:120;gap:0;
      padding-bottom:env(safe-area-inset-bottom);
      background:var(--navy2);border-top:3px solid var(--ink);
    }
    .mnavb{
      flex:1;min-height:52px;border:0;background:transparent;color:var(--cream2);cursor:pointer;
      font-family:var(--font-display);font-weight:var(--weight-display);font-size:13px;letter-spacing:.5px;
    }
    .mnavb.on{background:#ffffff1f;color:#fff;box-shadow:inset 0 3px 0 var(--peach)}
    .mnavb:focus-visible{outline:3px dotted var(--focus);outline-offset:-4px}

    /* the resource bar is pinned and never scrolls away */
    .bank.resbar{
      position:fixed;top:0;left:0;right:0;z-index:110;margin:0;border-radius:0;
      border-left:0;border-right:0;box-shadow:0 3px 10px #0006;
      padding-top:calc(7px + env(safe-area-inset-top));
    }
    /* room for the two fixed bars */
    .layout{padding-top:var(--mbar,60px);padding-bottom:calc(52px + env(safe-area-inset-bottom))}
    .zonesel{margin-top:8px}                       /* the safe-area top now belongs to the bank */

    /* pane switching */
    body.m-heroes .battle{display:none}
    body.m-battle .left{background:none;box-shadow:none;padding:0}
    body.m-battle .left .connchip,
    body.m-battle .left .tabs,
    body.m-battle .left .listwrap,
    body.m-battle .left .rugbox{display:none}
    /* The .layout reserved the bottom-nav strip with padding, so below the battle sky (or the
       navy roster panel) sat a band of raw body cream above the nav — a visible seam on every
       phone. The visible pane owns that strip now, and the roster's floor follows the measured
       bar height instead of a hardcoded 60px that stopped being true. */
    body.m-battle .layout{padding-bottom:0}
    body.m-battle .battle{
      min-height:calc(100dvh - var(--mbar,60px));
      padding-bottom:calc(58px + env(safe-area-inset-bottom));
    }
    body.m-heroes .left{min-height:calc(100dvh - var(--mbar,60px) - 52px)}
    body.m-heroes .listwrap{max-height:none}
  }

  /* =================== LEFT PANEL =================== */
  .left{
    background:linear-gradient(#7a4715,#5d3510);
    border-right:4px solid var(--ink);
    box-shadow:inset -4px 0 8px #0005;
    display:flex;flex-direction:column;height:100dvh;   /* ends level with .battle — the two
       columns used to stop 19px apart, leaving a strip of body cream under the roster */
  }
  @media(max-width:940px){.left{height:auto;max-height:none;border-right:0}}

  .bank{margin:5px 10px 5px;padding:6px 14px;text-align:center}
  /* compact single-row resource bar: candles · gloss · wax
     It must NEVER change height. It used to wrap to a second row once candles reached a
     long suffix ("2.06SxVg"), which pushed the whole left column down 40px mid-run — and on
     mobile it is position:fixed with the layout's top padding fed from its measured height
     (engine.js), so a wrap slid the page under the bar. One row, locked height: when the
     numbers outgrow the space the LABELS give way, never the layout. */
  .bank.resbar{display:flex;flex-wrap:nowrap;align-items:center;justify-content:center;
    gap:8px 10px;padding:5px 12px;min-height:50px;container-type:inline-size}
  /* One chip anatomy for all three currencies, hierarchy carried by SIZE, not by framing.
     Candles — the primary currency — was the only one with no chip at all, so the two
     secondary currencies looked like the important ones. It gets the same shell, bigger. */
  .res{display:inline-flex;align-items:center;gap:7px;min-width:0;
    padding:4px 12px;background:var(--surface);border:2.5px solid var(--ink);
    border-radius:11px;box-shadow:2px 2px 0 var(--ink)}
  .resbar .amount{font-size:20px;font-variant-numeric:tabular-nums;white-space:nowrap}
  .resbar .bres{flex:0 1 auto;min-width:0}
  .resbar .bres b{font-variant-numeric:tabular-nums}
  /* Three labels next to three distinct hand-drawn icons is the same fact told twice, and it
     cost ~70px in a 406px bar — enough that a long candle count clipped its own label. The
     PRIMARY currency keeps its word; gloss and faith are icon + number, with their meaning
     carried by the tooltip and by the panel each one opens. Measured: 398px → 333px needed,
     so the bar now has ~70px of headroom for however long the number gets. */
  .resbar .bres .reslab{display:none}
  .res{flex:0 0 auto}                 /* candles is never the item that gives way */
  .bres{flex:0 1 auto}
  /* 380px is measured, not guessed: framed, the three chips need 383px with the "candles"
     label and 337px without. A 390pt phone gives the bar a 366px content box (drops the
     label, 29px spare); the 460px desktop rail gives 406px (keeps it, 23px spare). */
  @container (max-width:380px){ .resbar .reslab{display:none} }
  @container (max-width:300px){ .resbar .amount{font-size:17px} .resbar .uicandle{height:22px} }
  .resbar .uicandle{height:26px}
  .reslab{font-size:10.5px;color:var(--muted-t);font-weight:normal;letter-spacing:0}
  .bank .row{display:flex;align-items:center;justify-content:center;gap:8px}
  .bank .coin{font-size:24px;filter:drop-shadow(0 1px 2px #0006)}
  .bank .amount{
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:24px;color:#fff;
    text-shadow:0 2px 0 #7a4d00,0 0 6px #0006;letter-spacing:1px;
  }
  .bres{
    font-family:var(--font-ui);font-weight:bold;font-size:13px;cursor:pointer;
    display:inline-flex;align-items:center;gap:5px;padding:4px 10px;min-height:30px;
    background:#fff;border:2.5px solid var(--ink);border-radius:10px;color:var(--ink);
    box-shadow:2px 2px 0 var(--ink);transition:transform .06s,box-shadow .06s;
  }
  .bres b{font-family:var(--font-display);font-weight:var(--weight-display);letter-spacing:.3px}
  .bres:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink)}
  .bres:active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}
  /* --- legibility: currency labels, menu sublabels, rug hint --- */
  /* FAQ quick-start block */
  /* language picker — How To Play is the one screen a non-English player must read before
     anything else makes sense, so it is the one screen that is translated. */
  .langbar{display:flex;flex-wrap:wrap;gap:5px;margin:0 0 12px}
  .langbtn{
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:11px;letter-spacing:.3px;
    padding:5px 9px;min-height:30px;cursor:pointer;color:var(--muted-t);
    background:var(--cream2);border:2.5px solid var(--ink);border-radius:8px;box-shadow:2px 2px 0 var(--ink);
  }
  .langbtn:hover{color:var(--ink)}
  .langbtn.on{background:var(--peach);color:var(--ink)}
  .langbtn:focus-visible{outline:3px dotted var(--focus);outline-offset:2px}
  @media(pointer:coarse){.langbtn{min-height:36px;font-size:12px;padding:6px 11px}}
  .quickstart{background:#fff8;border:2px solid var(--ink);border-radius:12px;padding:9px 12px;margin:0 0 6px}
  .quickstart .qs-h{font-family:var(--font-display);font-weight:var(--weight-display);color:var(--accent-t);font-size:14px;margin-bottom:6px}
  .quickstart ol{margin:0;padding-left:20px;display:flex;flex-direction:column;gap:5px}
  .quickstart li{font-size:13.5px;line-height:1.35}
  .rugbtn.doc.twoline{display:flex;flex-direction:column;align-items:center;gap:1px;line-height:1.15}
  .rugbtn.doc.twoline small{font-size:8.5px;font-weight:normal;opacity:.8;letter-spacing:0}
  .rughint{font-size:10.5px;color:var(--muted-t);line-height:1.3;margin:5px 2px 2px;text-align:center}
  .rughint b{color:var(--accent-t)}
  /* --- first-run "how it works" --- */
  .intromodal{max-width:460px}
  .introsteps{margin:2px 0 12px;padding-left:22px;display:flex;flex-direction:column;gap:9px}
  .introsteps li{font-size:14px;line-height:1.4}
  .introgoal{background:#fff8;border:2px dashed var(--peach);border-radius:10px;padding:9px 11px;font-size:13.5px;text-align:center;margin-bottom:12px}
  .introgoal b{color:var(--accent-t)}
  #introGo{width:100%;font-size:15px}
  .bres:focus-visible{outline:3px dotted var(--focus)}

  .tabs{display:flex;gap:4px;margin:0 12px}
  .tab{
    font-family:var(--font-ui);font-weight:bold;font-size:14px;cursor:pointer;
    background:var(--cream2);color:var(--ink);
    border:3px solid var(--ink);border-bottom:0;border-radius:10px 10px 0 0;
    padding:6px 16px 4px;position:relative;top:3px;opacity:.75;
  }
  .tab.on{opacity:1;top:0;padding-bottom:7px;z-index:2}
  .tab:focus-visible{outline:3px dotted var(--focus)}

  .listwrap{
    flex:1;overflow:hidden;display:flex;flex-direction:column;
    background:var(--cream);
    border:3px solid var(--ink);border-radius:0 12px 12px 12px;margin:0 10px 10px;
    position:relative;z-index:1;
  }
  .dpsbar{
    background:linear-gradient(#2b2b2bee,#000e);color:#fff;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:12.5px;padding:5px 12px;line-height:1.25;
    border-bottom:3px solid var(--ink);
  }
  .dpsbar .g{color:var(--cream)} .dpsbar .p{color:var(--peach)}
  .dpsbar .c{color:var(--cream)}   /* the candle rate — testers couldn't tell what a kill was worth */
  /* one slim control row: quantity selector + compact buy-all */
  /* A grid, not a flex row: "⚡ all" swaps to "⚡ 123" as upgrades come into reach, and with
     flex:1 on the quantity group that relabelling resized all four buy buttons underneath
     the cursor. Fixed track for the button, fixed tracks for the quantities. */
  .ctrlrow{display:grid;grid-template-columns:1fr auto;gap:6px;align-items:stretch;
    padding:4px 10px;background:#00000014;border-bottom:2px solid var(--cream3)}
  .qty{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
  .qty button{
    flex:1;font-family:var(--font-display);font-weight:var(--weight-display);font-size:11px;cursor:pointer;
    color:var(--ink);background:#fff;border:2.5px solid var(--ink);border-radius:8px;padding:3px 0;
    box-shadow:2px 2px 0 var(--ink);
  }
  .qty button.on{background:var(--peach);color:var(--ink);transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}
  .qty button:focus-visible{outline:2px dotted var(--focus)}
  .buyups{
    display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;
    min-width:74px;padding:3px 12px;cursor:pointer;   /* min-width: "⚡ all" ↔ "⚡ 123" must not resize the row */
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:11px;color:var(--ink);white-space:nowrap;
    background:var(--fgreen);border:2px solid var(--ink);border-radius:7px;
    box-shadow:2px 2px 0 var(--ink);
  }
  .buyups:not(:disabled):active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}
  .buyups:disabled{filter:grayscale(.7);opacity:.5;cursor:not-allowed}
  .buyups:focus-visible{outline:2px dotted var(--focus)}

  .scrolllist{overflow-y:auto;flex:1;padding:8px 8px 12px}

  /* hero rows: parchment strips */
  /* --port drives BOTH the portrait size and the space reserved for it below. */
  .hrow{
    --port:96px;
    background:var(--surface);
    border:3px solid var(--ink);border-radius:12px;margin-bottom:9px;
    box-shadow:4px 4px 0 var(--ink);padding:10px 11px;position:relative;overflow:hidden;min-height:0;
  }
  /* 15px, not 18: at 380px the roster showed 1.3 cards because every name wrapped to two
     lines and the level added a third. One line for all fifty names, measured. */
  .hname{
    font-family:var(--font-ui);font-weight:bold;font-size:15px;color:var(--ink);
    text-align:center;margin-bottom:1px;padding-right:108px;line-height:1.15;text-wrap:balance;
  }
  /* the level used to sit inline after the name, so any long name pushed "Lvl 180" onto a
     second line and split the number off the word. It is a different KIND of fact from the
     name — it gets its own line and its own weight. Empty (unhired) it collapses to zero. */
  .hname .lv{display:block;color:var(--peach-t);font-size:11.5px;line-height:1.2;letter-spacing:.3px}
  .hdps{font-family:var(--font-num);font-size:12px;font-weight:bold;text-align:center;padding-right:108px}
  .hirebtn{
    display:block;margin-top:6px;cursor:pointer;width:132px;text-align:center;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:16px;color:#fff;
    letter-spacing:.5px;background:var(--navy);
    border:3px solid var(--ink);border-radius:10px;padding:5px 4px 4px 20px;position:relative;
    box-shadow:3px 3px 0 var(--ink);
  }
  .hirebtn small{display:block;font-size:12.5px;font-family:var(--font-ui);font-weight:bold}
  .hirebtn small .c{color:var(--cream)}
  /* second line: what the purchase actually buys you, for the selected quantity */
  .hirebtn small.gain{font-size:11px;color:#dfffe0;text-shadow:0 1px 0 #4a0a2a;margin-top:-1px}
  .hirebtn:disabled{filter:grayscale(.7);opacity:.55;cursor:not-allowed}
  .hirebtn:not(:disabled):active{transform:translateY(1px)}
  .hirebtn:focus-visible{outline:3px dotted var(--focus)}
  /* The portrait is position:absolute, so it never pushes flow content aside: .upgrow spans the
     full width and ran UNDER it as soon as a hero had ~6 upgrades. Reserved here rather than as
     padding on .hrow, because shrinking the whole card wrapped hero names onto three lines.
     Only the boxes with a visible background need the clearance; the name and DPS lines are
     short text that stops well before the portrait. */
  /* A GRID, not a flex row. With flex:1 1 0 the buttons shared the width, so a hero with 7
     passives got 24px squares and a hero with 4 got 44px — the size announced how many upgrades
     the hero happened to have. Five fixed columns make every button the same size on every card;
     the 5 heroes who have 6 or 7 simply wrap onto a second row. */
  .upgrow{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-top:5px;
    /* cap the TRACK, not the button: without this a very wide card gives 60px columns holding
       44px buttons, and the gaps between them go uneven. */
    max-width:calc(5 * 44px + 4 * 5px);
    margin-right:calc(var(--port) + 9px)}
  .hirebtn{margin-right:calc(var(--port) + 9px)}
  /* --surface, not #fff: the icon set is 176 different emoji and several of the pale ones
     (🖱️ ☎️ 📄) vanished on pure white. A colour-emoji stack keeps the glyphs from falling
     back to a monochrome or missing face. */
  /* etait fixe a 38px : a 7 ameliorations la rangee faisait 296px pour 291 disponibles et
     cassait sur une 2e ligne. Elles se partagent maintenant la largeur, avec un plafond pour
     ne pas grossir quand un heros n'en a que 4. */
  .upg{
    width:100%;min-width:0;aspect-ratio:1;height:auto;font-size:19px;cursor:pointer;display:flex;align-items:center;justify-content:center;
    background:var(--surface);border:2.5px solid var(--ink);border-radius:8px;padding:0;
    box-shadow:2px 2px 0 var(--ink);line-height:1;
    font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif;
  }
  .upg .uii{height:1.25em;vertical-align:middle}
  .upg:disabled{filter:grayscale(.8);opacity:.45;cursor:not-allowed}
  .upg.bought{background:#d9efc8;cursor:default;box-shadow:1px 1px 0 var(--ink)}
  /* next upgrade shown ahead of its level gate — greyed preview with a lock badge */
  /* Locked used to be grayscale(1) + opacity .5 + a dashed border + a 11px padlock, which
     added up to an empty grey box that read as a failed image rather than a state. Keep the
     icon legible and let the REQUIRED LEVEL do the talking — that is the actionable fact. */
  .upg.locked{
    filter:grayscale(.55);opacity:.75;cursor:help;position:relative;overflow:visible;
    background:var(--cream2);border-style:dashed;
  }
  .upg.locked::after{
    content:"lv " attr(data-lv);position:absolute;left:50%;bottom:-8px;transform:translateX(-50%);
    background:var(--ink);color:var(--cream);border-radius:5px;padding:0 4px;
    font-family:var(--font-num);font-weight:bold;font-size:9px;line-height:13px;white-space:nowrap;
  }
  .upg:focus-visible{outline:2px dotted var(--focus)}

  /* ---- hero portrait art ---- */
  .hport{
    position:absolute;right:10px;top:50%;transform:translateY(-50%);
    width:var(--port);height:var(--port);border-radius:10px;object-fit:cover;
    border:3px solid var(--ink);box-shadow:2px 2px 0 var(--ink);background:#f7ead0;
  }
  /* emoji fallback for heroes whose portrait art hasn't shipped yet */
  .hportemoji{display:flex;align-items:center;justify-content:center;font-size:72px;
    background:#f7ead0}
  .hport.tap{cursor:pointer;pointer-events:auto;transition:box-shadow .08s}
  .hport.tap:hover{box-shadow:0 3px 10px #0007}
  /* testers never discovered the hero card — the portrait now says it's tappable */
  .hportinfo{
    position:absolute;right:13px;top:calc(50% + 28px);cursor:pointer;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:9.5px;letter-spacing:.3px;color:#fff;
    background:#000000b3;border:2px solid var(--ink);border-radius:6px;padding:2px 6px;
  }
  .hportinfo:hover{background:#000000e6}
  /* enlarged hero card modal */
  .herocardmodal{max-width:440px}
  .hcard-top{display:flex;gap:12px;align-items:flex-start;margin-bottom:12px}
  .hcard-img{width:172px;height:172px;flex:0 0 auto;border-radius:12px;object-fit:cover;
    border:3px solid var(--ink);box-shadow:2px 2px 0 var(--ink);background:#f7ead0;display:block}
  /* the portrait is a button now — strip the UA chrome, keep the art as the whole target */
  .hcard-imgbtn{position:relative;flex:0 0 auto;padding:0;border:0;background:none;
    cursor:zoom-in;line-height:0;border-radius:12px}
  .hcard-imgbtn:focus-visible{outline:3px dotted var(--focus);outline-offset:3px}
  /* same pill as .hportinfo on the roster, so "there is more here" reads the same way twice */
  .hczoom{
    position:absolute;right:7px;bottom:7px;pointer-events:none;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:9.5px;letter-spacing:.3px;
    color:#fff;background:#000000b3;border:2px solid var(--ink);border-radius:6px;padding:2px 6px;
    box-shadow:2px 2px 0 var(--ink);line-height:1.35;
  }
  .hcard-imgbtn:hover .hczoom{background:#000000e6}
  /* the enlarged view: as big as the viewport allows, never bigger than the source */
  .zoommodal{max-width:min(92vw,760px)}
  .zoombody{display:flex;align-items:center;justify-content:center;padding:14px}
  .hzimg{
    /* the source art is 512x512; going past that upscales and goes soft, which defeats the
       point of an enlarge button. */
    display:block;width:auto;height:auto;max-width:min(100%,512px);max-height:min(70vh,512px);
    border:4px solid var(--ink);border-radius:14px;box-shadow:5px 5px 0 var(--ink);background:#f7ead0;
    image-rendering:auto;
  }
  .hcard-emoji{display:flex;align-items:center;justify-content:center;font-size:96px}
  .hcard-meta{flex:1;min-width:0}
  .hcard-name{font-family:var(--font-display);font-weight:var(--weight-display);font-size:18px;line-height:1.1;color:var(--ink);text-wrap:balance}
  .hcard-no{font-size:11.5px;letter-spacing:.5px;color:var(--accent-t);font-weight:bold;margin:3px 0 7px}
  .hcard-fl{font-size:12.5px;line-height:1.35;color:var(--muted-t)}
  .hcard-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:12px}
  .hcstat{display:flex;flex-direction:column;gap:1px;background:#fff8;border:2px solid var(--ink);border-radius:9px;padding:6px 9px}
  .hcstat .hcl{font-size:10px;letter-spacing:.8px;color:var(--muted-t);font-weight:bold}
  .hcstat .hcv{font-family:var(--font-num);font-weight:bold;font-size:15px;color:var(--ink);
    overflow-wrap:anywhere}
  /* --- section headers + the two extra blocks (upgrade ladder, live run) --- */
  .hcsec{font-family:var(--font-display);font-weight:var(--weight-display);font-size:10.5px;letter-spacing:1.4px;text-transform:uppercase;
    color:var(--muted-t);margin:2px 0 7px;display:flex;align-items:center;gap:8px}
  .hcsec::after{content:"";flex:1;height:2px;background:currentColor;opacity:.35;border-radius:2px}
  .hcard-grid.run .hcstat{background:#2b2b2b0d}
  /* the two tiles a player actually decides on: how hard you hit, and what a rug pays now */
  .hcard-grid.lead{margin-bottom:14px}
  .hcstat.lead{background:var(--surface);border:3px solid var(--ink);box-shadow:3px 3px 0 var(--ink);
    border-radius:11px;padding:9px 12px;gap:2px}
  .hcstat.lead .hcl{font-size:10.5px;color:var(--peach-t)}
  .hcstat.lead .hcv{font-family:var(--font-display);font-weight:var(--weight-display);font-size:22px;line-height:1.05}
  .hcups{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}
  .hcup{display:flex;align-items:center;gap:9px;background:#fff8;border:2px solid var(--ink);
    border-radius:9px;padding:5px 9px}
  .hcup.on{background:#dff5d0;border-color:#8fc47a}
  .hcup.off{opacity:.55}
  .hcup .hcui{font-size:20px;flex:0 0 auto;width:24px;text-align:center}
  .hcup .hcun{flex:1;min-width:0;display:flex;flex-direction:column;line-height:1.25}
  .hcup .hcun b{font-size:12.5px;color:var(--ink)}
  .hcup .hcun i{font-size:11px;color:var(--muted-t);font-style:normal}
  .hcup .hcus{flex:0 0 auto;font-family:var(--font-num);font-weight:bold;font-size:11.5px;
    color:var(--muted-t);white-space:nowrap}
  .hcard-hire{width:100%;padding:11px;border:3px solid var(--ink);border-radius:11px;cursor:pointer;
    background:linear-gradient(#ffd9b0,#f7a76b);font-family:var(--font-display);font-weight:var(--weight-display);font-size:15px;color:var(--ink);box-shadow:2px 2px 0 var(--ink)}
  .hcard-hire small{display:block;font-family:var(--font-ui);font-size:12px;color:#1a4a10;margin-top:2px}  /* 4.12:1 → 5.27:1 on the peach button */
  .hcard-hire:not(:disabled):active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}
  .hcard-hire:disabled{filter:grayscale(.7);opacity:.55;cursor:not-allowed}
  .gimgemoji{display:flex;align-items:center;justify-content:center;
    width:34px;height:34px;flex:0 0 auto;font-size:26px}


  /* achievements tab */
  .achv{padding:10px}
  /* the one surface the flat-cartoon reskin never reached: .hrow got --surface + a 3px ink
     border + a sticker shadow, .arow kept the original parchment gradient and a #c9a96a
     hairline. Its dark stop also dragged the achievement name down to 4.32:1. */
  .arow{
    background:var(--surface);border:3px solid var(--ink);border-radius:10px;
    box-shadow:3px 3px 0 var(--ink);
    padding:8px 11px;margin-bottom:10px;font-size:13px;display:flex;gap:9px;align-items:center;
  }
  /* one checkbox drawn in CSS, two states — replaces ✅ vs ⬜, which were different shapes at
     different weights, so "not earned yet" looked like a font that failed to load. */
  .arow .tick{
    flex:0 0 auto;width:20px;height:20px;border:2.5px solid var(--ink);border-radius:6px;
    background:var(--surface);position:relative;
  }
  .arow.done .tick{background:var(--fgreen)}
  .arow.done .tick::after{
    content:"";position:absolute;left:5px;top:1px;width:5px;height:10px;
    border:solid var(--ink);border-width:0 2.5px 2.5px 0;transform:rotate(42deg);
  }
  .arow .am{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
  /* the counter that was missing entirely: how many, and what they are worth */
  .achvhead{
    display:flex;align-items:baseline;justify-content:space-between;gap:10px;
    padding:2px 2px 10px;font-size:13px;color:var(--muted-t);
  }
  .achvhead b{font-family:var(--font-display);font-weight:var(--weight-display);
    font-size:17px;color:var(--ink);font-variant-numeric:tabular-nums}
  .achvhead .ah-b{font-size:11.5px;color:var(--green-t);font-weight:bold}
  /* "slay 1,000 enemies" used to say nothing about the 860 you had already slain */
  .aprog{display:flex;align-items:center;gap:7px;margin-top:4px}
  .aprog .apbar{flex:1;height:7px;border:2px solid var(--ink);border-radius:5px;
    background:var(--surface);overflow:hidden}
  .aprog .apfill{display:block;height:100%;background:var(--peach)}
  .aprog .apn{flex:0 0 auto;font-family:var(--font-num);font-weight:bold;font-size:10.5px;
    color:var(--muted-t);font-variant-numeric:tabular-nums}
  .arow.done{background:#e2f5d4}   /* flat, like every other earned/bought state in the reskin */
  .arow b{color:var(--accent-t)}
  /* on the earned row the green ground already says "earned", so the accent is redundant —
     and at 4.14:1 over that green it was the last AA failure in the app. */
  .arow.done b{color:var(--ink)}
  .hidden{display:none}

  /* rug panel bottom of left */
  .rugbox{margin:0 10px 8px;padding:5px 10px;text-align:center;font-size:12px;color:#3a2508}
  .rugbox .gl{color:var(--accent-t);font-weight:bold}
  .rugbtn{
    margin-top:5px;width:100%;cursor:pointer;padding:7px;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:14px;color:var(--ink);
    background:var(--peach);
    border:3px solid var(--ink);border-radius:10px;box-shadow:3px 3px 0 var(--ink);
  }
  .rugbtn:disabled{filter:grayscale(.75);opacity:.55;cursor:not-allowed}
  .rugbtn:focus-visible{outline:3px dotted #fff}

  /* =================== BATTLE =================== */
  /* the battle column OWNS the viewport height and never exceeds it: the skill bar and the
     corner buttons are time-critical controls (30s skill windows on a boss timer) and used to
     fall below the fold on any laptop under ~700px of usable height. Everything fixed
     (zone chrome, nameplate, skills) is flex:0; the island is the only elastic part. */
  .battle{position:relative;display:flex;flex-direction:column;height:100dvh;min-height:560px;overflow:hidden}
  @media(max-width:940px){.battle{height:auto;min-height:640px}}

  /* clouds */
.bgblur{display:none}
  .cloud{display:none}
  .c1{width:130px;height:44px;top:12%;left:8%}
  .c2{width:90px;height:32px;top:24%;right:14%;opacity:.8}
  .c3{width:170px;height:52px;bottom:26%;left:16%;opacity:.7}
  .c4{width:110px;height:36px;bottom:14%;right:8%;opacity:.85}

  /* zone selector */
  /* The zone PICKER sat top-right while the zone NAME was centred underneath — one concept
     living in two places. Mobile already centred it; desktop now matches, so the whole zone
     block (picker · name · progress pips) reads as one stack. */
  .zonesel{
    align-self:center;margin:12px auto 0;padding:6px 10px;display:flex;align-items:center;gap:8px;z-index:5;
  }
  .zarrow{
    display:flex;align-items:center;justify-content:center;padding:0;   /* l'image etait alignee sur la baseline, donc decentree */
    width:34px;height:34px;font-size:16px;font-weight:bold;cursor:pointer;color:var(--ink);
    background:#fff;border:2.5px solid var(--ink);border-radius:50%;box-shadow:2px 2px 0 var(--ink);
  }
  .zarrow:disabled{opacity:.4;cursor:not-allowed}
  .zarrow:focus-visible{outline:3px dotted var(--focus)}
  .thumbs{display:flex;gap:6px}
  .thumb{
    width:74px;height:44px;position:relative;cursor:default;border-radius:8px;border:2px solid var(--ink);
    background:linear-gradient(var(--cream2),#d6f3ff);background-size:cover;background-position:center;overflow:hidden;
  }
  .thumb .ti{display:none}
  .thumb .tn{
    position:absolute;bottom:1px;right:4px;font-family:var(--font-display);font-weight:var(--weight-display);font-size:12px;color:#fff;
    text-shadow:0 1px 2px #000,0 0 3px #000;
  }
  .thumb.cur{border-color:#2e7d0f;box-shadow:0 0 8px #6f6}
  .thumb.cur::before{content:"⬇";position:absolute;top:-2px;left:50%;transform:translateX(-50%);font-size:11px;color:#fff;text-shadow:0 1px 1px #000}
  .progbtn{
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:10px;cursor:pointer;color:var(--ink);
    background:#fff;border:2.5px solid var(--ink);border-radius:8px;padding:5px 8px;box-shadow:2px 2px 0 var(--ink);
    display:inline-flex;align-items:center;gap:4px;
  }
  /* explicit box, or the 256px source blows the button apart. In FARM the auto-advance is OFF,
     so the icon greys out rather than claiming a state that isn't active. */
  .progbtn .pbi{width:15px;height:15px;object-fit:contain;display:block;flex:0 0 auto;pointer-events:none}
  .progbtn:not(.on) .pbi{filter:grayscale(1);opacity:.5}
  .progbtn.on{background:var(--fgreen)}
  .progbtn:focus-visible{outline:2px dotted var(--focus)}

  .zonetitle{position:relative;z-index:4;
    text-align:center;font-family:var(--font-ui);font-weight:bold;font-size:22px;color:#fff;
    text-shadow:0 2px 0 #2a6a9a,0 0 8px #0004;margin-top:2px;z-index:4;
  }

  /* how many jeets left before the zone opens — 10 pips + a count */
  .zoneprog{position:relative;z-index:4;display:flex;align-items:center;justify-content:center;
    gap:8px;margin-top:3px;font-family:var(--font-ui);font-weight:bold;font-size:12px;
    color:#fff;text-shadow:0 1px 0 #2a6a9a,0 0 6px #0005;letter-spacing:.3px;
  }
  .zoneprog .pips{display:flex;gap:3px}
  .zoneprog .pip{width:9px;height:9px;border-radius:50%;background:#ffffff88;
    border:2px solid var(--ink);box-sizing:border-box}
  .zoneprog .pip.on{background:var(--fgreen);border-color:var(--ink)}
  .zoneprog.boss{color:#ffd9d9}
  .zoneprog.boss .pips{display:none}

  /* island */
  /* ===== THE STAGE =====================================================
     A ROW on desktop: stage column | skill rail. Measured before the change, the island was
     562×314 in a 980×726 column — 25% of its own column, with 418px of dead sky beside it,
     while 384px of stacked chrome (53% of the viewport height) sat above and below it.
     Because the island takes the leftover HEIGHT and derives its width from the aspect
     ratio, every pixel of vertical chrome cost 1.79px of stage width. Standing the skills up
     beside the stage spends 120px of that dead width to buy back 141px of height. */
  .islandzone{flex:1 1 auto;min-height:0;display:flex;flex-direction:row;align-items:stretch;
    justify-content:center;gap:0;position:relative;z-index:3}
  /* flex-basis 0, not auto: with an auto basis the column claims its content width first
     and pushes the rail off the edge of the (overflow:hidden) battle column. */
  .stagecol{flex:1 1 0;min-width:0;display:flex;flex-direction:column;justify-content:flex-start;position:relative}

  /* ===== HUD OVERLAY (desktop) =====
     The zone block and the nameplate leave the column flow and float over the stage, so the
     island stops paying for them in height — and, through the aspect ratio, in width. Both
     land on the empty sky the zone art is composed around, and both already carry the 4-way
     ink outline they needed when they sat on the plain sky gradient. */
  @media(min-width:941px){
    .zonesel,.zonetitle,.zoneprog{position:absolute;z-index:7;pointer-events:none}
    /* The whole zone group moves to the top-LEFT: the centre column of the stage belongs to
       the enemy, and at this stage size the title was landing straight through its head.
       Picker, name and pips stay one block, just no longer on the enemy's axis. */
    .zonesel{left:14px;top:12px;margin:0;pointer-events:auto}
    .zonetitle{left:16px;top:82px;margin:0;text-align:left}
    .zoneprog{left:16px;top:110px;margin:0;justify-content:flex-start}
    /* THE HUD CARD. Sampled against the real zone-3 artwork, the nameplate's --red-t on the
       rock base measured 1.68:1 (it needs 3 at 22px, and 4.5 for the 15px HP line) — it had
       been designed for a flat sky gradient and was now sitting on illustration. The bottom
       scrim I first reached for made it WORSE, not better: darkening a mid-grey rock toward
       ink moves it toward the red's own luminance (~1.06:1). Text cannot be made legible on
       arbitrary art by tinting the art. It gets its own ground: --red-t on --cream is
       5.28:1, and it holds on every zone, forever, with no per-zone tuning. */
    .plate{
      position:absolute;left:50%;transform:translateX(-50%);bottom:14px;z-index:6;
      width:auto;max-width:min(88%,420px);margin:0;pointer-events:none;
      padding:7px 20px 9px;background:var(--cream);
      border:3px solid var(--ink);border-radius:14px;box-shadow:3px 3px 0 var(--ink);
    }
    .plate .mname{text-shadow:none}
    .plate .hpnum{text-shadow:none}
    .plate .bosstimer{color:var(--ink);text-shadow:none}
    .plate .buffline{color:var(--muted-t);text-shadow:none}
    .plate .hpouter{width:100%}
    .island{margin-top:0}
    /* the enemy anchors at 48.5% of a stage that is now twice as tall, which put its head
       straight through the zone title. Nudged down; it stays well inside the platform. */
    .mob{top:53%}

  }
  .island{
    /* the ONLY elastic element in the column: it takes whatever height is left once the
       reserved strips are served, and fills its box. */
    position:relative;flex:1 1 auto;min-height:120px;align-self:stretch;
    width:100%;margin:0;
    /* No frame, and no declared aspect-ratio. Both had stopped being true: a definite flex
       height plus max-width:96% already overrode the 1400/781 ratio (measured 1.53, so the
       art was cropped ~14% anyway), and the frame was cut through by the zone picker at one
       corner and the round buttons at the other — it enclosed nothing. The stage now simply
       fills its box and `cover` crops the art, which is what was happening in practice.
       Measured: 81.2% -> 88% of the column, and the hit area grows with it. */
    background-size:cover;background-position:center;background-repeat:no-repeat;
    pointer-events:auto;cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation;
    /* the WHOLE platform is the attack hit area (beta: the sprite alone was a
       tiny moving target on a phone). The sprite is a child, so its taps bubble here. */
  }
  /* the googly-eye rivets were pinned to the frame's top edge; with no frame they had
     nothing to sit on. The sticker motif still runs on the hire button and the mobile nav. */
  .grass,.rockbase,.minirock,.props{display:none}

  /* zone art is also declared in CSS, keyed on the theme class, so the island
     is never an empty frame if game.js is stale, cached or fails before it
     reaches the inline assignment in spawn(). the inline style JS sets is the
     same url, so it wins harmlessly when JS is healthy. */
  .battle.t0 .island{background-image:url(../assets/zones/zone-0.webp?v=2)}
  .battle.t1 .island{background-image:url(../assets/zones/zone-1.webp?v=4)}
  .battle.t2 .island{background-image:url(../assets/zones/zone-2.webp?v=4)}
  .battle.t3 .island{background-image:url(../assets/zones/zone-3.webp?v=4)}
  .battle.t4 .island{background-image:url(../assets/zones/zone-4.webp?v=4)}
  .battle.t5 .island{background-image:url(../assets/zones/zone-5.webp?v=4)}
  .battle.t6 .island{background-image:url(../assets/zones/zone-6.webp?v=2)}
  .battle.t7 .island{background-image:url(../assets/zones/zone-7.webp?v=4)}
  .battle.t8 .island{background-image:url(../assets/zones/zone-8.webp?v=4)}
  .battle.t9 .island{background-image:url(../assets/zones/zone-9.webp?v=4)}
  .grass{
    position:absolute;top:56px;left:0;right:0;height:96px;border-radius:50%;
    background:radial-gradient(ellipse at 50% 30%,var(--fgreen),var(--fgreend) 80%);
    border:3px solid #3d7a16;box-shadow:inset 0 10px 14px #ffffff44;
  }
  .rockbase{
    position:absolute;top:120px;left:9%;right:9%;height:96px;
    background:linear-gradient(var(--muted-t),var(--ink));
    clip-path:polygon(0 0,100% 0,88% 34%,74% 78%,58% 100%,46% 66%,34% 92%,18% 52%,8% 30%);
    border-radius:0 0 30px 30px;
  }
  .minirock{position:absolute;width:64px;height:34px}
  .minirock .g{position:absolute;top:0;left:0;right:0;height:16px;border-radius:50%;background:linear-gradient(var(--fgreen),var(--fgreend));border:2px solid #3d7a16}
  .minirock .r{position:absolute;top:10px;left:15%;right:15%;height:22px;background:linear-gradient(var(--muted-t),var(--ink));clip-path:polygon(0 0,100% 0,70% 100%,30% 70%)}
  .mr1{left:-46px;bottom:36px;transform:scale(.9)}
  .mr2{right:-52px;bottom:64px;transform:scale(.7)}
  .props{position:absolute;top:18px;left:0;right:0;text-align:center;font-size:40px;pointer-events:none}
  .props span{margin:0 14px;filter:drop-shadow(0 3px 2px #0004)}

.mobimg{height:100%;width:auto;pointer-events:none;display:block;margin:0 auto;
    filter:drop-shadow(0 6px 4px #0005)}
.bossimg{height:126%!important}
  .mob{
    position:absolute;left:50%;top:48.5%;transform:translate(-50%,-100%);z-index:4;pointer-events:auto;
    height:33%;display:flex;align-items:flex-end;justify-content:center;
    border:0;background:none;cursor:pointer;line-height:1;font-size:min(78px,9vw);padding:2px 10px;
    filter:drop-shadow(0 8px 6px #0004);
    transition:transform .05s;-webkit-tap-highlight-color:transparent;touch-action:manipulation;
  }
  .mob:active{transform:translate(-50%,-100%) scale(.93)}
  .mob:focus-visible{outline:4px dotted var(--focus);outline-offset:4px}
  .mob.hurt{animation:shake .1s linear}
  @keyframes shake{25%{transform:translate(calc(-50% - 6px),-100%)}75%{transform:translate(calc(-50% + 6px),-100%)}}
  /* crit punch — the enemy pops bigger on a critical hit */
  .mob.crithit{animation:crithit .18s ease}
  @keyframes crithit{0%{transform:translate(-50%,-100%) scale(1)}40%{transform:translate(-50%,-100%) scale(1.22)}100%{transform:translate(-50%,-100%) scale(1)}}
  /* stage shake + colour flash on boss arrival / defeat */
  /* amplitudes were set against a 562x314 stage; at 1090x682 a 6px nudge is invisible.
     Scaled to the stage so a boss landing still reads as an impact. */
  .stagecol.shk,.islandzone.shk{animation:stgshk .25s ease}
  .stagecol.shkL,.islandzone.shkL{animation:stgshkL .44s ease}
  @keyframes stgshk{10%,90%{transform:translateX(-5px)}30%,70%{transform:translateX(9px)}50%{transform:translateX(-13px)}}
  @keyframes stgshkL{10%,90%{transform:translate(-9px,2px)}30%,70%{transform:translate(15px,-4px)}50%{transform:translate(-23px,4px)}}
  .flashfx{position:absolute;inset:0;pointer-events:none;z-index:6;opacity:.5;mix-blend-mode:screen;border-radius:inherit;animation:flashfx .48s ease forwards}
  @keyframes flashfx{from{opacity:.5}to{opacity:0}}

  /* nameplate + hp */
  .plate{flex:0 0 auto;text-align:center;margin-top:6px;z-index:4}
  .mname{
    font-family:var(--font-ui);font-weight:bold;font-size:19px;color:var(--fred);
    text-shadow:0 1px 0 #fff,0 2px 2px #0005;
  }
  .hpnum{font-family:var(--font-display);font-weight:var(--weight-display);font-size:15px;color:var(--fred);text-shadow:0 1px 0 #fff}
  .hpouter{
    width:min(300px,74vw);height:24px;margin:3px auto 0;position:relative;
    background:#fff;border:3px solid var(--ink);border-radius:12px;overflow:visible;
    box-shadow:2px 2px 0 var(--ink);
  }
  .hpouter::before,.hpouter::after{
    content:"❖";position:absolute;top:-6px;font-size:24px;color:#555;text-shadow:0 1px 1px #000;
  }
  .hpouter::before{left:-16px}.hpouter::after{right:-16px}
  .hpfill{height:100%;border-radius:9px;background:linear-gradient(#a8f56a,var(--fgreen) 55%,var(--fgreend));width:100%;transition:width .07s linear}
  .skull{position:absolute;top:-9px;left:50%;transform:translateX(-50%);height:14px;width:auto;object-fit:contain;filter:grayscale(1);pointer-events:none}
  .bosstimer{
    margin-top:4px;font-family:var(--font-display);font-weight:var(--weight-display);font-size:24px;color:#fff;
    text-shadow:0 2px 0 var(--gold-deep),0 0 8px #0006;
  }
  .bosstimer.low{color:var(--fred);animation:blink .5s steps(2,start) infinite}
  @keyframes blink{to{visibility:hidden}}
  .buffline{font-family:var(--font-display);font-weight:var(--weight-display);font-size:11px;color:#fff;text-shadow:0 1px 0 var(--gold-deep);margin-top:3px;min-height:14px}

  .float{
    position:absolute;z-index:8;font-family:var(--font-display);font-weight:var(--weight-display);color:#fff;font-size:20px;
    pointer-events:none;animation:rise .8s ease-out forwards;text-shadow:0 2px 0 #a33,0 0 4px #0006;
  }
  .float.crit{color:var(--cream);font-size:28px;text-shadow:0 2px 0 var(--gold-deep)}
  .float.gold{color:#fff;font-size:14px;text-shadow:0 2px 0 var(--gold-deep)}
  @keyframes rise{to{transform:translateY(-70px);opacity:0}}

  /* skills */
  /* A vertical rail beside the stage — it stops competing with the island for height.
     Sized from the VIEWPORT height so four skills plus their labels always fit the rail:
     if the rail were ever taller than the stage it would grow .islandzone and push the log
     back out of the viewport, undoing the height lock. */
  .skillbar{flex:0 0 auto;display:flex;flex-direction:column;gap:10px;
    align-items:center;justify-content:center;padding:0 4px;z-index:8;position:relative}
  /* The rail was a COLUMN, and at 76px it was 5% of the viewport against a 68% stage — a
     leftover sliver, not a composition, with 63px skills and 11px labels. It only ever
     existed to stop the horizontal skill bar stealing the stage's height; now that the zone
     block and the nameplate are overlays, the skills can be one too. They join the right-edge
     HUD stack (badges above, round buttons below) and the stage takes the whole column.
     Sampled all five zone artworks over the band they cover: variation 11-25, all sky. */
  @media(min-width:941px){
    .skillbar{
      position:absolute;right:12px;top:96px;bottom:68px;   /* clears the badges and the buttons */
      width:auto;padding:0;justify-content:center;gap:8px;
    }
    .skillbar .skill{width:clamp(58px,11vh,94px);height:clamp(58px,11vh,94px)}
    .skillbar .skicon{width:100%;height:100%}
    /* every zone's right band is LIGHT (luma 131-204), so white-on-outline does not hold —
       the label gets the same cream ground as the rest of the HUD. */
    .skillbar .skhint{
      width:auto;max-width:104px;margin-top:5px;padding:1px 7px;font-size:11px;
      color:var(--ink);text-shadow:none;background:var(--cream);
      border:2px solid var(--ink);border-radius:7px;min-height:0;
    }
  }
  .skwrap{text-align:center}
  .skill{
    width:94px;height:94px;font-size:42px;cursor:pointer;position:relative;padding:0;overflow:visible;
    background:#fff;border:3px solid var(--ink);border-radius:14px;box-shadow:3px 3px 0 var(--ink);
  }
  .skill .cd{position:absolute;inset:0;border-radius:11px;background:conic-gradient(rgba(0,0,0,.7) var(--p,0%),transparent 0);pointer-events:none}
  /* locked: keep the art in FULL COLOUR (just a touch dimmed) with a lock badge — the visuals should always be seen */
  .skill.locked{opacity:.92;cursor:help}
  .skill.locked::after{
    /* etait l'emoji 🔒 ; on utilise le cadenas dessine, comme partout ailleurs */
    content:"";position:absolute;top:-8px;right:-8px;width:20px;height:20px;
    background:url(../assets/ui/ui-lock.webp?v=1) center/contain no-repeat;
    filter:drop-shadow(1px 1px 0 rgba(28,28,28,.45))}
  .skill.active{box-shadow:0 0 14px var(--peach),inset 0 2px 0 #fff7}
  .skill:focus-visible{outline:3px dotted var(--focus)}
  /* "lucky strikes" and "offering plate" wrap to two lines while "click storm" does not, so
     the four labels sat on three different baselines. Reserve both lines for all of them. */
  .skhint{font-family:var(--font-display);font-weight:var(--weight-display);font-size:12px;color:#fff;
    text-shadow:0 1px 2px #000;margin-top:4px;width:92px;
    min-height:2.4em;line-height:1.2;display:flex;align-items:flex-start;justify-content:center;text-align:center}

  /* round corner buttons */
  /* Grouped on one ground instead of five emoji floating loose on the artwork: since the frame
     came off, these sit directly on the zone illustration and their legibility depended on
     whatever was behind them. The label under each one is the fix for "what does 🎵 open?" —
     the aria-labels were already right, the visible affordance was missing. */
  .cornerbtns{
    position:absolute;bottom:10px;right:12px;display:flex;gap:6px;z-index:6;
    padding:6px 8px;border-radius:14px;
    background:rgba(247,239,220,.82);border:2.5px solid var(--ink);box-shadow:2px 2px 0 var(--ink);
  }
  .cornerbtns .rbtn{position:relative}
  .cornerbtns .rbtn::after{
    /* top:100% resolves against the PADDING box, and .rbtn is border-box with a 3px border —
       so calc(100% + 2px) landed 1px ABOVE the button's visible bottom and the label sat on the
       circle. Measured: label top 694 against button bottom 695. The 3px pays back the border,
       the 4px is the actual gap. */
    content:attr(data-lab);position:absolute;left:50%;top:calc(100% + 3px + 4px);transform:translateX(-50%);
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:8px;letter-spacing:.2px;
    color:var(--muted-t);white-space:nowrap;pointer-events:none;
  }
  .cornerbtns{padding-bottom:16px}
  @media(max-width:940px){.cornerbtns .rbtn::after{display:none}}   /* pas de place sous le pouce */
  .rbtn{
    width:44px;height:44px;font-size:19px;cursor:pointer;
    display:flex;align-items:center;justify-content:center;padding:0;
    background:radial-gradient(circle at 35% 30%,var(--cream),var(--peach) 60%,var(--ink));
    border:3px solid var(--ink);border-radius:50%;box-shadow:inset 0 2px 0 #fff7,0 2px 0 #0004;
  }
  /* the corner buttons hold drawn art now. An explicit box is mandatory: a bare <img> renders
     at its natural 256px and bursts the 44px circle. The label ::after is absolutely
     positioned, so it stays out of this flex flow. */
  .rbtn .rbi{width:27px;height:27px;object-fit:contain;display:block;pointer-events:none}
  /* muted has no drawn variant: grey the speaker and strike it through. ::after is already
     the data-lab caption, so the slash uses ::before. */
  #muteBtn.muted .rbi{filter:grayscale(1);opacity:.5}
  #muteBtn.muted::before{
    content:"";position:absolute;left:50%;top:50%;width:30px;height:3px;border-radius:2px;
    background:var(--ink);box-shadow:0 0 0 1.5px #fff9;
    transform:translate(-50%,-50%) rotate(-45deg);pointer-events:none;
  }
  .rbtn:focus-visible{outline:3px dotted var(--focus)}

  /* the log used to float absolutely at bottom-left, straight through the skill icons
     (measured overlap). It is now a reserved strip in the column flow — fixed height, so a
     burst of kill messages can never resize the island or cover a skill. It shows the tail
     of the feed; log() still keeps the last four in the DOM. */
  .log{
    flex:0 0 auto;z-index:6;height:52px;overflow:hidden;   /* 2 lignes + marges — voir log() dans util.js */
    display:flex;flex-direction:column;justify-content:flex-end;align-items:flex-start;
    margin:0 276px 8px 12px;                 /* right gutter clears the 5 corner buttons */
  }
  @media(min-width:941px){
    font-family:var(--font-num);font-size:11px;color:#fff;text-shadow:0 1px 1px #000;
  }
  .log .line{margin-top:2px;background:#0006;border-radius:6px;padding:2px 8px;max-width:100%}
  .log .who{color:#ffd0e6;font-weight:bold}

  textarea.code{
    position:absolute;bottom:64px;right:12px;width:min(420px,80vw);height:56px;z-index:7;
    background:#000c;color:#8f8;font-family:var(--font-num);font-size:11px;
    border:2px solid var(--ink);border-radius:8px;display:none;
  }
  /* Kyle ran the full viewport width, roster column included — he sprinted across the hero
     cards. He now enters at the stage's left edge. Still position:fixed and on <body>, so he
     is still visible on the phone's roster pane, where there is no separate stage. */
  .kyle{
    position:fixed;top:30%;left:-90px;z-index:40;cursor:pointer;border:0;background:none;
    font-size:44px;filter:drop-shadow(0 0 8px var(--peach));animation:cross 8s linear forwards;padding:6px;
  }
  @keyframes cross{to{left:105vw}}
  .kyle:focus-visible{outline:3px dotted var(--focus)}
  @media(min-width:941px){.kyle{left:calc(var(--rail) - 90px)}}

  /* zone themes: sky + island tint */
  .battle.t0{background:linear-gradient(#6f93b0 0%,#c9a98c 55%,#efb87e 100%)}
  .battle.t1{background:linear-gradient(#84c9f2 0%,#aadcf5 60%,#cfeafb 100%)}
  .battle.t2{background:linear-gradient(#e8a069 0%,#f0b585 55%,#f4c99b 100%)}
  .battle.t3{background:linear-gradient(#647f95 0%,#93b0c4 60%,#c3d6e2 100%)}
  .battle.t4{background:linear-gradient(#584a85 0%,#9c6f9e 55%,#e2a8b4 100%)}
  .battle.t5{background:linear-gradient(#2b1b21 0%,#6b2d2e 55%,#601d1d 100%)}
  .battle.t6{background:linear-gradient(#2a3b58 0%,#82b4d0 55%,#82d1e2 100%)}
  .battle.t7{background:linear-gradient(#0c130d 0%,#1c291d 55%,#406139 100%)}
  .battle.t8{background:linear-gradient(#0a1123 0%,#1c3050 55%,#2f4f7c 100%)}
  .battle.t9{background:linear-gradient(#f2ecc6 0%,#bbddb2 55%,#a4e5bb 100%)}

  /* onboarding coach toasts (top-centre, one-time nudges) */
  /* Toasts sit under the zone block, never on top of it. They used to be pinned to the very
     top of the viewport, which was free while the zone picker lived in the top-RIGHT corner;
     centring the picker over its own title put the two in the same place. Measured: the zone
     block ends at 119px on desktop, and at --mbar + ~100px on a phone (the pinned bank bar). */
  #coachBox{position:fixed;top:128px;left:50%;transform:translateX(-50%);
    z-index:9500;display:flex;flex-direction:column;gap:8px;width:min(92%,430px);pointer-events:none}
  @media(max-width:940px){#coachBox{top:calc(var(--mbar,60px) + 100px)}}
  .coach{pointer-events:auto;display:flex;align-items:flex-start;gap:9px;background:#12141af5;color:#f3ead4;
    border:2px solid var(--peach);border-radius:12px;padding:10px 12px;box-shadow:0 6px 20px #0008;
    font-size:14px;line-height:1.4;opacity:0;transform:translateY(-14px);transition:opacity .28s ease,transform .28s ease}
  .coach.in{opacity:1;transform:translateY(0)}
  /* .ci was a text emoji; as an <img> it has no intrinsic size limit and rendered at its
     natural 256px, covering the stage. Size it like the text it replaced. */
  .coach .ci{height:20px;width:auto;object-fit:contain;flex:0 0 auto;align-self:flex-start;margin-top:1px}
  .coach .ct{flex:1}
  .coach .ct b{color:#ff8fbf}
  .coach .cx{background:none;border:none;color:#9a8f78;font-size:16px;cursor:pointer;padding:0 2px;line-height:1}
  .coach .cx:hover{color:#fff}
  @media(prefers-reduced-motion:reduce){
    .bosstimer.low{animation:none}
    .float{animation-duration:.2s}
    .kyle{animation-duration:16s}
    .mob.hurt{animation:none}
    .mob.crithit,.islandzone.shk,.islandzone.shkL,.stagecol.shk,.stagecol.shkL{animation:none}
    .flashfx{display:none}
    .coach{transition:none}
  }

  /* ================================================================
     FONZ RESKIN — flat cartoon chrome matching the character style:
     cream, thick ink outlines, sticker shadows, tracksuit navy,
     peach accents, three-stripe motif.
     ================================================================ */
  /* (the palette now lives in the single :root at the top of this file) */

  /* ---- left column = the tracksuit ---- */
  .left{background:var(--navy2);border-right:4px solid var(--ink);box-shadow:none;position:relative}
  /* The three-stripe tracksuit motif used to run down the panel at right:10px — inside the
     content area, behind the roster (z-index 0 vs 1) and hard against the native scrollbar,
     so all you ever saw was a band of white hatching in the gaps. It reads as a rendering
     artefact, not a motif. Removed here; the stripe motif still carries the brand where it
     actually works — on the hire button (.hirebtn::before) and the mobile nav. */
  .bank{background:var(--cream);border:3px solid var(--ink);border-radius:14px;
    box-shadow:4px 4px 0 var(--ink);padding:10px 14px}
  .bank::before,.bank::after{display:none}
  .bank .amount{color:var(--ink);text-shadow:none;font-family:var(--font-display);font-weight:var(--weight-display)}
  .bres{background:var(--surface);border:2.5px solid var(--ink);box-shadow:2px 2px 0 var(--ink);color:var(--ink)}
  .bres:hover{box-shadow:3px 3px 0 var(--ink)}
  .bres:active{box-shadow:1px 1px 0 var(--ink)}
  #bankGloss b{color:var(--accent-t)}
  #bankWax b{color:var(--gold-t)}        /* 3.20:1 → 4.65:1 */
  .goldframe::before,.goldframe::after{display:none}

  .tab.on{background:var(--cream)}
  /* the square top-left corner exists to weld the list to the "The Cast" tab above it —
     so when renderGates() hides the tab row (early game, before Achievements exist) the
     corner must round back, or it welds to nothing. */
  #tabsRow.gated + .listwrap{border-radius:12px}
  .dpsbar{background:var(--ink);color:#fff;border-bottom:3px solid var(--ink)}
  .dpsbar .g{color:var(--fgreen)} .dpsbar .p{color:var(--peach)}
  .dpsbar .c{color:var(--peach)}
  .qty{background:transparent}   /* border lives on .ctrlrow now */

  /* ---- hero cards ---- */
  .hname{font-family:var(--font-ui);color:var(--ink)}
  .hname .lv{color:var(--peach-t)}       /* 2.98:1 → 4.65:1 */
  .hdps{color:var(--green-t)}            /* 3.77:1 → 4.76:1 */
  .hirebtn::before{ /* three stripes on the sleeve */
    content:"";position:absolute;left:5px;top:4px;bottom:4px;width:9px;
    background:repeating-linear-gradient(90deg,#fff 0 2px,transparent 2px 5px);
    border-radius:2px;opacity:.95;
  }
  .hirebtn small .c{color:var(--peach)}
  .hirebtn small.gain{color:#b7f0a8;text-shadow:none}
  .hportinfo{border-color:var(--ink);box-shadow:2px 2px 0 var(--ink)}
  .hirebtn:not(:disabled):active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--ink)}
  .hirebtn:disabled{filter:grayscale(.6);opacity:.55}
  /* the "next Fonz" placeholder that closes the roster (see renderRoster) — deliberately
     quieter than a real card: dashed, no shadow, nothing to press. */
  .hrow.ghost{
    min-height:0;display:flex;align-items:center;gap:12px;padding:12px 14px;
    background:#00000008;border:3px dashed var(--cream3);box-shadow:none;
  }
  .hrow.ghost .gq{
    flex:0 0 auto;width:52px;height:52px;border-radius:10px;border:3px dashed var(--cream3);
    display:flex;align-items:center;justify-content:center;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:24px;color:var(--cream3);
  }
  .hrow.ghost .gt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
  .hrow.ghost .gh{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
  .hrow.ghost .gh b{font-family:var(--font-ui);font-size:14px;color:var(--muted-t)}
  .hrow.ghost .gr{font-size:12px;line-height:1.35;color:var(--muted-t)}
  .hrow.ghost .gc{
    flex:0 0 auto;font-family:var(--font-num);font-weight:bold;
    font-size:11px;color:var(--muted-t);white-space:nowrap;
  }


  .rugbox{background:var(--cream);color:var(--ink)}
  .rugbox .gl{color:var(--peach-t)}
  .rugbtn:not(:disabled):hover{transform:none}
  .rugbtn:not(:disabled):active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--ink)}

  /* ---- battle chrome ---- */
  .zonesel{background:var(--cream);border:3px solid var(--ink);border-radius:14px;box-shadow:4px 4px 0 var(--ink)}
  .zarrow:not(:disabled):active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}
  .thumb{border:2.5px solid var(--ink);border-radius:8px}
  .thumb.cur{border-color:var(--ink);box-shadow:0 0 0 3px var(--fgreen)}
  .thumb.cur::before{color:#fff}
  .zonetitle{font-family:var(--font-ui);font-weight:bold;color:#fff;
    text-shadow:2px 2px 0 var(--ink),-1px -1px 0 var(--ink),1px -1px 0 var(--ink),-1px 1px 0 var(--ink)}
  .zoneprog{color:#fff;
    text-shadow:1.5px 1.5px 0 var(--ink),-1px -1px 0 var(--ink),1px -1px 0 var(--ink),-1px 1px 0 var(--ink)}
  .zoneprog.boss{color:var(--peach)}

  .mname{font-family:var(--font-ui);font-weight:bold;font-size:22px;color:var(--fred);
    text-shadow:1.5px 1.5px 0 #fff,-1px -1px 0 #fff,1px -1px 0 #fff,-1px 1px 0 #fff}
  .hpnum{color:var(--red-t);text-shadow:1px 1px 0 #fff}   /* 3.65:1 → 5.28:1 */
  .hpouter::before,.hpouter::after{color:var(--ink);text-shadow:none}
  .hpfill{background:linear-gradient(#a8e07f,var(--fgreen) 55%,var(--fgreend))}
  .bosstimer{color:#fff;text-shadow:2px 2px 0 var(--ink),-1px -1px 0 var(--ink),1px -1px 0 var(--ink),-1px 1px 0 var(--ink)}
  .buffline{color:#fff;text-shadow:1px 1px 0 var(--ink)}

  .skill.active{box-shadow:0 0 0 4px var(--peach),3px 3px 0 var(--ink)}
  .skill.locked{opacity:.92}
  .skhint{color:#fff;text-shadow:1px 1px 0 var(--ink),0 0 3px var(--ink)}

  .rbtn{background:var(--cream);border:3px solid var(--ink);border-radius:50%;box-shadow:2px 2px 0 var(--ink)}
  .log .line{background:var(--ink);color:#fff;border-radius:8px}
  .log .who{color:var(--peach)}
  textarea.code{background:var(--ink);color:var(--fgreen);border:3px solid var(--ink);border-radius:10px}

  /* full ink outline, not just a drop shadow: white damage numbers were sitting on the pale
     zone skies (t2 peach, t4 pink) with nothing separating them from the background. */
  .float{text-shadow:2px 2px 0 var(--ink),-1px -1px 0 var(--ink),1px -1px 0 var(--ink),-1px 1px 0 var(--ink)}
  .float.crit{color:var(--peach)}
  .float.gold{color:#fff}


  /* ================= TOUCH TARGETS =================
     Sized by INPUT DEVICE, not by width. This bump used to live only inside
     @media(max-width:940px), so an iPad at 1024 or 1366 got the desktop chrome —
     27px quantity buttons, 34px zone arrows, 32px tabs — under a finger. */
  @media(pointer:coarse){
    .zarrow{width:44px;height:44px;font-size:17px}
    .progbtn{min-height:44px;font-size:11px;padding:9px 12px}
    .rbtn{width:48px;height:48px;font-size:22px}
    .rbtn .rbi{width:30px;height:30px}
    .upg{font-size:21px}   /* la taille vient de la grille — voir .upgrow */
    .hirebtn{min-height:46px}
    .bres{min-height:44px;padding:6px 14px;font-size:14px}
    .docbuy{min-height:44px;padding:9px 12px}
    .modal .x{width:44px;height:44px;font-size:20px}
    .tab{min-height:44px;padding:9px 16px 7px}
    .qty button{min-height:44px;font-size:12px}
    .buyups{min-height:44px;padding:3px 16px;font-size:12px}
    .mnavb{min-height:56px}
    .hportinfo{min-height:28px;display:flex;align-items:center;font-size:10.5px}
    .coach .cx{min-width:44px;min-height:44px}
  }

  /* ---- the squeezed desktop band, 941-1150px ----
     Between the mobile breakpoint and a comfortable desktop the stage column is narrow enough
     that the centred enemy reaches the top-left HUD and the centred nameplate reaches the
     bottom-right corner buttons. Measured overlaps before this block: the progress row ran
     79px under the sprite at 941, 38px at 1024, 10px at 1080; the plate crossed the corner
     buttons by 120x7 at 941 and 51x7 at 1080. Nothing here changes the art or the wide
     desktop composition — it only reflows the two HUD blocks that collide. */
  @media(min-width:941px) and (max-width:1200px){
    /* pips over the count instead of beside it: 233px wide becomes ~117px, which clears the
       sprite at every width in the band without dropping the "N jeets to zone X" label. */
    .zoneprog{flex-direction:column;align-items:flex-start;gap:2px}
  }
  @media(min-width:941px) and (max-width:1200px){
    /* The TITLE is the wider half of this problem, and testing it with "The Streets" hid that:
       at the real longest names it ran 117px into the sprite at 941 and still 47px at 1080,
       clearing only past ~1192. Cap it against the sprite's own edge (50% of the column minus
       half the widest boss minus a gutter) and let it wrap; the pips move down to make room
       for the second line. Smaller type so the long names need two lines, not three. */
    .zonetitle{max-width:calc(50% - 162px);font-size:17px;line-height:1.15;white-space:normal}
    .zoneprog{top:150px}
  }
  @media(min-width:941px) and (max-width:1150px){
    .plate{bottom:38px}          /* clears the corner buttons, which top out 7px above it */
  }

  /* ================= MOBILE ================= */
  @media(max-width:940px){
    .zarrow{width:44px;height:44px;font-size:17px}
    .progbtn{font-size:11px;padding:9px 12px}
    .rbtn{width:48px;height:48px;font-size:22px}
    .rbtn .rbi{width:30px;height:30px}
    .upg{font-size:21px}   /* la taille vient de la grille — voir .upgrow */
    .hirebtn{min-height:46px}
    .bres{min-height:40px;padding:6px 14px;font-size:14px}
    .docbuy{min-height:40px;padding:9px 12px}
    .modal .x{width:44px;height:44px;font-size:20px}
    /* notch and home-indicator: viewport-fit=cover means we own the insets */
    .battle{padding-bottom:calc(6px + env(safe-area-inset-bottom))}
    .left{padding-bottom:env(safe-area-inset-bottom)}
    .zonesel{margin-top:calc(8px + env(safe-area-inset-top))}
    .modal{
      padding-bottom:env(safe-area-inset-bottom);
      max-height:calc(100dvh - 24px);      /* dvh follows the collapsing url bar */
    }
    .overlay{padding:12px 10px calc(12px + env(safe-area-inset-bottom))}
    .left::after{display:none}
    .zonesel{align-self:center;margin:8px auto 0;padding:5px 8px;gap:6px}
    .thumb{width:58px;height:36px}
    /* The 44px arrows and progbtn are set at the top of this same block. A later compaction
       pass re-declared them at 30x30 / 9px here, which simply won the cascade and undid the
       touch target on EVERY phone — the intent was in the file twice and the accident was
       last. Only the type scale belongs here; the box stays 44. */
    .zarrow{font-size:15px}
    /* min-height repeated on purpose: the 44px floor otherwise lives only in the
       @media(pointer:coarse) block, so it depended on the pointer probe alone. */
    .progbtn{font-size:10px;min-height:44px}
    .zonetitle{font-size:17px;margin-top:4px}
    .zoneprog{font-size:11px;gap:6px;margin-top:2px}
    .zoneprog .pip{width:8px;height:8px}
    /* on a phone the battle pane is height:auto, so the stage has no leftover height to take
       — the aspect ratio is its only source of one. Full width, uncropped art, still no frame. */
    /* 26% of a phone screen for the thing the whole game asks you to tap. The art is
       background-size:cover, so a taller box crops rather than stretches — the aspect-ratio
       stays as the floor and min-height lifts it on tall screens. */
    .island{width:100%;flex:0 0 auto;align-self:auto;aspect-ratio:1400/781;margin-top:8px;
            min-height:min(30dvh,300px)}
    /* the side rail is a desktop move — a phone has no spare width to stand it up in */
    .islandzone{flex-direction:column;gap:0}
    .stagecol{flex:0 0 auto}
    .skillbar{flex-direction:row;gap:10px;padding:10px 4px 4px;flex-wrap:wrap}
    .skill{width:80px;height:80px;border-radius:12px}
    .skicon{border-radius:9px}   /* bouton mobile : rayon 12px moins la bordure */
    .skhint{width:70px;font-size:10px}
    .mname{font-size:18px}
    .hpouter{width:min(280px,80vw)}
    .log{height:auto;max-height:56px;margin:8px 3% 0;align-items:center}
    .cornerbtns{position:static;justify-content:center;margin:6px auto 12px}
    textarea.code{position:static;display:block;margin:6px auto;width:94%}
    .battle{min-height:0;padding-bottom:6px}
    .layout{grid-template-columns:1fr}
    .hrow{--port:104px}   /* la reservation suit automatiquement */
    .hportinfo{right:11px;top:calc(50% + 30px);font-size:8.5px;padding:1px 4px}
    .hname,.hdps{padding-right:118px}
    .hrow{min-height:140px}   /* the LVL UP button carries a second line (the gain) now */
    .hirebtn{width:136px;font-size:14px}
    .bank .amount{font-size:22px}
    /* hub tabs: keep all four on one row on narrow screens */
    .hubmodal{max-width:100%}
    .hubtabs{gap:4px;padding:8px 8px 0}
    .hubtab{font-size:10px;padding:7px 2px;letter-spacing:-.2px;box-shadow:1px 1px 0 var(--ink)}
    .modal .body{padding:12px 12px}
    .connchip{font-size:12px;padding:10px;margin-bottom:6px}
    /* --- mobile declutter: shrink the chrome so the HEROES get the room --- */
    .bank{margin:6px 10px 4px;padding:6px 12px}
    .bank .row{gap:6px}
    .bank .coin,.uicandle{width:30px;height:30px}
    .uicandle.sm{width:14px;height:14px}
    .bres{padding:5px 10px}
    .dpsbar{font-size:12px;padding:6px 12px}
    .ctrlrow{padding:5px 10px}
    .qty button{padding:7px 2px;font-size:12px}
    .buyups{padding:7px 12px;font-size:12px}
    .rughint{font-size:9.5px;margin:3px 4px 2px;line-height:1.25}
    .rugbtn.doc.twoline{flex-direction:row;gap:5px;padding:8px 4px;font-size:10px}
    .rugbtn.doc.twoline small{display:none}   /* the intro + FAQ explain these; keep the buttons compact here */
    .rugbox{padding:8px 10px}
  }

  /* ================= TABLET (620–940px) =================
     This band gets the phone layout — one column, bottom nav — because the desktop grid
     needs 460px of rail plus a stage. On a 768pt tablet that left a hero card stretched
     across the full width and the roster reading as one very sparse column. Two columns
     use the width the phone layout was never designed to have. */
  @media(min-width:620px) and (max-width:940px){
    #heroList{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 12px;align-content:start}
    #achvList{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 10px;align-content:start}
    .island{max-width:min(96%,760px)}
    .log{margin-left:auto;margin-right:auto;max-width:760px}
    .connchip,.listwrap,.rugbox{max-width:900px;margin-left:auto;margin-right:auto}
  }


  /* ---- modals (save + FAQ) ---- */
  .overlay{
    position:fixed;inset:0;background:rgba(28,28,28,.55);z-index:400;
    display:none;align-items:center;justify-content:center;padding:16px;
  }
  .overlay.open{display:flex}
  .modal{
    background:var(--cream);border:4px solid var(--ink);border-radius:16px;
    box-shadow:6px 6px 0 var(--ink);max-width:640px;width:100%;max-height:86vh;
    display:flex;flex-direction:column;overflow:hidden;
  }
  .modal header{
    display:flex;align-items:center;justify-content:space-between;
    background:var(--navy);color:#fff;padding:10px 16px;border-bottom:3px solid var(--ink);
    font-family:var(--font-ui);font-weight:bold;font-size:18px;
  }
  /* Modal headers carry two kinds of icon: drawn art (.uii.b — Doctrine, Pass, Missions) and
     system emoji (.mh — Save, FAQ, $FONZ, Shrine, hero card, Stats, intro). They cannot be one
     set without four new assets, but they CAN share one box, so the row is at least metrically
     consistent instead of each glyph sitting at whatever height its font decides.
     TODO(art): shrine · hub · save · stats · faq · hero-card icons, then drop .mh. */
  .modal header .mh{
    display:inline-flex;align-items:center;justify-content:center;
    width:1.5em;height:1.5em;font-size:inherit;line-height:1;flex:0 0 auto;
    font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif;
  }
  /* space-between with three items (icon · title · ✕) centred the title — which is why the
     three drawn-icon modals (Doctrine, Pass, Missions) already read differently from the
     seven emoji ones. Icon and title are one group at the left; the ✕ is pushed right. */
  .modal header{justify-content:flex-start;gap:8px}
  .modal header .x{margin-left:auto}
  .modal header .x{
    width:34px;height:34px;border-radius:50%;background:#fff;border:2.5px solid var(--ink);
    color:var(--ink);font-family:var(--font-display);font-weight:var(--weight-display);font-size:15px;cursor:pointer;
    box-shadow:2px 2px 0 var(--ink);
  }
  .modal header .x:active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}
  .modal .body{padding:14px 18px;overflow-y:auto;color:var(--ink);font-size:15px;line-height:1.5}
  /* the balance you cannot have yet: say so, do not print a dash */
  .w3empty{color:var(--muted-t);font-size:13.5px}
  /* "available gloss" is the one number you need while you are spending it, and it used to
     scroll away with the explainer above the list. */
  .stickybar{
    position:sticky;top:-14px;z-index:2;margin:0 -18px 10px;padding:8px 18px;
    background:var(--cream);border-bottom:2px solid var(--cream3);
  }
  /* $FONZ hub: single modal, one row of tabs (Connect / Burn / Leaderboard / Stats) */
  .hubmodal{max-width:540px}
  .hubtabs{display:flex;gap:6px;padding:10px 14px 0}
  .hubtab{flex:1;padding:8px 4px;border:3px solid var(--ink);border-radius:10px;background:#efe4c8;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:12px;color:var(--ink);cursor:pointer;box-shadow:2px 2px 0 var(--ink);white-space:nowrap}
  .hubtab.on{background:var(--peach);color:var(--ink)}
  .hubtab:not(.on):hover{background:#f4e9cf}
  .hubtab:active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}
  .hubpanel .w3sec:first-child{margin-top:2px}
  /* persistent connection chip (always visible, top of the left column) → opens the hub */
  /* was margin:0 — edge-to-edge against the panel while .bank/.listwrap below it sat inset
     10px, so the very first element in the column broke the column's own left edge. */
  .connchip{display:block;width:auto;margin:8px 10px 5px;padding:8px 10px;border:3px solid var(--ink);border-radius:12px;
    background:linear-gradient(#ffd9b0,#f7a76b);font-family:var(--font-display);font-weight:var(--weight-display);font-size:13px;color:var(--ink);
    cursor:pointer;box-shadow:2px 2px 0 var(--ink);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .connchip:active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}
  .connchip b{color:var(--accent-t)}
  .connchip.half{background:linear-gradient(#fff0d0,#ffe0a0)}
  .connchip.done{background:linear-gradient(#c8f0d2,#8fe0a8)}
  .connchip.done b{color:var(--green-t)}
  @keyframes connpulse{0%,100%{box-shadow:2px 2px 0 var(--ink)}50%{box-shadow:2px 2px 0 var(--ink),0 0 0 5px rgba(247,167,107,.45)}}
  .connchip:not(.half):not(.done){animation:connpulse 2.2s ease-in-out infinite}   /* draw the eye only when disconnected */
  @media (prefers-reduced-motion:reduce){.connchip{animation:none!important}}

  /* 14 titres de meme poids ne disent pas par ou commencer : trois familles les ordonnent */
  .modal .faqgrp{
    font-family:var(--font-display);font-weight:var(--weight-display);
    font-size:10px;letter-spacing:1.3px;text-transform:uppercase;color:var(--muted-t);
    margin:9px 0 1px;display:flex;align-items:center;gap:8px;
  }
  .modal .faqgrp::after{content:"";flex:1;height:2px;background:var(--cream3);border-radius:2px}
  .modal .faqgrp:first-of-type{margin-top:4px}

  /* ===== disclosure sections =====
     The FAQ was 14 h3 sections of dense prose in one scroll — a design document inside a
     modal, with the quick start (the only part a new player needs) buried at the top of a
     3000px column. The Pass stacked four explainer cards above its own tier list. Same
     component for both: the headings become the index, and you open what you need. */
  .modal details.fold{border-bottom:2px solid var(--cream3)}
  .modal details.fold:last-child{border-bottom:0}
  .modal details.fold > summary{
    list-style:none;cursor:pointer;padding:9px 26px 9px 2px;position:relative;
    font-family:var(--font-ui);font-weight:bold;font-size:15px;color:var(--ink);
  }
  .modal details.fold > summary::-webkit-details-marker{display:none}
  .modal details.fold > summary::after{
    content:"";position:absolute;right:6px;top:50%;width:8px;height:8px;
    border:solid var(--muted-t);border-width:0 2.5px 2.5px 0;
    transform:translateY(-70%) rotate(45deg);transition:transform .18s ease;
  }
  .modal details.fold[open] > summary::after{transform:translateY(-25%) rotate(-135deg)}
  .modal details.fold > summary:hover{color:var(--peach-t)}
  .modal details.fold > summary:focus-visible{outline:3px dotted var(--focus);outline-offset:2px;border-radius:6px}
  .modal details.fold .foldb{padding:0 2px 12px}
  .modal details.fold .foldb > :first-child{margin-top:0}
  @media(prefers-reduced-motion:reduce){.modal details.fold > summary::after{transition:none}}
  /* in the Pass the folds are cards, matching the blocks around them */
  #bpBody details.fold{
    background:#f4f0e2;border:3px solid var(--ink);border-radius:12px;
    box-shadow:3px 3px 0 var(--ink);margin-bottom:9px;padding:0 11px;
  }
  #bpBody details.fold > summary{font-size:13px;padding:9px 24px 9px 0}
  #bpBody details.fold .foldb{font-size:12.5px;line-height:1.45;padding-bottom:10px}

  .modal h3{
    font-family:var(--font-ui);font-size:16px;margin:14px 0 4px;color:var(--ink);
    border-bottom:2px solid var(--cream3);padding-bottom:2px;
  }
  .modal h3:first-child{margin-top:0}
  .modal p{margin:4px 0 8px}
  /* every emphasised word in the FAQ, the Pass and the hub runs through this one rule —
     at --peachd it was 2.98:1, i.e. the whole emphasis layer of the game failed AA. */
  .modal b{color:var(--peach-t)}
  .modal .kbd{background:#fff;border:2px solid var(--ink);border-radius:6px;padding:0 6px;
    font-family:var(--font-num);font-weight:bold;font-size:13px;box-shadow:1px 1px 0 var(--ink)}

  /* ===== Save & Load =====
     A status line, then the one thing we recommend, then everything else folded. */
  .saveauto{
    background:#e2f5d4;border:3px solid var(--ink);border-radius:12px;box-shadow:3px 3px 0 var(--ink);
    padding:9px 12px;margin-bottom:11px;font-size:13.5px;line-height:1.4;
  }
  .saverec{
    background:linear-gradient(#fff6e8,#ffeccf);border:3px solid var(--ink);border-radius:12px;
    box-shadow:3px 3px 0 var(--ink);padding:11px 13px;margin-bottom:14px;
  }
  .saverec-h{font-family:var(--font-display);font-weight:var(--weight-display);font-size:14px;
    color:var(--peach-t);margin-bottom:5px}
  .saverec p{margin:0 0 9px;font-size:13.5px;line-height:1.45}
  .saverec .mbtn{margin:0}
  /* the destructive fold reads as dangerous before it is opened */
  .modal details.fold.danger > summary{color:var(--red-t)}
  .modal details.fold.danger[open]{background:#fff4f2;border-radius:10px;padding:0 8px;margin:0 -8px}

  .savearea{
    width:100%;height:84px;background:#fff;color:var(--ink);border:3px solid var(--ink);border-radius:10px;
    font-family:var(--font-num);font-size:11px;padding:8px;resize:none;
  }
  .mbtn{
    display:inline-block;margin:8px 8px 0 0;padding:9px 18px;cursor:pointer;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:14px;color:#fff;
    background:var(--navy);border:3px solid var(--ink);border-radius:10px;box-shadow:3px 3px 0 var(--ink);
  }
  .mbtn.peach{background:var(--peach);color:var(--ink)}
  .mbtn.green{background:var(--fgreen);color:var(--ink)}
  .mbtn.reset{background:var(--red-t);color:#fff}   /* white on --fred was 4.18:1 */
  /* on-brand confirm dialog — sits above any open modal */
  .overlay.confirmov{z-index:500}
  .confirmmodal{max-width:430px}
  .confirmmodal .body{font-size:15px}
  .confirmmodal #confirmBody{color:var(--ink)}
  .confirmmodal #confirmBody b{color:var(--peachd)}
  .confirmbtns{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap;margin-top:16px}
  .confirmbtns .mbtn{margin:0}
  .mbtn:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--ink)}
  /* keyboard focus: dedicated for modal buttons + a fallback for any button
     without its own focus style (a11y — every interactive control shows focus) */
  .mbtn:focus-visible,.modal .x:focus-visible{outline:3px dotted var(--focus);outline-offset:2px}
  button:focus-visible{outline:3px dotted var(--focus);outline-offset:2px}
  .savenote{font-size:13px;color:var(--muted-t);margin-top:6px}


  /* ================= LANDING ================= */
  /* justify-content:center + overflow:hidden meant that on any short viewport (a 1280x620
     laptop, a phone in landscape) the two links under the button were simply cut off the
     bottom, with no scroll to reach them. It centres when there is room and scrolls when
     there is not. The decorations stay clipped on purpose — they are decoration. */
  .landing{
    position:fixed;inset:0;z-index:300;display:flex;flex-direction:column;
    align-items:center;gap:14px;text-align:center;
    /* `safe center` and not plain `center`: once the content is taller than the viewport,
       centred flex content overflows past the START edge, and the scroll container cannot
       reach it — the logo would be unreachable above the scroll origin. */
    justify-content:safe center;
    background:linear-gradient(#5e7f9c 0%,#c9a98c 55%,#f0b47c 100%);
    background-size:cover;background-position:center;
    padding:20px;overflow-x:hidden;overflow-y:auto;
  }
  .l-logo,.l-mascot,.l-start,.l-sub,.l-faq{flex:0 0 auto}
  .landing.closed{display:none}
  /* the h1 is purely structural — it must not add margin or change the flex layout */
  .l-h1{margin:0;padding:0;font-size:0;line-height:0;flex:0 0 auto;display:block}
  .l-logo{
    width:min(560px,88vw);height:auto;
    filter:drop-shadow(0 6px 0 rgba(28,28,28,.25));
    animation:bob 3.4s ease-in-out infinite;
  }
  .l-mascot{
    height:min(34vh,330px);width:auto;margin-top:-6px;
    filter:drop-shadow(4px 6px 0 rgba(28,28,28,.55));
    animation:bob 3s ease-in-out infinite .4s;
  }
  @keyframes bob{50%{transform:translateY(-10px)}}
  .l-start{
    margin-top:4px;padding:18px 44px;cursor:pointer;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:clamp(20px,3.4vw,30px);letter-spacing:1px;color:var(--ink);
    background:var(--peach);border:4px solid var(--ink);border-radius:16px;
    box-shadow:6px 6px 0 var(--ink);animation:pulse 1.6s ease-in-out infinite;
  }
  .l-start:active{transform:translate(3px,3px);box-shadow:2px 2px 0 var(--ink);animation:none}
  @keyframes pulse{50%{transform:scale(1.045)}}
  /* at 390px this broke as "break resistance · hire the cast ·" / "never sell", leaving the
     separator dangling at the end of the line. balance evens the two lines out. */
  .l-sub{
    font-family:var(--font-num);font-weight:bold;font-size:14px;color:#fff;
    text-shadow:1.5px 1.5px 0 var(--ink);letter-spacing:1px;
    max-width:34ch;text-wrap:balance;line-height:1.45;
  }
  /* were 13px underlined text with an ~18px hit box — under the thumb minimum */
  .l-faq{
    background:none;border:0;cursor:pointer;font-family:var(--font-num);font-weight:bold;
    font-size:13px;color:#fff;text-decoration:underline;text-shadow:1px 1px 0 var(--ink);
    min-height:44px;padding:0 14px;display:inline-flex;align-items:center;
  }
  .l-faq:focus-visible{outline:3px dotted var(--focus);outline-offset:2px;border-radius:8px}
  .l-deco{position:absolute;pointer-events:none;filter:drop-shadow(2px 3px 0 rgba(28,28,28,.4))}
  .l-deco.d1{left:7%;bottom:18%;height:90px;animation:bob 3.6s ease-in-out infinite .2s}
  .l-deco.d2{right:8%;top:16%;height:100px;animation:bob 4.2s ease-in-out infinite .8s}
  .l-deco.d3{left:12%;top:14%;height:70px;animation:bob 3.2s ease-in-out infinite 1.1s}
  .l-deco.d4{right:10%;bottom:16%;height:74px;animation:bob 4.6s ease-in-out infinite .5s}
  @media(max-width:940px){
    .l-deco.d3,.l-deco.d4{display:none}
    .l-deco.d1{height:56px;left:3%;bottom:3%}
    .l-deco.d2{height:64px;right:3%;top:7%}
    .l-mascot{height:min(28vh,240px)}
  }
  @media(prefers-reduced-motion:reduce){.l-mascot,.l-start,.l-logo,.l-deco{animation:none}}


  /* ---- web3 / $FONZ ---- */
  .w3sec{background:var(--surface);border:3px solid var(--ink);border-radius:12px;box-shadow:3px 3px 0 var(--ink);padding:10px 14px;margin-bottom:12px}
  .w3sec h4{margin:0 0 6px;font-family:var(--font-ui);font-size:15px;color:var(--ink)}
  .w3badge{display:inline-block;font-family:var(--font-display);font-weight:var(--weight-display);font-size:12px;background:var(--fgreen);border:2.5px solid var(--ink);border-radius:8px;padding:4px 10px;margin:4px 6px 0 0;box-shadow:2px 2px 0 var(--ink)}
  .w3badge.fire{background:var(--peach)}
  .w3num{font-family:var(--font-display);font-weight:var(--weight-display);font-size:17px;color:var(--ink)}
  .burnrow{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}
  .burninput{flex:1;min-width:140px;font-family:var(--font-num);font-weight:bold;font-size:15px;padding:8px 10px;background:#fff;border:3px solid var(--ink);border-radius:10px;color:var(--ink)}
  .burnquote{font-size:14px;margin-top:8px;min-height:18px;color:var(--ink);line-height:1.35}
  .burnquote b{color:var(--green-t)}
  .burnquote .qdim{color:var(--muted);font-weight:normal;font-size:12px}
  .burnwarn{font-size:12.5px;color:var(--red-t);font-weight:bold;margin-top:6px}
  .lbrow{display:flex;align-items:center;gap:8px;padding:5px 8px;border-bottom:2px solid var(--cream3);font-family:var(--font-num);font-weight:bold;font-size:13px}
  .lbrow:last-child{border-bottom:0}
  .lbrow .rk{width:26px;font-family:var(--font-display);font-weight:var(--weight-display)}
  .lbrow .ad{flex:1}
  .lbrow .ad a{color:var(--ink)}
  .lbrow .amt{color:var(--peachd)}
  .lbrow .sh{width:52px;text-align:right;color:var(--green-t)}
  .lbrow .lbz{color:#9a6b10;font-weight:normal;font-size:11px}
  .lbrow.me{background:#ffe9df;border-radius:8px}
  /* season prize pool */
  .w3sec.season{background:linear-gradient(#fff5e6,#ffe9c9);border-color:var(--peach)}
  .w3sec.season h4{color:var(--accent-t)}
  .lbpool{text-align:center;margin:2px 0 8px}
  .lbpool .lbamt{font-family:var(--font-display);font-weight:var(--weight-display);font-size:34px;line-height:1;color:var(--peach-t);text-shadow:2px 2px 0 #fff}
  .lbpool .lbsub{font-size:12.5px;color:var(--muted-t);margin-top:3px}
  /* your personal season panel: rank / burns / zone / projected prize + climb CTA */
  .lbyou{margin:0 0 8px}
  .lbyou:empty{display:none}
  .lbyou-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:7px}
  .lbstat.wide{grid-column:1 / -1}   /* the multiplier split made this five stats, not four */
  .lbstat{display:flex;flex-direction:column;gap:1px;background:#fff9;border:2px solid var(--accent-t);border-radius:9px;padding:6px 9px}
  .lbstatl{font-size:10.5px;letter-spacing:1px;color:var(--muted-t);font-weight:bold}
  .lbstatv{font-family:var(--font-display);font-weight:var(--weight-display);font-size:19px;line-height:1.05;color:var(--accent-t)}
  .lbstatv small{font-family:var(--font-num);font-size:12px;color:var(--muted-t)}
  .lbyou-cta{font-size:13.5px;color:var(--ink);background:#fff8;border:2px dashed var(--peach);border-radius:9px;padding:7px 9px;text-align:center}
  .lbyou-cta b{color:var(--peach-t)}
  .lbyou-cta small{display:block;margin-top:3px;font-size:11px;color:var(--muted-t);font-weight:normal}
  .lbboard{margin-top:6px}
  .w3note{font-size:12px;color:var(--muted-t);margin-top:6px}
  .w3err{font-size:13px;color:var(--red-t);font-weight:bold;margin-top:6px;min-height:16px}

  /* rug cinematic — above every overlay, black, click to skip */
  .cine{
    position:fixed;inset:0;z-index:200;background:#1c1c1ce6;display:none;
    align-items:center;justify-content:center;cursor:pointer;
  }
  .cine.on{display:flex}
  /* framed like the rest of the game — a bare video on black looked like it came from another
     product entirely. The numbers under it turn a clip into a reward screen. */
  .cinecard{
    background:var(--cream);border:4px solid var(--ink);border-radius:16px;
    box-shadow:6px 6px 0 var(--ink);padding:10px;max-width:min(94vw,860px);
    display:flex;flex-direction:column;gap:8px;align-items:center;
  }
  .cinetitle{
    font-family:var(--font-display);font-weight:var(--weight-display);
    font-size:clamp(18px,4.4vw,30px);letter-spacing:1.2px;color:var(--ink);text-align:center;
  }
  .cine video{
    max-width:100%;max-height:62vh;width:auto;height:auto;display:block;
    border:3px solid var(--ink);border-radius:11px;background:#0000;
  }
  .cinesub{
    font-family:var(--font-num);font-size:clamp(12px,3vw,15px);color:var(--accent-t);
    text-align:center;line-height:1.35;
  }
  .cine.on::after{
    content:"tap to skip";position:absolute;right:14px;bottom:12px;
    font-family:var(--font-display);font-weight:var(--weight-display);font-size:11px;
    letter-spacing:.4px;color:#fff9;pointer-events:none;
  }

  /* faith sources — a rates list, not a prose sentence */
  .faithsrc{margin:0 0 10px;padding:0 0 0 18px;font-size:13px;line-height:1.5}
  .faithsrc li{margin:2px 0}
  .faithsrc b{color:var(--accent-t)}
