
  :root{
    /* 🔻 THE BOTTOM-RIGHT DOCK. Two fixed rows share this corner — HELP/ZONES/TARGETS (.deployrow) and the
       1×/2×/5× speed buttons (.speedrow) — and the second is positioned relative to the first. That
       relationship used to be a bare `bottom:44px` that silently went stale when the theme raised every
       control to 40px, overlapping the two by 6px. Named here so both rows read from one source. */
    --sky-dock-bottom:10px;   /* how far the lower row sits off the bottom edge */
    --sky-dock-gap:8px;       /* clear air between the two rows */
    /* ── SKYSUNDER semantic foundation · Phase 2 (Codex 2026-07-29) ──
       New work consumes --ss-* tokens. The original variable names remain aliases so
       existing gameplay UI can migrate a component at a time without changing handlers. */
    --ss-surface-parchment:#f1e6c8;
    --ss-surface-parchment-muted:#e3d2aa;
    --ss-surface-stone:#2a2a2f;
    --ss-surface-void:#0e0e10;
    --ss-surface-indigo:#1b1e3a;
    --ss-surface-indigo-deep:#141126;
    --ss-surface-card:#171331;
    --ss-surface-control:#241f4a;
    --ss-surface-control-hover:#2f2963;
    --ss-surface-panel:rgba(28,24,54,.933);
    --ss-surface-panel-soft:rgba(28,24,54,.72);
    --ss-surface-overlay:rgba(11,9,24,.76);

    --ss-border-subtle:#3d3568;
    --ss-border-strong:#6a5b92;
    --ss-border-gold:#c8a35a;
    --ss-border-focus:#a66cff;

    --ss-text-on-dark:#e8e2f5;
    --ss-text-muted-on-dark:#aaa0ca;
    --ss-text-on-parchment:#2b2118;
    --ss-text-muted-on-parchment:#65533f;
    --ss-gold-antique:#c8a35a;
    --ss-gold-bright:#e8b64c;
    --ss-gold-light:#f6d98a;
    --ss-fissure:#8a3dff;
    --ss-fissure-soft:#b07fe8;
    --ss-arcane:#7ee0ff;
    --ss-positive:#5fd3a8;
    --ss-danger:#e86a5a;

    --ss-rarity-common:#9a8fc0;
    --ss-rarity-uncommon:#58d68b;
    --ss-rarity-rare:#b07fe8;
    --ss-rarity-legendary:#e8b64c;
    --ss-rarity-mythic:#c8f8ff;

    --ss-font-display:'Cinzel',Georgia,'Times New Roman',serif;
    /* Source Sans 3 is the approved body face and arrives with ui-theme-baseline.css, which
       overrides this token. The inline fallback used to name Alegreya Sans, which this build no
       longer requests - so if that stylesheet ever failed to load, every body string fell back to
       a system face without anything saying so. Name only faces we actually load. */
    --ss-font-body:'Source Sans 3','Segoe UI',system-ui,sans-serif;
    --ss-type-display-xl:clamp(42px,153.6px,92px);
    --ss-type-heading-lg:30px;
    --ss-type-heading-md:22px;
    --ss-type-heading-sm:18px;
    --ss-type-action:20px;
    --ss-type-body-lg:16px;
    --ss-type-body:15px;
    --ss-type-body-sm:14px;
    --ss-type-label:13px;
    --ss-type-caption:12px;
    --ss-leading-tight:1.2;
    --ss-leading-body:1.55;
    --ss-leading-reading:1.7;

    --ss-space-1:4px;
    --ss-space-2:8px;
    --ss-space-3:12px;
    --ss-space-4:16px;
    --ss-space-5:20px;
    --ss-space-6:24px;
    --ss-space-7:32px;
    --ss-space-8:40px;

    --ss-radius-sm:6px;
    --ss-radius-md:10px;
    --ss-radius-lg:16px;
    --ss-radius-round:999px;
    --ss-control-min:36px;
    --ss-control-touch:44px;
    --ss-reading-measure:68ch;

    --ss-shadow-sm:0 4px 14px rgba(0,0,0,.35);
    --ss-shadow-md:0 12px 34px rgba(0,0,0,.48);
    --ss-shadow-lg:0 20px 80px rgba(0,0,0,.67);
    --ss-shadow-gold:0 0 34px rgba(232,182,76,.35);
    --ss-shadow-fissure:0 0 28px rgba(138,61,255,.28);

    --ss-layer-canvas:0;
    --ss-layer-scene:10;
    --ss-layer-hud:20;
    --ss-layer-dock:30;
    --ss-layer-overlay:40;
    --ss-layer-modal:50;
    --ss-layer-tooltip:220;
    --ss-layer-debug:400;

    --ss-duration-fast:120ms;
    --ss-duration-base:180ms;
    --ss-duration-slow:320ms;
    --ss-ease-standard:cubic-bezier(.2,.7,.2,1);
    --ss-ease-emphasis:cubic-bezier(.34,1.1,.5,1);

    /* Legacy compatibility aliases. Values intentionally match the audited build. */
    --sky-deep:var(--ss-surface-indigo-deep);
    --panel:var(--ss-surface-panel);
    --panel-edge:var(--ss-border-subtle);
    --ink:var(--ss-text-on-dark);
    --ink-dim:#9a8fc0;
    --gold:var(--ss-gold-bright);
    --arcane:var(--ss-arcane);
    --violet:var(--ss-fissure-soft);
    --friend:var(--ss-positive);
    --foe:var(--ss-danger);
    --r0:var(--ss-rarity-common);
    --r1:var(--ss-rarity-uncommon);
    --r2:var(--ss-rarity-rare);
    --r3:var(--ss-rarity-legendary);
    --r4:var(--ss-rarity-mythic);
  }
  *{box-sizing:border-box;margin:0;padding:0;user-select:none;-webkit-user-select:none}
  /* ── THE FIXED STAGE ─────────────────────────────────────────────────────────────────────────────
     The whole game is laid out at exactly STAGE_W x STAGE_H virtual pixels and the BODY is scaled to fit
     the window. Josh, 2026-07-31: "We are having a serious problem with scaling and screen resolution. We
     need an absolute screen size fix."

     Why the body and not a wrapper: transforming the body makes it the containing block for every
     position:fixed descendant, so the HUD rows, the speed buttons and the tooltip all land inside the
     stage and their pixel offsets stay virtual. A wrapper would have left `position:fixed` anchored to the
     viewport and those pieces would have drifted out of the layout at every size but one.

     What this buys: one layout, one set of numbers, identical at every resolution and at any browser zoom.
     What it costs: letterboxing on aspect ratios other than 16:9, which is the usual trade and is why the
     bars are painted in the void colour rather than black.

     THE STAGE IS A UNIT SYSTEM, NOT A RESOLUTION. Moving it from 1600x900 to 1920x1080 does not change how
     big anything looks — the scale absorbs the difference — it changes what a virtual pixel MEANS. Nothing
     was resized to follow it, so every existing px value still covers the same distance on screen and the
     1920x1080 stage simply carries 320x180 more room than the layout currently uses. That headroom is for
     the panel rearrangement, and it is why FIELD_FILL and FIELD_VMAX had to be rescaled rather than left.

     Gameplay is unaffected, and that is a property of the sim rather than a hope: platGeom derives the play
     area as FRACTIONS of the drawn isle, PLAY_SPAN and PLATE_EDGE are fractions, and the ETA readout is in
     canonical sim units. With W and H now constants, those fractions resolve to the same numbers on every
     machine — which is strictly MORE deterministic than before, when the field was as wide as the window. */
  html{width:100%;height:100%;overflow:hidden;background:#07060f}
  body{position:absolute;left:0;top:0;width:1920px;height:1080px;overflow:hidden;
    transform-origin:0 0;background:var(--sky-deep);
    font-family:var(--ss-font-body);color:var(--ink)}

  /* ── Reusable UI primitives ─────────────────────────────────────────────
     Prefix keeps the foundation isolated from gameplay classes. These classes
     are presentation-only: no IDs, event hooks, or layout coordinates. */
  .ss-surface{
    color:var(--ss-text-on-dark);
    background:var(--ss-surface-panel);
    border:1px solid var(--ss-border-subtle);
    border-radius:var(--ss-radius-lg);
    box-shadow:var(--ss-shadow-md);
  }
  .ss-surface--soft{background:var(--ss-surface-panel-soft)}
  .ss-surface--stone{
    color:var(--ss-text-on-dark);
    background:
      linear-gradient(145deg,rgba(255,255,255,.035),transparent 38%),
      var(--ss-surface-stone);
    border-color:#4a4654;
  }
  .ss-surface--void{
    color:var(--ss-text-on-dark);
    background:
      radial-gradient(circle at 50% 0,rgba(138,61,255,.12),transparent 55%),
      var(--ss-surface-void);
  }
  .ss-surface--parchment{
    color:var(--ss-text-on-parchment);
    background:
      radial-gradient(circle at 18% 12%,rgba(255,255,255,.34),transparent 32%),
      linear-gradient(135deg,var(--ss-surface-parchment),var(--ss-surface-parchment-muted));
    border-color:var(--ss-border-gold);
    box-shadow:inset 0 0 0 1px rgba(74,49,22,.18),var(--ss-shadow-md);
  }
  .ss-frame{
    position:relative;
    border:1px solid var(--ss-border-gold);
    box-shadow:inset 0 0 0 1px rgba(255,226,159,.12),var(--ss-shadow-sm);
  }
  .ss-frame::before{
    content:'';
    position:absolute;
    inset:4px;
    border:1px solid rgba(200,163,90,.28);
    border-radius:calc(var(--ss-radius-lg) - 4px);
    pointer-events:none;
  }
  .ss-divider{
    height:1px;
    border:0;
    background:linear-gradient(90deg,transparent,var(--ss-border-gold),transparent);
    opacity:.72;
  }
  .ss-title{
    font-family:var(--ss-font-display);
    font-size:var(--ss-type-heading-md);
    font-weight:700;
    line-height:var(--ss-leading-tight);
    letter-spacing:.08em;
    color:var(--ss-gold-bright);
    text-transform:uppercase;
  }
  .ss-body{
    max-width:var(--ss-reading-measure);
    font-size:var(--ss-type-body);
    line-height:var(--ss-leading-body);
    color:var(--ss-text-on-dark);
  }
  .ss-caption{
    font-size:var(--ss-type-caption);
    line-height:var(--ss-leading-body);
    color:var(--ss-text-muted-on-dark);
  }
  .ss-button{
    min-height:var(--ss-control-min);
    display:inline-flex;
    align-items:center;
    justify-content:center;
    gap:var(--ss-space-2);
    padding:8px 16px;
    border:1px solid var(--ss-border-subtle);
    border-radius:var(--ss-radius-md);
    font-family:var(--ss-font-display);
    font-size:var(--ss-type-label);
    font-weight:700;
    line-height:1;
    letter-spacing:.08em;
    text-transform:uppercase;
    cursor:pointer;
    transition:
      transform var(--ss-duration-fast) var(--ss-ease-standard),
      border-color var(--ss-duration-fast) var(--ss-ease-standard),
      color var(--ss-duration-fast) var(--ss-ease-standard),
      background var(--ss-duration-fast) var(--ss-ease-standard),
      box-shadow var(--ss-duration-fast) var(--ss-ease-standard);
  }
  .ss-button:hover:not(:disabled):not([aria-disabled="true"]){border-color:var(--ss-border-gold, #c8a35a)}
  .ss-button:active:not(:disabled):not([aria-disabled="true"]){box-shadow:inset 0 2px 6px rgba(0,0,0,.35)}
  .ss-button:focus-visible{
    outline:2px solid var(--ss-border-focus);
    outline-offset:3px;
    box-shadow:0 0 0 4px rgba(138,61,255,.2);
  }
  .ss-button:disabled,.ss-button[aria-disabled="true"]{opacity:.44;cursor:not-allowed}
  .ss-button--primary{
    color:#1c1430;
    border-color:var(--ss-gold-bright);
    background:linear-gradient(180deg,var(--ss-gold-light),var(--ss-gold-bright) 60%,#c08c2c);
    box-shadow:var(--ss-shadow-gold);
  }
  .ss-button--secondary{
    color:var(--ss-fissure-soft);
    border-color:var(--ss-fissure-soft);
    background:rgba(23,19,49,.76);
    box-shadow:var(--ss-shadow-fissure);
  }
  .ss-button--quiet{
    color:var(--ss-text-on-dark);
    background:rgba(23,19,49,.64);
  }
  .ss-button--danger{
    color:#fff4f0;
    border-color:var(--ss-danger);
    background:rgba(232,106,90,.18);
  }
  .ss-button--icon{width:var(--ss-control-min);padding:0}
  .ss-badge{
    min-height:20px;
    display:inline-flex;
    align-items:center;
    gap:4px;
    padding:2px 7px;
    border:1px solid currentColor;
    border-radius:var(--ss-radius-sm);
    font-size:var(--ss-type-caption);
    font-weight:700;
    line-height:1.2;
    letter-spacing:.06em;
    text-transform:uppercase;
    color:var(--ss-text-muted-on-dark);
    background:rgba(14,14,16,.34);
  }
  .ss-badge--common{color:var(--ss-rarity-common)}
  .ss-badge--uncommon{color:var(--ss-rarity-uncommon)}
  .ss-badge--rare{color:var(--ss-rarity-rare)}
  .ss-badge--legendary{color:var(--ss-rarity-legendary)}
  .ss-stat-row{
    min-height:28px;
    display:grid;
    grid-template-columns:minmax(0,1fr) auto;
    align-items:center;
    gap:var(--ss-space-3);
    font-size:var(--ss-type-body-sm);
    line-height:var(--ss-leading-body);
  }
  .ss-stat-row__label{color:var(--ss-text-muted-on-dark)}
  .ss-stat-row__value{color:var(--ss-text-on-dark);font-weight:700;font-variant-numeric:tabular-nums}
  .ss-modal{
    width:min(640px,calc(1920px - 32px));
    max-height:min(885.6px,760px);
    display:flex;
    flex-direction:column;
    overflow:hidden;
  }
  .ss-modal__header{
    display:flex;
    align-items:center;
    gap:var(--ss-space-4);
    padding:var(--ss-space-5) var(--ss-space-6) var(--ss-space-3);
  }
  .ss-modal__body{
    overflow:auto;
    padding:var(--ss-space-3) var(--ss-space-6) var(--ss-space-5);
    font-size:var(--ss-type-body);
    line-height:var(--ss-leading-reading);
  }
  .ss-modal__actions{
    display:flex;
    justify-content:flex-end;
    gap:var(--ss-space-2);
    padding:var(--ss-space-4) var(--ss-space-6);
    border-top:1px solid var(--ss-border-subtle);
  }
  .ss-dock{
    position:fixed;
    z-index:var(--ss-layer-dock);
    background:var(--ss-surface-panel);
    border:1px solid var(--ss-border-subtle);
    border-radius:var(--ss-radius-md);
    box-shadow:var(--ss-shadow-md);
  }
  .ss-tooltip{
    z-index:var(--ss-layer-tooltip);
    max-width:min(360px,calc(1920px - 24px));
    padding:var(--ss-space-3);
    color:var(--ss-text-on-dark);
    background:rgba(14,14,16,.97);
    border:1px solid var(--ss-border-gold);
    border-radius:var(--ss-radius-md);
    box-shadow:var(--ss-shadow-lg);
    font-size:var(--ss-type-body-sm);
    line-height:var(--ss-leading-body);
  }
  :where(button,input,select,textarea,a,[role="button"]):focus-visible{
    outline:2px solid var(--ss-border-focus);
    outline-offset:2px;
  }
  @media(prefers-reduced-motion:reduce){
    .ss-button{transition:none}
    .ss-button:hover:not(:disabled):not([aria-disabled="true"]){transform:none}
  }
  #cv{position:absolute;inset:0;display:block;z-index:var(--ss-layer-canvas)}
  .screen{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:var(--ss-layer-scene)}
  #endScreen{background-size:cover;background-position:center;background-repeat:no-repeat}
  .hidden{display:none !important}
  h1.logo{font-family:var(--ss-font-display);font-weight:900;font-size:var(--ss-type-display-xl);letter-spacing:.08em;
    background:linear-gradient(180deg,#fdf3d0 0%,var(--gold) 55%,#a5741f 100%);
    -webkit-background-clip:text;background-clip:text;color:transparent;line-height:1}
  .tagline{font-family:var(--ss-font-display);font-size:clamp(13px,34.56px,19px);letter-spacing:.35em;color:var(--arcane);margin-top:10px;text-transform:uppercase}
  .howto{margin-top:26px;background:var(--ss-surface-panel);border:1px solid var(--ss-border-subtle);border-radius:12px;
    padding:18px 26px;max-width:660px;font-size:14.5px;line-height:var(--ss-leading-reading);color:var(--ink-dim)}
  .howto b{color:var(--ink)}
  #diffRow{margin-top:30px;display:flex;align-items:center;gap:8px}
  .diffLabel{font-family:'Cinzel',serif;font-size:11px;letter-spacing:.22em;color:var(--ink-dim);margin-right:6px}
  .diffBtn{font-family:'Cinzel',serif;font-size:12px;letter-spacing:.14em;padding:7px 16px;border-radius:9px;background:var(--panel);
    border:1px solid var(--panel-edge);color:var(--ink-dim);cursor:pointer}
  .diffBtn.sel{color:var(--gold);border-color:var(--gold);box-shadow:0 0 16px rgba(232,182,76,.25)}
  .diffBtn[data-diff="brutal"].sel{color:var(--foe);border-color:var(--foe);box-shadow:0 0 16px rgba(232,106,90,.3)}
  .diffBtn[data-diff="easy"].sel{color:var(--friend);border-color:var(--friend);box-shadow:0 0 16px rgba(95,211,168,.3)}
  .diffBtn[data-aidiff="brutal"].sel{color:var(--foe);border-color:var(--foe);box-shadow:0 0 16px rgba(232,106,90,.3)}
  .diffBtn[data-aidiff="easy"].sel{color:var(--friend);border-color:var(--friend);box-shadow:0 0 16px rgba(95,211,168,.3)}
  /* collapsible YOUR BANNER tab (Josh 2026-07-24): collapses to just the avatar, with a name pushout */
  #idRow{transition:padding .18s ease}
  #idNameTag{display:none}
  /* 🚨 "no name" attention pulse: bright blinking rings radiating from the banner, top-right (Josh 2026-07-24) */
  /* spotlight: circular hole in the dim+blur backdrop. Centre + radii are MEASURED at runtime
     (positionNameSpot) into these vars, so it lines up at any resolution/zoom. Fallbacks are approximate. */
  #nameOv{--spotX:calc(100% - 41px);--spotY:41px;--spotR1:80px;--spotR2:132px;
    -webkit-mask-image:radial-gradient(circle at var(--spotX) var(--spotY),transparent 0 var(--spotR1),#000 var(--spotR2));
    mask-image:radial-gradient(circle at var(--spotX) var(--spotY),transparent 0 var(--spotR1),#000 var(--spotR2))}
  /* while the warning is open the banner is lifted ABOVE the overlay and the beacon, so the chip stays
     crisp and the rings appear to radiate out from underneath it (idRow's z-index is inline → !important) */
  #idRow.idSpot{z-index:45!important}
  #namePulse{position:absolute;top:20px;left:0;width:46px;height:46px;border-radius:50%;z-index:40;pointer-events:none;
    background:rgba(255,236,170,.95);box-shadow:0 0 30px 10px rgba(255,214,92,.85);animation:npBlink .55s ease-in-out infinite}
  #namePulse::before,#namePulse::after{content:'';position:absolute;inset:0;border-radius:50%;border:3px solid rgba(255,214,92,.9);animation:npRing 1.3s ease-out infinite}
  #namePulse::after{animation-delay:.65s}
  @keyframes npBlink{0%,100%{opacity:.35;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}
  @keyframes npRing{0%{transform:scale(1);opacity:.95;border-width:4px}100%{transform:scale(9);opacity:0;border-width:1px}}
  /* collapsed = ONLY the avatar chip + its name banner: no panel box, border, padding or shadow */
  /* !important throughout: idRow's panel box (bg/border/padding) is set INLINE and would otherwise win */
  #idRow.idCollapsed{padding:0!important;gap:0!important;background:none!important;border:none!important;box-shadow:none!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
  /* !important is required: the profile row carries an inline display:flex that would otherwise win */
  #idRow.idCollapsed>*{display:none!important}
  #idRow.idCollapsed>#avpick-wrap{display:block!important}
  #idRow.idCollapsed #avpick-btn{display:none!important}
  #idRow.idCollapsed #avpick-cur{margin:0;cursor:pointer}
  /* expand: grow from the icon (top-right) on both axes and bounce at full size */
  @keyframes idPop{0%{transform:scale(.22);opacity:.4}55%{transform:scale(1.02);opacity:1}75%{transform:scale(.993)}88%{transform:scale(1.005)}100%{transform:scale(1)}}
  #idRow.idPop{transform-origin:top right;animation:idPop .42s cubic-bezier(.34,1.1,.5,1) both}
  #idRow.idPopRev{transform-origin:top right;animation:idPop .1s cubic-bezier(.34,1.1,.5,1) reverse both} /* minimize: same motion, played backwards into the icon — snappy */
  /* name banner docks flush against the avatar: flat right edge, no arrow tail */
  #idRow.idCollapsed>#idNameTag{display:block!important;position:absolute;right:100%;top:50%;transform:translateY(-50%);
    background:var(--panel);border:1px solid var(--panel-edge);border-right:none;border-radius:9px 0 0 9px;padding:5px 12px 5px 11px;white-space:nowrap;
    font-family:'Cinzel',serif;font-size:12px;font-weight:700;color:var(--gold);letter-spacing:.04em;pointer-events:none;box-shadow:0 4px 14px rgba(0,0,0,.35)}
  #idRow:not(.idCollapsed) #avpick-cur{cursor:pointer}
  /* difficulty select screen (Josh 2026-07-24) — moved off the main menu, on the loading backdrop */
  #diffScreen{background-size:cover;background-position:center;background-repeat:no-repeat}
  /* score readout: framed in the same tt_frame border-image the tooltips use (Josh 2026-07-24) */
  #diffScore{font-family:'Cinzel',serif;font-weight:900;font-size:clamp(22px,69.12px,38px);letter-spacing:.1em;
    color:var(--gold);text-shadow:0 3px 14px rgba(0,0,0,.75),0 0 26px rgba(232,182,76,.4);white-space:nowrap;
    padding:10px 26px;margin:14px 0 20px;box-shadow:0 8px 28px #000d;
    border-image:url('assets/images/ui/tt_frame.png') 30 fill / 15px stretch;border-width:10px;border-style:solid;background:none}
  /* ── per-banner stance controls on the deploy panel ── */
  .stRow{display:flex;align-items:center;gap:4px;margin-top:4px;flex-wrap:wrap}
  .stTag{font-size:11px;line-height:1;padding:2px 5px;border-radius:5px;letter-spacing:.06em;
    background:rgba(23,19,49,.7);border:1px solid var(--panel-edge);color:var(--gold);font-weight:700}
  .stBtn{font-size:12px;line-height:1;padding:3px 6px;border-radius:6px;cursor:pointer;
    background:rgba(23,19,49,.7);border:1px solid var(--panel-edge);color:var(--ink-dim)}
  .stBtn:hover{border-color:var(--gold);color:var(--ink)}
  .stBtn.on{background:#2d2247;border-color:var(--gold);color:var(--gold);box-shadow:0 0 8px rgba(232,182,76,.3)}
  .stTgt{font-size:10px;color:var(--ink-dim);letter-spacing:.02em}
  /* An aggressive banner with no designated target still works — it takes the nearest — but forgetting
     to point it is the easiest mistake in the new system, so it pulses until you look at it. */
  .stWarn{font-size:10px;color:#e8a04c;letter-spacing:.02em;animation:stanceWarn 1.1s ease-in-out infinite}
  @keyframes stanceWarn{0%,100%{opacity:.42}50%{opacity:1}}
  .formRow{display:flex;align-items:center;gap:4px;margin-top:4px;flex-wrap:wrap}
  .formBtn{font-size:10px;letter-spacing:.08em;text-transform:uppercase;padding:4px 7px}
  /* ── mode select: two big readable cards, because this is a real fork in the game ── */
  /* Two plain buttons, stacked exactly like the main menu (Josh 2026-07-27: "nothing else") */
  /* No heading above this any more, so the 22px that used to separate it from one is gone. Buttons stay
     the same width as each other so neither mode reads as the privileged one. */
  #modePicks{display:flex;flex-direction:column;align-items:center;gap:14px;width:min(380px,1651.2px)}
  #modePicks .big{margin-top:0;width:100%;text-align:center}

  /* ── 🟡 ASCENT AND SKIRMISH ARE GOLD AGAIN (Josh 2026-08-01: "make the ASCENT and SKIRMISH buttons
   * yellow again in the singleplayer menu") ──────────────────────────────────────────────────────────
   *
   * THE MARKUP ALREADY SAYS THEY SHOULD BE. Both are plain `.big`, and `button.big` IS the gold primary in
   * assets/ui-theme-baseline.css — the same declaration that paints TO BATTLE and DEPLOY. BACK beside them
   * is `.big.ghost`, the dark secondary. So the intent in skysunder.html is already "two gold picks and a
   * quiet way out"; nothing needed inventing, it needed un-overriding.
   *
   * WHAT WAS OVERRIDING IT. Further down that same theme sheet, `#modePicks .big` repaints every button on
   * this screen charcoal — `color`, `border-color`, `background` and `box-shadow`, all four `!important`.
   * That is why these two looked identical to BACK and unlike every other primary in the game.
   *
   * WHY `!important` HERE, WHEN THE PIPS FIX EARLIER TODAY WENT THE OTHER WAY. On the pips the collision
   * was accidental — a bare `.on` utility catching an element that had no business matching it — so the fix
   * was to stop matching, and the `!important` came out. This one is not accidental: `#modePicks .big` is
   * aimed squarely at these buttons and means it. There is no class to rename out of its way, so the only
   * thing that beats an `!important` is an `!important` at higher specificity. Two ids and two classes
   * against one id and one class.
   *
   * `:not(.ghost)` IS THE WHOLE POINT. Josh named ASCENT and SKIRMISH; BACK is `.big.ghost` and stays dark,
   * which is also what keeps the screen readable — three gold buttons would leave nothing to look at first.
   * Values are lifted verbatim from the theme's own primary rather than re-mixed, so these match TO BATTLE
   * exactly and follow it if the palette ever moves. */
  #modeScreen #modePicks .big:not(.ghost){
    color:#21170d !important;
    border-color:#f1cf7a !important;
    background:
      linear-gradient(180deg,rgba(255,255,255,.35),transparent 32%),
      linear-gradient(180deg,#efd07e,#c99b3f 58%,#8f6527) !important;
    box-shadow:
      inset 0 0 0 2px rgba(74,42,11,.38),
      inset 0 -4px 8px rgba(62,31,6,.28),
      0 5px 14px rgba(0,0,0,.4) !important;
  }
  /* The theme's hover for this screen sets PALE GOLD TEXT — fine on charcoal, illegible on a gold button,
     and it would have shipped as "the label vanishes when you point at it". Hover now brightens the plate
     and keeps the dark ink. */
  #modeScreen #modePicks .big:not(.ghost):hover,
  #modeScreen #modePicks .big:not(.ghost):focus-visible{
    color:#21170d !important;
    border-color:#ffe6a8 !important;
    background:
      linear-gradient(180deg,rgba(255,255,255,.45),transparent 32%),
      linear-gradient(180deg,#ffe093,#d8a848 58%,#9c6f24) !important;
    box-shadow:
      inset 0 0 0 2px rgba(74,42,11,.38),
      inset 0 -4px 8px rgba(62,31,6,.28),
      0 0 20px rgba(232,182,76,.42) !important;
  }
  #modeBtnRow,#ascBtnRow{display:flex;justify-content:center;gap:14px;flex-wrap:wrap;transform:translateY(120px)}
  @media (max-width:0px){ /* DISABLED, was: (max-height:820px) — max-height:820 fails stage 1080 */ #modeBtnRow,#ascBtnRow{transform:translateY(60px)}}
  #modeBtnRow .big,#ascBtnRow .big{width:200px;margin:0;padding:14px 0;text-align:center}
  /* ── ascent tier ladder ── */
  #ascPicks{display:flex;justify-content:center;align-items:center;gap:9px;flex-wrap:wrap;margin:28px 0 6px;max-width:1843.2px}
  .ascBtn{font-family:'Cinzel',serif;font-size:15px;letter-spacing:.14em;padding:12px 22px;border-radius:10px;
    background:rgba(23,19,49,.82);border:1px solid var(--panel-edge);color:var(--ink-dim);cursor:pointer;position:relative}
  .ascBtn.sel{color:var(--gold);border-color:var(--gold);box-shadow:0 0 18px rgba(232,182,76,.28)}
  .ascBtn.locked{opacity:.4;cursor:not-allowed}
  .ascBtn .lockic{display:block;font-size:10px;letter-spacing:0;margin-top:3px;color:var(--ink-dim)}
  #ascMods{max-width:min(560px,1766.4px);margin:10px auto 0;font-size:12px;line-height:1.65;
    color:var(--ink-dim);background:rgba(23,19,49,.7);border:1px solid var(--panel-edge);border-radius:12px;padding:12px 16px;text-align:left}
  #ascMods .amRow{margin-bottom:5px}
  #ascMods .amRow:last-child{margin-bottom:0}
  #ascMods b{color:var(--gold)}
  #ascMods .amNone{color:var(--ink-dim);font-style:italic}
  /* Ascent Tutorial coachmarks — highlight target + anchored card (ascenttut20).
     Gated steps block clicks via #ascTutPop (590). #ascTutPop + #ascTutHi are body-level fixed overlays
     (not inside #prep) so battle tips stay visible when deploy hides prep. Lifts help visuals only;
     child z-index inside #prepBody cannot beat the overlay; .ascTutClickProxy forwards clicks to the
     real Recruit / upgrade button; .ascTutHoverProxy forwards hover to showTipNow on #goldPill. */
  .ascTutHi{position:fixed;z-index:589;pointer-events:none;border:2px solid var(--gold);border-radius:10px;
    box-shadow:0 0 0 9999px rgba(7,6,19,.55),0 0 18px rgba(232,182,76,.35)}
  .ascTutPop{position:fixed;inset:0;z-index:590;pointer-events:auto}
  .ascTutPop.ascTutRelic{pointer-events:none}
  .ascTutPop.ascTutRelic .ascTutPopCard{pointer-events:auto}
  #relicOv.ascTutLift{z-index:595!important}
  .ascTutPop.ascTutGated .ascTutPopCard{pointer-events:none}
  .ascTutClickProxy{position:fixed;z-index:593;margin:0;padding:0;border:0;background:transparent;
    cursor:pointer;pointer-events:auto;opacity:0.001}
  .ascTutClickProxy:disabled{cursor:not-allowed;pointer-events:none}
  .ascTutHoverProxy{position:fixed;z-index:593;margin:0;padding:0;border:0;background:transparent;
    cursor:help;pointer-events:auto;opacity:0.001}
  #prep #townPanel.ascTutLift{position:relative;z-index:591}
  body:has(#prep:not(.hidden)) #prepBody > #shopPanel.ascTutLift{z-index:591!important}
  .ascTutTarget{position:relative;z-index:592;pointer-events:auto}
  .ascTutTarget .ubtn,.ascTutTarget.ubtn,.ascTutTarget .buy,.ascTutTarget.buy{pointer-events:auto}
  .ascTutPopCard{position:fixed;max-width:min(320px,88vw);pointer-events:auto;z-index:1;
    background:linear-gradient(180deg,rgba(32,26,62,.98),rgba(18,14,40,.98));
    border:1px solid var(--gold);border-radius:12px;padding:14px 16px 12px;box-shadow:0 14px 40px rgba(0,0,0,.65);
    font-family:'Cinzel',serif;font-size:13px;line-height:1.55;color:var(--ink);text-align:left}
  .ascTutPopCard p{margin:0 0 10px}
  .ascTutPopCard .ubtn{display:block;margin-left:auto;font-size:11px;padding:6px 14px}
  .card.ascTutLocked{position:relative;opacity:.72}
  .card.ascTutLocked .buy,.card.ascTutLocked .ss-unit-buy{pointer-events:none}
  .ascTutLock{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
    background:rgba(7,6,19,.45);border-radius:inherit;pointer-events:auto;cursor:not-allowed;z-index:3}
  .ascTutLock .artic{height:auto;width:auto;max-height:48px;max-width:48px;display:block;
    filter:drop-shadow(0 2px 6px rgba(0,0,0,.65))}
  #diffPicks{display:flex;flex-wrap:nowrap;justify-content:center;align-items:center;gap:7px;margin:26px 0 4px;max-width:1843.2px}
  #diffPicks .diffBtn{font-size:11.5px;letter-spacing:.1em;padding:11px 14px;background:rgba(23,19,49,.82);white-space:nowrap;flex:0 0 auto}
  /* Brutal+ smoulders; Brutal++ burns. Both pulse always so the danger reads at a glance (Josh 2026-07-24) */
  @keyframes angryPulse{0%,100%{border-color:#c0392b;color:#ff8f7a;box-shadow:0 0 8px rgba(207,74,74,.35)}
    50%{border-color:#ff4d3d;color:#ffb3a3;box-shadow:0 0 22px rgba(255,77,61,.8)}}
  @keyframes fieryPulse{0%,100%{border-color:#ff5a1f;color:#ffb37a;box-shadow:0 0 12px rgba(255,90,31,.5)}
    50%{border-color:#ffd24a;color:#fff0c2;box-shadow:0 0 30px rgba(255,140,30,.95),0 0 52px rgba(255,60,10,.55)}}
  /* idle: a slow smoulder. hover: full-speed fury (Josh 2026-07-24) */
  #diffPicks .diffBtn[data-diff="brutalp"]{animation:angryPulse 6s ease-in-out infinite}
  #diffPicks .diffBtn[data-diff="brutalpp"]{animation:fieryPulse 4s ease-in-out infinite}
  #diffPicks .diffBtn[data-diff="brutalp"]:hover,#diffPicks .diffBtn[data-diff="brutalp"]:focus-visible{animation-duration:1.5s}
  #diffPicks .diffBtn[data-diff="brutalpp"]:hover,#diffPicks .diffBtn[data-diff="brutalpp"]:focus-visible{animation-duration:.8s}
  /* animations own color/border/shadow, so selection reads via background + lift instead */
  #diffPicks .diffBtn[data-diff="brutalp"].sel{background:rgba(207,74,74,.34);transform:scale(1.07)}
  #diffPicks .diffBtn[data-diff="brutalpp"].sel{background:rgba(255,90,20,.38);transform:scale(1.09)}
  /* transform (not margin) so the buttons drop ~300px without shifting the centred stack above them */
  #diffBtnRow{display:flex;justify-content:center;gap:14px;flex-wrap:wrap;transform:translateY(250px)}
  @media (max-width:0px){ /* DISABLED, was: (max-height:900px) — max-height:900 fails stage 1080 */ #diffBtnRow{transform:translateY(160px)}}
  @media (max-width:0px){ /* DISABLED, was: (max-height:720px) — max-height:720 fails stage 1080 */ #diffBtnRow{transform:translateY(90px)}}
  #diffBtnRow .big{width:200px;margin:0;padding:14px 0;text-align:center} /* identical footprint for START and BACK */
  /* end screen: FIGHT AGAIN → VIEW YOUR ARMY → MAIN MENU, all one size (Josh 2026-07-24) */
  #endBtns{display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:6px}
  #endBtns .big{width:250px;margin:0;padding:14px 0;text-align:center}
  #endBtns #endScore{display:none} /* skirmish keeps its score in #endStats; only the climb prints it here */
  /* 🏔️ ASCENSION END SCREEN — slim bottom-right: ALTITUDE + optional SUMMIT STREAK + small tier.
     Every rule is scoped to .asc, which endScreenAscChrome() sets only for an Ascent run, so the skirmish
     end screen keeps the centred stack it has always had. Both blocks are taken out of flow rather than
     restructured into wrappers — that way the default layout cannot be disturbed by this at all. */
  #endScreen.asc{justify-content:flex-start;padding-top:64.8px;align-items:center}
  /* Title dead-centre of the top band — ui-theme `#endScreen > *{position:relative}` must not shove it. */
  #endScreen.asc #endLogo{align-self:center;text-align:center;width:100%;margin-left:auto;margin-right:auto}
  #endScreen.asc #endTag,#endScreen.asc #endStats{display:none} /* "you placed #N of 8" — the climb has no rivals */
  #endScreen.asc #endBtns{position:absolute;left:115.2px;bottom:86.4px;margin:0;width:min(340px,883.2px);
    flex-direction:row;flex-wrap:wrap;align-items:stretch;justify-content:flex-start;gap:10px}
  #endScreen.asc #endBtns #endScore{display:block;order:0;width:100%;font-family:'Cinzel',serif;font-weight:700;
    font-size:clamp(19px,48px,33px);letter-spacing:.09em;color:var(--gold);line-height:1;
    text-shadow:0 2px 14px #000,0 0 26px rgba(0,0,0,.55)}
  /* Mockup order: ENDLESS, NEW RUN, then MAIN MENU sharing its row with the small Final Army button.
     Done with `order` so the DOM stays exactly as the skirmish screen expects it. */
  #endScreen.asc #endEndlessBtn{order:1;width:100%}
  #endScreen.asc #againBtn{order:2;width:100%}
  #endScreen.asc #endMenuBtn{order:3;width:calc(100% - 108px)} /* 98px sibling + the 10px gap */
  #endScreen.asc #endCompoBtn{order:4;width:98px;padding:6px 0;font-size:10.5px;letter-spacing:.05em;line-height:1.2}
  /* TWO IDs so this beats ui-theme `#endScreen > *{position:relative}` (101) — without that win,
     Ascent level / streak sat in flow instead of hard bottom-right. (2026-08-02) */
  #endScreen #endAsc{position:absolute;right:96px;bottom:75.6px;text-align:right;max-width:1036.8px;pointer-events:none}
  #endAscMeta,#endStreak{font-family:'Cinzel',serif;font-weight:700;letter-spacing:.08em;line-height:1.14;
    font-size:clamp(23px,82.56px,56px);color:#f4e8ca;text-shadow:0 3px 16px #000,0 0 30px rgba(0,0,0,.65)}
  #endAscMeta{margin-bottom:4px}
  #endStreak:empty{display:none}
  /* Tier / difficulty — secondary, smaller than altitude and streak. */
  #endAscTier{font-family:'Cinzel',serif;font-weight:700;letter-spacing:.16em;line-height:1.2;
    font-size:clamp(11px,25.92px,15px);color:var(--gold);text-transform:uppercase;margin-top:10px;
    text-shadow:0 1px 8px #000}
  #endAscUnlock:empty{display:none}
  /* Short windows: the two blocks are the only things anchored to the bottom, so pull them in together. */
  @media (max-width:0px){ /* DISABLED, was: (max-height:720px) — max-height:720 fails stage 1080 */ #endScreen.asc{padding-top:32.4px}#endScreen.asc #endBtns{bottom:43.2px}#endScreen #endAsc{bottom:43.2px}}
  @media (max-width:0px){ /* DISABLED, was: (max-width:760px) — max-width:760 fails stage 1920 */ #diffPicks .diffBtn{font-size:10px;padding:9px 9px;letter-spacing:.04em}}
  #tutBody h4{font-family:'Cinzel',serif;font-size:13px;letter-spacing:.14em;color:var(--gold);margin:16px 0 6px;text-transform:uppercase}
  #tutBody h4:first-child{margin-top:0}
  #tutBody p{font-size:13.5px;line-height:1.65;color:var(--ink);margin-bottom:4px}
  #tutBody b{color:var(--gold)}
  #tutBody .key{display:inline-block;border:1px solid var(--panel-edge);border-radius:4px;padding:0 6px;margin:0 2px;font-size:11px;color:var(--arcane)}
  .key{display:inline-block;min-width:22px;text-align:center;padding:1px 6px;border:1px solid var(--panel-edge);
    border-bottom-width:3px;border-radius:5px;background:#2a2450;color:var(--gold);font-weight:700;font-size:13px}
  button.big{margin-top:26px;font-family:var(--ss-font-display);font-weight:700;font-size:var(--ss-type-action);letter-spacing:.15em;
    padding:16px 54px;border-radius:var(--ss-radius-md);border:1px solid var(--ss-gold-bright);color:#1c1430;cursor:pointer;
    background:linear-gradient(180deg,var(--ss-gold-light),var(--ss-gold-bright) 60%,#c08c2c);box-shadow:var(--ss-shadow-gold);
    transition:border-color var(--ss-duration-fast) var(--ss-ease-standard),box-shadow var(--ss-duration-fast) var(--ss-ease-standard)}
  button.big:hover{box-shadow:0 0 18px rgba(232,182,76,.35)}
  button.big.ghost{background:transparent;color:var(--ss-fissure-soft);border-color:var(--ss-fissure-soft);box-shadow:0 0 24px rgba(176,127,232,.2)}

  /* ---------- compendium ---------- */
  /* ONE NUMBER FOR THE LIST RAIL. The parchment pane was sitting well right of the list with a band of
     dead space between them, which is what Josh saw as "not centered". The cause was two rules that each
     looked right on its own: this file sizes #compLeft at a fixed 252px, and the 2026-07-31 UI merge made
     #compBody a grid whose first track is minmax(250px,330px). A grid track next to a 1fr sibling grows
     to its maximum, so the track went to 330px while the panel inside it stayed 252px, and the parchment
     began 78px late. Both now read the same variable, so they cannot drift apart again. */
  #compCard{--comp-rail:252px;max-width:960px;width:1804.8px;height:907.2px;text-align:left;padding:20px 24px;display:flex;flex-direction:column}
  #compCard #compBody{flex:1;display:flex;gap:16px;min-height:0;margin-top:12px;
    grid-template-columns:var(--comp-rail) minmax(420px,1fr)}
  #compCard #compLeft{width:var(--comp-rail);flex:none;display:flex;flex-direction:column;border-right:1px solid var(--panel-edge);padding-right:12px;min-height:0}
  /* THE PORTRAIT SITS TOP-RIGHT of the entry, opposite the name, where Josh ringed it. It was inline
     immediately before the title, which crowded the heading and left the whole right of that line empty.
     The heading becomes a row so the two can sit at its two ends; `order` moves the art without moving it
     in the markup, so every detail page keeps building its heading the same way. It is also given more
     size than it had inline — there is room for it out there, which is the point of the move. */
  #compDetail h3{display:flex;align-items:center;gap:14px}
  /* overflow:visible — rarity glow (.artic.rgl*) is a drop-shadow; hidden clipped it into a hard square
     around the plate (Josh 2026-08-16). Padding + negative margin keep the slot’s layout size 88px while
     giving the aura room to bloom without shoving the title. */
  #compDetail h3 .compBig{order:2;margin:-14px -4px -14px auto;padding:14px;width:88px;height:88px;flex:0 0 auto;
    overflow:visible;box-sizing:content-box}
  #compDetail h3 .compBig img{max-height:88px;max-width:88px}
  /* Bannerman detail portrait (bannermanart3): beat .compBig .uart 48px cap; contain in the 88px
     slot like other units — no scale(1.52) (overfill after body-weighted re-crop). */
  #compDetail h3 .compBig .uart[data-u="banner"]{width:88px;height:88px;max-width:88px;max-height:88px;object-fit:contain;object-position:center bottom;transform:none}
  #compSearch{width:100%;background:#151130;border:1px solid var(--panel-edge);border-radius:8px;color:var(--ink);padding:7px 10px;font-size:13px;font-family:inherit;margin-bottom:7px;outline:none}
  #compSearch:focus{border-color:var(--arcane)}
  /* HIDDEN BY DEFAULT, because the default category is All. It used to default to flex, so the row was on
     screen the moment the compendium opened and only went away once a chip was clicked — the markup's
     starting state disagreed with compCat's. syncCompSortRow() turns it on; nothing has to turn it off. */
  #compSorts{display:none;gap:4px;flex-wrap:wrap;margin-bottom:7px}
  /* The header is one row: title, the category chips, a rule, Close. The chips take the free space and
     wrap inside it, so a narrow window folds them onto a second line rather than pushing Close off. */
  #compHead{display:flex;align-items:center;gap:12px}
  #compHead #compCats{display:flex;gap:4px;flex-wrap:wrap;flex:1;min-width:0;margin:0}
  .compHeadDiv{flex:0 0 1px;align-self:stretch;background:var(--panel-edge);margin:2px 0}
  .sortChip{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;padding:3px 7px;border-radius:6px;background:var(--panel);border:1px solid var(--panel-edge);color:var(--ink-dim);cursor:pointer;font-family:'Cinzel',serif}
  .sortChip.sel{color:var(--gold);border-color:var(--gold)}
  .compSub{font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--arcane);margin:7px 0 2px;padding-left:8px;opacity:.85}
  #compList{overflow-y:auto;flex:1;min-height:0;padding-right:8px;margin-right:-8px} /* scrollbar hugs the panel edge — hover boxes clear both sides equally (Josh 2026-07-21) */
  .compSec{font-family:'Cinzel',serif;font-size:11px;letter-spacing:.2em;color:var(--ink-dim);margin:12px 0 5px;text-transform:uppercase}
  .compItem{display:flex;gap:9px;align-items:center;padding:6px 8px;border-radius:8px;cursor:pointer;font-size:13.5px;border:1px solid transparent}
  .compItem .ci{width:28px;height:28px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;font-size:16px}
  .compItem .ci img{max-height:26px;max-width:26px;width:auto;height:auto;object-fit:contain}
  .encha{height:1.25em;width:auto;vertical-align:-.22em;filter:drop-shadow(0 1px 1px rgba(0,0,0,.45))}
  .compBig .encha{height:auto;width:auto;max-height:48px;max-width:48px;vertical-align:middle;display:block}
  .compItem:hover{background:#2a2450}
  .compItem.sel{background:#2f2963;border-color:var(--gold)}
  #compDetail{flex:1;overflow-y:auto;padding:14px 10px 6px 14px}
  #compDetail h3{font-family:'Cinzel',serif;font-size:22px;margin-bottom:4px}
  #compDetail h4{font-family:'Cinzel',serif;font-size:12px;letter-spacing:.18em;color:var(--ink-dim);text-transform:uppercase;margin:16px 0 6px;border-bottom:1px solid var(--panel-edge);padding-bottom:5px}
  .kwart{width:32px;height:32px;object-fit:contain;vertical-align:middle}
  .kwlist{font-size:13px;color:var(--ink);line-height:1.45}
  .kwrow{display:flex;gap:11px;align-items:flex-start;margin:0 0 10px}
  .kwic{flex:0 0 36px;width:36px;text-align:center;font-size:20px;line-height:1.1}
  .kwtx{flex:1;min-width:0}
  .kwbadges{display:flex;gap:7px;flex-wrap:wrap;margin:9px 0 2px}
  .kwbadge{width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;background:#151130;border:1px solid var(--panel-edge);border-radius:9px}
  .kwbadge .kwart{width:30px;height:30px}
  .kwic.impic{flex:0 0 52px;width:52px;height:52px;font-size:34px;display:inline-flex;align-items:center;justify-content:center}
  /* 🎖 ONE CELL FOR EVERY IMPRINT.
     2026-07-30: never pin BOTH axes without object-fit — that stretched medallions into ovals.
     2026-08-03 impsize1: width-driven so narrower canvases didn't shrink vs wider ones — still
     left gold-circle diameters uneven because source crops differed (spikes/fabric filled some
     plates more than others).
     2026-08-05 impsize2: assets re-normalized to 144×144 with matched gold-circle diameter
     (~100px); square contain slot so list / badges / tips all share one visual scale. */
  .kwic.impic .impart{width:44px;height:44px;object-fit:contain;display:block}
  .statart{height:1.5em;width:auto;vertical-align:-.32em;margin-right:4px;display:inline-block;object-fit:contain}
  #compDetail h4 .statart,#compDetail h4 img.statart{height:1.35em;width:auto;vertical-align:-.28em;display:inline-block;object-fit:contain}
  .compBig .statart,.compBig img.statart{max-height:88px;max-width:88px;width:auto;height:auto;display:block;object-fit:contain;margin:0}
  .dtag{display:inline-block;border:1px solid var(--panel-edge);border-radius:5px;padding:1px 8px;font-size:10px;letter-spacing:.1em;text-transform:uppercase;margin:0 6px 0 0;color:var(--ink-dim)}
  .statgrid{display:grid;grid-template-columns:auto 1fr;gap:5px 18px;font-size:13.5px;margin:10px 0;max-width:420px}
  .statgrid span{color:var(--ink-dim)}
  .statgrid b{color:var(--ink);font-weight:700}
  .compBig{font-size:38px;margin-right:12px;width:50px;height:50px;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}
  .compProse{font-size:13.5px;color:var(--ink-dim);line-height:1.65}
  .pill{background:var(--ss-surface-panel);border:1px solid var(--ss-border-subtle);border-radius:var(--ss-radius-md);padding:6px 14px;font-size:14px}
  .goldtxt{color:var(--gold);font-weight:700}

  .port{width:86px;height:104px;border-radius:10px;border:2px solid var(--panel-edge);
    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
    background:linear-gradient(180deg,#241f45,#171331);position:relative;flex:none}
  .port .em{font-size:38px;line-height:1}
  .port .pn{font-family:'Cinzel',serif;font-size:10px;letter-spacing:.05em;text-align:center;padding:0 4px}
  .port.mini{width:36px;height:36px;border-radius:50%;border-width:1.5px;background:#141028;overflow:hidden;justify-content:center} /* framed to match the coin avatars (Josh 2026-07-21) */
  .port.mini .em{font-size:17px}

  #clash{position:absolute;inset:0;z-index:40;display:flex;flex-direction:column;align-items:center;justify-content:center;
    background:#0b0918d9;overflow:hidden;cursor:pointer}
  .clashRow{display:flex;align-items:center;gap:26px;margin:7px 0}
  .clashRow .vs{font-family:'Cinzel',serif;font-weight:900;font-size:24px;color:var(--foe);opacity:0;transform:scale(3)}
  .clashRow .port{opacity:0}
  @keyframes slamL{0%{transform:translateX(-1344px) rotate(-6deg);opacity:1}70%{transform:translateX(6px);opacity:1}100%{transform:translateX(0);opacity:1}}
  @keyframes slamR{0%{transform:translateX(1344px) rotate(6deg);opacity:1}70%{transform:translateX(-6px);opacity:1}100%{transform:translateX(0);opacity:1}}
  /* 🏔 THE ASCENSION SWEEP (Josh 2026-07-29: "ASCENSION text flying in from the left center, slowing and moving in
   the center for about 3 seconds, then flying off again after").
   One keyframe run covers all three phases, so there is nothing to sequence and nothing to get out of step:
   in from the left, a still hold, out to the right. The percentages are derived from ASC_SWEEP_T rather than
   guessed — 0.7s in, 2.5s held, 0.7s out of a 3.9s run is 17.9% and 82.1%, rounded to 18/82.
   The hold has been retimed three times: 3.0s on first pass, cut to 1.0s, then 2.0s, then +0.5s to 2.5s
   (Josh 2026-07-29: "make it last 0.5 seconds longer"). The TRAVEL is untouched at every step, so it always
   arrives and leaves with the same weight — only the pause in the middle moves, which is why the two stops and
   ASC_SWEEP_T are the only numbers that ever need to change.
   The per-keyframe timing functions are what make it read as physical: DECELERATING on the way in (it arrives
   and settles), linear through the hold, ACCELERATING on the way out (it leaves). A single ease on the whole
   animation would have it drifting through the hold, which is the thing that looks cheap. */
@keyframes ascSweep{
  0%{transform:translateX(-1190.4px);opacity:0;animation-timing-function:cubic-bezier(.16,.84,.28,1)}
  18%{transform:translateX(0);opacity:1;animation-timing-function:linear}
  82%{transform:translateX(0);opacity:1;animation-timing-function:cubic-bezier(.72,0,.84,.16)}
  100%{transform:translateX(1190.4px);opacity:0}
}
/* ⚡ THE WORD LANDS ON THE IMPACT (Josh 2026-08-01: "I like the ASCENSION animation in the stinger to be the
   other option, where it pops up right when the islands clash together. then fades away.")

   THIS REPLACES THE SWEEP, WHICH IS KEPT ABOVE. The sweep is a fine animation and it is still one line away
   (`t.classList.add('ascSweep')` in runClash), but it was written against a plain dark screen, where it had
   to supply the motion itself. There is a cinematic behind the word now, and the cinematic has a moment: two
   isles meet at 2.2s with a white flash and a 1s shake. A word gliding past on its own schedule ignores that;
   a word that ARRIVES on the hit is caused by it.

   THE WHOLE ANIMATION IS OFFSET, NOT DELAYED, and that is the one thing to be careful with. The `::after`
   glint overlay and #ascFx both time themselves as fractions of `--ascT`, so a CSS `animation-delay` on the
   title would slide the word out of step with its own glint. Instead the run STARTS at t=0 with the word
   invisible and the impact sits at a percentage inside it — 1.5s of a 3.1s run is 48.4%, so the beats below
   are pinned to 48%. One clock, as before; only the shape of the curve has changed.

   The overshoot is deliberate: 1.5 → 0.96 → 1 reads as something struck rather than something faded in. */
@keyframes ascPop{
  0%  {opacity:0;transform:scale(1.5);animation-timing-function:linear}
  48% {opacity:0;transform:scale(1.5);animation-timing-function:cubic-bezier(.16,.9,.3,1)}  /* ← THE HIT: 1.5s of 3.1s */
  55% {opacity:1;transform:scale(.96);animation-timing-function:cubic-bezier(.4,0,.2,1)}
  61% {opacity:1;transform:scale(1);animation-timing-function:linear}
  76% {opacity:1;transform:scale(1)}
  100%{opacity:0;transform:scale(1.04)}
}
/* The wordmark is the whole screen here, not a caption under a bracket, so it drops the 26px top margin the
   tournament title carries and takes its own weight. */
#clashTitle.ascSweep{margin-top:0;font-size:clamp(30px,134.4px,86px);letter-spacing:.28em;
  text-shadow:0 0 26px rgba(232,182,76,.45),0 4px 18px rgba(0,0,0,.55);white-space:nowrap;
  position:relative;z-index:2;transform-origin:50% 50%}
/* 🌩 ASCENSION STORM (Josh 2026-07-29: "can you add cross screen sparks and lightning and a glint that runs
   along the word from left to right").
   EVERY duration here is a fraction of --ascT, which runClash sets from ASC_SWEEP_T. That is the whole trick:
   the linger has now been retimed four times, and each retime would otherwise have left the storm firing
   against a word that had already left — or worse, still crackling over an empty screen. Nothing in this
   block names a number of seconds. */
@keyframes ascGlint{
  0%{background-position:-130% 0;opacity:0}
  12%{opacity:1}
  88%{opacity:1}
  100%{background-position:230% 0;opacity:0}
}
/* The glint is a SECOND copy of the word laid exactly over the first, clipped to the glyphs, with a bright
   band sliding through it. Done this way rather than by clipping the title itself, because the title is solid
   gold with two text-shadows — turning it into a background-clip surface would throw both away, and the
   shadows are what let it read against a bright sky. content:attr() takes the word from data-word so the
   sweep code stays the only place that decides what it says. */
#clashTitle.ascSweep::after{
  content:attr(data-word);position:absolute;left:0;top:0;white-space:nowrap;pointer-events:none;
  color:transparent;
  /* text-shadow INHERITS, and a shadow is painted from the glyph silhouette regardless of the text colour —
     so without this the overlay would redraw the title's gold glow on top of the real one, doubling it for
     the whole run rather than only where the band passes. */
  text-shadow:none;
  background-image:linear-gradient(100deg,rgba(255,255,255,0) 42%,rgba(255,255,255,.98) 50%,rgba(255,255,255,0) 58%);
  background-size:260% 100%;background-repeat:no-repeat;
  -webkit-background-clip:text;background-clip:text;
  /* ⚡ RETIMED WITH THE WORD (2026-08-01). This ran from 24% to 64% of the run, which was right while the
     word slid in at 18% — and became wrong the moment the word stopped appearing until 58%: the glint
     would have swept across an EMPTY SCREEN and finished just as the word arrived. Both numbers are still
     fractions of --ascT, so the pair stays in step through any future retime; they now start after the
     pop has landed (64%) and finish before the fade begins (86%). */
  animation:ascGlint calc(var(--ascT,3.9s)*0.22) cubic-bezier(.35,0,.65,1) calc(var(--ascT,3.9s)*0.64) both;
}
#ascFx{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden}
/* Two pulses, on the first two strikes. A wash rather than a white flash: the clash background is already
   near-black, and a full-frame flash at this size is unpleasant rather than dramatic. */
@keyframes ascWash{
  0%{opacity:0}5%{opacity:.55}13%{opacity:.08}19%{opacity:.4}30%{opacity:0}100%{opacity:0}
}
#ascGlow{position:absolute;inset:0;opacity:0;
  background:radial-gradient(ellipse at 50% 42%,rgba(150,200,255,.30),rgba(120,90,210,.16) 45%,transparent 72%);
  animation:ascWash var(--ascT,3.9s) linear both}
#ascBolts{position:absolute;inset:0;width:100%;height:100%}
/* pathLength="1" on every bolt, so one dasharray draws all of them regardless of their real length — a bolt
   redrawn by hand later needs no new numbers here. */
.ascBolt{fill:none;stroke:#d8ecff;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;opacity:0;
  stroke-dasharray:1;stroke-dashoffset:1;
  filter:drop-shadow(0 0 6px rgba(160,210,255,.95)) drop-shadow(0 0 18px rgba(120,90,210,.7));
  animation:ascStrike calc(var(--ascT,3.9s)*0.26) linear calc(var(--ascT,3.9s)*var(--d,0)) both}
/* Draw fast, then flicker and fade — a bolt that fades out evenly reads as a drawn line, not a discharge. */
@keyframes ascStrike{
  0%{opacity:0;stroke-dashoffset:1}
  14%{opacity:1;stroke-dashoffset:0}
  26%{opacity:.2}
  38%{opacity:.95}
  62%{opacity:.35}
  100%{opacity:0;stroke-dashoffset:0}
}
/* Sparks cross the WHOLE screen — they start off the left edge and end off the right, so none of them is
   seen to appear or vanish in frame. */
@keyframes ascSparkFly{
  0%{transform:translate3d(0,0,0);opacity:0}
  10%{opacity:1}
  82%{opacity:1}
  100%{transform:translate3d(2457.6px,var(--dy,0px),0);opacity:0}
}
.ascSpark{position:absolute;left:-268.8px;top:var(--y,50%);height:2px;width:var(--len,90px);border-radius:2px;
  opacity:0;transform:translate3d(0,0,0);
  background:linear-gradient(90deg,rgba(255,226,150,0),rgba(255,230,168,.85) 62%,#fff);
  filter:drop-shadow(0 0 6px rgba(255,200,90,.85));
  animation:ascSparkFly calc(var(--ascT,3.9s)*var(--v,0.5)) linear calc(var(--ascT,3.9s)*var(--t,0)) both}
@media(prefers-reduced-motion:reduce){
  #ascFx{display:none}
  #clashTitle.ascSweep::after{display:none}
}
@keyframes vsPop{0%{opacity:0;transform:scale(3)}60%{opacity:1;transform:scale(.9)}100%{opacity:1;transform:scale(1)}}
  @keyframes flashK{0%{opacity:.9}100%{opacity:0}}
  .flash{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none}
  #clashTitle{font-family:'Cinzel',serif;font-weight:900;letter-spacing:.2em;font-size:clamp(22px,76.8px,44px);
    color:var(--gold);margin-top:26px;opacity:0;transition:opacity .6s}
  #clashSkip{position:absolute;bottom:18px;right:22px;font-size:12px;color:var(--ink-dim)}

  /* 🧭 Ascent preview — sits exactly where the standings panel does, since it replaces it */
  #ascentPanel{position:absolute;top:12px;right:12px;z-index:20;width:212px;background:var(--panel);
    border:1px solid var(--panel-edge);border-radius:12px;padding:8px 10px;font-size:12px;
    /* capped so a big camp scrolls instead of growing down over the shop UI (Josh 2026-07-27) */
    max-height:calc(1080px - 108px);overflow-y:auto}
  #ascentPanel h3{font-family:'Cinzel',serif;font-size:11px;letter-spacing:.2em;color:var(--foe);margin-bottom:2px;text-transform:uppercase}
  #ascFlavor{font-size:10px;color:var(--ink-dim);font-style:italic;margin-bottom:6px;line-height:1.3}
  #ascentPanel .tip{font-size:10.5px;color:var(--ink-dim);margin-top:7px;text-align:center;line-height:1.35}
  .ascrow{display:flex;gap:6px;align-items:center;margin-bottom:4px;border-radius:8px;padding:2px 4px;
    background:#241d3f;border:1px solid transparent;cursor:help}
  .ascrow:hover{border-color:var(--foe);background:#2d2247}
  /* Sized for the painted portrait, with the emoji fallback still centred in the same slot. 26px is what the
     two-line name beside it already occupies, so the rows do not grow. */
  .ascic{width:28px;text-align:center;font-size:15px;flex:0 0 28px}
  .ascic .artic{height:auto;width:auto;max-height:26px;max-width:28px;vertical-align:middle;display:block;margin:0 auto}
  .ascnm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px}
  .ascct{font-size:12px;color:var(--gold);font-weight:700;flex:0 0 auto}
  .asctier{font-size:9px;color:var(--ink-dim);letter-spacing:.06em}
  #ascMeta{margin-top:8px;padding-top:7px;border-top:1px solid var(--panel-edge);
    font-size:10.5px;color:var(--ink-dim);line-height:1.5}
  #ascMeta b{color:var(--ink)}
  /* 🔧 The cogwheel duplicates the menu's SETTINGS button, so hide it while the title screen is up —
     it is still the only way into settings mid-run. Driven off #titleScreen's own class rather than a JS
     toggle, because every screen-hiding id list in this file has been missed at least once. The two are
     real siblings with the button later in the DOM, so the general sibling combinator applies.
     (Josh 2026-07-27) */
  #titleScreen:not(.hidden) ~ #setRow,#titleScreen:not(.hidden) ~ #compRow{display:none !important}
  /* 🗺️ ZONES and HELP: deploy-only, bottom-right of the planning screen.
     They used to sit left of the cog column (or in it); right side keeps the left clear for the roster. */
  .deployrow{position:fixed;right:12px;bottom:var(--sky-dock-bottom);left:auto;z-index:200;display:none;gap:8px;align-items:center}
  /* ⏱ SPEED ROW — bottom-right. Battle docks to the corner; deploy lifts it above HELP/ZONES/TARGETS.
     `display` is JS-driven (place + battle), so the lift hangs off `#placeHud:not(.hidden)` rather than
     a “battle is running” selector. */
  .speedrow{position:fixed;right:12px;bottom:var(--sky-dock-bottom);left:auto;z-index:200;display:none;gap:5px;align-items:center}
  /* ⏱ ⬆ CLEAR HELP/ZONES/TARGETS WHILE DEPLOYING (Josh 2026-07-31). Offset is derived from
     `--ss-control-height` + dock vars so theme control height changes cannot recreate the old overlap.
     Battle keeps the base dock position (Dave 2026-08-07: drop into the corner once the toggles hide). */
  #placeHud:not(.hidden) ~ .speedrow{
    bottom:calc(var(--sky-dock-bottom) + var(--ss-control-height,40px) + var(--sky-dock-gap))}
  .spdBtn{font-family:'Cinzel',serif;font-size:10px;letter-spacing:.06em;padding:4px 8px;min-width:30px;
    border-radius:6px;background:rgba(16,13,30,.82);border:1px solid rgba(150,150,170,.35);
    color:var(--ink-dim);cursor:pointer;line-height:1.1}
  .spdBtn:hover{border-color:rgba(232,182,76,.6);color:var(--ink)}
  .spdBtn.on{color:var(--gold);border-color:var(--gold);box-shadow:0 0 8px rgba(232,182,76,.25)}
  .spdBtn.locked,.spdBtn:disabled{opacity:.35;cursor:not-allowed;pointer-events:none}
  .spdBtn.locked:hover,.spdBtn:disabled:hover{border-color:rgba(150,150,170,.35);color:var(--ink-dim)}
  #placeHud:not(.hidden) ~ .deployrow{display:flex}
  #standings{position:absolute;top:12px;right:12px;z-index:20;width:224px;background:var(--panel);
    border:1px solid var(--panel-edge);border-radius:12px;padding:10px 12px;font-size:13px}
  #standings h3{font-family:'Cinzel',serif;font-size:11px;letter-spacing:.2em;color:var(--ink-dim);margin-bottom:8px;text-transform:uppercase}
  #standings .tip{font-size:10.5px;color:var(--ink-dim);margin-top:6px;text-align:center}
  .strow{display:flex;gap:8px;align-items:center;margin-bottom:7px;border-radius:8px;padding:2px 4px}
  .strow.click{cursor:pointer}
  .strow.click:hover{background:#2a2450}
  .strow .mid{flex:1;min-width:0}
  .strow .nm{display:flex;justify-content:space-between;font-size:12.5px;margin-bottom:2px}
  .strow.me .nm{color:var(--gold);font-weight:700}
  .strow.dead{opacity:.35}
  .strow.dead .nm{text-decoration:line-through}
  .hpbar{height:5px;border-radius:3px;background:#0d0b1d;overflow:hidden}
  .hpbar i{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,#4fae87,var(--friend))}
  .strow.me .hpbar i{background:linear-gradient(90deg,#c08c2c,var(--gold))}

  #prep{position:absolute;inset:0;z-index:15;display:flex;flex-direction:column;pointer-events:none}
  #prep>*{pointer-events:auto}
  #prepHead{display:flex;gap:10px;align-items:center;padding:12px 16px;flex-wrap:wrap}
  #prepHead .title{font-family:'Cinzel',serif;font-weight:700;font-size:18px;letter-spacing:.08em;color:var(--gold)}
  /* Income delta nested in #goldPill — same size/family as the amount (prep-layout owns the stack). */
  #goldPill #incomeTxt{font-size:inherit;color:var(--gold);font-weight:700}
  #prepTimer{font-family:var(--ss-font-ui,'Source Sans 3','Segoe UI',system-ui,sans-serif);font-size:18px;color:var(--arcane);min-width:60px;text-align:center}
  #readyBtn{margin-left:auto;margin-right:246px;font-family:'Cinzel',serif;font-weight:700;letter-spacing:.12em;
    padding:10px 26px;border-radius:9px;border:1px solid var(--gold);cursor:pointer;color:#1c1430;
    background:linear-gradient(180deg,#f6d98a,var(--gold) 60%,#c08c2c)}
  #prepBody{flex:1;display:flex;gap:12px;padding:0 16px 14px;min-height:0}
  .prepPanel{background:rgba(28,24,54,.92);border:1px solid var(--ss-border-subtle);border-radius:14px;padding:12px;overflow-y:auto}
  .pgut{flex:0 0 7px;cursor:col-resize;border-radius:4px;align-self:stretch;margin:14px 0}
  .pgut:hover,.pgut.drag{background:var(--panel-edge)}
  #townPanel{flex:1 1 300px;min-width:230px;display:flex;flex-direction:column;min-height:0}
  #shopPanel{flex:1.35 1 380px;min-width:260px}
  #armyPanel{flex:1.1 1 300px;min-width:240px;margin-right:232px}
  .prepPanel h2{font-family:'Cinzel',serif;font-size:13px;letter-spacing:.22em;color:var(--ink-dim);
    text-transform:uppercase;margin-bottom:10px;border-bottom:1px solid var(--panel-edge);padding-bottom:7px}
  .townMain{flex:1;min-width:0;min-height:0;overflow-y:auto}
  .bld{display:flex;align-items:center;gap:8px;border:1px solid #2e2757;border-radius:9px;
    padding:7px 9px;margin-bottom:7px;background:#171331;font-size:12.5px}
  .bld .ic{font-size:19px;width:46px;height:40px;flex:0 0 auto;display:flex;align-items:center;justify-content:center}
  .bld .bmid{flex:1;min-width:0}
  .bld .bn{font-weight:700;font-size:13px}
  .bld .be{color:var(--ink-dim);font-size:11px;line-height:1.35}
  .pips{display:inline-flex;gap:2px;margin-left:6px;vertical-align:1px}
  .pips i{width:6px;height:6px;border-radius:2px;background:#2e2757}
  /* `.lit`, not `.on` — see the note on pips() in assets/game/05-town.js. A bare `.on` is the theme's
     selected-control skin in assets/ui-theme-baseline.css and it carries !important. */
  .pips i.lit{background:var(--gold)}
  .ubtn{font-family:inherit;font-size:11.5px;padding:5px 8px;border-radius:7px;cursor:pointer;
    border:1px solid var(--ss-border-subtle);background:var(--ss-surface-control);color:var(--ss-text-on-dark);flex:none}
  .ubtn:hover:not(:disabled){background:var(--ss-surface-control-hover);border-color:#8a6f34}
  .ubtn:disabled{opacity:.38;cursor:default}
  .ubtn .c{color:var(--gold);font-weight:700}
  #oddsBox{flex:0 0 auto;width:100%;margin-top:auto;padding:6px 2px 0;font-size:11px;color:var(--ink-dim);
    line-height:1.45;text-align:right;font-variant-numeric:tabular-nums}
  #oddsBox b{font-weight:700;color:var(--gold)}
  #oddsBox span{font-weight:700}
  .or0{color:var(--r0)}.or1{color:var(--r1)}.or2{color:var(--r2)}.or3{color:var(--r3)}

  #shopCards{display:flex;flex-direction:column;gap:6px;padding:0 16px} /* gutter so in-army arrows sit outside cards without clipping */
  .card{border:1px solid;border-radius:11px;padding:6px 9px;background:var(--ss-surface-card);position:relative;overflow:hidden}
  .card .cardArtBg{position:absolute;right:-6%;top:50%;transform:translateY(-50%) scaleX(-1);height:165%;width:auto;max-width:none;filter:blur(2.5px);opacity:.5;z-index:0;pointer-events:none;object-fit:contain}
  /* ▶◀ already-in-army — arrows live JUST OUTSIDE the card; tips never cross the border (Josh 2026-07-29) */
  .card.inArmy{overflow:visible}
  .card.inArmy::before,.card.inArmy::after{
    content:"";position:absolute;top:50%;width:0;height:0;z-index:3;pointer-events:none;
    border-style:solid;opacity:.75;
    filter:drop-shadow(0 0 4px rgba(232,182,76,.55));
  }
  .card.inArmy::before{
    left:0;
    border-width:8px 0 8px 11px;border-color:transparent transparent transparent var(--gold);
    /* tip = right edge of the 11px triangle; keep it ≥2px outside the card left edge */
    transform:translate(calc(-100% - 2px),-50%);
    animation:inArmyL 1.05s ease-in-out infinite;
  }
  .card.inArmy::after{
    right:0;
    border-width:8px 11px 8px 0;border-color:transparent var(--gold) transparent transparent;
    transform:translate(calc(100% + 2px),-50%);
    animation:inArmyR 1.05s ease-in-out infinite;
  }
  @keyframes inArmyL{
    0%,100%{transform:translate(calc(-100% - 14px),-50%);opacity:.4}
    50%{transform:translate(calc(-100% - 2px),-50%);opacity:1}
  }
  @keyframes inArmyR{
    0%,100%{transform:translate(calc(100% + 14px),-50%);opacity:.4}
    50%{transform:translate(calc(100% + 2px),-50%);opacity:1}
  }
  .card .top,.card .cd,.card .buy{position:relative;z-index:1}
  .card.t0{border-color:var(--r0)}.card.t1{border-color:var(--r1)}.card.t2{border-color:var(--r2)}.card.t3{border-color:var(--r3);box-shadow:0 0 14px rgba(232,182,76,.22)}.card.t4{border-color:var(--r4);box-shadow:0 0 14px rgba(180,245,255,.28)}
  .card .top{display:flex;align-items:center;gap:8px}
  .card .cmeta{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
  .card .cmeta .tag{margin-left:auto;align-self:flex-end}
  .card .cmeta .pillrow{margin:1px 0 0}
  .card .cmeta .cdmeta{margin:2px 0 0}
  .card .qty{color:var(--gold);font-weight:700;margin-left:5px}
  .card .cic{font-size:20px;width:44px;height:44px;flex:0 0 auto;display:flex;align-items:center;justify-content:center}
  .card .cn{font-weight:700;font-size:14px}
  .card .cfor{display:block;font-size:10px;font-weight:600;letter-spacing:.04em;line-height:1.2;color:var(--ink-dim)}
  .card .tag{margin-left:auto;font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;padding:1px 6px;border-radius:4px;border:1px solid currentColor;white-space:nowrap}
  .mpill{display:inline-block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;padding:0 5px;border-radius:4px;border:1px solid currentColor;color:var(--ink-dim);margin-right:4px;line-height:14px;white-space:nowrap;vertical-align:middle}
  .mpill.tier-1{color:#c98f5e;border-color:rgba(201,143,94,.45)}
  .mpill.tier-2{color:#b8c4d6;border-color:rgba(184,196,214,.45)}
  .mpill.tier-3{color:var(--gold);border-color:rgba(232,182,76,.55)}
  .mpill.tier-4{color:#ff7ba6;border-color:rgba(255,123,166,.6);text-shadow:0 0 6px rgba(255,123,166,.35)}
  .mpill.tier-5{color:#c8f8ff;border-color:rgba(180,245,255,.75);text-shadow:0 0 8px rgba(200,248,255,.55)}
  .mpill.ty-light{color:#8fd8ff}.mpill.ty-medium{color:var(--gold)}.mpill.ty-heavy{color:var(--violet)}.mpill.ty-special{color:var(--arcane)}
  .pillrow{margin:1px 0 2px}
  .card.t0 .tag{color:var(--r0)}.card.t1 .tag{color:var(--r1)}.card.t2 .tag{color:var(--r2)}.card.t3 .tag{color:var(--r3)}.card.t4 .tag{color:var(--r4)}
  .card .cd{font-size:11.5px;color:var(--ink-dim);margin:3px 0 5px;line-height:1.34}
/* 🔮 SPELL UPGRADE PREVIEW — what the next tier actually buys, on the card you are choosing.
   A grid rather than a sentence: the eye compares two columns of numbers far faster than it parses
   "damage rises from 55 to 77". The arrow column is fixed-width so every row's → lines up. The NEW
   value is gold and the old one dimmed, so the improvement is what you read first. */
.spup{margin:5px 0 2px;padding-top:5px;border-top:1px solid rgba(255,255,255,.10);
  display:grid;grid-template-columns:1fr auto 12px auto;gap:2px 6px;align-items:baseline}
/* the row wrapper must NOT become a grid cell itself — display:contents lifts its four spans into
   the parent grid so the arrows and numbers align down the card instead of each row laying itself
   out independently */
.spuprow{display:contents}
.spuplab{font-size:10.5px;color:var(--ink-dim);letter-spacing:.02em}
.spupfrom{font-size:11px;color:var(--ink-dim);opacity:.75;text-align:right;font-variant-numeric:tabular-nums}
.spuparr{font-size:10px;color:var(--ink-dim);opacity:.6;text-align:center}
.spupto{font-size:12px;color:var(--gold);font-weight:700;text-align:right;font-variant-numeric:tabular-nums}
  .card .buy{padding:6px 10px;font-size:13px}
  .card .top{gap:9px}
  .card .cic.framed{margin:-2px 0}
  .card .buy{width:100%}
  .card.sold{opacity:.45}
  #rollRow{display:flex;gap:8px;margin-bottom:10px}
  #rollBtn{flex:1;font-family:'Cinzel',serif;font-weight:700;letter-spacing:.08em;font-size:13px;
    padding:9px;border-radius:9px;border:1px solid var(--violet);background:#241f4a;color:var(--violet);cursor:pointer}
  #rollBtn:hover:not(:disabled){background:#2f2963}
  #rollBtn:disabled{opacity:.4;cursor:default}
  #lockBtn{width:52px;font-size:17px;border-radius:9px;border:1px solid var(--panel-edge);background:#241f4a;color:var(--ink-dim);cursor:pointer}
  #lockBtn.on{border-color:var(--gold);color:var(--gold);box-shadow:0 0 10px rgba(232,182,76,.3)}
  #lockBtn img{height:26px;width:auto;max-width:40px;display:block;margin:0 auto;object-fit:contain}

  .abat{display:flex;align-items:center;gap:8px;border:1px solid #2e2757;border-radius:9px;
    padding:6px 9px;margin-bottom:6px;background:#171331;font-size:12px}
  #placeMine .abat,#placeFoe .abat{flex-wrap:nowrap;padding:7px 10px}
  #placeMine .abat .statline,#placeFoe .abat .statline{flex-wrap:wrap;gap:7px 10px;row-gap:2px} /* stats wrap instead of clipping */
  #placeMine .abat .cnt,#placeFoe .abat .cnt{margin-left:2px}
  .abat .ic{font-size:34px;flex:0 0 auto;display:inline-flex;align-items:center;gap:2px}
  .abat .ic .artic{max-height:58px;max-width:58px;height:auto;width:auto;vertical-align:middle}
  /* Deploy / recap banner rows share uart — cover-crop so Bannerman is not dwarfed by the pole. */
  .abat .ic[data-u="banner"] .artic,.abat .ic .uart[data-u="banner"]{width:58px;height:58px;max-width:58px;max-height:58px;object-fit:cover;object-position:center 64%}
  td .artic{max-height:22px;max-width:22px;height:auto;width:auto;vertical-align:-5px}
  .impart{height:1.15em;width:1.15em;object-fit:contain;vertical-align:-.22em}
  .impbadge{display:inline-flex;align-items:center}
  .abat .ic .impbadge .impart{height:30px;width:30px;object-fit:contain}
  .compProse .impart{height:1.5em;width:1.5em;object-fit:contain;vertical-align:-.35em;margin-right:2px}
  .compbtnart{height:1.35em;width:auto;vertical-align:-.30em}
  #compHdr{display:inline-flex;align-items:center;gap:12px;justify-content:center}
  .compHdrArt{height:1.7em;width:auto;filter:drop-shadow(0 2px 6px rgba(0,0,0,.5))}
  .abat .amid{flex:1;min-width:0}
  .abat .an{font-weight:700;font-size:12.5px}
  .abat .as{color:var(--ink-dim);font-size:11px}
  .abat .cnt{font-weight:700;white-space:nowrap}
  .abat.empty{border-style:dashed;color:var(--ink-dim)}
  .abat .disb{font-size:10px;padding:3px 6px}
  .spellchips{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}
  .schip{border:1px solid #2e2757;border-radius:7px;padding:3px 8px;font-size:11.5px;background:#171331}

  #placeHud{position:absolute;inset:0;z-index:15;pointer-events:none}
  #placeHud>*{pointer-events:auto}
  /* Hint/spells sit over the field but must never steal clicks — #placeHud>* would otherwise win
     over a bare placeHint pointer-events none rule (Dave 2026-08-07: hover-a-unit "froze" deploy). */
  #placeHud>#placeHint,#placeHud>#placeSpells{pointer-events:none}
  /* Title on its own centred line above the controls (Josh 2026-07-27). As one flex ROW the GROUP was
     centred, which put the title left of centre with BEGIN BATTLE shoving it across — "DEPLOY VS CLOUD
     RUFFIANS" was never actually in the middle of anything. */
  #placeTop{position:absolute;top:10px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;
    gap:6px;align-items:center}
  #placeTopRow{display:flex;gap:12px;align-items:center;justify-content:center}
  /* ⏱ Timer BELOW the matchup, both centred (Josh 2026-08-07).
     History: right-of-title absolute overflowed the chip; a 1fr|auto|1fr grid kept the title centred
     with the clock beside it. New ask puts the clock under DEPLOY vs … so a simple column flex is enough —
     title and clock each centre on the same axis, and the clock stays a SIBLING (never nested in
     #placeTitle) because textContent='DEPLOY vs …' would wipe a child node. */
  #placeTitleRow{display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:6px;width:auto;max-width:min(920px,92vw)}
  #placeTop .title{font-family:'Cinzel',serif;font-weight:700;font-size:16px;letter-spacing:.1em;color:var(--gold);
    text-align:center}
  #placeTimer{font-family:'Cinzel',serif;font-size:20px;color:var(--arcane);
    align-self:center;line-height:1;white-space:nowrap;text-align:center}
  #placeGo{font-family:'Cinzel',serif;font-weight:700;letter-spacing:.1em;padding:8px 22px;border-radius:9px;
    border:1px solid var(--gold);cursor:pointer;color:#1c1430;background:linear-gradient(180deg,#f6d98a,var(--gold) 60%,#c08c2c)}
  /* Deploy overlay toggles — clear the field without opening Settings (Dave 2026-07-28) */
  .placeOvBtn{font-family:'Cinzel',serif;font-size:11px;letter-spacing:.1em;padding:6px 12px;border-radius:8px;
    border:1px solid var(--panel-edge);background:rgba(20,16,44,.92);color:var(--ink-dim);cursor:pointer}
  .placeOvBtn.on{color:var(--friend);border-color:var(--friend);box-shadow:0 0 10px rgba(95,211,168,.22)}
  .placeOvBtn.off{color:var(--foe);border-color:rgba(232,106,106,.55)}
  #placeHint.hidden{display:none!important}
  #placeFoe{position:absolute;top:64px;right:12px;width:370px;background:var(--panel);border:1px solid var(--panel-edge);
    border-radius:12px;padding:12px 15px;font-size:15px;max-height:calc(1080px - 130px);overflow-y:auto}
  #placeFoe .pfHead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
  #placeFoe h3{font-family:'Cinzel',serif;font-size:14px;letter-spacing:.18em;color:var(--foe);margin:0;text-transform:uppercase}
  #placeFoe .pfMinBtn{flex:0 0 auto;width:28px;height:28px;padding:0;border-radius:6px;border:1px solid var(--panel-edge);
    background:rgba(20,16,44,.92);color:var(--ink-dim);cursor:pointer;font-family:'Cinzel',serif;font-size:18px;line-height:1;
    display:inline-flex;align-items:center;justify-content:center}
  #placeFoe .pfMinBtn:hover{color:var(--foe);border-color:var(--foe)}
  #placeFoe.pfMin{max-height:none;overflow:hidden}
  #placeFoe.pfMin .pfHead{margin-bottom:0}
  #placeFoe.pfMin #pfBody{display:none}
  #placeMine{position:absolute;top:64px;left:12px;width:370px;background:var(--panel);border:1px solid var(--panel-edge);
    border-radius:12px;padding:12px 15px;font-size:15px;max-height:calc(1080px - 130px);overflow-y:auto} /* expanded to FIT the cards (Josh 2026-07-21); +25% type/icons Dave 2026-08-07 */
  #placeMine .pmHead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
  #placeMine h3{font-family:'Cinzel',serif;font-size:14px;letter-spacing:.18em;color:var(--gold);margin:0;text-transform:uppercase}
  #placeMine .pmMinBtn{flex:0 0 auto;width:28px;height:28px;padding:0;border-radius:6px;border:1px solid var(--panel-edge);
    background:rgba(20,16,44,.92);color:var(--ink-dim);cursor:pointer;font-family:'Cinzel',serif;font-size:18px;line-height:1;
    display:inline-flex;align-items:center;justify-content:center}
  #placeMine .pmMinBtn:hover{color:var(--gold);border-color:var(--gold)}
  #placeMine.pmMin{max-height:none;overflow:hidden}
  #placeMine.pmMin .pmHead{margin-bottom:0}
  #placeMine.pmMin #pmBody{display:none}
  /* Deploy rails only — ~25% larger type + unit icons (Dave 2026-08-07). Leaves scout/end-screen .abat alone. */
  #placeMine .abat,#placeFoe .abat{padding:9px 12px;font-size:15px}
  #placeMine .abat .ic,#placeFoe .abat .ic{font-size:43px}
  #placeMine .abat .ic .artic,#placeFoe .abat .ic .artic{max-height:73px;max-width:73px}
  /* Bannerman: do NOT keep the 73px Dave bump — the place portrait cell is 53×60
     (ui-theme-baseline) with overflow:hidden, so 73px was clipped off-centre. Fill the
     cell like other troops, then cover-crop the flagpole so the body sits in the cut-out. */
  #placeMine .abat .ic[data-u="banner"] .artic,#placeMine .abat .ic .uart[data-u="banner"],
  #placeFoe .abat .ic[data-u="banner"] .artic,#placeFoe .abat .ic .uart[data-u="banner"]{
    width:100%;height:100%;max-width:50px;max-height:58px;
    object-fit:cover;object-position:center 42%}
  #placeMine .abat .ic .impbadge .impart,#placeFoe .abat .ic .impbadge .impart{height:38px;width:38px}
  #placeMine .abat .an,#placeFoe .abat .an{font-size:16px}
  #placeMine .abat .as,#placeFoe .abat .as{font-size:14px}
  #placeMine .abat .cnt,#placeFoe .abat .cnt{font-size:16px}
  #placeMine .schip,#placeFoe .schip{font-size:14px;padding:4px 10px}
  #placeHint{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);font-size:12.5px;line-height:1.55;
    max-width:min(880px,1766.4px);text-align:center;color:var(--ink);text-shadow:0 1px 3px #000;
    background:linear-gradient(180deg,rgba(20,16,44,.90),rgba(12,9,28,.94));border:1px solid var(--panel-edge);
    border-radius:11px;padding:9px 16px;box-shadow:0 6px 22px rgba(0,0,0,.5);
    pointer-events:none;z-index:6}
  #placeHint b{color:var(--gold)}
  /* spellbook + keybindings, sitting directly above the hint (Josh 2026-07-26) */
  #placeSpells{position:absolute;bottom:78px;left:50%;transform:translateX(-50%);display:flex;align-items:center;justify-content:center;
    gap:8px;flex-wrap:wrap;width:fit-content;max-width:min(880px,1766.4px);z-index:6;pointer-events:none}
  #placeSpells .psLbl{font-family:'Cinzel',serif;font-size:9.5px;letter-spacing:.2em;color:var(--ink-dim);text-transform:uppercase}
  #placeSpells .ps{display:inline-flex;align-items:center;gap:6px;padding:5px 10px 5px 6px;border-radius:9px;
    background:linear-gradient(180deg,rgba(30,24,62,.94),rgba(16,12,38,.96));border:1px solid var(--panel-edge);
    box-shadow:0 4px 14px rgba(0,0,0,.45);pointer-events:auto;cursor:help} /* chips take hover; shell stays pass-through */
  #placeSpells .ps.armed{border-color:var(--arcane);box-shadow:0 0 14px rgba(126,224,255,.5);cursor:crosshair}
  #placeSpells .ps.spent{opacity:.45}
  #placeSpells .ps .ic{font-size:15px;line-height:1}
  #placeSpells .ps .nm{font-size:11.5px;color:var(--ink)}
  #placeSpells .ps .tr{font-size:9.5px;color:var(--violet);font-weight:700}
  #placeSpells .psNone{font-size:11.5px;color:var(--ink-dim);padding:2px 4px}

  #battleHud{position:absolute;inset:0;z-index:15;pointer-events:none}
  #battleTop{position:absolute;top:12px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:16px;pointer-events:auto}
  .sideTag{font-family:'Cinzel',serif;font-weight:700;font-size:15px;padding:7px 16px;border-radius:9px;
    background:var(--panel);border:1px solid var(--panel-edge)}
  .sideTag.you{color:var(--gold)}.sideTag.foe{color:var(--foe)}
  #vsTimer{font-family:'Cinzel',serif;font-size:22px;color:var(--arcane)}
  #armyBars{position:absolute;top:58px;left:50%;transform:translateX(-50%);width:420px;display:flex;gap:8px}
  #armyBars .hpbar{flex:1;height:7px}
  #armyBars .foebar i{background:linear-gradient(90deg,#a33b2e,var(--foe))}
  #battleBottom{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:8px;pointer-events:auto}
  .sb{width:64px;border-radius:10px;border:1px solid var(--panel-edge);background:var(--panel);cursor:pointer;
    padding:7px 4px 6px;text-align:center;color:var(--ink);font-family:inherit;position:relative;overflow:hidden}
  .sb .ic{font-size:21px}.sb .k{position:absolute;top:3px;left:6px;font-size:10px;color:var(--ink-dim)}
  .sb .cdcover{position:absolute;left:0;bottom:0;width:100%;background:#0d0b1dcc}
  .sb.armed{border-color:var(--arcane);box-shadow:0 0 14px rgba(126,224,255,.5)}
  /* 🔀 Slot drag-reorder ghosts / drop targets (Josh 2026-08-05…06 slotdrag1–6) */
  .slotDragAble{cursor:grab}
  .slotDragAble:active,.slotDragging .slotDragAble{cursor:grabbing}
  .slotDragSrc{opacity:.45}
  .slotDragOver{outline:2px solid var(--arcane);outline-offset:2px;box-shadow:0 0 12px rgba(126,224,255,.35)}
  .slotDragGhost{position:fixed;z-index:9999;pointer-events:none;opacity:.92;transform:scale(1.02);
    box-shadow:0 8px 24px rgba(0,0,0,.55);margin:0}
  /* Spell hold-click ghost: bare spell art, sized to fit the full plate (slotdrag6). */
  .slotDragGhostSpell{display:grid;place-items:center;overflow:visible;padding:0;box-sizing:border-box;
    border:none;border-radius:12px;background:transparent;box-shadow:0 10px 28px rgba(0,0,0,.55)}
  .slotDragGhostSpell>.spellSlotArt,.slotDragGhostSpell>.ic{display:contents}
  .slotDragGhostSpell img,.slotDragGhostSpell .artic,.slotDragGhostSpell .iart{
    width:100%!important;height:100%!important;max-width:100%;max-height:100%;
    object-fit:contain;object-position:center;display:block;border-radius:10px;
    filter:drop-shadow(0 4px 10px rgba(0,0,0,.45))}
  /* Banner hold-click ghost: portrait circle only (slotdrag6) — not the full card. */
  .slotDragGhostPortrait{display:grid;place-items:center;overflow:visible;padding:0;box-sizing:border-box;
    border:none;border-radius:50%;background:transparent}
  .slotDragGhostPortrait>.ic{width:100%;height:100%;min-width:0;display:grid;place-items:center;
    overflow:hidden;border-radius:50%;box-sizing:border-box;
    border:1px solid rgba(200,163,90,.55);
    background:radial-gradient(circle,rgba(138,61,255,.16),transparent 68%),rgba(14,14,16,.82);
    box-shadow:0 10px 28px rgba(0,0,0,.55)}
  .slotDragGhostPortrait>.ic .artic,.slotDragGhostPortrait>.ic .iart,.slotDragGhostPortrait>.ic img{
    width:92%!important;height:92%!important;max-width:92%;max-height:92%;
    object-fit:contain;object-position:center;display:block}
  .slotDragGhostPortrait>.ic[data-u="militia"] .artic{
    object-fit:cover;object-position:44% 15%;transform:translate(-5px,-10px);
    width:100%!important;height:100%!important;max-width:100%;max-height:100%}
  .slotDragGhostPortrait>.ic[data-u="banner"] .artic{
    object-fit:cover;object-position:center 64%;width:100%!important;height:100%!important;max-width:100%;max-height:100%}
  .slotDragGhostPortrait .impbadge{display:none!important}
  body.slotDragging{cursor:grabbing!important;user-select:none}
  #placeSpells .ps.slotDragAble{cursor:grab}
  .sb.slotDragAble{cursor:grab}
  /* 📣 THE BATTLE TICKER, FRAMED (Josh 2026-07-29: "this needs to be framed"). It was bare text with a
     shadow, which over a bright stone battlefield is barely a notice at all — the screenshot that came with
     the request is the word sitting on a cliff edge. Same panel language as every other overlay so it reads
     as chrome rather than as something painted onto the ground.
     Hidden by VISIBILITY, not display: it must not collapse, or an empty frame would sit under every battle
     the moment the message clears. The .on class is driven off the text actually being non-empty — see the
     ticker write in the HUD refresh — so the frame exists exactly when there is something in it. */
  #ticker{position:absolute;bottom:96px;left:50%;transform:translateX(-50%);font-size:14px;color:var(--ink);
    text-shadow:0 1px 3px #000;text-align:center;
    padding:7px 16px;border-radius:10px;border:1px solid var(--panel-edge);background:var(--panel);
    box-shadow:0 4px 18px rgba(0,0,0,.45);
    white-space:nowrap;max-width:1382.4px;overflow:hidden;text-overflow:ellipsis;
    opacity:0;visibility:hidden;transition:opacity .14s}
  #ticker.on{opacity:1;visibility:visible}

  .ovWrap{position:absolute;inset:0;z-index:var(--ss-layer-dock);display:flex;align-items:center;justify-content:center;
    background:var(--ss-surface-overlay)}
  #compOv,#scoutOv{z-index:44}
  /* HOW TO PLAY is opened FROM INSIDE the compendium now, so it has to sit above it. compOv is 44 and
     the profile row is 45; tooltips live at 220 and must stay on top. (Josh 2026-07-27) */
  #tutOv{z-index:46} /* readable over the MP wait screen (Josh 2026-07-21) */
  .ovCard{background:var(--ss-surface-panel);border:1px solid var(--ss-border-subtle);border-radius:var(--ss-radius-lg);padding:30px 42px;
    text-align:center;max-width:560px;box-shadow:var(--ss-shadow-lg)}
  /* ---- commander duel ---- */
  #duelScreen{z-index:31;background:#070613d0}
  .duelStage{display:flex;align-items:center;justify-content:center;gap:26px;width:min(880px,1804.8px);transition:transform .55s cubic-bezier(.4,0,.2,1)}
  /* merged duel→recap: split the commanders out to the left & right edges so the recap card has a clear center */
  #duelScreen.recap .duelStage{transform:translateY(-86.4px) scale(.82)}
  #duelScreen.recap #duelL{transform:translateX(-614.4px)}
  #duelScreen.recap #duelR{transform:translateX(614.4px)}
  #duelScreen.recap #duelVs{opacity:0;transform:scale(.4)}
  #duelScreen.recap .duelHint{display:none}
  #roundOverlay.overDuel{background:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;z-index:33}
  #roundOverlay.overDuel .ovCard{animation:recapRise .5s cubic-bezier(.4,0,.2,1)}
  @keyframes recapRise{0%{transform:translateY(64px);opacity:0}100%{transform:translateY(0);opacity:1}}
  .duelSide{flex:1 1 0;display:flex;flex-direction:column;align-items:center;gap:10px;will-change:transform;transition:transform .55s cubic-bezier(.4,0,.2,1)}
  .duelAva{width:118px;height:118px;border-radius:50%;background:#151130;border:3px solid var(--panel-edge);
    display:flex;align-items:center;justify-content:center;font-size:62px;overflow:hidden;box-shadow:0 8px 30px #000a}
  .duelAva img{width:100%;height:100%;object-fit:cover}
  .duelName{font-family:'Cinzel',serif;font-size:16px;letter-spacing:.05em;color:var(--ink);text-align:center;max-width:200px}
  .duelBarWrap{position:relative;width:200px}
  .duelBar{width:100%;height:20px;border-radius:10px;background:#0c0a1e;border:1px solid var(--panel-edge);overflow:hidden}
  .duelFill{height:100%;width:100%;border-radius:10px;transition:width .65s cubic-bezier(.5,0,.2,1)}
  .duelDmg{position:absolute;left:50%;top:-6px;transform:translate(-50%,0);font-family:'Cinzel',serif;font-weight:800;
    font-size:34px;color:var(--foe);text-shadow:0 2px 8px #000c;opacity:0;pointer-events:none}
  .duelDmg.show{animation:duelDmgPop 1.1s ease-out forwards}
  .duelVs{font-size:40px;color:var(--gold);flex:0 0 auto;text-shadow:0 2px 10px #000a;opacity:.9;transition:opacity .4s ease,transform .4s ease}
  .duelHint{position:absolute;bottom:22px;left:0;right:0;text-align:center;color:var(--ink-dim);font-size:12px;letter-spacing:.14em}
  #duelL.attack{animation:duelLungeR .5s ease-in-out}
  #duelR.attack{animation:duelLungeL .5s ease-in-out}
  #duelL.hurt,#duelR.hurt{animation:duelShake .5s ease-in-out}
  @keyframes duelLungeR{0%{transform:translateX(0)}45%{transform:translateX(40px)}100%{transform:translateX(0)}}
  @keyframes duelLungeL{0%{transform:translateX(0)}45%{transform:translateX(-40px)}100%{transform:translateX(0)}}
  @keyframes duelShake{0%,100%{transform:translateX(0)}15%{transform:translateX(-11px) rotate(-2deg)}30%{transform:translateX(10px) rotate(2deg)}45%{transform:translateX(-8px)}60%{transform:translateX(7px)}75%{transform:translateX(-4px)}}
  @keyframes duelDmgPop{0%{opacity:0;transform:translate(-50%,6px) scale(.6)}20%{opacity:1;transform:translate(-50%,-10px) scale(1.15)}70%{opacity:1;transform:translate(-50%,-30px) scale(1)}100%{opacity:0;transform:translate(-50%,-52px) scale(.9)}}
  .ovCard h2{font-family:var(--ss-font-display);font-size:var(--ss-type-heading-lg);letter-spacing:.08em;margin-bottom:8px}
  .ovCard h2.win{color:var(--gold)}.ovCard h2.lose{color:var(--foe)}
  .ovCard .sub{color:var(--ink-dim);font-size:15px;line-height:1.7}
  .ovCard .simlist{margin:14px 0 4px;font-size:13.5px;color:var(--ink-dim);line-height:1.9;text-align:left}
  /* The climb's own standing, centred — .simlist above is left-aligned for the rival-news list it was
     wrongly sharing. */
  .roAlt{text-align:center;color:var(--ink);margin:2px 0 2px}
  .ovCard .simlist b{color:var(--ink)}
  .vsWrap{display:flex;align-items:center;justify-content:center;gap:22px;margin:10px 0 6px}
  .vsWrap .v{font-family:'Cinzel',serif;font-weight:900;font-size:26px;color:var(--foe)}
  .stTable{width:100%;margin:12px 0 2px;border-collapse:collapse;font-size:12.5px}
  .stTable th{font-family:'Cinzel',serif;font-size:9.5px;letter-spacing:.15em;text-transform:uppercase;color:var(--ink-dim);
    padding:4px 8px;border-bottom:1px solid var(--panel-edge);text-align:right}
  .stTable th:first-child{text-align:left}
  .stTable td{padding:3px 8px;text-align:right;color:var(--ink)}
  .stTable td:first-child{text-align:left;color:var(--ink-dim)}

  #scoutCard{max-width:520px;text-align:left}
  #scoutHead{display:flex;align-items:center;gap:14px;margin-bottom:12px}
  /* scouting: borderless, large avatar */
  #scoutHead .port{border:none;background:none;width:auto;height:auto;padding:0;gap:0}
  #scoutHead .port .em{font-size:88px;line-height:1}
  #scoutHead .port .em img.emava{width:88px;height:88px;border-radius:50%;box-shadow:0 0 0 2px rgba(185,142,58,.85);display:block;object-fit:contain}
  .port .em img.emava{width:64px;height:64px;border-radius:50%;object-fit:contain}
  .port.mini .em img.emava{width:30px;height:30px;border-radius:50%;display:block;margin:auto} /* was 34px inside a 30px box — it overflowed its own frame */
  #scoutHead .port .em .emava{height:120px;width:auto;box-shadow:none;border-radius:0;object-fit:contain}
  #scoutHead .port .pn{display:none}
  #scoutHead .who .n1{font-family:'Cinzel',serif;font-weight:700;font-size:20px}
  #scoutHead .who .n2{font-size:12.5px;color:var(--ink-dim)}
  #scoutHead .who .n3{font-size:12.5px;margin-top:3px}
  #scoutBats{margin:8px 0}
  #recapBody{max-height:calc(1080px - 140px);overflow-y:auto} /* tall enough to fit whole reports — scroll only as a last resort (Josh 2026-07-21) */
  #recapBubble.open{width:344px}
  #scoutClose{margin-top:16px;width:100%}
  /* 🖼️ scrollbars wear the tooltip frame (Josh 2026-07-21) */
  ::-webkit-scrollbar{width:13px;height:13px}
  ::-webkit-scrollbar-track{background:#0d0b1d;border-radius:7px}
  ::-webkit-scrollbar-thumb{border-image:url('assets/images/ui/tt_frame.png') 30 fill / 5px stretch;border-width:5px;border-style:solid;background:#2e2757;border-radius:7px;min-height:36px}
  ::-webkit-scrollbar-thumb:hover{background:#3a3170}
  ::-webkit-scrollbar-corner{background:#0d0b1d}
  #ttBar{position:fixed;width:32px;height:5px;background:#0d0b1d;border:1px solid var(--panel-edge);border-radius:3px;z-index:12000;pointer-events:none;display:none;overflow:hidden}
  #ttBar i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--violet),var(--arcane));}
  /* 🖼 THE SHADOW FOLLOWS THE FRAME, NOT THE BOX (Josh 2026-07-30: "THe tooltips have a improper outline
     around that is clearing the shaded area around its borders").
     `box-shadow` is drawn around the element's BORDER BOX — a hard rectangle, rounded by border-radius —
     and it is painted BEHIND the element. That is invisible while the background is opaque and obvious
     the moment it is not: tt_frame.png is a painted frame with fully transparent corners (measured: alpha
     0 for the first ten pixels diagonally in, 0 for the first two at the mid-edges), so the shadow's own
     rectangular silhouette showed through the corners and read as a hard outline cutting across the soft
     frame. The radius made it worse rather than better, because a 9px rounded rect is not the shape the
     frame's corners are.
     `filter: drop-shadow` is computed from the element's RENDERED ALPHA instead, border-image included,
     so the shading hugs the frame itself and there is no rectangle to see through it. Two passes: a tight
     one for definition against a busy battlefield, a wide soft one for depth. */
  #ttBox{position:fixed;max-width:300px;padding:12px 14px;font-size:12px;line-height:1.55;color:var(--ink);z-index:12000;pointer-events:none;display:none;white-space:pre-line;
    filter:drop-shadow(0 2px 3px rgba(0,0,0,.55)) drop-shadow(0 8px 18px rgba(0,0,0,.5));
    border-image:url('assets/images/ui/tt_frame.png') 30 fill / 15px stretch;border-width:10px;border-style:solid;background:none}
  #ttBox.ttShow{animation:ttIn .3s ease-out}
  @keyframes ttIn{0%{clip-path:inset(0 0 96% 0);opacity:.5}60%{opacity:1}100%{clip-path:inset(0 0 0 0);opacity:1}}
  .ttic{height:1.5em;width:auto;vertical-align:middle;display:inline-block;margin:-0.35em 0}
  .ttcard{white-space:normal;font-size:11.5px;line-height:1.5;min-width:236px}
  .ttcard .ttic{height:1.65em;vertical-align:middle;margin:-0.4em 0}/* icons -25% (Josh): negative margins keep line-height steady */
  .ttcard .kwart{width:26px;height:26px;margin:-5px 2px -5px 0;vertical-align:middle}
  .statline{display:inline-flex;gap:11px;align-items:center;font-size:12px}
  .statline .stx{display:inline-flex;align-items:center;gap:3px}
  .statline b{color:var(--ink);font-size:12.5px;font-weight:700}
  .ttd{font-size:10px;font-weight:700}
  .tthead{display:flex;gap:9px;align-items:center;margin-bottom:2px}
  .ttport{width:56px;height:56px;border-radius:8px;border:1px solid #b98e3a;overflow:hidden;flex:none;display:flex;align-items:center;justify-content:center;background:#0d0b1d}
  /* Beat `.artic.uart{height:1.5em}` (two classes) so unit-stat tip portraits fill the port again. */
  .ttport img,.ttport .artic,.ttport .uart{width:100%;height:100%;max-height:none;max-width:none;object-fit:contain;transform:scale(3);transform-origin:center;vertical-align:middle;display:block}/* zoom the ART, not the box; ×3 per Josh 2026-07-20 */
  /* bannermanart3: body-weighted PNG (bannermanart2) already matches other unit ARs — no tip over-zoom
     (banzoom1's scale(4.45) spilled past .ttport after the re-crop). Inherit default scale(3). */
  #ttBox .ttic{height:1.65em;width:auto;vertical-align:middle;display:inline-block;margin:-0.4em 0}
  .ttname{font-family:'Cinzel',serif;font-size:15px;color:var(--gold);letter-spacing:.05em;line-height:1.15}
  /* Tip meta badges (T1 / Light / race / class): left-to-right row, wrap if the tip is narrow. */
  .ttchips{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:4px;margin-top:3px;color:var(--ink-dim);font-size:10.5px}
  .ttchips .pillrow{display:contents}
  .ttchips .mpill{margin:0;flex:0 0 auto}
  .ttsec{display:flex;align-items:center;gap:8px;margin:7px 0 3px;font-family:'Cinzel',serif;font-size:9.5px;letter-spacing:.22em;color:#d9b45e;text-transform:uppercase;white-space:nowrap}
  .ttsec::before{content:'';height:1px;background:linear-gradient(90deg,transparent,#b98e3a88);flex:1}
  .ttsec::after{content:'';height:1px;background:linear-gradient(90deg,#b98e3a88,transparent);flex:1}
  .ttgrid{display:grid;grid-template-columns:1fr 1fr;gap:1px 12px}
  .ttrow{display:flex;justify-content:space-between;gap:8px;align-items:baseline}
  .ttlab{color:var(--ink-dim);font-size:10.5px;letter-spacing:.04em}
  .ttab{margin:2px 0}
  .ttab b{color:#d9b45e;font-size:10.5px;letter-spacing:.08em}
  .ttflav{font-style:italic;color:var(--ink-dim);margin-top:6px;border-top:1px solid #b98e3a44;padding-top:5px}
  .ttic.hp{animation:tticPulse 2.6s ease-in-out infinite}
  @keyframes tticPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.18)}}
  @media (max-width:0px){ /* DISABLED, was: (max-width:1280px) — max-width:1280 fails stage 1920 */  #townPanel{width:276px} #shopPanel{width:350px} #armyPanel{width:280px} }

/* SKYSUNDER art icons (v12) — img slots that behave like inline emoji, with emoji fallback */
.artic{height:1.35em;width:auto;vertical-align:-.28em;filter:drop-shadow(0 1px 1px rgba(0,0,0,.45))}
.bld .ic .artic{height:auto;width:auto;max-height:40px;max-width:46px;vertical-align:middle;display:block}
.compBig .artic{height:auto;width:auto;max-height:48px;max-width:48px;vertical-align:middle;display:block}
/* Compendium list row (bannermanart3): body-weighted plate — contain like peers, no flagpole zoom. */
.compItem .ci .uart[data-u="banner"]{width:26px;height:26px;max-width:26px;max-height:26px;object-fit:contain;object-position:center bottom;transform:none}

  /* Shop/prep screen backdrop — current town painting (pilot overrides framing; keep path in sync) */
  #prep{background:linear-gradient(rgba(12,9,26,.60),rgba(12,9,26,.78)),url("assets/town-preparation-background.png") center/cover no-repeat}

  /* unit portraits in shop card + compendium (v12) — battle canvas stays emoji */
  .artic.uart{height:1.5em;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}
  /* rarity glow by troop tier — SAME palette as --r0…--r3 (Dave 2026-08-01). This used to be its own
     white/green/purple/gold set, so a Rare card wore a cyan frame around a GREEN-glowing portrait and
     the colours read as two different systems. One palette now: card frame, tag and portrait glow agree. */
  .artic.rgl0{filter:drop-shadow(0 1px 2px rgba(0,0,0,.55)) drop-shadow(0 0 4px rgba(154,143,192,.95))}
  .artic.rgl1{filter:drop-shadow(0 1px 2px rgba(0,0,0,.55)) drop-shadow(0 0 4px rgba(88,214,139,1)) drop-shadow(0 0 9px rgba(88,214,139,.75))}
  .artic.rgl2{filter:drop-shadow(0 1px 2px rgba(0,0,0,.55)) drop-shadow(0 0 5px rgba(176,127,232,1)) drop-shadow(0 0 11px rgba(176,127,232,.8))}
  .artic.rgl3{filter:drop-shadow(0 1px 2px rgba(0,0,0,.55)) drop-shadow(0 0 5px rgba(232,182,76,1)) drop-shadow(0 0 12px rgba(232,182,76,.85))}
  .artic.rgl4{filter:drop-shadow(0 1px 2px rgba(0,0,0,.55)) drop-shadow(0 0 5px rgba(180,245,255,1)) drop-shadow(0 0 12px rgba(220,255,255,.85))}
  .card .cic .uart{height:auto;width:auto;max-height:42px;max-width:42px;vertical-align:middle;display:block}
  /* rarity portrait frame around the unit icon (assigned by tier) — large & prominent */
  .card .cic.framed{width:92px;height:92px;position:relative;display:flex;align-items:center;justify-content:center;background:none}
  /* rarity frame ring (::after). Troop uart stays under it (z:2 < 3); item iart is raised above. */
  .card .cic.framed::after{content:"";position:absolute;inset:0;background:var(--frame) center/contain no-repeat;pointer-events:none;z-index:3}
  /* UNIT portraits: near-black backdrop, overlapped by unit art + glow */
  .card .cic.framed:not(.roundframe)::before{content:"";position:absolute;inset:15%;background:radial-gradient(ellipse 66% 76% at 50% 47%,rgba(6,5,13,.88) 0%,rgba(6,5,13,.55) 50%,rgba(6,5,13,.12) 82%),#0a0a16 var(--rbg,none) var(--rbgx,50%) var(--rbgy,50%)/cover no-repeat;border-radius:9px;z-index:0}
  .card .cic.framed .unitShadow{position:absolute;left:50%;bottom:20%;transform:translateX(-50%);width:54%;height:11%;background:radial-gradient(ellipse at center,rgba(0,0,0,.62) 0%,rgba(0,0,0,.28) 55%,transparent 75%);z-index:1;pointer-events:none}
  .card .cic.framed:not(.roundframe) .uart{position:relative;z-index:2;max-height:62px;max-width:62px;top:4px}
  .card .cic.framed.shiftR4 .uart{left:4px}
  /* ITEM portraits: art under the rarity ring (::after z:3). itemart10 clips via .iart-face. */
  .card .cic.roundframe .iart-face{position:absolute;inset:8%;z-index:2;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:50%;clip-path:circle(50% at 50% 50%);-webkit-clip-path:circle(50% at 50% 50%);pointer-events:none}
  .card .cic.roundframe .iart-face .iart{position:relative;z-index:1;width:100%;height:100%;max-height:none;max-width:none;object-fit:contain;border-radius:50%;clip-path:none}
  .card .cic.roundframe > .iart{position:relative;z-index:2;width:84%;height:84%;max-height:none;max-width:none;object-fit:contain;border-radius:50%;clip-path:circle(50% at 50% 50%)}
  /* itemart10: do not clip-path .cic — keeps full ::after metal; art clipped in .iart-face. */
  .card .cic.framed.roundframe{position:relative;overflow:hidden;border-radius:50%;clip-path:none;isolation:isolate}
  /* ELITE / HEROIC pulsing aura */
  @keyframes auraPulse{0%,100%{box-shadow:0 0 6px 1px var(--auraA),0 0 12px 3px var(--auraB)}50%{box-shadow:0 0 15px 4px var(--auraA),0 0 30px 10px var(--auraB)}}
  .aura-elite{--auraA:rgba(64,224,208,.9);--auraB:rgba(64,224,208,.4);animation:auraPulse 1.7s ease-in-out infinite}
  .aura-heroic{--auraA:rgba(255,198,64,.95);--auraB:rgba(255,168,20,.45);animation:auraPulse 1.5s ease-in-out infinite}
  .aura-emperor{--auraA:rgba(255,255,255,.95);--auraB:rgba(215,228,255,.5);animation:auraPulse 1.6s ease-in-out infinite} /* ⚜️ EMPEROR: white glow (Josh 2026-07-20) */
  .card.aura-elite,.card.aura-heroic,.abat.aura-elite,.abat.aura-heroic,.card.aura-emperor,.abat.aura-emperor{position:relative;z-index:1}
  /* purchased banners settle down: no pulse, just a faint steady outline (Josh 2026-07-21) */
  .abat.aura-elite,.abat.aura-heroic,.abat.aura-emperor{animation:none;box-shadow:0 0 0 1.5px var(--auraA),0 0 6px 1px var(--auraB)}
  .abat.selBat{outline:1.5px solid var(--gold);background:rgba(217,180,94,.10)} /* 🎯 selected in the deploy HUD (Josh 2026-07-21) */
  /* race banner as the whole troop card's backdrop */
  .card.raceCard{background-color:#171331}
  .card.raceCard .cn{text-shadow:0 1px 3px rgba(0,0,0,.85)}
  .card.raceCard .cd{color:#cdc6e8}
  .compBig .uart{height:auto;width:auto;max-height:48px;max-width:48px;vertical-align:middle;display:block}
  /* bannermanart3: beat .compBig .uart 48px cap — detail header is an 88px slot; no over-zoom. */
  #compDetail h3 .compBig .uart[data-u="banner"]{width:88px;height:88px;max-width:88px;max-height:88px;object-fit:contain;object-position:center bottom;transform:none}
  .compItem .ci .uart{height:auto;width:auto;max-height:28px;max-width:28px;vertical-align:middle;display:block}

  .card .cic .iart{height:auto;width:auto;max-height:42px;max-width:42px;vertical-align:middle;display:block}
  .compBig .iart{height:auto;width:auto;max-height:48px;max-width:48px;vertical-align:middle;display:block}
  .compItem .ci .iart{height:auto;width:auto;max-height:28px;max-width:28px;vertical-align:middle;display:block}

/* Skysunder MP avatar picker (v12) */
.emava{height:1.15em;width:auto;vertical-align:-.24em;border-radius:50%}
.port .em .emava{height:1em;vertical-align:-.1em;border-radius:50%;box-shadow:0 0 0 1px rgba(0,0,0,.45)}
.port.mini .em .emava{height:1em}
#avpick-wrap{display:flex;align-items:center;gap:8px;margin-top:2px}
#avpick-cur{width:46px;height:46px;border-radius:50%;border:2px solid var(--gold);background:#151130;overflow:hidden;display:flex;align-items:center;justify-content:center;flex:none}
#avpick-cur img{width:100%;height:100%;object-fit:cover}
#avpick-cur .emj{font-size:26px;line-height:1}
#avpick-btn{font-size:12px;padding:6px 10px}
#avpick-modal{position:fixed;inset:0;z-index:400;background:rgba(6,5,14,.85);display:none;align-items:center;justify-content:center}
#avpick-panel{background:var(--panel);border:1px solid var(--panel-edge);border-radius:14px;padding:14px;width:min(760px,1766.4px);max-height:885.6px;display:flex;flex-direction:column;box-shadow:0 12px 40px rgba(0,0,0,.6)}
#avpick-head{display:flex;align-items:center;justify-content:space-between;font-family:'Cinzel',serif;color:var(--gold);font-size:15px;letter-spacing:.08em;margin-bottom:10px}
#avpick-close{background:none;border:none;color:var(--ink);font-size:18px;cursor:pointer;line-height:1}
#avpick-grid{display:grid;grid-template-columns:repeat(auto-fill,64px);gap:8px;overflow-y:auto;padding-right:4px;justify-content:center}
.avtile{width:64px;height:64px;border-radius:50%;cursor:pointer;border:2px solid transparent;background:#151130;object-fit:cover;transition:border-color .1s}
.avtile:hover{border-color:var(--gold)}

  .compItem .ci .bart{height:auto;width:auto;max-height:28px;max-width:28px;vertical-align:middle;display:block}

  /* Main menu redesign (v12) — bg art + logo image + stacked buttons */
  /* The still backdrop and the two floating isles moved into the layered menu scene on 2026-07-31;
   assets/ui/parallax/parallax.css owns the title screen background now. */
  /* Opaque void BEFORE parallax.css (linked at end of body). Without this, #cv's clear shows through
     the transparent title for a frame and used to flash the early procedural sky (Josh 2026-08-03). */
  #titleScreen{background-color:#0b1020;background-image:none}
  /* menu backdrop drifts x/y on a very slow loop (Josh 2026-07-24). A ::before layer with background:inherit
     reuses the parent's image (no duplicating the base64) and is overscanned so panning never reveals an edge. */
  #titleScreen::before{content:'';position:absolute;inset:-3%;z-index:0;pointer-events:none;
    background:inherit;background-size:cover;background-position:center;
    transform:scale(1.06);animation:menuDrift 120s linear infinite}
  /* LINEAR + a closed elliptical path: constant velocity with no dead stops at the keyframes,
     and 0% == 100% so the loop is seamless — it never stops moving (Josh 2026-07-24) */
  @keyframes menuDrift{
    0%{transform:translate(1.5%,0) scale(1.06)}
    12.5%{transform:translate(1.06%,.78%) scale(1.06)}
    25%{transform:translate(0,1.1%) scale(1.06)}
    37.5%{transform:translate(-1.06%,.78%) scale(1.06)}
    50%{transform:translate(-1.5%,0) scale(1.06)}
    62.5%{transform:translate(-1.06%,-.78%) scale(1.06)}
    75%{transform:translate(0,-1.1%) scale(1.06)}
    87.5%{transform:translate(1.06%,-.78%) scale(1.06)}
    100%{transform:translate(1.5%,0) scale(1.06)}}
  @media(prefers-reduced-motion:reduce){#titleScreen::before{animation:none}}
  #titleScreen .logoimg{width:clamp(320px,883.2px,600px);height:auto;filter:drop-shadow(0 8px 26px rgba(0,0,0,.65));margin-bottom:4px}
  #titleScreen .tagline{color:#d3c4ff;text-shadow:0 2px 10px #000,0 0 22px rgba(120,90,210,.45)}
  #titleScreen #diffRow{margin-top:22px;background:rgba(12,9,28,.92);padding:8px 14px;border-radius:12px}
  #menuBtns{display:flex;flex-direction:column;gap:11px;margin-top:22px;width:min(380px,1651.2px)}
  #menuBtns .big{margin-top:0;width:100%;font-size:19px;padding:14px 20px;display:flex;align-items:center;justify-content:center;gap:10px}
  #menuBtns .big.ghost{background:linear-gradient(180deg,rgba(30,24,58,.92),rgba(18,14,40,.94));color:#cbb8ff;border-color:var(--panel-edge);box-shadow:0 4px 16px rgba(0,0,0,.5)}
  /* Ghost/neutral menu hover is owned by ui-theme-baseline.css (#titleScreen #menuBtns .big:hover) —
     gold lift/glow, not violet. Bookend flank animation removed 2026-08-03 (Josh). */
  /* ── TITLE SCREEN OVERRIDES ─────────────────────────────────────────────────────────────────────
     These three rules restate values that live in assets/ui-theme-baseline.css. That sheet loads at the
     end of the body, so an equal-weight selector here would lose to it; each one below therefore carries
     one extra class or id, and each restates only the property it is changing.

     ONE WORD, CENTRED. Josh: "combine it into one word SKYSUNDER and center it". The wordmark was a crop
     of the two-word sheet, pinned to its right edge - so it could not be joined by moving the crop, and
     was off-centre because the crop was. tools/make-wordmark.py re-sets the two words as one image with
     the gap closed to the sheet's own letter spacing; this points at that file and centres it. The
     emblem's -18px nudge goes for the same reason: it was compensating for the crop. */
  #titleScreen .ss-title-wordmark{
    background-image:url("assets/ui/skysunder-wordmark-oneword.png");
    background-size:contain;background-position:center;
    aspect-ratio:7.57;      /* the built file's own ratio - make-wordmark.py prints it */
    /* The stylesheet pulls the wordmark UP by 20-34px, which was tuned to swallow the dead space under the
       emblem's old crop. Both pieces are cut to their ink now, so that dead space is gone and a negative
       margin would simply overlap them. This is the one number that sets the gap between emblem and
       wordmark; raise it if they want more air. */
    /* OPTICALLY CENTRED ON THE U, not on the word. Josh: "absolute center is not quite right ... move it
       to the right about 20 px so the U is centered over the middle of the screen". He is reading a real
       thing: SKYSUNDER's left half (S K Y S) is narrower than its right (N D E R), so geometric centring
       leaves the U left of centre. make-wordmark.py measures the U at 47.91% of the file's width and
       prints this figure — 2.09% of the element, 15px at the 720px cap. A percentage rather than pixels
       so it stays correct as the title scales down with the viewport. */
    transform:translateX(2.09%);
    margin-top:4px}
  /* THE EMBLEM, CENTRED (Josh: "Can you center the logo above SKYSUNDER"). Its box was already centred by
     the flex column — the ART inside the box was not. The stylesheet draws it as a 425% left-aligned crop
     of the sheet, and the sheet's monogram starts 113px in, so the crop carried ~23% of dead margin on its
     left and the emblem read as sitting right of centre. The -18px nudge there was paying for exactly that.
     Removing the nudge alone was not enough, because the offset is inside the background, not the element.
     So the emblem is now its own file, cut to its ink, drawn contain/centre: nothing left to compensate. */
  #titleScreen .ss-title-emblem{
    background-image:url("assets/ui/skysunder-emblem.png");
    background-size:contain;background-position:center;
    aspect-ratio:0.755;                /* the built file's own ratio - make-wordmark.py prints it */
    transform:none}

  /* NO CAPACITY CAPTIONS ON THE PREP HEADINGS (Josh 2026-07-31). ui-theme-baseline.css hangs "10
     BUILDINGS", "UP TO 5" and "MAX 6" off these three headings as ::after content. Two ids beats its one,
     and content:none removes the pseudo-element rather than merely blanking it, so it takes no space. */
  #prep #townPanel h2::after,#prep #shopPanel h2::after,#prep #armyPanel h2::after{content:none}

  
  /* THE VIOLET TINT, AT 10% (Josh 2026-07-31: "reduce the violet tint to ten percent of what it is right
     now"). Second pass — an earlier one took it to 80%, and this takes those same stops to a tenth.
     WHICH STOPS. Only the violet ones. The wash is a mix of tint and plain darkening, and cutting the
     darkening as well would wash the screen out rather than de-purple it — which is what the 80% pass
     already noted. "Violet" is measured, not judged: how far R and B lead G. The three darkening stops
     sit at 7, 10 and 12; the five tint stops at 21, 33, 52, 109 and 135. Nothing lands in between, so
     the split is the gap, and rgba(5,5,15), rgba(6,5,17) and rgba(2,2,9) keep their original alpha.
     Composited over menu-sky.webp this takes the menu from mean RGB (30,23,62) to (57,67,128) and mean
     luminance 0.150 to 0.328 — the sky reads blue again instead of violet murk. If the wordmark ever
     looks weak against it, raise the NEUTRAL vignette rather than any of these: it darkens without
     bringing the purple back.

     ── 🌒 THE VIGNETTE IS HALVED (Josh 2026-08-01: "make the main menu vignette about 50% weaker") ──
     THREE layers were darkening the edges, not one, and halving any single one would have been invisible:
       · #menuScene .vig      rgba(4,6,14,.62)   the parallax scene's own vignette (parallax.css)
       · ::before radial edge  rgba(6,5,17,.76)   this rule
       · ::after neutral vig   rgba(2,2,9,.65)    this rule
     Stacked, that composited to about 97% darkening at a mid-height side edge — 1-(1-.62)(1-.76)(1-.65)
     against the 7% tint — which is why the clouds died before they reached the frame. All three alphas
     are now half, taking the edge to roughly 73%. Verified by eye against a before shot: the edges open
     up and the centre is unchanged, which is what a vignette change should look like.
     THE BOTTOM SCRIM rgba(5,5,15,.88) IN THE LINEAR GRADIENT IS DELIBERATELY UNTOUCHED. It is not a
     vignette — it is what keeps the build tag, the copyright and the socials legible against the sky — and
     it is also why the very bottom edge is still dark after this change. If Josh wants that lighter too it
     is a separate ask, and this is the stop to move. */
  .screen#titleScreen::before{
    background:
      linear-gradient(180deg,rgba(13,6,27,.076),rgba(22,9,42,.07) 43%,rgba(5,5,15,.88)),
      radial-gradient(ellipse 72% 58% at 50% 33%,rgba(103,42,151,.0368),rgba(36,14,66,.0192) 50%,rgba(6,5,17,.38) 100%)}
  .screen#titleScreen::after{
    background:
      radial-gradient(ellipse 42% 34% at 50% 28%,rgba(133,61,196,.0152),transparent 72%),
      radial-gradient(ellipse 95% 86% at 50% 50%,transparent 45%,rgba(2,2,9,.325) 100%)}

  #idRow{background:rgba(18,14,42,.92)!important}

  
  
  
  
  
  @keyframes floatA{0%,100%{transform:translateY(0)}50%{transform:translateY(-16px)}}
  
  #titleScreen .logoWrap,#titleScreen #diffRow,#titleScreen #menuBtns,#titleScreen #idRow,#titleScreen #buildTag{position:relative;z-index:2}

  /* ⬆️ THE MAIN MENU IS RAISED SO SINGLEPLAYER SITS ON THE SCREEN'S CENTRE LINE (Josh 2026-08-01).
   *
   * MEASURED, NOT GUESSED. In the running game at the 1920x1080 stage, #startBtn's centre was 102px
   * BELOW H/2 (screen-space 541.6 against a 455.5 centre, / stageScale 0.8435). The stage is a FIXED
   * 1920x1080 that is uniformly scaled to the window (01-stage.js), so a pixel figure here is exact at
   * every window size — it is virtual-stage px, not device px.
   *
   * WHY `top` AND NOT padding, margin OR transform. All four were considered and three are traps:
   *   · #titleScreen's padding is `clamp(52px,86.4px,92px) 28px 34px` in ui-theme-baseline.css, and the
   *     stack is 745px tall in a 1080 stage. Shifting the centre by padding means +204px on the bottom
   *     (a centred box moves by half of what you take off one side), which leaves the content box ~10px
   *     taller than the stack — and #titleScreen is `overflow: hidden`, so the next logo revision clips.
   *   · a negative margin-top on .logoWrap has the same halving and the same clipping risk at the top.
   *   · `transform` is already spoken for: .logoWrap runs the logoBreathe animation, which animates
   *     transform and would simply overwrite it.
   * `top` on an already-relative box moves it visually, affects nothing else's flow, has no factor of two
   * in it, and cannot clip because the box keeps its original slot. The three selectors are exactly the
   * title screen's IN-FLOW children — the logo, the button column and the socials row. #leaderboard,
   * #idRow, #buildTag, the copyright and #menuScene are all absolutely positioned and must NOT move.
   *
   * NO TEST GUARDS THIS. tests/stub.js has no layout engine, so "the button's centre is at H/2" cannot be
   * asserted headlessly — it was measured in the browser and re-measured after the change. If the logo
   * art's height ever changes, this number needs re-measuring; nothing will tell you. */
  #titleScreen{--sky-menu-lift:102px}
  #titleScreen .logoWrap,
  #titleScreen #menuBtns,
  #titleScreen #menuSocials{top:calc(-1 * var(--sky-menu-lift))}
  .logoWrap{display:inline-block;line-height:0}
  /* slow breathing pulse: drifts a few px and swells ~5%. Animates the WRAP so the crystal
     glow overlays ride along with the logo instead of drifting apart (Josh 2026-07-24) */
  @keyframes logoBreathe{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-5px) scale(1.05)}}
  #titleScreen .logoWrap{transform-origin:center center;animation:logoBreathe 46s ease-in-out infinite;will-change:transform}
  @media(prefers-reduced-motion:reduce){#titleScreen .logoWrap{animation:none}}
  /* Bug report badge on the main menu (Discord / Patreon / Reddit icons removed). */
  #feedbackBtn.sky-social-btn{--sky-social:42px;
    display:inline-flex;align-items:center;justify-content:center;
    width:var(--sky-social);height:var(--sky-social);padding:0;
    color:#fff;border-radius:11px;text-decoration:none;border:none;cursor:pointer;
    transition:transform .12s ease,box-shadow .12s ease;
    background:#0E0E10;box-shadow:0 4px 16px rgba(0,0,0,.45)}
  #feedbackBtn.sky-social-btn:hover{transform:translateY(-1px) scale(1.03);box-shadow:0 6px 22px rgba(0,0,0,.62);background:#16161a}
  #feedbackBtn.sky-social-btn img{display:block;width:22px;height:22px;pointer-events:none}
  .logoWrap .logoimg{position:relative;z-index:1}
  .logoGlow{position:absolute;transform:translate(-50%,-50%);pointer-events:none;z-index:2;mix-blend-mode:screen;border-radius:50%;
    background:radial-gradient(circle,rgba(120,190,255,.95) 0%,rgba(60,140,255,.55) 34%,rgba(40,100,255,0) 70%)}
  .logoGlow.g1{left:49.5%;top:20%;width:28%;height:54%;animation:crystalPulse 4.5s ease-in-out infinite}
  .logoGlow.g2{left:calc(49% + 10px);top:calc(83% - 30px);width:8.5%;height:16%;animation:crystalPulse 4.5s ease-in-out infinite 1.6s}
  @keyframes crystalPulse{0%,100%{opacity:.3;transform:translate(-50%,-50%) scale(.82)}50%{opacity:.85;transform:translate(-50%,-50%) scale(1.15)}}
/* chat: real tabs, full-width input, pop-open (Josh 2026-07-20) */
.chatTab{padding:4px 11px 5px;border:1px solid var(--panel-edge);border-bottom:none;border-radius:8px 8px 0 0;background:#151130;cursor:pointer;color:var(--ink-dim);letter-spacing:.14em;position:relative;top:1px;font-size:10px}
.chatTab.sel{background:var(--panel);color:var(--gold);border-color:var(--arcane);border-bottom:1px solid var(--panel)}
#chatHead{border-bottom:1px solid var(--panel-edge)}
#chatIn{width:calc(100% - 12px);box-sizing:border-box;display:block}
@keyframes chatPop{0%{transform:scale(.12,.08);opacity:.35}68%{transform:scale(1.045,1.06);opacity:1}86%{transform:scale(.985,.975)}100%{transform:scale(1)}}
#chatBox.popIn{animation:chatPop .42s cubic-bezier(.3,1.2,.45,1);transform-origin:100% 100%}
@keyframes chatShrink{0%{transform:scale(1)}22%{transform:scale(1.04,1.05)}100%{transform:scale(.12,.08);opacity:.3}}
#chatBox.popOut{animation:chatShrink .3s cubic-bezier(.55,0,.8,.4) forwards;transform-origin:100% 100%}
