  :root{
    /* v1.7 "Refined Dark" tokens — same soul, premium execution */
    --bg:#0d0b12; --panel:#15121d; --panel2:#1c1826; --panel3:#241f31;
    --line:rgba(255,255,255,.07); --line2:rgba(255,255,255,.12);
    --text:#ede9e0; --muted:#9a93a8;
    --rank:#dc2626; --rank-soft:rgba(220,38,38,.14);
    /* --loss is a notch lighter than the rank-E red (--rank/--rank-soft) — small
       normal-weight diagnostic text (log losses, Geass risk, required-today tags)
       measured under WCAG AA contrast against --bg at the raw rank red (4.05:1,
       just short of the 4.5:1 normal-text minimum); this shade clears it (~5.2:1). */
    --gain:#22c55e; --loss:#ef4444; --gold:#d4a017; /* daily-points graph: net up/down + rank-up */
    --card-grad:linear-gradient(180deg, var(--panel2) 0%, var(--panel) 100%);
    --card-shadow:0 12px 32px -20px rgba(0,0,0,.8);
  }
  *{box-sizing:border-box; margin:0; padding:0;}
  body{
    background:radial-gradient(1200px 500px at 50% -100px, #171226 0%, var(--bg) 60%) fixed, var(--bg);
    color:var(--text);
    font-family:'Sora',sans-serif; font-size:15px; line-height:1.6;
    min-height:100vh; padding-bottom:80px;
  }
  .wrap{max-width:560px; margin:0 auto; padding:0 16px;}
  header{text-align:center; padding:14px 0 2px; position:relative;}
  header .brand{display:block; margin:0 auto; width:330px; max-width:86vw; height:auto;}
  /* version pill: sits at the top-right of the 560px column (viewport corner on a phone);
     a SIBLING of #brand, which renderBrand() rewrites wholesale on every rank change */
  .ver-btn{position:absolute; top:12px; right:max(14px, calc(50% - 264px)); z-index:2;
    background:rgba(255,255,255,.03); border:1px solid var(--line); color:var(--muted); border-radius:999px;
    padding:4px 10px; font-family:'Sora',sans-serif; font-size:11px; font-weight:600; letter-spacing:.06em;
    cursor:pointer; transition:color .15s, border-color .15s;}
  .ver-btn:hover{color:var(--rank); border-color:var(--rank);}

  /* VACATION BANNER */
  .vacation-banner{
    display:flex; align-items:center; justify-content:space-between; gap:10px;
    background:linear-gradient(90deg, rgba(212,160,23,.14), rgba(212,160,23,.05));
    border:1px solid rgba(212,160,23,.4); border-radius:12px;
    padding:11px 14px; margin-top:10px; font-size:12.5px; color:var(--text);
  }
  .vacation-banner button{
    background:transparent; border:1px solid rgba(212,160,23,.6); color:#d4a017;
    border-radius:7px; padding:6px 12px; font-size:11.5px; cursor:pointer;
    font-family:'Sora',sans-serif; white-space:nowrap; flex-shrink:0;
  }
  .vacation-banner button:hover{background:rgba(212,160,23,.15);}
  /* Geass recovery lock — the same banner shape as vacation (an app-state strip
     above the hero), recoloured to the loss red so it reads as a penalty rather
     than the amber "you're safe" of vacation. No button: a lock can only be
     lifted by redeeming the broken Geass, never dismissed. */
  .vacation-banner.lock{
    background:linear-gradient(90deg, rgba(239,68,68,.14), rgba(239,68,68,.05));
    border-color:rgba(239,68,68,.4);
  }

  /* HERO */
  .hero{
    background:var(--card-grad); border:1px solid var(--line); border-radius:14px;
    padding:26px 20px 22px; margin-top:10px; text-align:center;
    border-top:2px solid var(--rank);
    box-shadow:var(--card-shadow);
  }
  .hero .badge{width:150px; height:150px; margin:0 auto; filter:drop-shadow(0 0 18px var(--rank-soft));}
  /* ---- Streak aura ----
     Two stages behind the rank badge, armed by render() via data-aura: '1' at a
     5-day streak, '2' at 10 (where momentum also hits its cap), nothing below
     that or while a Geass recovery lock stands — the lock's contract is that the
     climb back is unassisted, so it shouldn't look decorated either.

     Both layers live on the WRAPPER's pseudo-elements, not inside the badge:
     render() replaces #heroBadge's innerHTML wholesale on every call, so
     anything painted inside would be destroyed and rebuilt several times a tick.

     Every animated property is transform or opacity, and the gradients are
     painted ONCE and then only moved. Nothing animates filter, box-shadow or
     background — this app has already paid for that mistake twice (46 reel cards
     forcing a backdrop root; a drop-shadow per strip card), and the badge is on
     screen permanently, which is exactly where a per-frame repaint hurts most.
     Colours come from var(--rank)/var(--rank-soft), so the aura re-tints itself
     on every rank change for free. */
  .hero .badge{position:relative;}
  .hero .badge::before, .hero .badge::after{
    content:''; position:absolute; left:50%; top:50%; border-radius:50%;
    pointer-events:none; z-index:-1; opacity:0;
  }
  /* The halo starts AT the badge edge and reaches past it. The first version
     faded to fully transparent by 72% of a 85px radius — about 61px — while the
     badge itself is 75px from centre, so the entire glow painted behind the
     artwork and only a sliver escaped: at a 5-day streak the aura was, in
     practice, invisible. Verified by freezing the animation and diffing the
     rendered pixels against no-aura. Colour comes from var(--rank) directly
     rather than --rank-soft, whose .16 alpha was the other half of the problem. */
  .hero .badge::before{
    width:230px; height:230px; margin:-115px 0 0 -115px;
    background:radial-gradient(closest-side,
      transparent 44%,
      color-mix(in srgb, var(--rank) 42%, transparent) 62%,
      color-mix(in srgb, var(--rank) 12%, transparent) 82%,
      transparent 100%);
  }
  .hero .badge[data-aura="1"]::before,
  .hero .badge[data-aura="2"]::before{ animation:auraPulse 3.4s ease-in-out infinite; }
  @keyframes auraPulse{
    0%,100%{ opacity:.55; transform:scale(.96); }
    50%    { opacity:1;   transform:scale(1.05); }
  }
  /* Stage 2 adds a slow sweeping ring on top of the pulse. */
  /* Tighter arcs through a narrower mask band at full opacity: the original was
     a 12px-wide band at .6, which read as a smudge rather than a ring. */
  .hero .badge::after{
    width:206px; height:206px; margin:-103px 0 0 -103px;
    background:conic-gradient(from 0deg,
      transparent 0deg, var(--rank) 26deg, transparent 62deg,
      transparent 180deg, var(--rank) 206deg, transparent 242deg, transparent 360deg);
    -webkit-mask:radial-gradient(closest-side, transparent 84%, #000 90%, #000 95%, transparent 99%);
            mask:radial-gradient(closest-side, transparent 84%, #000 90%, #000 95%, transparent 99%);
  }
  .hero .badge[data-aura="2"]::after{ opacity:.95; animation:auraSpin 7s linear infinite; }
  @keyframes auraSpin{ to{ transform:rotate(360deg); } }
  /* 2.2 (v2.2): stage 3 — the Sovereign stage at 30 days. Same two layers,
     turned up: the halo breathes deeper and slower, the ring runs faster and
     brighter with a counter-lean start. Transform/opacity only (AURA-3). */
  .hero .badge[data-aura="3"]::before{ animation:auraPulse3 2.6s ease-in-out infinite; }
  @keyframes auraPulse3{
    0%,100%{ opacity:.7;  transform:scale(.94); }
    50%    { opacity:1;   transform:scale(1.12); }
  }
  .hero .badge[data-aura="3"]::after{ opacity:1; animation:auraSpin 3.8s linear infinite; }
  /* 2.11: the badge flexes on tap — magnitude follows the aura stage. */
  .hero .badge.flex{ animation:auraFlex 0.9s cubic-bezier(.3,.7,.3,1); }
  .hero .badge[data-aura="2"].flex{ animation:auraFlex2 0.9s cubic-bezier(.3,.7,.3,1); }
  .hero .badge[data-aura="3"].flex{ animation:auraFlex3 0.9s cubic-bezier(.3,.7,.3,1); }
  @keyframes auraFlex{  35%{ transform:scale(1.05); } 65%{ transform:scale(.99); } }
  @keyframes auraFlex2{ 35%{ transform:scale(1.09) rotate(-1.2deg); } 65%{ transform:scale(.985) rotate(.6deg); } }
  @keyframes auraFlex3{ 30%{ transform:scale(1.14) rotate(-2deg); } 55%{ transform:scale(.97) rotate(1.2deg); } 75%{ transform:scale(1.05); } }
  @media (prefers-reduced-motion: reduce){
    .hero .badge[data-aura="1"]::before,
    .hero .badge[data-aura="2"]::before,
    .hero .badge[data-aura="3"]::before{ animation:none; opacity:.85; transform:scale(1); }
    .hero .badge[data-aura="2"]::after,
    .hero .badge[data-aura="3"]::after{ animation:none; opacity:.7; }
    .hero .badge.flex{ animation:none; }
  }
  .rank-name{
    font-family:'Cinzel',serif; font-weight:700; font-size:30px;
    letter-spacing:.14em; margin-top:10px; color:var(--rank);
  }
  .rank-title{font-size:12px; color:var(--muted); letter-spacing:.18em; text-transform:uppercase;}
  .points{margin-top:14px; font-size:14px; color:var(--muted);}
  .points b{color:var(--text); font-size:22px; font-weight:600; display:inline-block;}
  .points b.pulse{animation:ptsPulse .55s ease-out;}
  @keyframes ptsPulse{
    0%{transform:scale(1); text-shadow:0 0 0 transparent;}
    35%{transform:scale(1.3); color:var(--rank); text-shadow:0 0 16px var(--rank-soft);}
    100%{transform:scale(1); text-shadow:0 0 0 transparent;}
  }

  .bar{
    display:flex; gap:4px; margin:14px auto 6px; max-width:340px;
    position:relative; /* 2.4: anchors the gain sweep */
  }
  /* 2.4 (v2.2, Tin): a tick visibly FEEDS the rank bar and the level ring —
     a one-shot shimmer sweep + ring pulse, fired from render() only on real
     gain ticks (animatePoints), never on boot replays. Transform/opacity only. */
  .bar.gain-flash::after, .rs-track.gain-flash::after{
    content:''; position:absolute; inset:-2px; pointer-events:none;
    background:linear-gradient(105deg, transparent 30%, rgba(255,255,255,.38) 50%, transparent 70%);
    transform:translateX(-120%);
    animation:barSweep .7s cubic-bezier(.3,.7,.2,1) forwards;
  }
  @keyframes barSweep{ to{ transform:translateX(120%); } }
  /* The sweep's ::after runs to translateX(120%) and PARKS there (fill-mode
     forwards) until render() strips the class at 800ms — and .bar, unlike its
     twin .rs-track (overflow:hidden), clipped nothing, so a 344px white-gradient
     bar sat ~320px OUTSIDE the hero card at the pip row (Tin's screenshot,
     2026-09-14). Clip left/right only while flashing: the -14px vertical slack
     keeps segPop's scaleY(1.7) + 9px glow unclipped. NOT .hero{overflow:hidden}
     — that would cut the badge aura. */
  .bar.gain-flash{clip-path:inset(-14px 0);}
  .lvl-ring.gain-flash{ animation:ringPulse .6s cubic-bezier(.3,.7,.3,1); }
  @keyframes ringPulse{ 40%{ transform:scale(1.12); } }
  .bar span{flex:1; height:7px; border-radius:99px; background:var(--panel3); transform-origin:center;}
  .bar span.f{background:var(--rank);}
  .bar span.f.pop{animation:segPop .5s cubic-bezier(.3,1.7,.4,1) backwards;}
  @keyframes segPop{
    0%{transform:scaleY(.25); opacity:.3; box-shadow:0 0 0 transparent;}
    55%{transform:scaleY(1.7); opacity:1; box-shadow:0 0 9px var(--rank);}
    100%{transform:scaleY(1); opacity:1; box-shadow:0 0 0 transparent;}
  }
  @media (prefers-reduced-motion: reduce){
    .points b.pulse, .bar span.f.pop{animation:none;}
  }
  .bar-label{font-size:12px; color:var(--muted);}
  .bar-label b{color:var(--rank); font-weight:600;}

  /* 2.4 (2026-09-05, Tin): the rank strip — the hero bar's always-on twin, sticky at
     the top of the Status view. The fill is a scaleX transform (compositor-only),
     the sweep reuses barSweep, the "+N" reuses floatUp, the letter swap is rsSwap
     (rs-prefixed so AURA-3 audits it). z-index 40 sits under .fullday-fx (45). */
  .rank-strip{position:sticky; top:0; z-index:40; display:flex; align-items:center; gap:10px; height:34px;
    margin:0 -16px; padding:0 16px; background:var(--bg); border-bottom:1px solid var(--line);
    font-size:11.5px; color:var(--muted); white-space:nowrap;}
  .rs-rank{font-family:'Cinzel',serif; font-weight:700; font-size:13px; letter-spacing:.1em; color:var(--rank);
    min-width:26px; display:inline-block; transform-origin:center;}
  .rs-rank.swap{animation:rsSwap .6s cubic-bezier(.3,1.5,.4,1);}
  @keyframes rsSwap{ 0%{transform:scale(.5); opacity:0;} 60%{transform:scale(1.25); opacity:1;} 100%{transform:scale(1); opacity:1;} }
  .rs-track{flex:1; position:relative; height:6px; border-radius:99px; background:var(--panel3); overflow:hidden;}
  .rs-fill{position:absolute; inset:0; border-radius:99px; background:var(--rank); transform-origin:left center;
    transform:scaleX(0); transition:transform .65s cubic-bezier(.2,.8,.3,1);}
  .rs-num b{color:var(--text); font-weight:600; font-size:12.5px;}
  .rs-num b.pulse{animation:ptsPulse .55s ease-out;}
  .rs-next b{color:var(--rank); font-weight:600;}
  .rs-lock{margin-right:3px;}
  .rs-float{position:absolute; right:14px; top:50%; pointer-events:none; color:var(--rank); font-weight:600; font-size:12px;
    text-shadow:0 0 12px var(--rank-soft); animation:floatUp .95s ease-out forwards;}
  .rs-float.neg{color:var(--loss);}
  @media (max-width:400px){ .rank-strip{gap:7px; font-size:10.5px;} .rs-xp{display:none;} }
  @media (prefers-reduced-motion: reduce){ .rs-fill{transition:none;} .rs-rank.swap, .rs-num b.pulse{animation:none;} }

  /* align-items:center is load-bearing, not tidiness. Without it the default
     `stretch` applies PER FLEX LINE, so on a wrapped line that also holds the 58px
     level ring every neighbouring pill is stretched to 58px tall with its text
     pinned at the top by its padding — which is what "the active days chip looks
     broken" was. The lock is what makes it visible: the longer momentum label
     widens that chip past the ~316px a 390px phone has, forcing the wrap. */
  .chips{display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:10px; margin-top:14px;}
  .chip{
    white-space:nowrap;
    background:var(--panel); border:1px solid var(--line); border-radius:99px;
    padding:6px 13px; font-size:11.5px; color:var(--muted);
  }
  .chip b{color:var(--text); font-weight:600;}
  .chip.mom b{color:var(--rank);}
  .chip.wkbuff{border-color:rgba(212,160,23,.45); background:rgba(212,160,23,.08);}
  .chip.wkbuff b{color:var(--gold);}
  .chip.wkbuff.locked{background:rgba(239,68,68,.10); border-color:rgba(239,68,68,.45);}
  .chip.wkbuff.locked b{color:var(--loss);}
  /* Level is the one number here that can only ever go up, so it gets the gold
     the app reserves for permanent things (prestige, the 100% bonus) rather than
     the rank colour, which changes as you rise and fall.

     Drawn as a RING rather than a pill: the other chips report a value, this one
     reports a value AND a progress toward the next one, which a bar-in-a-pill
     can't do without stealing the whole row's width. The ring reads at a glance
     from across the room, which is what you want from the number that never falls. */
  .lvl-ring{
    position:relative; width:58px; height:58px; flex:0 0 58px;
    display:flex; align-items:center; justify-content:center; cursor:help;
  }
  .lvl-ring svg{ position:absolute; inset:0; width:100%; height:100%; transform:rotate(-90deg); }
  .lr-track{ fill:none; stroke:var(--line2); stroke-width:4; }
  /* stroke-dashoffset is set from render(); C = 2*pi*19 = 119.38. This is a
     one-shot TRANSITION on a single 46px element that changes only when XP
     moves — not a permanent per-frame animation, which is what the
     transform/opacity rule exists to prevent. */
  .lr-fill{
    fill:none; stroke:var(--gold); stroke-width:4; stroke-linecap:round;
    stroke-dasharray:119.38; stroke-dashoffset:119.38;
    transition:stroke-dashoffset .65s cubic-bezier(.2,.8,.3,1);
  }
  .lr-lbl{
    position:absolute; top:11px; left:0; right:0; text-align:center;
    font-size:7.5px; font-weight:700; letter-spacing:.12em; color:var(--muted);
  }
  .lr-num{
    position:absolute; top:20px; left:0; right:0; text-align:center;
    font-size:19px; font-weight:700; color:var(--gold); line-height:1.1;
  }
  /* ---- Level-up ----
     Deliberately NOT a fourth full-screen ceremony. The app already stacks a
     rank-up nova, a badge unlock, a loot reveal and the 100% banner, and any of
     them can fire on the same tick as this; a blocking overlay here would be the
     one too many. It plays on the ring itself, blocks nothing, and is registered
     in no overlay chain. Transform/opacity only, same rule as the aura. */
  .lvl-ring.levelup .lr-num{ animation:lvlNum .9s cubic-bezier(.2,1.6,.35,1) both; }
  .lvl-ring.levelup svg{ animation:lvlSpin .9s cubic-bezier(.3,.9,.2,1) both; }
  .lvl-ring.levelup::after{
    content:''; position:absolute; left:50%; top:50%; width:58px; height:58px;
    margin:-29px 0 0 -29px; border-radius:50%; border:2px solid var(--gold);
    pointer-events:none; animation:lvlBurst .9s ease-out both;
  }
  .lvl-ring.levelup::before{
    content:''; position:absolute; left:50%; top:50%; width:96px; height:96px;
    margin:-48px 0 0 -48px; border-radius:50%; pointer-events:none;
    background:radial-gradient(closest-side, rgba(212,160,23,.45) 0%, transparent 70%);
    animation:lvlGlow .9s ease-out both;
  }
  @keyframes lvlNum{
    0%{ transform:scale(1); opacity:1; }
    22%{ transform:scale(1.55); opacity:1; }
    55%{ transform:scale(.92); opacity:1; }
    100%{ transform:scale(1); opacity:1; }
  }
  @keyframes lvlSpin{
    0%{ transform:rotate(-90deg) scale(1); }
    30%{ transform:rotate(-90deg) scale(1.12); }
    100%{ transform:rotate(-90deg) scale(1); }
  }
  @keyframes lvlBurst{
    0%{ opacity:.95; transform:scale(1); }
    100%{ opacity:0; transform:scale(2.4); }
  }
  @keyframes lvlGlow{
    0%{ opacity:0; transform:scale(.6); }
    35%{ opacity:1; transform:scale(1); }
    100%{ opacity:0; transform:scale(1.15); }
  }
  @media (prefers-reduced-motion: reduce){
    .lvl-ring.levelup .lr-num, .lvl-ring.levelup svg,
    .lvl-ring.levelup::after, .lvl-ring.levelup::before{ animation:none; }
    .lr-fill{ transition:none; }
  }
  /* while a Geass recovery lock stands, momentum is pinned at ×1.00 — the chip
     has to look like a penalty, or a stuck multiplier reads as a broken one */
  .chip.mom.locked{background:rgba(239,68,68,.10); border-color:rgba(239,68,68,.45);}
  .chip.mom.locked b{color:var(--loss);}
  /* Today's own completion, live. The calendar has always shown this per day
     AFTER the fact; the one day you actually act on had no number anywhere. */
  .chip.today b{color:var(--rank);}
  .chip.today.done{background:var(--rank-soft); border-color:var(--rank);}
  .chip.today.done b{color:var(--rank);}
  .chip.today.rest{opacity:.75;}

  /* LADDER */
  .ladder{
    display:flex; justify-content:space-between; align-items:flex-end;
    margin:18px 2px 0; gap:2px;
  }
  /* min-width:0, or a flex item refuses to shrink below its content. The 40px
     SVG gave the eight rungs a 334px min-content floor, so on a 360px phone the
     Grand Monarch rung escaped the hero card by 29px and the whole page picked
     up horizontal scroll. Same lesson the .loot-hdr-* captions already carry. */
  .rung{text-align:center; flex:1; min-width:0; opacity:.32; transition:opacity .3s;}
  .rung svg{max-width:100%;}
  .rung.reached{opacity:1;}
  .rung.current .lbl{color:var(--rank); font-weight:600;}
  .rung svg{width:40px; height:40px;}
  .rung .lbl{font-size:10px; color:var(--muted); letter-spacing:.05em; display:block; margin-top:2px;}

  /* PANELS */
  section{
    background:var(--card-grad); border:1px solid var(--line); border-radius:14px;
    padding:20px 18px; margin-top:16px;
    box-shadow:var(--card-shadow);
  }
  h2{
    font-family:'Cinzel',serif; font-weight:600; font-size:14px;
    letter-spacing:.22em; color:var(--muted); text-transform:uppercase;
    margin-bottom:14px;
  }
  h2 span{color:var(--rank);}

  .rollover-row{
    margin-top:14px; display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:center;
  }
  .rollover-label{font-size:12px; color:var(--muted);}
  .rollover-pick{display:flex; gap:5px; flex-wrap:wrap; justify-content:center;}
  .rollover-pick button{
    background:var(--panel2); border:1px solid var(--line); border-radius:6px;
    color:var(--muted); font-size:11px; padding:4px 8px; cursor:pointer;
    font-family:'Sora',sans-serif; transition:all .12s;
  }
  .rollover-pick button.on{
    background:var(--rank); border-color:var(--rank); color:#0b0a10; font-weight:600;
  }

  .sync-row{
    margin-top:14px; display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:center;
  }
  .sync-label{font-size:12px; color:var(--muted);}
  .sync-controls{display:flex; gap:6px; flex-wrap:wrap; justify-content:center; align-items:center;}
  .sync-controls input{
    background:var(--panel2); border:1px solid var(--line); border-radius:6px;
    color:var(--text); font-size:11px; padding:4px 8px; font-family:'Sora',sans-serif;
  }
  .sync-controls button{
    background:var(--panel2); border:1px solid var(--line); border-radius:6px;
    color:var(--muted); font-size:11px; padding:4px 8px; cursor:pointer;
    font-family:'Sora',sans-serif; transition:all .12s;
  }
  .sync-status{font-size:11px; color:var(--muted); margin-top:4px;}

  /* 2.20 (v2.2, Tin): the row is a GRID so the schedule line and the pill row
     span the FULL width under the right-side controls — the old single flex
     line squeezed .meta to ~40% on a phone and stacked the pills three-high
     beside dead space. .meta becomes display:contents so its children place
     into the parent grid; row 1 keeps the checkbox/name/controls exactly
     where they were. The tick-ring geometry is untouched (left:25px still
     lands on the checkbox — column 1 starts at the same padding edge). */
  .task{
    display:grid;
    grid-template-columns:auto auto minmax(0,1fr) auto auto auto auto;
    column-gap:12px; align-items:center;
    background:var(--panel); border:1px solid var(--line); border-radius:10px;
    padding:11px 12px; margin-bottom:8px;
    position:relative;
  }
  .task .check{grid-row:1; grid-column:1;}
  .task .emoji{grid-row:1; grid-column:2;}
  .task .meta{display:contents;}
  .task .name{grid-row:1; grid-column:3; min-width:0; overflow-wrap:anywhere; word-break:break-word;}
  .task .edit-btn{grid-row:1; grid-column:4;}
  .task .pts{grid-row:1; grid-column:5;}
  .task .geass-icon{grid-row:1; grid-column:6;}
  .task .del{grid-row:1; grid-column:7;}
  .task .sched{grid-row:2; grid-column:3 / -1;}
  .task .geass-risk{grid-row:3; grid-column:3 / -1;}
  .task .task-rules{grid-row:4; grid-column:3 / -1;}
  /* On a phone the four control columns squeezed the name to ~120px and long
     names wrapped five lines deep (Tin's screenshot). Below 520px the NAME
     takes the whole width and the controls drop down beside the schedule
     line — everything readable, nothing lost. */
  @media (max-width:520px){
    .task .name{grid-column:3 / -1;}
    .task .sched{grid-column:3;}
    .task .edit-btn{grid-row:2; grid-column:4;}
    .task .pts{grid-row:2; grid-column:5;}
    .task .geass-icon{grid-row:2; grid-column:6;}
    .task .del{grid-row:2; grid-column:7;}
  }
  /* ===== task-clear animation ===== */
  .check{position:relative;}
  .task.cleared{border-color:var(--rank); box-shadow:0 0 18px var(--rank-soft); transition:box-shadow .2s, border-color .2s;}
  .slashfx{
    position:absolute; inset:0; pointer-events:none; border-radius:10px; overflow:hidden;
  }
  .slashfx::before{
    content:''; position:absolute; inset:0;
    background:linear-gradient(105deg, transparent 38%, color-mix(in srgb, var(--rank) 55%, transparent) 50%, transparent 62%);
    transform:translateX(-130%);
    animation:slash .5s cubic-bezier(.3,.7,.2,1) .2s forwards;
  }
  @keyframes slash{ to{transform:translateX(130%);} }
  .check.pop{animation:checkPop .5s cubic-bezier(.34,1.56,.64,1);}
  @keyframes checkPop{
    0%{transform:scale(1) rotate(0deg);}
    28%{transform:scale(.74,1.28) rotate(-4deg);}
    52%{transform:scale(1.32,.82) rotate(3deg);}
    72%{transform:scale(.93,1.08) rotate(-1deg);}
    100%{transform:scale(1) rotate(0deg);}
  }
  .clear-glow{
    position:absolute; inset:0; border-radius:10px; pointer-events:none; z-index:1;
    animation:clearGlowPulse .85s ease-out forwards;
  }
  @keyframes clearGlowPulse{
    0%{box-shadow:0 0 0 rgba(0,0,0,0);}
    22%{box-shadow:0 0 26px 3px var(--rank-soft);}
    100%{box-shadow:0 0 0 rgba(0,0,0,0);}
  }
  .clear-ring{
    /* 2.3 (v2.2): translate(-50%,-50%) + left:25px centres on the checkbox
       (x=25); the old translate(0,-50%) put the ring ~11px right of the tick.
       The keyframes below MUST restate the same translate. */
    position:absolute; left:50%; top:50%; width:24px; height:24px;
    transform:translate(-50%,-50%) scale(.4); border-radius:50%;
    border:2.5px solid var(--rank); pointer-events:none; z-index:2; opacity:.9;
    animation:clearRingOut .65s cubic-bezier(.15,.7,.3,1) forwards;
  }
  .clear-ring.r2{border-width:1.5px; opacity:.6; animation-delay:.08s; animation-duration:.75s;}
  @keyframes clearRingOut{
    0%{transform:translate(-50%,-50%) scale(.4); opacity:.9;}
    100%{transform:translate(-50%,-50%) scale(3.2); opacity:0;}
  }
  .clear-ripple{
    position:absolute; left:50%; top:50%; width:24px; height:24px;
    transform:translate(-50%,-50%) scale(.5); border-radius:50%;
    border:1.5px solid var(--rank); pointer-events:none; z-index:2; opacity:.7;
    animation:clearRippleOut 1s cubic-bezier(.1,.6,.25,1) forwards;
  }
  .clear-ripple.r2{animation-delay:.14s;}
  @keyframes clearRippleOut{
    0%{transform:translate(-50%,-50%) scale(.5); opacity:.7;}
    100%{transform:translate(-50%,-50%) scale(4.4); opacity:0;}
  }
  .spark{
    position:absolute; left:50%; top:50%; width:5px; height:5px;
    border-radius:1.5px; background:var(--rank); pointer-events:none; z-index:2;
    animation:sparkFly .55s ease-out forwards;
  }
  @keyframes sparkFly{
    from{transform:translate(-50%,-50%) rotate(0deg) scale(1); opacity:1;}
    to{transform:translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) rotate(220deg) scale(.25); opacity:0;}
  }
  .clear-confetti{
    position:absolute; left:50%; top:50%; width:6px; height:9px;
    pointer-events:none; z-index:2; opacity:0;
    animation:confettiFall .95s cubic-bezier(.15,.5,.3,1) forwards;
  }
  @keyframes confettiFall{
    0%{opacity:0; transform:translate(-50%,-50%) rotate(0deg) translate(0,0);}
    12%{opacity:1;}
    100%{opacity:0; transform:translate(-50%,-50%) rotate(var(--rot)) translate(var(--dx), var(--dy));}
  }
  .float-pts{
    position:absolute; right:48px; top:50%; pointer-events:none; z-index:2;
    color:var(--rank); font-weight:600; font-size:14.5px;
    text-shadow:0 0 12px var(--rank-soft);
    animation:floatUp .95s ease-out forwards;
  }
  @keyframes floatUp{
    0%{opacity:0; transform:translateY(-50%);}
    18%{opacity:1; transform:translateY(-70%);}
    100%{opacity:0; transform:translateY(-230%);}
  }
  @media (prefers-reduced-motion: reduce){
    .slashfx, .float-pts, .rs-float, .spark, .clear-glow, .clear-ring, .clear-ripple, .clear-confetti{display:none;}
    .check.pop{animation:none;}
    .task.reforged{animation:none;}
    .reforge-glow, .reforge-clip{display:none;}
  }

  /* ===== Edit "re-forged" fx — a commitment re-inscribed (distinct from a clear) ===== */
  .task.reforged{ animation:reforgePop .5s cubic-bezier(.2,.8,.3,1); }
  @keyframes reforgePop{
    0%{transform:scale(1);}
    35%{transform:scale(1.018);}
    100%{transform:scale(1);}
  }
  .reforge-glow{
    position:absolute; inset:-1px; border-radius:inherit; pointer-events:none; z-index:2;
    border:1.5px solid var(--rank); opacity:0;
    box-shadow:0 0 22px 2px var(--rank-soft), inset 0 0 14px var(--rank-soft);
    animation:reforgeGlow .85s ease-out forwards;
  }
  @keyframes reforgeGlow{
    0%{opacity:0;}
    22%{opacity:1;}
    100%{opacity:0;}
  }
  .reforge-clip{
    position:absolute; inset:0; overflow:hidden; border-radius:inherit;
    pointer-events:none; z-index:3;
  }
  .reforge-sweep{
    position:absolute; top:0; bottom:0; left:0; width:44%;
    transform:translateX(-160%) skewX(-18deg); opacity:0; mix-blend-mode:screen;
    background:linear-gradient(90deg, transparent, var(--rank-soft) 42%, #f8fafc 50%, var(--rank-soft) 58%, transparent);
    animation:reforgeSweep .72s cubic-bezier(.4,0,.2,1) forwards;
  }
  @keyframes reforgeSweep{
    0%{transform:translateX(-160%) skewX(-18deg); opacity:0;}
    16%{opacity:.85;}
    84%{opacity:.85;}
    100%{transform:translateX(320%) skewX(-18deg); opacity:0;}
  }

  /* ===== 100% day celebration ===== */
  /* Below the overlays (50) and the rank detail (60): this is a celebration, and
     a modal that is up has a better claim on the screen than a banner does. */
  .fullday-fx{position:fixed; inset:0; z-index:45; pointer-events:none; display:flex; align-items:center; justify-content:center; overflow:hidden;}
  .fd-flash{position:absolute; inset:0; background:radial-gradient(circle at 50% 44%, var(--rank-soft), transparent 62%); opacity:0; animation:fdFlash 2.4s ease-out forwards;}
  @keyframes fdFlash{0%{opacity:0;} 8%{opacity:.85;} 32%{opacity:.45;} 100%{opacity:0;}}
  .fd-ring{position:absolute; top:44%; left:50%; width:40px; height:40px; margin:-20px 0 0 -20px; border-radius:50%; border:2px solid var(--rank); opacity:0; animation:fdRing 1.5s cubic-bezier(.2,.7,.3,1) forwards;}
  .fd-ring.r2{animation-delay:.16s; border-width:1px;}
  .fd-ring.r3{animation-delay:.32s; border-width:1px;}
  @keyframes fdRing{0%{transform:scale(.2); opacity:.9;} 65%{opacity:.3;} 100%{transform:scale(10); opacity:0;}}
  .fd-banner{position:relative; text-align:center; transform:scale(.8); opacity:0; animation:fdBanner 2.4s cubic-bezier(.16,1.05,.3,1) forwards;}
  @keyframes fdBanner{0%{transform:scale(.82) translateY(8px); opacity:0;} 12%{transform:scale(1.05); opacity:1;} 20%{transform:scale(1);} 76%{opacity:1; transform:scale(1);} 100%{opacity:0; transform:scale(1.02) translateY(-5px);}}
  .fd-title{font-family:'Cinzel',serif; font-weight:700; font-size:34px; letter-spacing:.06em; color:var(--rank); text-shadow:0 0 24px var(--rank-soft), 0 2px 12px rgba(0,0,0,.6); line-height:1.1;}
  .fd-sub{margin-top:8px; font-size:12px; letter-spacing:.18em; text-transform:uppercase; color:var(--text); opacity:.85;}
  .fd-bonus{margin-top:6px; font-size:13px; font-weight:600; color:var(--rank); text-shadow:0 0 14px var(--rank-soft);}
  .fd-spark{position:absolute; top:44%; left:50%; width:7px; height:7px; margin:-3.5px 0 0 -3.5px; border-radius:50%; background:var(--rank); box-shadow:0 0 8px 2px var(--rank-soft); opacity:0; animation:fdSpark 1.5s ease-out forwards;}
  @keyframes fdSpark{0%{transform:translate(0,0) scale(1); opacity:0;} 10%{opacity:1;} 100%{transform:translate(var(--tx,0),var(--ty,0)) scale(.2); opacity:0;}}
  @media (prefers-reduced-motion: reduce){
    .fd-flash, .fd-ring, .fd-spark{display:none;}
    .fd-banner{animation:none; opacity:1; transform:none;}
  }

  /* ===== Geass fulfillment fx (distinct from a normal task clear) ===== */
  .task.geass-fulfill{
    animation:geassRowFlash 1.05s ease-out;
  }
  @keyframes geassRowFlash{
    0%{box-shadow:0 0 0 rgba(147,51,234,0);}
    18%{box-shadow:0 0 24px 3px rgba(147,51,234,.6), 0 0 52px 12px rgba(150,20,140,.35);}
    100%{box-shadow:0 0 0 rgba(147,51,234,0);}
  }
  .geass-fulfill-ring{
    position:absolute; left:50%; top:50%; width:30px; height:30px;
    transform:translate(-50%,-50%) scale(.3);
    border-radius:50%; border:2px solid rgba(147,51,234,.9);
    pointer-events:none; z-index:3;
    animation:geassRingOut .85s cubic-bezier(.15,.7,.25,1) forwards;
  }
  @keyframes geassRingOut{
    0%{transform:translate(-50%,-50%) scale(.3); opacity:.9; border-color:rgba(147,51,234,.9);}
    55%{border-color:rgba(160,30,150,.7);}
    100%{transform:translate(-50%,-50%) scale(7.5); opacity:0; border-color:rgba(150,20,150,0);}
  }
  .geass-fulfill-wing{
    position:absolute; left:50%; top:50%; width:58px; height:auto;
    transform:translate(-50%,-54%) scale(.15) rotate(-8deg);
    pointer-events:none; z-index:4; opacity:0;
    filter:drop-shadow(0 0 10px rgba(147,51,234,.9)) drop-shadow(0 0 20px rgba(160,20,150,.6));
    animation:geassWingBurst .9s cubic-bezier(.2,.85,.25,1) forwards;
  }
  .geass-fulfill-wing svg{display:block; width:100%; height:auto;}
  @keyframes geassWingBurst{
    0%{opacity:0; transform:translate(-50%,-54%) scale(.12) rotate(-10deg);}
    28%{opacity:1; transform:translate(-50%,-58%) scale(1.08) rotate(2deg);}
    68%{opacity:1; transform:translate(-50%,-62%) scale(1) rotate(0deg);}
    100%{opacity:0; transform:translate(-50%,-82%) scale(.85) rotate(0deg);}
  }
  .geass-fulfill-label{
    position:absolute; left:50%; top:2px; transform:translate(-50%,0);
    font-family:'Cinzel',serif; font-weight:700; font-size:10px; letter-spacing:.24em;
    color:#c084fc; text-shadow:0 0 10px rgba(147,51,234,.8), 0 0 22px rgba(150,20,140,.55);
    pointer-events:none; z-index:4; opacity:0; white-space:nowrap;
    animation:geassLabelUp 1.05s ease-out forwards;
  }
  @keyframes geassLabelUp{
    0%{opacity:0; transform:translate(-50%,6px);}
    22%{opacity:1; transform:translate(-50%,0);}
    72%{opacity:1; transform:translate(-50%,-4px);}
    100%{opacity:0; transform:translate(-50%,-12px);}
  }
  @media (prefers-reduced-motion: reduce){
    .task.geass-fulfill{animation:none;}
    .geass-fulfill-ring, .geass-fulfill-wing, .geass-fulfill-label{display:none;}
  }

  .task .name{flex:1; font-size:14.5px;}
  .task .pts{font-size:12.5px; color:var(--muted); white-space:nowrap;}
  .task.done .name{color:var(--muted);}
  .task.done .name .name-text{text-decoration:line-through;}
  .task.done .name .geass-tag{opacity:.55;}
  .task.done .geass-risk{text-decoration:none;}
  .task.done{border-color:var(--rank);}
  .check{
    width:26px; height:26px; min-width:26px; border-radius:8px;
    border:1.5px solid var(--line2); background:transparent; cursor:pointer;
    display:flex; align-items:center; justify-content:center;
    color:transparent; font-size:14px; transition:all .15s;
  }
  .task.done .check{background:var(--rank); border-color:var(--rank); color:#0b0a10;}
  .del{
    background:none; border:none; color:var(--muted); cursor:pointer;
    font-size:15px; padding:2px 4px; opacity:.5;
  }
  .del:hover{opacity:1; color:#dc2626;}
  .del.armed{opacity:1; color:#dc2626; font-size:12px; font-weight:600; white-space:nowrap;}
  .edit-btn{
    background:none; border:none; color:var(--muted); cursor:pointer;
    font-size:13px; padding:2px 4px; opacity:.5;
  }
  .edit-btn:hover{opacity:1; color:var(--rank);}

  /* EDIT COMMITMENT MODAL */
  .edit-field{text-align:left; margin-top:16px;}
  .edit-label{
    display:block; font-size:10.5px; letter-spacing:.14em; text-transform:uppercase;
    color:var(--muted); margin-bottom:8px;
  }
  .edit-field input[type=text]{
    width:100%; background:var(--bg); border:1px solid var(--line);
    border-radius:8px; padding:10px 12px; color:var(--text);
    font-family:'Sora',sans-serif; font-size:14px;
  }
  .edit-field input[type=text]:focus{outline:none; border-color:var(--rank);}
  .edit-note{font-size:10.5px; color:var(--muted); margin-top:8px; line-height:1.4;}
  .edit-emoji-none{
    min-width:auto !important; padding:0 12px; height:36px; font-size:11px !important;
    letter-spacing:.05em; color:var(--muted); text-transform:uppercase;
  }
  .edit-actions{display:flex; gap:8px; margin-top:22px;}
  .edit-actions button{
    flex:1; border-radius:8px; padding:10px 0; cursor:pointer;
    font-family:'Sora',sans-serif; font-size:14px; font-weight:600;
    border:1px solid var(--line); transition:all .15s;
  }
  .edit-cancel{background:var(--panel2); color:var(--muted);}
  .edit-cancel:hover{color:var(--text); border-color:var(--muted);}
  .edit-save{background:var(--rank); color:#0b0a10; border-color:var(--rank);}
  .edit-save:hover{filter:brightness(1.1);}

  .adder{display:flex; gap:8px; margin-top:12px;}
  .adder input[type=text]{
    flex:1; background:var(--bg); border:1px solid var(--line);
    border-radius:10px; padding:9px 12px; color:var(--text);
    font-family:'Sora',sans-serif; font-size:14px; min-width:0;
  }
  .adder input[type=number]{
    width:70px; background:var(--bg); border:1px solid var(--line);
    border-radius:10px; padding:9px 8px; color:var(--text);
    font-family:'Sora',sans-serif; font-size:14px;
  }
  .adder button{
    background:var(--rank); border:none; border-radius:10px; padding:9px 16px;
    color:#0b0a10; font-weight:600; cursor:pointer; font-family:'Sora',sans-serif;
    font-size:14px;
  }
  input:focus{outline:1px solid var(--rank);}
  .hint{font-size:12px; color:var(--muted); margin-top:10px;}
  .rule-list{
    list-style:none; display:flex; flex-direction:column; gap:7px;
    margin-top:10px; padding:0;
  }
  .rule-list li{
    font-size:12px; color:var(--muted); padding-left:14px; position:relative; line-height:1.5;
  }
  .rule-list li b{color:#fff; font-weight:700;}
  .rule-list li::before{content:'—'; position:absolute; left:0; top:0; color:var(--rank);}
  .hint b{color:var(--text); font-weight:600;}

  .bind-row{display:flex; gap:8px; margin-top:8px;}
  .bind-row button{
    flex:1; border-radius:8px; padding:9px 0; cursor:pointer;
    font-family:'Sora',sans-serif; font-size:14px; font-weight:600;
    border:1px solid transparent; transition:all .15s;
  }
  .bind-btn{background:var(--rank); color:#0b0a10; border-color:var(--rank);}
  .geass-btn{
    background:transparent; color:#dc2626; border-color:#dc2626;
  }
  .geass-btn:hover{background:rgba(220,38,38,.12);}
  .geass-btn.armed{
    background:#dc2626; color:#0b0a10; font-size:12px; letter-spacing:.01em;
  }
  .geass-btn.sworn{
    background:transparent; color:#9333ea; border-color:#9333ea;
  }
  .geass-btn.sworn:hover{background:rgba(147,51,234,.12);}
  .geass-btn.sworn.armed{
    background:#9333ea; color:#0b0a10;
  }
  .task.geass{
    border-color:#9333ea;
    box-shadow:inset 3px 0 0 #9333ea;
    background:linear-gradient(90deg, rgba(147,51,234,.10), transparent 60%), var(--panel2);
  }
  .task .geass-tag{
    font-size:9.5px; font-weight:700; letter-spacing:.06em; color:#dc2626;
    background:rgba(220,38,38,.14); border:1px solid rgba(220,38,38,.45);
    border-radius:4px; padding:1px 5px; margin-left:6px; white-space:nowrap;
    vertical-align:middle;
  }
  .task .single-tag{
    font-size:9.5px; font-weight:700; letter-spacing:.06em; color:var(--rank);
    background:var(--rank-soft); border:1px solid color-mix(in srgb, var(--rank) 40%, transparent);
    border-radius:4px; padding:1px 5px; margin-left:6px; white-space:nowrap;
    vertical-align:middle;
  }
  .task .multi-tag{
    font-size:9.5px; font-weight:700; letter-spacing:.06em; color:var(--rank);
    background:var(--rank-soft); border:1px solid color-mix(in srgb, var(--rank) 40%, transparent);
    border-radius:4px; padding:1px 5px; margin-left:6px; white-space:nowrap;
    vertical-align:middle;
  }
  .task.done .name .multi-tag{opacity:.55;}
  /* Line 2 of a Geass row: the oath's three facts as pills, matching the
     .geass-tag / .single-tag idiom above (9.5px, 700, .06em, radius 4, 1px 5px).
     Wraps on a narrow screen instead of squeezing the schedule line. Note
     .geass-risk is the WRAPPER here, not the cost text — see the renderer. */
  .task .geass-risk{display:flex; flex-wrap:wrap; gap:4px; margin-top:4px; align-items:center;}
  .task .geass-pill{
    font-size:9.5px; font-weight:700; letter-spacing:.06em; line-height:1.6;
    border-radius:4px; padding:1px 5px; white-space:nowrap;
  }
  .task .geass-keep{color:var(--gain); background:rgba(34,197,94,.13); border:1px solid rgba(34,197,94,.45);}
  /* the live weekly buff, quoted on every row beside the schedule (Tin 2026-09-14) */
  .task .week-pill{font-size:9.5px; font-weight:700; letter-spacing:.06em; line-height:1.6; border-radius:4px; padding:1px 5px; white-space:nowrap; margin-left:6px;
    color:var(--gold); background:rgba(212,160,23,.12); border:1px solid rgba(212,160,23,.45); vertical-align:middle;}
  .task .geass-cost{color:var(--loss); background:rgba(239,68,68,.13); border:1px solid rgba(239,68,68,.45);}
  /* Red, identical to the cost pill beside it. This was purple to echo
     .task.geass's own edge, but the lock IS part of the punishment — reading it
     as a third, differently-coloured category made the row look like three
     unrelated facts instead of "here is the gain, here is what a miss costs". */
  .task .geass-lock{color:var(--loss); background:rgba(239,68,68,.13); border:1px solid rgba(239,68,68,.45);}
  /* the status phrase wraps as a UNIT ("required today" never splits mid-phrase
     beside the controls column) */
  .task .freq-required{color:var(--loss); font-weight:600; white-space:nowrap;}
  .task .freq-optional{color:var(--muted); white-space:nowrap;}
  .task .freq-done{color:var(--gain); font-weight:600; white-space:nowrap;}
  .task .freq-count{color:var(--muted); font-weight:400;}
  .geass-icon{
    display:flex; align-items:center; justify-content:center;
    width:30px; flex-shrink:0;
  }
  .geass-icon svg{
    width:28px; height:18px; overflow:visible;
    filter:drop-shadow(0 0 3px #dc2626) drop-shadow(0 0 7px rgba(220,38,38,.65));
  }

  .daypick{display:flex; gap:6px; margin-top:10px;}
  .singlepick{display:flex; gap:8px; margin-top:10px; align-items:center;}
  .singlepick input[type=date]{
    flex:1; background:var(--panel2); border:1px solid var(--line); border-radius:8px;
    color:var(--text); font-family:'Sora',sans-serif; font-size:12px;
    padding:6px 10px; cursor:pointer; color-scheme:dark; min-width:0;
  }
  .singlepick input[type=date]:focus{outline:1px solid var(--rank); border-color:var(--rank);}
  .single-date-label{font-size:12px; color:var(--muted); white-space:nowrap;}
  .daypick button{
    flex:1; background:var(--panel2); border:1px solid var(--line); border-radius:7px;
    padding:6px 0; color:var(--muted); cursor:pointer; font-size:11.5px;
    font-family:'Sora',sans-serif; letter-spacing:.05em; transition:all .12s;
  }
  .daypick button.on{background:var(--rank); border-color:var(--rank); color:#0b0a10; font-weight:600;}
  #tierPick button, #editTierPick button{display:flex; flex-direction:column; align-items:center; gap:2px; line-height:1.1;}
  #tierPick .tier-pts, #editTierPick .tier-pts{font-size:9.5px; opacity:.75;}
  #tierPick button.on .tier-pts, #editTierPick button.on .tier-pts{opacity:.85;}
  .daypick input.custom-count{
    flex:0 0 46px; width:46px; background:var(--panel2); border:1px solid var(--line);
    border-radius:7px; padding:6px 4px; color:var(--text); font-family:'Sora',sans-serif;
    font-size:11.5px; text-align:center;
  }
  .daypick input.custom-count:focus{outline:1px solid var(--rank); border-color:var(--rank);}
  .emojipick{
    display:flex; gap:5px; margin-top:10px; overflow-x:auto; padding-bottom:6px;
    scrollbar-width:thin; scrollbar-color:var(--line) transparent;
  }
  .emojipick::-webkit-scrollbar{height:4px;}
  .emojipick::-webkit-scrollbar-track{background:transparent;}
  .emojipick::-webkit-scrollbar-thumb{background:var(--line); border-radius:2px;}
  .emojipick::-webkit-scrollbar-thumb:hover{background:var(--muted);}
  .emojipick::-webkit-scrollbar-button{display:none; width:0; height:0;}
  .emojipick button{
    background:var(--panel2); border:1px solid var(--line); border-radius:8px;
    min-width:36px; height:36px; font-size:17px; cursor:pointer; flex-shrink:0;
    display:flex; align-items:center; justify-content:center; transition:all .12s;
  }
  .emojipick button.on{border-color:var(--rank); background:color-mix(in srgb, var(--rank) 22%, var(--panel2));}
  .emoji-picker{margin-top:10px;}
  .emoji-picker .emojipick{margin-top:8px;}
  .emoji-cats{
    display:block; text-align:center; white-space:nowrap;
    overflow-x:auto; padding-bottom:4px;
    scrollbar-width:thin; scrollbar-color:var(--line) transparent;
  }
  .emoji-cats::-webkit-scrollbar{height:4px;}
  .emoji-cats::-webkit-scrollbar-track{background:transparent;}
  .emoji-cats::-webkit-scrollbar-thumb{background:var(--line); border-radius:2px;}
  .emoji-cats::-webkit-scrollbar-button{display:none; width:0; height:0;}
  .emoji-cats button{
    display:inline-block; margin:0 3px;
    background:var(--panel2); border:1px solid var(--line); border-radius:7px;
    padding:5px 12px; color:var(--muted); cursor:pointer; font-size:11.5px;
    font-family:'Sora',sans-serif; white-space:nowrap; transition:all .12s;
  }
  .emoji-cats button.on{border-color:var(--rank); color:var(--rank); font-weight:600;}

  /* SORT BAR */
  .sortbar{
    display:flex; align-items:center; gap:6px; margin:2px 0 12px;
    overflow-x:auto; padding-bottom:2px;
    scrollbar-width:none;
  }
  .sortbar::-webkit-scrollbar{display:none;}
  .sortbar .sortbar-label{
    font-size:10.5px; letter-spacing:.14em; text-transform:uppercase;
    color:var(--muted); flex-shrink:0; margin-right:2px;
  }
  .sortbar button{
    background:var(--panel2); border:1px solid var(--line); border-radius:7px;
    padding:5px 12px; color:var(--muted); cursor:pointer; font-size:11.5px;
    font-family:'Sora',sans-serif; white-space:nowrap; flex-shrink:0; transition:all .12s;
  }
  .sortbar button.on{border-color:var(--rank); color:var(--rank); font-weight:600;}
  /* 'Geass first' is a toggle, not one of the five mutually-exclusive modes —
     the rule keeps it from reading as a sixth radio option. */
  .sortbar .sortbar-sep{
    width:1px; align-self:stretch; background:var(--line);
    flex-shrink:0; margin:2px 4px;
  }
  .sortbar button.geass-sort.on{border-color:#9333ea; color:#9333ea;}

  /* GEASS / OTHER group rules in the commitment list */
  .list-divider{
    display:flex; align-items:center; gap:8px;
    margin:14px 0 8px;
    font-size:10.5px; letter-spacing:.14em; text-transform:uppercase;
    color:var(--muted);
  }
  .list-divider:first-child{margin-top:2px;}
  .list-divider::after{
    content:''; flex:1; height:1px; background:var(--line);
  }
  /* 2.14 (v2.2, Tin): the time-of-day rules sit CENTRED like the block
     headers — left-tucked labels read ugly on the phone. Same mirror-::before
     pattern the block header already used. */
  .list-divider::before{
    content:''; flex:1; height:1px; background:var(--line);
  }
  .list-divider.geass::before{background:rgba(147,51,234,.35);}
  .list-divider.geass{color:#9333ea;}
  /* the block header (Geass / Other) leads its group; the time-of-day rule
     under it is the quieter subdivision */
  .list-divider.list-block{font-size:11.5px; font-weight:800; letter-spacing:.16em; margin-top:16px;}
  /* The block header is a title for the whole run below it, so it sits CENTRED
     between two rules rather than tucked against the left edge like the
     time-of-day subdivisions. ::after already draws the right-hand rule; this
     adds its mirror on the left. */
  .list-divider.list-block::before{
    content:''; flex:1; height:1px; background:var(--line);
  }
  .list-divider.geass.list-block::before{background:rgba(147,51,234,.35);}
  .list-divider.list-block + .list-divider{margin-top:6px;}
  .list-divider.geass::after{background:rgba(147,51,234,.35);}

  /* ---- Custom rules per commitment (descriptive only — never judged) ---- */
  .task .rule-chip{
    font-size:9.5px; font-weight:700; letter-spacing:.06em; color:var(--muted);
    background:var(--panel2); border:1px solid var(--line);
    border-radius:4px; padding:1px 5px; margin-left:6px; white-space:nowrap;
    vertical-align:middle; cursor:pointer; font-family:'Sora',sans-serif;
  }
  .task .rule-chip:hover, .task .rule-chip.open{color:var(--rank); border-color:var(--rank);}
  .task-rules{display:block; margin-top:5px;}
  .task-rule{
    display:block; font-size:11px; color:var(--muted); line-height:1.45;
    letter-spacing:.01em; padding-left:2px;
   overflow-wrap:anywhere; word-break:break-word;}
  .day-detail .dd-rules{
    padding:2px 4px 9px 30px; border-bottom:1px solid var(--line);
  }
  .day-detail .dd-rule{font-size:11px; color:var(--muted); line-height:1.5;}
  /* rules editor — shared by the add flow and the edit modal */
  .rule-row{display:flex; gap:6px; align-items:center; margin-bottom:6px;}
  .rule-row input[type=text]{
    flex:1; min-width:0; background:var(--bg); border:1px solid var(--line);
    border-radius:8px; padding:8px 10px; color:var(--text);
    font-family:'Sora',sans-serif; font-size:13px;
  }
  .rule-row input[type=text]:focus{outline:none; border-color:var(--rank);}
  .rule-del{
    background:none; border:none; color:var(--muted); cursor:pointer;
    font-size:15px; padding:2px 6px; opacity:.5; flex-shrink:0;
  }
  .rule-del:hover{opacity:1; color:#dc2626;}
  .rule-add{
    background:var(--panel2); border:1px solid var(--line); border-radius:7px;
    padding:6px 12px; color:var(--muted); cursor:pointer; font-size:11.5px;
    font-family:'Sora',sans-serif; transition:all .12s;
  }
  .rule-add:hover:not([disabled]){color:var(--rank); border-color:var(--rank);}
  .rule-add[disabled]{opacity:.4; cursor:default;}
  /* a standalone label for the add-commitment flow, which has no .edit-field
     wrapper to hang .edit-label off */
  .field-label{
    display:block; font-size:10.5px; letter-spacing:.14em; text-transform:uppercase;
    color:var(--muted); margin-top:14px;
  }

  .task .emoji{font-size:17px; width:24px; text-align:center; flex-shrink:0;}

  /* HABIT STREAKS */
  .streak-row{display:flex; align-items:center; gap:12px; background:var(--panel2); border:1px solid var(--line); border-radius:10px; padding:9px 12px; margin-bottom:8px;}
  .streak-row .emoji{font-size:17px; width:24px; text-align:center; flex-shrink:0;}
  .streak-meta{flex:1; min-width:0;}
  .streak-name{font-size:14px; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .streak-sched{font-size:11px; color:var(--muted); margin-top:2px;}
  .streak-flame{display:flex; align-items:baseline; gap:2px; flex-shrink:0; color:var(--muted);}
  .streak-flame .fl{font-size:15px; filter:grayscale(1); opacity:.4; align-self:center;}
  .streak-flame b{font-size:18px; font-weight:700; line-height:1;}
  .streak-flame.lit{color:var(--rank);}
  .streak-flame.lit .fl{filter:none; opacity:1;}
  .streak-flame.lit b{color:var(--rank);}
  .streak-empty{font-size:13px; color:var(--muted); padding:6px 2px;}
  .modepick{display:flex; gap:6px; margin-top:10px;}
  .modepick button{
    flex:1; background:var(--panel2); border:1px solid var(--line); border-radius:7px;
    padding:7px 0; color:var(--muted); cursor:pointer; font-size:12px;
    font-family:'Sora',sans-serif; transition:all .12s;
  }
  .modepick button.on{border-color:var(--rank); color:var(--rank); font-weight:600;}
  /* .task .meta is display:contents now (see the grid above) — its children are the grid items */
  .task .sched{font-size:10.5px; color:var(--muted); letter-spacing:.04em; display:block;}
  .task.off{opacity:.45;}
  .task.off .sched{color:var(--rank);}

  /* LOG */
  .log-entry{
    display:flex; justify-content:space-between; gap:10px;
    font-size:13px; padding:7px 2px; border-bottom:1px solid var(--line);
  }
  .log-entry:last-child{border-bottom:none;}
  .log-entry .when{color:var(--muted); font-size:11.5px; white-space:nowrap;}
  .log-entry .gain{color:#22c55e; font-weight:700; background:rgba(34,197,94,.12);
    border-radius:4px; padding:0 5px;}
  .log-entry .loss{color:var(--loss); font-weight:700; background:rgba(239,68,68,.10);
    border-radius:4px; padding:0 5px;}
  .log-entry .neutral{color:var(--muted);}
  /* 2.13: kind-coloured edges + day headers make the list scannable */
  .log-entry{border-left:3px solid transparent; padding-left:8px;}
  .log-entry.le-gain{border-left-color:rgba(34,197,94,.55);}
  .log-entry.le-loss{border-left-color:rgba(239,68,68,.55);}
  .log-entry.le-info{border-left-color:rgba(212,160,23,.4);}
  .log-day{font-size:10px; font-weight:800; letter-spacing:.18em; text-transform:uppercase;
    color:var(--muted); padding:10px 0 4px; border-bottom:1px solid var(--line);
    margin-bottom:2px;}

  footer{
    text-align:center; margin-top:22px; font-size:12px; color:var(--muted);
  }
  footer button{
    background:var(--panel); border:1px solid var(--line); color:var(--muted);
    border-radius:10px; padding:9px 16px; cursor:pointer; margin:0 4px;
    font-family:'Sora',sans-serif; font-size:12px;
  }
  footer button:hover{color:var(--text); border-color:var(--muted);}
  footer button.danger{border-color:var(--loss); color:var(--loss); font-weight:600;}
  /* The four footer actions are a deliberate row, not four bare siblings that
     happen to land on one line under footer{text-align:center}. TWO of them
     rewrite their own label (Vacation <-> End Vacation, Reset rank <-> Confirm
     wipe), and a content-sized button re-centres the whole set every time it
     does — the armed reset sentence used to wrap onto its own line and centre
     independently of the trio above it, which is what read as misaligned.
     flex-basis sizes the cells instead, so a label change moves nothing. Same
     idiom as .rollover-row / .sync-row further down this footer. */
  .footer-actions{display:flex; flex-wrap:wrap; gap:8px; justify-content:center;}
  .footer-actions button{flex:1 1 120px; margin:0; padding:9px 8px; min-width:0;}
  /* The confirmation sentence lives out here rather than inside the button —
     that is what keeps the button label short enough to be width-stable. */
  .footer-confirm{
    margin-top:8px; font-size:11.5px; color:var(--loss); font-weight:600; letter-spacing:.02em;
  }
  .footer-confirm[hidden]{display:none;}

  /* TABS */
  .tabs{
    display:flex; gap:8px; margin-top:14px;
  }
  .tabs button{
    position:relative;
    flex:1; background:var(--panel); border:1px solid var(--line); border-radius:12px;
    padding:11px 0; color:var(--muted); cursor:pointer;
    font-family:'Sora',sans-serif; font-weight:600; font-size:12px;
    letter-spacing:.14em; text-transform:uppercase;
    /* letter-spacing is applied AFTER the last character too, so the centred
       text box carries one extra tracking-width of empty space on its right and
       the label reads as sitting left of centre (most visible on phones, where
       the buttons are narrowest). A matching text-indent pushes it back by the
       same amount. Kept in lockstep with letter-spacing in both media queries
       below — change one, change the other. */
    text-indent:.14em;
    transition:all .15s;
  }
  .tabs button.active{
    background:linear-gradient(180deg, var(--panel3), var(--panel2));
    color:var(--text); border-color:var(--rank);
    box-shadow:0 0 0 1px var(--rank) inset, 0 8px 24px -12px var(--rank-soft);
  }
  /* SIX tabs share the row since the Wall arrived — keep the longest label on one
     line, and give the whole set a little back on narrow phones rather than
     letting it wrap. The steps used to start at 400px, which left 412px (Pixel
     6-9) and 428px (iPhone Plus/Pro Max) uncovered: the row overflowed by 35-51px
     and the FAILS tab sat mostly off-screen. */
  /* NO min-width:0 here, unlike .rung: these buttons must stay at least as wide
     as their own label, or the text overflows inside the box (the row fits and
     the words don't). Fitting six tabs is the media steps' job, not shrinking
     the buttons past their content. */
  .tabs button{white-space:nowrap;}
  @media (max-width:440px){
    .tabs{gap:5px;}
    .tabs button{font-size:10.5px; letter-spacing:.04em; text-indent:.04em; padding:9px 0;}
  }
  @media (max-width:400px){
    .tabs{gap:4px;}
    .tabs button{font-size:9.5px; letter-spacing:.02em; text-indent:.02em; padding:9px 0;}
  }
  @media (max-width:360px){
    .tabs{gap:3px;}
    .tabs button{font-size:8.5px; letter-spacing:0; text-indent:0; padding:8px 0;}
  }
  /* Tab entrance — a short lift+fade on whichever view just appeared, so a tab
     change reads as a change rather than an instant swap. Transform/opacity
     only, and the class removes itself on animationend so nothing lingers. */
  /* ---- Boot sync curtain ----
     Blocks input for the length of the boot pull. Without it, a habit ticked
     while the pull is in flight sets userEditedSinceBoot, which makes pullSync
     compare against the LIVE modifiedAt instead of BOOT_MODIFIED_AT — the local
     copy then "wins" and the remote is discarded outright. Measured on a 4s
     pull: a tick at 1s ended with the phone's 577 points replaced by this
     device's 112, pushed to the cloud twice. Blocking input for the window makes
     that race structurally impossible rather than something to reconcile after.
     Normally sub-second and never seen; only a slow connection shows it. */
  #syncCurtain{
    position:fixed; inset:0; z-index:90; display:none;
    align-items:center; justify-content:center;
    background:rgba(13,11,18,.82); backdrop-filter:blur(2px);
  }
  #syncCurtain.show{display:flex;}
  .sc-card{display:flex; flex-direction:column; align-items:center; gap:11px; padding:0 24px; text-align:center;}
  .sc-spin{
    width:30px; height:30px; border-radius:50%;
    border:2.5px solid var(--line2); border-top-color:var(--rank);
    animation:scSpin .8s linear infinite;
  }
  /* transform only — AURA-3 walks every @keyframes in the document asserting exactly that */
  @keyframes scSpin{ to{ transform:rotate(360deg); } }
  .sc-label{font-family:'Cinzel',serif; font-size:15px; font-weight:600; letter-spacing:.08em; color:var(--text);}
  .sc-sub{font-size:11.5px; color:var(--muted); max-width:26ch; line-height:1.45;}
  @media (prefers-reduced-motion: reduce){ .sc-spin{ animation:none; } }

  /* Retimed after Tin reported "no animation at all". It was never failing to
     run — instrumenting a real tab switch showed the class landing correctly and
     the animation starting at currentTime 0 with opacity 0, every time. It was
     simply below the threshold of noticing: 180ms and 6px of travel on a
     full-page container is over before the eye settles, and the ~25ms the
     PROGRESS render blocks first makes the whole interaction read as a snap.
     Longer, further, and with a slight scale so it reads as the panel arriving
     rather than a brightness flicker. Still transform/opacity only (AURA-3
     walks every @keyframes asserting exactly that). */
  .tab-enter{ animation:tabEnter .3s cubic-bezier(.16,.84,.34,1); }
  /* The in-tab switch entrance: swapping 7d/30d/All or bar/Elo, and the calendar's
     month<->year.
     TWO variants on purpose. The CHARTS get opacity only — no transform of any
     kind. Both charts are measured by geometry: thinProgVals reads painted glyph
     rectangles right after the render, and a dozen suite checks measure label
     positions immediately after clicking a range button. A translate or scale is
     still mid-flight at that moment, so every rect comes back shifted and the
     axis reads as broken. Opacity changes no rect, so it is safe everywhere and
     still plainly announces the swap.
     The CALENDAR has no geometry checks and no measured labels, so it can afford
     the small rise that makes the change feel like a change.
     Both names start with tabEnter so AURA-3's prefix audit covers them. */
  .swap-enter{ animation:tabEnterSoft .24s ease-out; }
  @keyframes tabEnterSoft{
    from{ opacity:0; }
    to  { opacity:1; }
  }
  .swap-enter-y{ animation:tabEnterSoftY .24s cubic-bezier(.16,.84,.34,1); }
  @keyframes tabEnterSoftY{
    from{ opacity:0; transform:translateY(8px); }
    to  { opacity:1; transform:none; }
  }
  @keyframes tabEnter{
    from{ opacity:0; transform:translateY(14px) scale(.985); }
    to  { opacity:1; transform:none; }
  }
  /* The active pill's underline sweeps in with it. */
  .tabs button.active::after{
    content:''; position:absolute; left:50%; bottom:5px; height:2px; width:34%;
    transform:translateX(-50%) scaleX(0); transform-origin:center;
    background:var(--rank); border-radius:2px;
    animation:tabUnderline .22s cubic-bezier(.2,.9,.3,1) forwards;
  }
  @keyframes tabUnderline{ to{ transform:translateX(-50%) scaleX(1); } }
  @media (prefers-reduced-motion: reduce){
    .tab-enter{ animation:none; }
    .swap-enter, .swap-enter-y{ animation:none; }
    .tabs button.active::after{ animation:none; transform:translateX(-50%) scaleX(1); }
  }
  /* LOOT tab notification pip — count of pending boxes, tier-agnostic (the
     app's own var(--rank), not any of the rare/legendary/mythic tier colors),
     hidden entirely at 0. Lives on the button itself (not inside #lootView)
     so it stays visible without ever navigating to the tab. */
  .tab-pip{
    position:absolute; top:-6px; right:-6px; min-width:16px; height:16px; padding:0 4px;
    border-radius:999px; background:var(--rank); color:#0b0a10; box-shadow:0 0 0 2px var(--bg);
    font-family:'Sora',sans-serif; font-size:10px; font-weight:700; line-height:16px;
    text-align:center; pointer-events:none;
  }

  /* BADGES TAB */
  .badge-summary{
    display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
    margin:2px 0 16px; font-size:12.5px; color:var(--muted);
  }
  .badge-summary b{color:var(--rank); font-size:15px;}
  .badge-fam{margin-bottom:22px;}
  .badge-fam-head{display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; margin-bottom:2px;}
  .badge-fam-name{
    font-family:'Sora',sans-serif; font-weight:600; font-size:13px;
    letter-spacing:.1em; text-transform:uppercase; color:var(--text);
  }
  .badge-fam-count{font-size:10.5px; color:var(--muted); letter-spacing:.06em;}
  .badge-fam-flavor{font-size:11.5px; color:var(--muted); margin-bottom:10px; line-height:1.45;}
  .badge-row{
    display:flex; gap:10px; overflow-x:auto; padding-bottom:4px;
    scrollbar-width:none;
  }
  .badge-row::-webkit-scrollbar{display:none;}
  .badge-tile{
    flex:0 0 96px; background:var(--panel2); border:1px solid var(--line);
    border-radius:10px; padding:10px 7px 9px; text-align:center;
  }
  .badge-tile.earned{border-color:color-mix(in srgb, var(--bt-accent, var(--rank)) 55%, transparent);
    box-shadow:0 6px 20px -14px var(--bt-accent, var(--rank));}
  .badge-tile.out{border-style:dashed; opacity:.75;}
  .badge-tile .bt-art{width:52px; height:56px; margin:0 auto 6px;}
  .badge-tile .bt-art svg{display:block; width:100%; height:100%;}
  .bt-locked-rank{width:100%; height:100%; filter:grayscale(1); opacity:.32;}
  .badge-tile .bt-tier{
    font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase;
    color:var(--bt-accent, var(--muted)); margin-bottom:3px;
  }
  .badge-tile.locked .bt-tier, .badge-tile.out .bt-tier{color:var(--muted);}
  .badge-tile .bt-sub{font-size:9.5px; color:var(--muted); line-height:1.35;}

  /* LOOT TAB */
  /* "Next Legendary in N · Mythic in M" — muted, .hint-adjacent (same size/
     color/spacing as .hint) but its own class: it's a live forward-looking
     readout, not an empty-state message, so it shouldn't be confused with one
     if a later pass greps for .hint. */
  .loot-countdown{font-size:12px; color:var(--muted); margin:2px 0 12px;}
  .loot-cat-adder-btn{
    background:var(--rank); color:#0b0a10; border:1px solid var(--rank); border-radius:10px;
    padding:9px 16px; cursor:pointer; font-family:'Sora',sans-serif; font-size:13px; font-weight:600;
  }
  .loot-inv-row{
    display:flex; align-items:center; gap:10px; background:var(--panel2); border:1px solid var(--line);
    border-radius:10px; padding:10px 12px; margin-bottom:8px; font-size:13px;
  }
  .loot-pending-card{
    background:var(--panel2); border:1px solid var(--line); border-radius:10px;
    padding:10px 12px; margin-bottom:8px; font-size:13px;
  }
  .loot-pending-head{display:flex; align-items:center; gap:10px; margin-bottom:8px;}
  .loot-open-as-list{display:flex; flex-direction:column; gap:8px;}
  .loot-open-as{display:flex; flex-direction:column; gap:2px;}
  .loot-open-as-btn{
    background:var(--panel); border:1px solid var(--line); border-radius:8px;
    padding:7px 10px; color:var(--text); cursor:pointer; font-family:'Sora',sans-serif;
    font-size:12.5px; text-align:left;
  }
  .loot-open-as-btn:hover:not(:disabled){border-color:var(--rank); color:var(--rank);}
  .loot-open-as-btn:disabled{opacity:.45; cursor:not-allowed;}
  .loot-open-as-why{font-size:11px; color:var(--loss);}

  /* ===== The chest =====
     A pending box used to be a text button; it is the single most rewarding thing
     in the app and it looked like a form control. Same <button class="loot-open-as-btn">
     underneath (every click target, disabled state and label is unchanged) — the
     chest is inert decoration inside it. Tier colours come from the shared palette;
     --rc/--rc2 mirror the reel's rarity vars so the two read as one system. */
  .lb-chest-btn{display:flex; align-items:center; gap:12px; padding:10px 12px;}
  /* the editor's showcase chest (.lb-showcase) shares these — without them it
     fell back to the rare-blue defaults at every tier (5.8, 2026-09-05) */
  .lb-chest-btn.loot-tier-rare, .lb-showcase.loot-tier-rare{--rc:#2563eb; --rc2:#7cb0ff; --rcT:rgba(59,130,246,.45);}
  .lb-chest-btn.loot-tier-legendary, .lb-showcase.loot-tier-legendary{--rc:#d4a017; --rc2:#ffdc7a; --rcT:rgba(212,160,23,.45);}
  .lb-chest-btn.loot-tier-mythic, .lb-showcase.loot-tier-mythic{--rc:#e0143c; --rc2:#a855f7; --rcT:rgba(200,30,120,.5);}
  .lb-chest{
    position:relative; flex:0 0 34px; width:34px; height:30px;
    transition:transform .25s ease;
  }
  /* Idle chests are STATIC. A pending list can be dozens of boxes long, and an
     infinite float on each is permanent compositor work for something nobody is
     looking at. The lift and the lid come on hover, where the motion is doing a
     job. The drop ceremony's own chests still animate — there are at most six. */
  .loot-open-as-btn:hover:not(:disabled) .lb-chest{transform:translateY(-3px);}
  .lb-chest-body{
    position:absolute; left:0; right:0; bottom:0; height:18px; border-radius:3px 3px 6px 6px;
    background:linear-gradient(180deg, var(--rc,#2563eb), rgba(0,0,0,.62));
    box-shadow:inset 0 -3px 6px rgba(0,0,0,.45);
  }
  .lb-chest-lid{
    position:absolute; left:0; right:0; top:0; height:14px; border-radius:8px 8px 2px 2px;
    background:linear-gradient(180deg, var(--rc2,#7cb0ff), var(--rc,#2563eb));
    transform-origin:50% 100%; transition:transform .28s cubic-bezier(.2,1.3,.4,1);
  }
  .lb-chest-lock{
    position:absolute; left:50%; top:10px; width:7px; height:9px; margin-left:-3.5px;
    border-radius:2px; background:var(--gold); z-index:2;
  }
  .lb-chest-shine{position:absolute; inset:0; overflow:hidden; border-radius:7px; pointer-events:none;}
  /* Hover-only: a pending list can hold dozens of chests, and an always-on sweep
     on each is a permanent repaint for something you only look at one at a time. */
  .lb-chest-shine::after{
    content:''; position:absolute; top:-45%; left:-70%; width:38%; height:190%;
    background:linear-gradient(90deg, transparent, rgba(255,255,255,.55), transparent);
    transform:rotate(18deg); opacity:0;
  }
  .loot-open-as-btn:hover:not(:disabled) .lb-chest-shine::after{
    opacity:1; animation:lbChestShine 1.5s ease-in-out infinite;
  }
  .lb-chest-label{flex:1 1 auto; text-align:left;}
  .loot-open-as-btn:hover:not(:disabled){transform:translateY(-1px);}
  .loot-open-as-btn:hover:not(:disabled) .lb-chest-lid{transform:rotateX(52deg) translateY(-3px);}
  .loot-open-as-btn:disabled .lb-chest{animation:none; filter:grayscale(.7);}
  @keyframes lbChestFloat{0%,100%{transform:translateY(0);} 50%{transform:translateY(-3px);}}
  @keyframes lbChestShine{0%,55%{left:-70%;} 100%{left:130%;}}
  @media (prefers-reduced-motion: reduce){
    .lb-chest, .lb-chest-shine::after{animation:none;}
  }

  /* ===== "LOOT SECURED" — the drop reveal =====
     Raised once when boxes are newly earned, so a 100% day announces itself
     instead of quietly incrementing a pip on a tab you might not visit. */
  #lootDropOverlay .ld-stage{
    position:relative; display:flex; flex-direction:column; align-items:center; gap:14px;
    padding:34px 30px; text-align:center;
  }
  #lootDropOverlay .ld-title{
    font-family:'Cinzel',Georgia,serif; font-size:26px; letter-spacing:.16em; color:var(--gold);
    text-shadow:0 0 26px rgba(240,180,41,.6);
    animation:ldTitleIn .6s cubic-bezier(.2,1.2,.3,1) both;
  }
  #lootDropOverlay .ld-sub{font-size:13px; color:var(--muted); animation:ldTitleIn .6s .08s cubic-bezier(.2,1.2,.3,1) both;}
  #lootDropOverlay .ld-row{display:flex; flex-wrap:wrap; justify-content:center; gap:16px; margin-top:6px;}
  #lootDropOverlay .ld-box{
    display:flex; flex-direction:column; align-items:center; gap:8px;
    animation:ldBoxIn .55s cubic-bezier(.2,1.35,.35,1) both;
    animation-delay:calc(.18s + var(--i) * .13s);   /* they land one after another */
  }
  #lootDropOverlay .ld-box.loot-tier-rare{--rc:#2563eb; --rc2:#7cb0ff; --rcT:rgba(59,130,246,.5);}
  #lootDropOverlay .ld-box.loot-tier-legendary{--rc:#d4a017; --rc2:#ffdc7a; --rcT:rgba(212,160,23,.5);}
  #lootDropOverlay .ld-box.loot-tier-mythic{--rc:#e0143c; --rc2:#a855f7; --rcT:rgba(200,30,120,.55);}
  #lootDropOverlay .ld-box .lb-chest{
    flex:0 0 58px; width:58px; height:52px;
    animation:lbChestFloat 2.8s ease-in-out infinite;
  }
  #lootDropOverlay .ld-box .lb-chest-body{height:31px; border-radius:4px 4px 9px 9px;}
  #lootDropOverlay .ld-box .lb-chest-lid{height:24px; border-radius:13px 13px 3px 3px;}
  #lootDropOverlay .ld-box .lb-chest-lock{top:17px; width:11px; height:14px; margin-left:-5.5px;}
  #lootDropOverlay .ld-box-tier{
    font-family:'Sora',sans-serif; font-weight:700; font-size:10px; letter-spacing:.1em;
    text-transform:uppercase; color:var(--rc2);
  }
  #lootDropOverlay .ld-more{align-self:center; font-size:12px; color:var(--muted);}
  #lootDropOverlay .ld-tap{font-size:11px; color:var(--muted); opacity:.7; margin-top:10px;}
  @keyframes ldTitleIn{from{opacity:0; transform:translateY(-12px) scale(.94);} to{opacity:1; transform:none;}}
  @keyframes ldBoxIn{
    0%{opacity:0; transform:translateY(26px) scale(.6) rotate(-8deg);}
    70%{opacity:1;}
    100%{opacity:1; transform:none;}
  }
  @media (prefers-reduced-motion: reduce){
    #lootDropOverlay .ld-title, #lootDropOverlay .ld-sub, #lootDropOverlay .ld-box{animation:none;}
  }
  .loot-result-text{font-weight:600; margin-bottom:8px;}
  .loot-result-dismiss{
    background:var(--panel); border:1px solid var(--line); border-radius:8px;
    padding:6px 10px; color:var(--text); cursor:pointer; font-family:'Sora',sans-serif; font-size:12px;
  }
  /* Own full-width line: as an in-row flex cell it stole width from the flex:1
     name field, shifting every cell after it left and knocking that one row out
     of alignment with all the others. It is a footnote, not a column. */
  .loot-item-nothing-tag{font-size:10px; color:var(--muted); white-space:nowrap; flex:0 0 100%; order:10;}
  .loot-tier-badge{
    font-family:'Sora',sans-serif; font-weight:700; font-size:10.5px; letter-spacing:.08em;
    text-transform:uppercase; padding:3px 8px; border-radius:6px; background:var(--panel); color:var(--muted);
    border:1px solid var(--line2);
  }
  /* Tier accents. ONE palette across the whole loot feature — these chips, the
     weight sliders below and the opening ceremony all use the same pair per
     tier: rare #3b82f6, legendary --gold (#d4a017), mythic the crimson/violet
     fusion #e0143c -> #9333ea. (Deliberately NOT the app's rank palette: rank E
     red #dc2626 and rank S violet #8b5cf6 are a different, unrelated scale.) */
  /* The Loot log's chips are listed alongside the pending-card badges so the two
     can never drift. Without the .loot-log-tier half the log's chips fell back to
     `currentColor` (the row's muted grey) while the text beside them was already
     tier-coloured — the two halves of the same row disagreeing. */
  .loot-tier-badge.loot-tier-rare,
  .loot-log-tier.loot-tier-rare{color:#3b82f6; border-color:#3b82f6;}
  .loot-tier-badge.loot-tier-mythic,
  .loot-log-tier.loot-tier-mythic{
    color:#e0143c; border-color:#e0143c;
    background:linear-gradient(90deg, rgba(224,20,60,.16), rgba(147,51,234,.18));
  }
  .loot-tier-badge.loot-tier-legendary,
  .loot-log-tier.loot-tier-legendary{color:var(--gold); border-color:var(--gold);}
  /* an eternal ITEM can reach the inventory badge, which had no rule for it and
     fell back to the muted default — indistinguishable from an unstyled tier */
  .loot-tier-badge.loot-tier-eternal,
  .loot-log-tier.loot-tier-eternal{
    color:#f4f4f5; border-color:#f4f4f5;
    background:linear-gradient(90deg, rgba(10,10,10,.55), rgba(255,255,255,.18));
  }
  /* Per-ITEM rarity — same three-tier palette, independent of which tier table
     the item sits in (a rare box can drop a mythic-tagged item). The config
     pill cycles rare -> legendary -> mythic on tap; .rar-* is the same colors
     as a plain text/background utility for everywhere else the rarity shows. */
  .loot-item-rarity-pill{
    flex:0 0 22px; width:22px; height:22px; border-radius:6px; border:1px solid var(--line2);
    background:var(--panel); color:var(--muted); font-family:'Sora',sans-serif; font-weight:700;
    font-size:11px; line-height:1; cursor:pointer; padding:0;
  }
  .loot-item-rarity-pill.rar-rare{color:#3b82f6; border-color:#3b82f6;}
  .loot-item-rarity-pill.rar-legendary{color:var(--gold); border-color:var(--gold);}
  .loot-item-rarity-pill.rar-mythic{
    color:#e0143c; border-color:#e0143c;
    background:linear-gradient(90deg, rgba(224,20,60,.16), rgba(147,51,234,.18));
  }
  .rar-rare{color:#3b82f6;}
  .rar-legendary{color:var(--gold);}
  .rar-mythic{color:#e0143c;}
  /* Mythic as TEXT reads as the crimson->violet fusion the badge already uses,
     rather than the flat crimson (which is hard to tell from rank E's red at
     small sizes). Painted through the glyphs via background-clip, so it needs an
     explicit transparent fill; the flat .rar-mythic stays for borders and pills. */
  .rar-mythic-grad{
    background:linear-gradient(90deg,#e0143c,#9333ea);
    -webkit-background-clip:text; background-clip:text;
    color:transparent; font-weight:700;
  }
  .loot-log-plain{color:var(--text);}
  .loot-pending-date{color:var(--muted); font-size:11.5px;}
  .loot-inv-emoji{font-size:17px; width:22px; text-align:center; flex-shrink:0;}
  .loot-inv-img{width:22px; height:22px; object-fit:cover; border-radius:5px; flex-shrink:0;}
  .loot-result-img{width:18px; height:18px; object-fit:cover; border-radius:4px; vertical-align:-4px;}
  /* ---- Loot log: the permanent record, at the bottom of the LOOT tab ---- */
  .loot-log-row{
    display:flex; align-items:center; gap:8px;
    padding:7px 2px; border-bottom:1px solid var(--line);
    font-size:12.5px; min-width:0;
  }
  .loot-log-row:last-of-type{border-bottom:none;}
  .loot-log-date{color:var(--muted); font-size:11px; flex:0 0 auto; font-variant-numeric:tabular-nums;}
  .loot-log-tier{
    flex:0 0 auto; font-size:9px; font-weight:800; letter-spacing:.08em;
    text-transform:uppercase; padding:1px 5px; border-radius:4px; border:1px solid currentColor;
  }
  .loot-log-img{width:16px; height:16px; object-fit:cover; border-radius:3px; flex:0 0 auto;}
  .loot-log-emoji{flex:0 0 auto; font-size:14px; line-height:1;}
  /* the only cell allowed to absorb slack — min-width:0 so a long item name
     ellipses instead of shoving the date and tier chip out of their columns */
  .loot-log-text{flex:1 1 auto; min-width:0; overflow:hidden;  white-space:normal; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;}
  .loot-log-cat{color:var(--muted); font-size:10.5px; flex:0 0 auto; max-width:34%;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .loot-log-earned{opacity:.72;}   /* earning is the quietest of the three */
  .loot-log-more{
    margin-top:10px; display:flex; align-items:center; gap:10px;
    justify-content:space-between; flex-wrap:wrap;
  }
  .loot-log-more-btn{
    padding:5px 12px; font-size:11.5px; font-weight:700; letter-spacing:.04em;
    border-radius:7px; border:1px solid var(--line); background:var(--panel);
    color:var(--text); cursor:pointer;
  }
  .loot-log-more-btn:hover{border-color:var(--rank); color:var(--rank);}
  .loot-log-toggle{
    display:block; width:100%; text-align:left; margin-bottom:8px;
    padding:5px 2px; font-size:11px; font-weight:700; letter-spacing:.12em;
    text-transform:uppercase; color:var(--muted);
    background:none; border:none; border-bottom:1px solid var(--line); cursor:pointer;
  }
  .loot-log-toggle:hover{color:var(--rank);}
  .loot-inv-name{flex:1; min-width:0;}
  .loot-inv-count{color:var(--muted); font-size:12px; white-space:nowrap; flex:0 0 auto;}
  /* Consume (-1) button. Compact by default; the tier-accent hover reuses the
     one palette across the loot feature (see the tier-badge comment above).
     min 40px square touch target even though the glyph itself is tiny. */
  .loot-inv-minus{
    background:var(--panel); border:1px solid var(--line); border-radius:8px;
    color:var(--muted); cursor:pointer; font-family:'Sora',sans-serif;
    font-size:13px; font-weight:700; min-width:40px; min-height:40px;
    flex:0 0 auto; transition:all .15s;
  }
  .loot-inv-minus.tier-rare:hover{border-color:#3b82f6; color:#3b82f6;}
  .loot-inv-minus.tier-legendary:hover{border-color:var(--gold); color:var(--gold);}
  .loot-inv-minus.tier-mythic:hover{border-color:#e0143c; color:#e0143c;}
  .loot-inv-minus.armed{
    border-color:#dc2626; color:#dc2626; font-size:12px; font-weight:600;
    white-space:nowrap; padding:0 10px;
  }
  .loot-cat-card{
    background:var(--panel2); border:1px solid var(--line); border-radius:10px;
    padding:14px; margin-bottom:12px;
  }
  .loot-cat-head{display:flex; gap:8px; align-items:center; margin-bottom:10px;}
  .loot-cat-head input[type=text]{
    background:var(--bg); border:1px solid var(--line); border-radius:8px;
    padding:8px 10px; color:var(--text); font-family:'Sora',sans-serif; font-size:13px; min-width:0;
  }
  .loot-cat-name{flex:1;}
  .loot-cat-emoji{flex:0 0 46px; width:46px; text-align:center;}
  .loot-tier-pick{margin-bottom:8px;}
  /* The tier tabs are about the BOX's rarity, so they take the loot palette —
     not var(--rank), which .daypick button.on otherwise supplies and which made
     every selected tier read as whatever rank you happen to be standing at (all
     three looked gold at Rank C). Unselected tabs keep a quiet tint of the same
     colour so the three are still tellable apart at a glance. */
  .loot-tier-pick button.loot-tier-rare{color:#3b82f6;}
  .loot-tier-pick button.loot-tier-legendary{color:var(--gold);}
  .loot-tier-pick button.loot-tier-mythic{color:#e0143c;}
  .loot-tier-pick button.on.loot-tier-rare{
    background:#3b82f6; border-color:#3b82f6; color:#0b0a10;
  }
  .loot-tier-pick button.on.loot-tier-legendary{
    background:var(--gold); border-color:var(--gold); color:#0b0a10;
  }
  .loot-tier-pick button.on.loot-tier-mythic{
    background:linear-gradient(90deg,#e0143c,#9333ea); border-color:#e0143c; color:#fff;
  }
  /* The always-visible copy of the tier's `title` text. A title= tooltip is
     unreachable on touch, so the same sentence is printed under the pills —
     which also gives the Σ readout below its own line instead of sharing the
     tier row's edge. */
  .loot-tier-help{
    font-size:11.5px; color:var(--muted); line-height:1.45; font-style:italic;
    margin-bottom:10px;
  }
  .loot-sum-row{font-size:12px; color:var(--muted); margin-bottom:8px; font-weight:600;}
  .loot-sum-warn{color:var(--loss) !important;}
  /* Column captions above the item rows. Every cell mirrors the matching
     .loot-item-* flex basis below so the captions line up over their controls,
     but under its OWN class names on purpose — the suite selects item controls
     by nth-match position (":nth-match(.loot-item-weight, 1)"), and this row
     renders BEFORE them, so reusing a class would shift every one of those
     indices by one (the same trap the category drop-chance row documents).
     Presentational only: aria-hidden, since each control already carries its
     own title/aria-label. */
  /* flex-wrap MIRRORS .loot-item-row's. Without it the two rows wrapped at
     different widths and the columns came apart: between 481 and 487px the item
     row pushed its delete button onto a second line (freeing 29px, which its
     name field absorbed) while this row, unable to wrap, kept squeezing — so
     every caption from NAME rightward sat 29px left of the control it names.
     The 480px media query below was meant to cover exactly this, but the row
     actually stops fitting at 487px, so it fired seven pixels too late.
     Matching the wrap behaviour fixes it structurally, with no magic width to
     re-measure whenever a column's size changes. */
  .loot-hdr-row{
    display:flex; flex-wrap:wrap; row-gap:6px; gap:6px; align-items:center; margin-bottom:5px;
    font-family:'Sora',sans-serif; font-size:9px; font-weight:700; letter-spacing:.1em;
    text-transform:uppercase; color:var(--muted); opacity:.7;
  }
  /* NAME sits over a left-aligned <input>, so it matches that input's TEXT
     origin (1px border + 8px padding), not its box edge.
     min-width is 18px, NOT 0, to match the floor its <input> actually has:
     min-width:0 lets a border box reach zero, but padding+border (8+8+1+1) can
     never collapse, so the input bottoms out at 18px and the row wraps there.
     A 0-floor caption kept shrinking past that point and desynced the wrap.
     This is the name cell only — the min-width:0 on the qty/rarity captions
     below is a different fix (see there) and stays untouched. */
  .loot-hdr-name{flex:1; min-width:18px; text-indent:9px;}
  .loot-hdr-gap-emoji{flex:0 0 38px;}
  .loot-hdr-gap-img{flex:0 0 30px;}
  /* Mirrors .loot-item-img-clear, which is now ALWAYS rendered (hidden when the
     row has no image) precisely so this column exists on every row. Before that,
     a row WITH a picture was 26px wider through the middle than one without, so
     the rows did not line up with each other, let alone with these captions. */
  .loot-hdr-gap-clear{flex:0 0 20px;}
  /* min-width:0 is the whole fix. A flex item defaults to min-width:auto, so a
     caption wider than its basis GROWS instead of holding the column width:
     "Rar" needs ~23.8px against a 22px basis, so that one cell ran 1.78px wide,
     which came out of the flexible name cell and dragged every caption BEFORE
     it 1.78px left. That, not type metrics, was the visible misalignment.

     Deliberately NO text-indent here, unlike .tabs button. The trailing
     letter-space does shift a centred glyph run, but at 9px/.1em that is ~0.45px
     — measured smaller than the residual it was meant to cancel, and no
     non-flaky check can tell the two apart (a mutation removing it passed the
     whole suite). It was maintenance coupling buying nothing. */
  .loot-hdr-qty{flex:0 0 42px; min-width:0; text-align:center;}
  .loot-hdr-rarity{flex:0 0 22px; min-width:0; text-align:center;}
  /* LEFT-aligned to where the slider starts — the same rule NAME follows, and
     what Tin asked for after centring was tried twice and still read wrong.
     The 152px cell holds the slider (~77px) then a number box, a % and steppers
     filling its right half, so centring on the CELL put the caption over the
     number box; centring on the slider (padding-right:77px, the previous fix)
     was arithmetically on target yet still read as "floating in the middle",
     because nothing else in this header is centred over a sub-part of its cell.
     Left-aligned, the caption's first glyph sits on the slider's own left edge,
     so the column reads top-to-bottom like NAME does. No text-indent: the
     slider starts flush at the cell's content edge, unlike NAME's input which
     carries a 1px border + 8px padding before its text. */
  .loot-hdr-chance{flex:0 0 152px; min-width:0; text-align:left;}
  /* Explicit, not intrinsic: .del sets no width, so this 23px was a measured
     guess at "15px glyph + padding" that any font change would silently break.
     .loot-item-del below now states the same 23px. */
  .loot-hdr-gap-del{flex:0 0 23px;}
  /* Category-level drop chance — visually the same slider/number/stepper cell
     as an item's weight below, but under its OWN class names: tests select
     the item-level controls by nth-match position (":nth-match(.loot-item-
     weight, 1)" etc.), and this row renders earlier in the card, so sharing a
     class would silently shift every one of those indices by one. */
  .loot-cat-drop-row{display:flex; align-items:center; gap:10px; margin-bottom:10px;}
  .loot-cat-drop-label{font-size:12px; color:var(--muted); font-weight:600; flex:1; min-width:0;}
  .loot-cat-drop-wrap{display:flex; align-items:center; gap:5px; flex:0 0 152px; min-width:0;}
  .loot-cat-drop-slider{
    -webkit-appearance:none; appearance:none; flex:1 1 0; width:0; min-width:0; height:6px;
    background:transparent; cursor:pointer; outline:none;
    --loot-fill-1:#3b82f6; --loot-fill-2:#3b82f6;
  }
  .loot-cat-drop-slider::-webkit-slider-runnable-track{
    height:6px; border-radius:999px;
    background:linear-gradient(90deg, var(--loot-fill-1) 0%, var(--loot-fill-2) var(--val,0%), var(--panel) var(--val,0%), var(--panel) 100%);
    transition:background .12s ease-out;
  }
  .loot-cat-drop-slider::-webkit-slider-thumb{
    -webkit-appearance:none; width:15px; height:15px; margin-top:-4.5px; border-radius:50%;
    background:var(--panel2); border:2px solid var(--loot-fill-2, var(--rank));
    box-shadow:0 0 7px 1px color-mix(in srgb, var(--loot-fill-2, var(--rank)) 65%, transparent);
    cursor:pointer; transition:box-shadow .12s ease-out;
    position:relative; z-index:2; /* same paint-order fix as the item weight thumb below */
  }
  .loot-cat-drop-slider::-moz-range-track{ height:6px; border-radius:999px; background:var(--panel); }
  .loot-cat-drop-slider::-moz-range-progress{
    height:6px; border-radius:999px; background:linear-gradient(90deg, var(--loot-fill-1), var(--loot-fill-2));
  }
  .loot-cat-drop-slider::-moz-range-thumb{
    width:15px; height:15px; border-radius:50%; border:2px solid var(--loot-fill-2, var(--rank));
    background:var(--panel2); box-shadow:0 0 7px 1px color-mix(in srgb, var(--loot-fill-2, var(--rank)) 65%, transparent);
    cursor:pointer;
  }
  .loot-cat-drop-num{
    flex:0 0 38px; width:38px; background:var(--panel2); border:1px solid var(--line);
    border-radius:7px; padding:5px 2px; color:var(--text); font-family:'Sora',sans-serif;
    font-size:11.5px; text-align:center; font-variant-numeric:tabular-nums;
    -moz-appearance:textfield; appearance:textfield;
  }
  .loot-cat-drop-num:focus{outline:1px solid var(--rank); border-color:var(--rank);}
  .loot-cat-drop-num::-webkit-outer-spin-button,
  .loot-cat-drop-num::-webkit-inner-spin-button{-webkit-appearance:none; margin:0;}
  .loot-cat-drop-pct{font-size:11px; color:var(--muted);}
  .loot-cat-drop-step{display:flex; flex-direction:column; gap:2px; flex:0 0 15px;}
  /* flex-wrap is a no-op for the control cells at >=481px (the fixed cells fit
     inside .wrap and .loot-item-name has min-width:0, so it shrinks rather than
     wrapping) — it is here so .loot-item-nothing-tag can take its own line
     instead of stealing width from the name field and shifting that one row's
     cells out of line with every other row's. */
  .loot-item-row{display:flex; flex-wrap:wrap; row-gap:6px; gap:6px; align-items:center; margin-bottom:6px;}
  .loot-item-row input[type=text]{
    background:var(--panel); border:1px solid var(--line); border-radius:7px;
    padding:6px 8px; color:var(--text); font-family:'Sora',sans-serif; font-size:12.5px; min-width:0;
  }
  .loot-item-name{flex:1;}
  .loot-item-emoji{flex:0 0 38px; width:38px; text-align:center;}
  .loot-item-img-btn{
    flex:0 0 30px; width:30px; height:30px; padding:0; display:flex; align-items:center; justify-content:center;
    background:var(--panel); border:1px solid var(--line); border-radius:7px; font-size:15px; cursor:pointer;
    overflow:hidden;
  }
  .loot-item-img-thumb{width:100%; height:100%; object-fit:cover; border-radius:6px;}
  .loot-item-img-clear{
    flex:0 0 20px; width:20px; height:20px; padding:0; display:flex; align-items:center; justify-content:center;
    background:var(--panel); border:1px solid var(--line); border-radius:6px; color:var(--muted);
    font-size:12px; line-height:1; cursor:pointer;
  }
  /* Always in the layout, invisible when the row has no image — that is what
     keeps every row's columns on the same x positions, and what lets the header
     have a cell to align against. Not display:none, which would collapse it. */
  .loot-item-img-clear.is-empty{visibility:hidden; pointer-events:none;}
  /* Stated, not intrinsic — .del carries no width; .loot-hdr-gap-del mirrors it. */
  .loot-item-del{flex:0 0 23px; width:23px;}
  .loot-item-qty{
    flex:0 0 42px; width:42px; background:var(--panel); border:1px solid var(--line);
    border-radius:7px; padding:6px 2px; color:var(--text); font-family:'Sora',sans-serif;
    font-size:12px; text-align:center;
  }
  /* Weight slider — a plain <input type=range> with no native look (both
     vendor thumb pseudos rebuilt) plus a live-glide fill: --val (a %, kept in
     sync by onLootWeightInput on every drag tick) drives a hard-stop gradient
     on the track so the filled portion visibly tracks the thumb. The two tier
     color stops (--loot-fill-1/-2) are set once per category-card render by a
     `tier-${tier}` class — same two vars feed the thumb's border/glow via
     inheritance into the pseudo-elements, so mythic's red→purple "fusion"
     falls out of the same rule as rare/legendary's flat colors for free. */
  /* The weight cell holds THREE views of one number — the slider, a typeable
     field and a ±1 stepper — kept in lockstep by syncLootWeightUI(). */
  .loot-item-weight-wrap{display:flex; align-items:center; gap:5px; flex:0 0 152px; min-width:0;}
  /* width:0 + flex-basis:0, not just min-width:0 — a range input keeps a ~129px
     intrinsic width in Chrome even with appearance:none, and that alone made the
     three-part cell overflow its own track. */
  .loot-item-weight{
    -webkit-appearance:none; appearance:none; flex:1 1 0; width:0; min-width:0; height:6px;
    background:transparent; cursor:pointer; outline:none;
  }
  .loot-cat-drop-slider.tier-rare{ --loot-fill-1:#3b82f6; --loot-fill-2:#3b82f6; }
  .loot-cat-drop-slider.tier-legendary{ --loot-fill-1:var(--gold); --loot-fill-2:var(--gold); }
  .loot-cat-drop-slider.tier-mythic{ --loot-fill-1:#e0143c; --loot-fill-2:#9333ea; }
  .loot-item-weight.tier-rare{ --loot-fill-1:#3b82f6; --loot-fill-2:#3b82f6; }
  .loot-item-weight.tier-legendary{ --loot-fill-1:var(--gold); --loot-fill-2:var(--gold); }
  .loot-item-weight.tier-mythic{ --loot-fill-1:#e0143c; --loot-fill-2:#9333ea; }
  .loot-item-weight::-webkit-slider-runnable-track{
    height:6px; border-radius:999px;
    background:linear-gradient(90deg, var(--loot-fill-1) 0%, var(--loot-fill-2) var(--val,0%), var(--panel) var(--val,0%), var(--panel) 100%);
    transition:background .12s ease-out;
  }
  .loot-item-weight::-webkit-slider-thumb{
    -webkit-appearance:none; width:15px; height:15px; margin-top:-4.5px; border-radius:50%;
    background:var(--panel2); border:2px solid var(--loot-fill-2, var(--rank));
    box-shadow:0 0 7px 1px color-mix(in srgb, var(--loot-fill-2, var(--rank)) 65%, transparent);
    cursor:pointer; transition:box-shadow .12s ease-out;
    /* Without an explicit stacking context the track's own gradient (painted on its
       ::-webkit-slider-runnable-track pseudo, immediately above) can render over the
       thumb instead of under it. z-index only takes effect on a positioned element. */
    position:relative; z-index:2;
  }
  .loot-item-weight::-moz-range-track{ height:6px; border-radius:999px; background:var(--panel); }
  .loot-item-weight::-moz-range-progress{
    height:6px; border-radius:999px; background:linear-gradient(90deg, var(--loot-fill-1), var(--loot-fill-2));
  }
  .loot-item-weight::-moz-range-thumb{
    width:15px; height:15px; border-radius:50%; border:2px solid var(--loot-fill-2, var(--rank));
    background:var(--panel2); box-shadow:0 0 7px 1px color-mix(in srgb, var(--loot-fill-2, var(--rank)) 65%, transparent);
    cursor:pointer;
  }
  /* Typeable readout. Styled to match .custom-count (the app's other small number
     field) rather than reusing that class, which is scoped to .daypick. */
  .loot-item-weight-num{
    flex:0 0 38px; width:38px; background:var(--panel2); border:1px solid var(--line);
    border-radius:7px; padding:5px 2px; color:var(--text); font-family:'Sora',sans-serif;
    font-size:11.5px; text-align:center; font-variant-numeric:tabular-nums;
    -moz-appearance:textfield; appearance:textfield;
  }
  .loot-item-weight-num:focus{outline:1px solid var(--rank); border-color:var(--rank);}
  /* Chrome's own spinner off — the ▲/▼ pair beside it IS the stepper, and two
     sets of arrows inside 38px is unreadable. */
  .loot-item-weight-num::-webkit-outer-spin-button,
  .loot-item-weight-num::-webkit-inner-spin-button{-webkit-appearance:none; margin:0;}
  .loot-item-weight-pct{font-size:11px; color:var(--muted);}
  .loot-item-weight-step{display:flex; flex-direction:column; gap:2px; flex:0 0 15px;}
  .loot-item-weight-step button{
    width:15px; height:12px; padding:0; display:flex; align-items:center; justify-content:center;
    background:var(--panel2); border:1px solid var(--line); border-radius:4px;
    color:var(--muted); font-size:6.5px; line-height:1; cursor:pointer;
  }
  .loot-item-weight-step button:hover{border-color:var(--rank); color:var(--rank);}
  .loot-item-weight-step button:active{background:var(--panel);}
  .loot-cat-drop-step button{
    width:15px; height:12px; padding:0; display:flex; align-items:center; justify-content:center;
    background:var(--panel2); border:1px solid var(--line); border-radius:4px;
    color:var(--muted); font-size:6.5px; line-height:1; cursor:pointer;
  }
  .loot-cat-drop-step button:hover{border-color:var(--rank); color:var(--rank);}
  .loot-cat-drop-step button:active{background:var(--panel);}
  /* Phone widths can't hold name + emoji + qty + a three-part weight cell + delete
     on one line — the name field was squeezed to ~18px and the card overflowed the
     viewport sideways. Give the weight cell its own full-width line instead. */
  @media (max-width:480px){
    .loot-item-row{flex-wrap:wrap; row-gap:6px;}
    .loot-item-name{flex:1 1 90px; min-width:0;}
    /* .loot-item-nothing-tag is deliberately NOT re-ordered here — its global
       order:10 already puts it below the full-width weight cell. */
    .loot-item-del{order:8;}
    .loot-item-weight-wrap{order:9; flex:1 1 100%;}
    /* The weight cell drops to its own full-width line here, so a "Chance"
       caption up on the header line would point at nothing. */
    .loot-hdr-chance{display:none;}
  }
  /* Per-habit drop chance — one global control rather than one per category, so
     it lives in its own section above the tables. Own class names again (the
     suite selects the per-category controls positionally), and a fractional
     scale: 0-3% in 0.1 steps, where the category sliders are whole percents. */
  .loot-habit-help{font-size:11.5px; color:var(--muted); line-height:1.45; font-style:italic; margin-bottom:12px;}
  .loot-habit-drop-row{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
  .loot-habit-drop-label{font-size:12.5px; color:var(--text); font-weight:600; flex:1; min-width:0;}
  .loot-habit-drop-wrap{display:flex; align-items:center; gap:5px; flex:0 0 176px; min-width:0;}
  .loot-habit-drop-slider{
    -webkit-appearance:none; appearance:none; flex:1 1 0; width:0; min-width:0; height:6px;
    background:transparent; cursor:pointer; outline:none;
    --loot-fill-1:#3b82f6; --loot-fill-2:#9333ea;
  }
  .loot-habit-drop-slider::-webkit-slider-runnable-track{
    height:6px; border-radius:999px;
    background:linear-gradient(90deg, var(--loot-fill-1) 0%, var(--loot-fill-2) var(--val,0%), var(--panel) var(--val,0%), var(--panel) 100%);
    transition:background .12s ease-out;
  }
  .loot-habit-drop-slider::-webkit-slider-thumb{
    -webkit-appearance:none; width:15px; height:15px; margin-top:-4.5px; border-radius:50%;
    background:var(--panel2); border:2px solid var(--loot-fill-2, var(--rank));
    box-shadow:0 0 7px 1px color-mix(in srgb, var(--loot-fill-2, var(--rank)) 65%, transparent);
    cursor:pointer; transition:box-shadow .12s ease-out;
    position:relative; z-index:2; /* same paint-order fix as the weight thumbs */
  }
  .loot-habit-drop-slider::-moz-range-track{ height:6px; border-radius:999px; background:var(--panel); }
  .loot-habit-drop-slider::-moz-range-progress{
    height:6px; border-radius:999px; background:linear-gradient(90deg, var(--loot-fill-1), var(--loot-fill-2));
  }
  .loot-habit-drop-slider::-moz-range-thumb{
    width:15px; height:15px; border-radius:50%; border:2px solid var(--loot-fill-2, var(--rank));
    background:var(--panel2); box-shadow:0 0 7px 1px color-mix(in srgb, var(--loot-fill-2, var(--rank)) 65%, transparent);
    cursor:pointer;
  }
  .loot-habit-drop-num{
    flex:0 0 46px; width:46px; background:var(--panel2); border:1px solid var(--line);
    border-radius:7px; padding:5px 2px; color:var(--text); font-family:'Sora',sans-serif;
    font-size:11.5px; text-align:center; font-variant-numeric:tabular-nums;
    -moz-appearance:textfield; appearance:textfield;
  }
  .loot-habit-drop-num:focus{outline:1px solid var(--rank); border-color:var(--rank);}
  .loot-habit-drop-num::-webkit-outer-spin-button,
  .loot-habit-drop-num::-webkit-inner-spin-button{-webkit-appearance:none; margin:0;}
  .loot-habit-drop-pct{font-size:11px; color:var(--muted);}
  .loot-habit-drop-step{display:flex; flex-direction:column; gap:2px; flex:0 0 15px;}
  .loot-habit-drop-step button{
    width:15px; height:12px; padding:0; display:flex; align-items:center; justify-content:center;
    background:var(--panel2); border:1px solid var(--line); border-radius:4px;
    color:var(--muted); font-size:6.5px; line-height:1; cursor:pointer;
  }
  .loot-habit-drop-step button:hover{border-color:var(--rank); color:var(--rank);}
  .loot-habit-drop-step button:active{background:var(--panel);}
  .loot-habit-note{font-size:11.5px; color:var(--muted); margin-top:10px;}
  .loot-habit-note.off{opacity:.7;}
  .loot-add-item-btn{
    background:var(--panel); border:1px solid var(--line); border-radius:8px;
    padding:8px 12px; color:var(--text); cursor:pointer; font-family:'Sora',sans-serif;
    font-size:12.5px; margin-top:2px;
  }
  .loot-add-item-btn:hover{border-color:var(--rank); color:var(--rank);}

  /* GEASS BIND OVERLAY */
  #geassBindOverlay{
    position:fixed; inset:0; z-index:60;
    display:none; align-items:center; justify-content:center; flex-direction:column;
    background:radial-gradient(circle at center, rgba(60,0,30,.97) 0%, rgba(8,0,16,.98) 60%, rgba(3,2,6,.99) 100%);
    -webkit-backdrop-filter:blur(2px); backdrop-filter:blur(2px);
  }
  #geassBindOverlay.show{display:flex;}
  #geassBindEye{
    width:min(310px, 84vw); height:auto; pointer-events:none;
    border-radius:50%;
    mask-image:radial-gradient(circle, black 36%, transparent 70%);
    -webkit-mask-image:radial-gradient(circle, black 36%, transparent 70%);
    filter:saturate(1.2) brightness(1.05);
    animation:gboEyeIn .38s cubic-bezier(.2,.9,.3,1) both;
    box-shadow:0 0 60px 10px rgba(200,30,100,.45), 0 0 130px 40px rgba(120,20,140,.22);
  }
  @keyframes gboEyeIn{from{opacity:0; transform:scale(.45);}to{opacity:1; transform:scale(1);}}
  .gbo-stage{text-align:center; margin-top:22px; z-index:2; position:relative;}
  .gbo-label{
    font-family:'Cinzel',serif; font-size:11px; letter-spacing:.45em;
    color:#dc2626; opacity:0; animation:gboFadeIn .4s ease .55s forwards;
  }
  .gbo-cmd{
    font-family:'Cinzel',serif; font-weight:700; font-size:clamp(17px,5vw,24px);
    color:#fffdf5; margin-top:13px; max-width:26ch; line-height:1.4;
    text-shadow:0 0 24px rgba(210,40,100,.55); letter-spacing:.04em;
    opacity:0; animation:gboFadeIn .5s ease .8s forwards;
  }
  .gbo-sub{
    margin-top:10px; font-family:'Sora',sans-serif; font-size:11.5px;
    letter-spacing:.1em; color:rgba(220,38,38,.65); font-style:italic;
    opacity:0; animation:gboFadeIn .5s ease 1.05s forwards;
  }
  @keyframes gboFadeIn{from{opacity:0; transform:translateY(5px);}to{opacity:1; transform:translateY(0);}}
  @media (prefers-reduced-motion: reduce){
    #geassBindEye,.gbo-label,.gbo-cmd,.gbo-sub{animation:none !important; opacity:1 !important;}
  }

  /* CALENDAR */
  .cal-head{
    display:flex; align-items:center; justify-content:space-between; margin-bottom:14px;
  }
  .cal-head .title{
    font-family:'Cinzel',serif; font-weight:600; font-size:16px; letter-spacing:.12em;
  }
  .cal-head button{
    background:var(--panel2); border:1px solid var(--line); color:var(--text);
    border-radius:8px; width:34px; height:34px; cursor:pointer; font-size:16px;
  }
  .cal-head button:hover{border-color:var(--rank); color:var(--rank);}
  .cal-view-toggle{
    display:flex; gap:6px; margin-bottom:14px;
  }
  .cal-view-toggle button{
    flex:1; background:var(--panel2); border:1px solid var(--line); color:var(--muted);
    border-radius:8px; padding:7px 0; cursor:pointer; font-size:12px;
    font-family:'Sora',sans-serif; letter-spacing:.05em; transition:all .12s;
  }
  .cal-view-toggle button.on{background:var(--rank); border-color:var(--rank); color:#0b0a10; font-weight:600;}
  /* 4.5: the app's toggle switch (first use: the Progress tab's Rules box). Track 38x22,
     knob slides 16px — transform + colour transitions only, no keyframes. */
  .switch{position:relative; width:38px; height:22px; flex:0 0 38px; border-radius:999px; background:var(--panel3);
    border:1px solid var(--line2); padding:0; cursor:pointer; transition:background-color .18s, border-color .18s;}
  .switch-knob{position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; background:var(--muted);
    transform:translateX(0); transition:transform .18s cubic-bezier(.3,.7,.3,1), background-color .18s;}
  .switch.on{background:var(--rank); border-color:var(--rank);}
  .switch.on .switch-knob{transform:translateX(16px); background:#0b0a10;}
  .switch:focus-visible{outline:2px solid var(--rank); outline-offset:2px;}
  .rules-box{display:flex; align-items:center; justify-content:space-between; gap:12px; margin:-6px 0 14px;
    padding:9px 12px; background:var(--panel2); border:1px solid var(--line); border-radius:10px;}
  .rules-box-title{font-size:12.5px; font-weight:600; color:var(--text);}
  .rules-box-sub{font-size:10.5px; color:var(--muted); line-height:1.4;}
  @media (prefers-reduced-motion: reduce){ .switch, .switch-knob{transition:none;} }

  /* ===== PROGRESS graph ===== */
  .prog-empty{ color:var(--muted); font-size:13px; text-align:center; padding:40px 10px; line-height:1.5; }
  /* A long "All" range squeezed every day into one fixed-width plot — readable
     up to about a month, a smear past it. Past PROG_SCROLL_THRESHOLD days,
     renderProgress()/renderMomentum() wrap the chart in THIS outer scroller
     instead of shrinking columns further, and every renderer switches to a
     constant per-day pixel width (PROG_PERDAY) so a day is always the same
     width no matter how much history there is. It has to be an OUTER wrapper,
     not overflow-x on .prog-plot itself: .prog-day/.prog-rank hang BELOW the
     plot via bottom:-20px/-38px (see those rules below), and overflow-x on an
     element also clips overflow-y unless explicitly set — clipping the labels
     that live outside the box. Same overflow-x idiom as .emojipick etc. above. */
  .prog-scroll{ overflow-x:auto; padding-bottom:44px; scrollbar-width:thin; scrollbar-color:var(--line) transparent; }
  .prog-plot{ position:relative; display:flex; align-items:stretch; gap:6px; margin:26px 0 46px; }
  .prog-col{ position:relative; flex:1; cursor:pointer; }
  .prog-col:focus-visible{ outline:2px solid var(--rank); outline-offset:2px; border-radius:3px; }
  /* the bar itself lifts slightly, so the whole column reads as one target */
  .prog-col:hover .prog-bar{ filter:brightness(1.25); }
  /* Scroll mode: gap 0 so a day is EXACTLY PROG_PERDAY wide, end to end. With
     the 6px gap still in force the real pitch was 30px against the 24px both
     SVG charts use for their viewBox, so (a) the plot's min-width (n x 24) fell
     (n-1) x 6 short and the zero line and every gridline stopped before the
     newest columns — 293px short on a real save, 1,432px at 240 days — and
     (b) the "mirrored" scrollers showed windows two days apart, so a day's bar
     did not sit above that day's momentum point. The bar keeps its own inset
     (.prog-bar is left/right 18%), which is what still separates the columns. */
  .prog-plot.prog-fixed{ gap:0; }
  .prog-plot.prog-fixed .prog-col{ flex:0 0 24px; }
  .prog-zero{ position:absolute; left:0; right:0; height:1px; background:var(--line); }
  .prog-bar{ position:absolute; left:18%; right:18%; border-radius:3px 3px 0 0; background:var(--gain); }
  .prog-bar.neg{ background:var(--loss); border-radius:0 0 3px 3px; opacity:.9; }
  .prog-val{ position:absolute; left:0; right:0; text-align:center; font-size:10px; font-weight:600; color:var(--text); z-index:2; }
  /* Set by thinProgVals() when the columns are narrower than the numbers they
     carry. .prog-val spans its column (left:0;right:0) but is NOT clipped, so a
     "+260" needing ~24px inside a ~15px column simply painted over its
     neighbours — at 30 days every label collided with both its neighbours and
     the row read as noise. display:none rather than visibility:hidden so the
     glyph run measurement above never sees a hidden label. */
  .prog-val.is-thinned{ display:none; }
  .prog-val.negv{ color:var(--loss); }
  .prog-day{ position:absolute; bottom:-20px; left:0; right:0; text-align:center; font-size:10px; color:var(--muted); }
  .prog-rank{ position:absolute; bottom:-38px; left:0; right:0; text-align:center; font-size:12px; font-weight:700; }
  /* "Which active day was this?" printed under the date. Scroll mode ONLY — see
     the renderer. Every row below it shifts down by 14px, and every one of those
     shifts is scoped to .prog-scroll so a <=31-day chart keeps its exact previous
     geometry (the byte-identity contract PROG-SCROLL-3 defends). */
  .prog-ord{
    position:absolute; bottom:-33px; left:0; right:0; text-align:center;
    font-size:8.5px; font-weight:600; letter-spacing:.02em; color:#6f6880;
  }
  .prog-scroll .prog-rank{ bottom:-51px; }
  .prog-badge{ position:absolute; left:50%; transform:translateX(-50%); background:var(--panel2); border:1px solid var(--gold);
    font-size:10px; font-weight:700; padding:1px 6px; border-radius:20px; white-space:nowrap; z-index:3; box-shadow:0 0 10px rgba(212,160,23,.35); }
  /* Month boundaries on a long, scrolled bar chart: without them a run of day
     numbers rolling 30, 31, 1, 2 gives no clue which month you're looking at.
     Emitted by the renderer ONLY in scroll mode, and the extra bottom room is
     applied to .prog-scroll's .prog-plot alone, so a <=31-day chart keeps its
     exact previous geometry (PROG-SCROLL-3). The rule sits on the 1st's own
     column's LEFT edge, i.e. between the two months. */
  /* The rule is a pseudo-element hung on the column's left edge, NOT a
     border-left with a negative margin: under border-box the 1px border sat
     INSIDE the 24px flex-basis, so the negative margin cancelled nothing and a
     month-start column advanced 23px — seven boundaries in, the zero line
     overran the newest column by 7px and the bar chart drifted 7px off the
     momentum curve. The pitch must stay exactly PROG_PERDAY everywhere. */
  .prog-col.month-start::before{ content:''; position:absolute; left:-1px; top:0; bottom:0;
    border-left:1px dashed rgba(139,131,160,.55); pointer-events:none; }
  /* ---- Day preview popover (click a column) ----
     A lightweight popover, deliberately NOT a registered overlay: it has no
     backdrop, doesn't trap focus, and must never outrank the Reckoning gate. */
  .prog-pop{
    position:absolute; z-index:6; width:210px; max-width:calc(100% - 8px);
    background:var(--panel2); border:1px solid var(--line); border-radius:10px;
    padding:9px 11px; box-shadow:0 8px 22px rgba(0,0,0,.45);
    font-size:12px; pointer-events:auto;
  }
  .prog-pop[hidden]{ display:none; }
  .prog-pop-date{ font-weight:800; font-size:12.5px; margin-bottom:6px; }
  .prog-pop-row{ display:flex; justify-content:space-between; gap:10px; padding:2px 0; min-width:0; }
  .prog-pop-row span:first-child{ color:var(--muted); }
  .prog-pop-row span:last-child{ font-variant-numeric:tabular-nums; white-space:nowrap; }
  .prog-pop-gain{ color:var(--good, #4ade80); }
  .prog-pop-loss{ color:var(--bad, #f87171); }
  .prog-pop-open{
    width:100%; margin-top:8px; padding:6px 8px; font-size:11.5px; font-weight:700;
    border-radius:7px; border:1px solid var(--line); background:var(--panel);
    color:var(--text); cursor:pointer;
  }
  .prog-pop-open:hover{ border-color:var(--rank); color:var(--rank); }
  .prog-month{
    position:absolute; bottom:-69px; left:-11px; right:-11px; text-align:center;
    font-size:9px; font-weight:700; letter-spacing:.06em; color:#8b83a0; white-space:nowrap;
  }
  .prog-scroll .prog-plot{ margin-bottom:77px; }
  /* .mom-svg shares this rule rather than the class itself: the momentum chart
     is drawn in exactly the same idiom, but a distinct class keeps the points
     chart's own selectors (and its checks) unambiguous now that the PROGRESS
     tab holds two SVGs. */
  .prog-svg, .mom-svg{ width:100%; height:auto; display:block; margin-top:6px; }

  /* ===== MOMENTUM curve ===== (same .prog-svg sizing idiom as the Elo line) */
  .mom-now{ font-size:12.5px; color:var(--muted); margin-bottom:2px; }
  .mom-now b{ color:var(--gold); font-family:'Cinzel',serif; font-size:16px; letter-spacing:.06em; }
  .mom-legend{ display:flex; flex-wrap:wrap; gap:6px 14px; margin-top:8px; font-size:11px; color:var(--muted); }
  .mom-legend span{ display:inline-flex; align-items:center; gap:5px; }
  .mom-legend i{ width:14px; height:0; border-top:2px solid var(--gold); flex:0 0 auto; }
  .mom-legend i.dash{ border-top-style:dashed; }
  .mom-legend i.lock{ height:9px; width:9px; border:none; border-radius:2px; background:rgba(220,38,38,.55); }

  /* ===== COMPLETION record ===== */
  .cstat-summary{ margin-bottom:14px; }
  .cstat-total{ font-family:'Cinzel',serif; font-weight:700; font-size:26px; color:var(--rank); letter-spacing:.04em; }
  .cstat-sub{ font-size:12.5px; color:var(--muted); margin-left:8px; }
  .cstat-note{ font-size:11.5px; color:var(--muted); margin-top:5px; line-height:1.45; font-style:italic; }
  .cstat-row{ display:flex; align-items:center; gap:10px; padding:6px 0; border-bottom:1px solid var(--line); }
  .cstat-row:last-child{ border-bottom:none; }
  /* 4.2 (v2.2, Tin): long names wrap to TWO lines instead of ellipsizing at
     38% — the title attr stays as the full-name fallback. */
  .cstat-name{ flex:0 0 38%; min-width:0; font-size:12.5px; color:var(--text);
    white-space:normal; display:-webkit-box; -webkit-line-clamp:2;
    -webkit-box-orient:vertical; overflow:hidden; line-height:1.35; }
  .cstat-bar{ flex:1 1 auto; min-width:0; height:8px; background:var(--panel2);
    border-radius:5px; overflow:hidden; }
  .cstat-bar i{ display:block; height:100%; background:var(--rank); border-radius:5px; }
  .cstat-n{ flex:0 0 auto; font-size:12px; font-weight:600; color:var(--muted); min-width:32px; text-align:right; }
  /* Habits that are no longer live still hold their completions in the total,
     so they stay listed — just visibly retired rather than mixed in with the
     ones you can still tick today. */
  .cstat-row.cstat-past .cstat-name{ color:var(--muted); }
  .cstat-row.cstat-past .cstat-bar i{ opacity:.45; }
  .cstat-tag{
    font-size:9px; font-weight:700; letter-spacing:.07em; text-transform:uppercase;
    color:var(--muted); border:1px solid var(--line2); border-radius:4px;
    padding:1px 4px; margin-left:6px; vertical-align:middle;
  }

  /* ===== THE RECKONING gate ===== deliberately no close button and no backdrop
     handler; see maybeShowReckoning(). Styling rides the What's New card. */
  #reckonOverlay.show{ animation:editOverlayIn .2s ease both; }
  #reckonOverlay.show .rank-info-card{ animation:editCardIn .36s cubic-bezier(.16,1.06,.3,1) both; }
  .reckon-count{ font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:var(--muted); margin-top:14px; }
  .reckon-habit{ font-family:'Cinzel',serif; font-weight:600; font-size:19px; color:var(--rank);
    margin:6px 0 2px; line-height:1.3; word-break:break-word; }
  .reckon-prompt{ font-size:12.5px; color:var(--muted); font-style:italic; margin-bottom:12px; }
  .reckon-input{
    width:100%; background:var(--bg); border:1px solid var(--line); border-radius:9px;
    padding:10px 11px; color:var(--text); font-family:'Sora',sans-serif; font-size:13px;
    line-height:1.5; resize:vertical; box-sizing:border-box;
  }
  .reckon-input:focus{ outline:none; border-color:var(--rank); }
  .wn-btn:disabled{ opacity:.35; cursor:not-allowed; filter:none; }
  /* The two ways to answer a prompt, deliberately NOT peers in styling: "Next"
     stays the gold primary because explaining is what the gate is for, and
     "I did it" is an outlined secondary in the gain colour so it reads as the
     other, rarer answer rather than as a second primary or a way out. */
  .reckon-actions{ display:flex; gap:8px; margin-top:18px; align-items:stretch; }
  .reckon-actions .wn-btn{ margin-top:0; flex:1 1 auto; }
  .reckon-did{
    flex:0 0 auto; background:transparent; color:var(--gain);
    border:1px solid var(--gain); border-radius:8px; padding:11px 13px;
    font-family:'Sora',sans-serif; font-weight:600; font-size:13px;
    cursor:pointer; white-space:nowrap; transition:background .15s, box-shadow .15s;
  }
  .reckon-did:hover{ background:rgba(34,197,94,.12); box-shadow:0 0 12px rgba(34,197,94,.18); }
  /* ---- Adaptation ----
     Plays over the gate's own card once a reason is given. A layer INSIDE the
     card, never a second overlay: the gate is deliberately undismissable and
     focus-trapped, and a registered overlay would give Escape and openOverlayEl
     a second claimant. Everything animated is transform or opacity; the glow is
     a static radial gradient whose opacity moves, not an animated filter. */
  .reckon-adapt{
    position:absolute; inset:0; z-index:4; border-radius:inherit;
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px;
    background:rgba(11,10,16,.92);
    animation:raIn .18s ease-out;
    /* the glow below is deliberately wider than the wheel; the card is an
       overflow:auto box, so contain it here or it grows a scrollbar mid-spin */
    overflow:hidden;
  }
  @keyframes raIn{ from{opacity:0;} to{opacity:1;} }
  /* "Notch": the wheel arrives, HOLDS, then turns exactly one spoke (45deg) and
     stops dead, with a shockwave and a flash on the lock. Mahoraga's wheel does
     not free-spin — it advances one deliberate notch per adaptation — and a
     single decisive click reads far better at this size than a long spin.
     transform-origin is 50% 50%, i.e. the real hub. It used to be `70px 76px`:
     those are SVG user units from the 140x152 badge viewBox, applied to a CSS
     box of 118x128px, which put the pivot 11px right and 12px below centre —
     the wheel visibly wobbled instead of turning. The art repeats every 90deg
     (the spokes alternate dark/white), so 45deg is a half-symmetry step and the
     turn is unmistakable; the old 320deg landed back on a near-identical pose. */
  .reckon-adapt .ra-wheel{
    position:relative; width:118px; height:128px; display:block;
    transform-origin:50% 50%;
    animation:raSpin 1.65s cubic-bezier(.7,0,.2,1) both;
  }
  .reckon-adapt .ra-wheel::before{
    content:''; position:absolute; left:50%; top:50%; width:190px; height:190px; margin:-95px 0 0 -95px;
    border-radius:50%; background:radial-gradient(closest-side, rgba(212,160,23,.4) 0%, transparent 70%);
    animation:raGlow 1.65s ease-out both;
  }
  .reckon-adapt .ra-wheel svg{ position:relative; width:100%; height:100%; display:block; }
  /* Ends held at full opacity, NOT faded out: the layer is removed on the same
     tick the animation ends, so fading to 0 meant the last thing on screen was
     an empty scrim over the previous habit's text. */
  @keyframes raSpin{
    0%  { transform:rotate(0deg) scale(.86); opacity:0; }
    14% { transform:rotate(0deg) scale(1); opacity:1; }
    34% { transform:rotate(0deg) scale(1); opacity:1; }
    62% { transform:rotate(45deg) scale(1); opacity:1; }
    66% { transform:rotate(45deg) scale(1.05); opacity:1; }
    72% { transform:rotate(45deg) scale(1); opacity:1; }
    100%{ transform:rotate(45deg) scale(1); opacity:1; }
  }
  @keyframes raGlow{ 0%{opacity:0;} 34%{opacity:.35;} 64%{opacity:1;} 100%{opacity:.5;} }
  /* The impact: a ring thrown off the rim, and a one-frame flash. Both are
     transform/opacity only — the ring is a static border that scales. */
  .reckon-adapt .ra-shock{
    position:absolute; left:50%; top:50%; width:150px; height:150px; margin:-75px 0 0 -75px;
    border-radius:50%; border:2px solid var(--gold); opacity:0; pointer-events:none;
    animation:raShock 1.65s ease-out both;
  }
  @keyframes raShock{
    0%,60%{ opacity:0; transform:scale(.72); }
    64%   { opacity:.9; transform:scale(.86); }
    100%  { opacity:0; transform:scale(1.9); }
  }
  .reckon-adapt .ra-flash{
    position:absolute; inset:0; opacity:0; pointer-events:none;
    background:radial-gradient(circle at 50% 42%, rgba(255,236,180,.55) 0%, transparent 55%);
    animation:raFlash 1.65s linear both;
  }
  @keyframes raFlash{ 0%,60%{opacity:0;} 63%{opacity:.85;} 74%{opacity:0;} 100%{opacity:0;} }
  .reckon-adapt .ra-word{
    font-family:'Cinzel',serif; font-weight:700; font-size:17px; letter-spacing:.34em;
    color:var(--gold); text-shadow:0 0 18px rgba(212,160,23,.45);
    animation:raWord 1.65s cubic-bezier(.2,1.5,.3,1) both;
  }
  /* Stamps in ON the lock rather than drifting up beforehand. */
  @keyframes raWord{
    0%,60%{ opacity:0; transform:scale(1.5); }
    70%   { opacity:1; transform:scale(1); }
    100%  { opacity:1; transform:scale(1); }
  }
  @media (prefers-reduced-motion: reduce){
    #reckonOverlay.show, #reckonOverlay.show .rank-info-card{ animation:none; }
    /* playAdaptation also skips itself entirely under this query — this is only
       a backstop for a mid-flight layer if the setting changes while it plays. */
    .reckon-adapt, .reckon-adapt .ra-wheel, .reckon-adapt .ra-wheel::before,
    .reckon-adapt .ra-word, .reckon-adapt .ra-shock, .reckon-adapt .ra-flash{ animation:none; }
  }
  /* Skip reasons surfaced in the calendar's day detail */
  .dd-skips{ margin-top:12px; border-top:1px solid var(--line); padding-top:10px; }
  .dd-skips-head{ font-size:10.5px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin-bottom:7px; }
  .dd-skip{ font-size:12.5px; color:var(--text); line-height:1.5; margin-bottom:5px; }
  .dd-skip-name{ color:var(--muted); }
  .dd-skip-name::after{ content:' — '; }

  #progMonthSel{
    flex:1; background:var(--panel2); border:1px solid var(--line); color:var(--muted);
    border-radius:8px; padding:7px 4px; cursor:pointer; font-size:12px;
    font-family:'Sora',sans-serif; letter-spacing:.05em; text-align:center;
  }
  #progMonthSel.on{background:var(--rank); border-color:var(--rank); color:#0b0a10; font-weight:600;}
  .year-grid{
    display:grid; grid-template-columns:repeat(auto-fit, minmax(128px,1fr)); gap:12px;
    margin-bottom:14px;
  }
  .year-month{
    background:var(--panel2); border:1px solid var(--line); border-radius:10px; padding:8px;
  }
  .year-month .ym-label{
    text-align:center; font-size:10px; letter-spacing:.1em; color:var(--muted);
    margin-bottom:6px; font-family:'Cinzel',serif; font-weight:600;
  }
  .year-month .ym-grid{
    display:grid; grid-template-columns:repeat(7,1fr); gap:2px;
  }
  .year-month .ym-cell{
    aspect-ratio:1; border-radius:2px; background:var(--panel2);
    border:1px solid var(--line); cursor:pointer;
  }
  .year-month .ym-cell.empty{background:transparent; border:none; cursor:default;}
  .year-month .ym-cell.future{opacity:.3; cursor:default;}
  .year-month .ym-cell.missed{background:rgba(220,38,38,.28); border-color:rgba(220,38,38,.55);}
  .year-month .ym-cell:not(.empty):not(.future):hover{outline:1.5px solid var(--rank); outline-offset:-1px;}
  .ym-footer{
    display:flex; align-items:center; justify-content:center; gap:6px;
    margin-top:8px;
  }
  .ym-pie{
    width:20px; height:20px; min-width:20px; border-radius:50%; position:relative;
  }
  .ym-pie::after{
    content:''; position:absolute; inset:4px; border-radius:50%; background:var(--panel2);
  }
  .ym-pct{font-size:10.5px; color:var(--muted); font-weight:600; letter-spacing:.02em;}
  .cal-grid{
    display:grid; grid-template-columns:repeat(7,1fr); gap:5px;
  }
  .cal-grid .dow{
    text-align:center; font-size:10.5px; color:var(--muted);
    letter-spacing:.1em; padding-bottom:4px;
  }
  .cal-cell{
    aspect-ratio:1; border-radius:9px; border:1px solid var(--line);
    background:var(--panel); position:relative; cursor:pointer;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:1px; font-size:12.5px; color:var(--muted); transition:border-color .15s;
  }
  .cal-cell .cell-pct{   /* 3.2: under the day number, muted — it read as noise above it */
    font-size:8px; font-weight:600; letter-spacing:.01em; line-height:1; opacity:.6;
  }
  .cal-cell .cell-day{line-height:1;}
  .cal-cell:hover{border-color:var(--rank);}
  .cal-cell.empty{background:transparent; border:none; cursor:default;}
  .cal-cell.future{opacity:.35; cursor:default;}
  .cal-cell.today{border:1.5px solid var(--rank);}
  .cal-cell.full{color:#0b0a10; font-weight:600; border-color:var(--rank);}
  .cal-cell.deep{color:#0b0a10; font-weight:600;}
  .cal-cell.rankup{color:#0b0a10; font-weight:700; box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.35);}
  .cal-cell.rankup::after{
    content:'▲'; position:absolute; top:2px; right:3px; font-size:8px;
    color:rgba(255,255,255,.9); line-height:1;
  }
  .cal-cell.derank{color:#0b0a10; font-weight:700; box-shadow:inset 0 0 0 1.5px rgba(220,38,38,.65);}
  .cal-cell.derank::after{
    content:'▼'; position:absolute; top:2px; right:3px; font-size:8px;
    color:rgba(255,255,255,.9); line-height:1;
  }
  .cal-cell.missed::after{
    content:''; position:absolute; bottom:4px; left:50%; transform:translateX(-50%);
    width:12px; height:2px; border-radius:1px; background:#dc2626;
  }
  /* emergency day — excused after the fact. The real numbers still show (the
     history stays honest); the dashed ring is what marks it as forgiven. */
  .cal-cell.emerg{border-style:dashed !important; border-width:1.5px !important;}
  /* ::before, top-LEFT (3.2): .missed/.rankup/.derank all own ::after, so as an ::after
     the siren replaced the missed bar (and the rank carets, which hold the top-right) */
  .cal-cell.emerg::before{
    content:'🚨'; position:absolute; top:1px; left:2px; font-size:7px; line-height:1;
  }
  .donut-heading{
    text-align:center; font-size:11px; letter-spacing:.16em; color:var(--muted);
    text-transform:uppercase; margin-top:6px;
  }
  .donut-wrap{position:relative; width:136px; height:136px; margin:12px auto 4px;}
  .donut{width:100%; height:100%; border-radius:50%; transition:background .4s;}
  .donut::after{
    content:''; position:absolute; inset:17px; border-radius:50%; background:var(--panel);
  }
  .donut-center{
    position:absolute; inset:0; display:flex; flex-direction:column;
    align-items:center; justify-content:center;
  }
  .donut-center .pct{
    font-family:'Cinzel',serif; font-weight:700; font-size:25px; color:var(--rank);
  }
  .donut-center .cap{font-size:10px; color:var(--muted); letter-spacing:.03em; margin-top:2px;}
  .month-bar-wrap{margin-top:14px;}
  .month-bar-label{
    display:flex; justify-content:space-between; font-size:12px;
    color:var(--muted); margin-bottom:6px;
  }
  .month-bar-label b{color:var(--rank); font-weight:600;}
  .month-bar{
    height:10px; border-radius:5px; background:var(--panel2);
    border:1px solid var(--line); overflow:hidden;
  }
  .month-bar i{
    display:block; height:100%; background:var(--rank); border-radius:5px;
    transition:width .3s;
  }
  .cal-stats{display:flex; gap:8px; margin-top:12px; flex-wrap:wrap;}   /* 3.2: hugs the grid */
  .cal-stats .chip b{color:var(--rank);}
  .day-detail{
    margin-top:14px; border-top:1px solid var(--line); padding-top:14px; display:none;
  }
  .day-detail.show{display:block;}
  .day-detail .dd-date{
    font-family:'Cinzel',serif; font-weight:600; font-size:13px;
    letter-spacing:.15em; color:var(--rank); margin-bottom:8px;
  }
  .day-detail .dd-row{
    display:flex; justify-content:space-between; font-size:13.5px;
    padding:5px 2px; border-bottom:1px solid var(--line);
  }
  .day-detail .dd-row:last-child{border-bottom:none;}
  /* A sworn Geass reads as purple everywhere else in the app (its icon, its row
     border on the Status list, its group header) — the day detail was the one
     place it looked identical to an ordinary commitment. */
  .day-detail .dd-row.geass{border-bottom-color:rgba(147,51,234,.55);}
  .day-detail .dd-row.geass .dd-name{color:#c084fc;}
  .day-detail .dd-row .pts{color:var(--muted); font-size:12px;}
  .dd-hint{
    font-size:11px; color:var(--muted); font-style:italic;
    margin-bottom:8px; line-height:1.5;
  }
  /* emergency-day control in the day popup */
  .dd-emerg{
    width:100%; margin-top:10px; padding:9px 10px; border-radius:8px; cursor:pointer;
    background:transparent; border:1.5px dashed rgba(220,38,38,.55); color:#dc2626;
    font-family:inherit; font-size:11.5px; font-weight:600; letter-spacing:.04em;
    transition:background .15s, border-color .15s;
  }
  .dd-emerg:hover{background:rgba(220,38,38,.1); border-color:rgba(220,38,38,.85);}
  .dd-emerg.armed{background:rgba(220,38,38,.18); border-style:solid;}
  .dd-emerg.undo{border-color:var(--line); color:var(--muted);}
  .dd-emerg.undo:hover{background:var(--panel2); border-color:var(--muted);}
  .dd-emerg.vac{border-color:rgba(212,160,23,.55); color:var(--gold);}
  .dd-emerg.vac:hover{background:rgba(212,160,23,.12); border-color:rgba(212,160,23,.85);}
  .dd-emerg.vac.armed{background:rgba(212,160,23,.2); border-style:solid;}
  .dd-emerg-banner.vac{background:rgba(212,160,23,.09); border-color:rgba(212,160,23,.5); color:var(--gold);}
  .dd-emerg-banner{
    margin-bottom:8px; padding:8px 10px; border-radius:8px; line-height:1.5;
    background:rgba(220,38,38,.09); border:1px dashed rgba(220,38,38,.5);
    color:#f0705f; font-size:11.5px;
  }
  .day-detail .dd-row.dd-check{
    justify-content:flex-start; align-items:center; gap:9px; cursor:pointer;
    transition:background .12s; border-radius:6px;
  }
  .day-detail .dd-row.dd-check:hover{background:var(--panel2);}
  .dd-check-btn{
    width:18px; height:18px; min-width:18px; border-radius:5px;
    border:1.5px solid var(--line); background:transparent;
    display:inline-flex; align-items:center; justify-content:center;
    font-size:11px; color:#0b0a10; transition:all .15s;
  }
  .dd-row.dd-check.done .dd-check-btn{background:var(--rank); border-color:var(--rank);}
  .dd-name{flex:1; min-width:0;}
  .dd-row.dd-check.done .dd-name{color:var(--muted); text-decoration:line-through;}
  .dd-row.dd-check .pts{margin-left:auto;}
  .cal-legend{
    display:flex; gap:14px; margin-top:12px; font-size:11.5px; color:var(--muted); flex-wrap:wrap;
  }
  .cal-legend span{display:flex; align-items:center; gap:5px;}
  .cal-legend i{width:11px; height:11px; border-radius:3px; display:inline-block;}

  /* RANK UP OVERLAY */
  .overlay{
    position:fixed; inset:0; background:rgba(5,4,10,.95);
    display:none; align-items:center; justify-content:center;
    flex-direction:column; z-index:50; cursor:pointer; overflow:hidden;
  }
  .overlay.show{display:flex;}
  /* Power-surge pacing per tier: higher ranks charge longer — heavier moment */
  .overlay{--consumeDur:1.9s; --revealDur:1.15s;
    /* ambient light (rays + tier-3 beam) tracks the rank: OLD colour while charging,
       NEW colour from the reveal on — the swap is hidden under the nova flash */
    --rayC:var(--oldC, var(--rank));}
  .overlay.reveal{--rayC:var(--rank);}
  .overlay.tier-2{--consumeDur:2.2s; --revealDur:1.3s;}
  .overlay.tier-3{--consumeDur:2.6s; --revealDur:1.5s;}

  /* RANK INFO OVERLAY (tap a rank in the ladder) */
  .rung{cursor:pointer;}
  .rank-info-overlay{
    position:fixed; inset:0; background:rgba(5,4,10,.92);
    display:none; align-items:center; justify-content:center; z-index:60;
    padding:20px;
  }
  .rank-info-overlay.show{display:flex;}
  /* Edit modal open/close motion (scoped so the rank-info modal is untouched) */
  #editOverlay.show{ animation:editOverlayIn .22s ease both; }
  #editOverlay.show #editCard{ animation:editCardIn .42s cubic-bezier(.16,1.06,.3,1) both; }
  /* inner sections cascade in one after another */
  #editOverlay.show #editCard > *:not(.rank-info-close){ animation:editItemIn .44s cubic-bezier(.2,.85,.3,1) both; }
  #editOverlay.show #editCard > *:nth-child(2){ animation-delay:.05s; }
  #editOverlay.show #editCard > *:nth-child(3){ animation-delay:.09s; }
  #editOverlay.show #editCard > *:nth-child(4){ animation-delay:.14s; }
  #editOverlay.show #editCard > *:nth-child(5){ animation-delay:.19s; }
  #editOverlay.show #editCard > *:nth-child(6){ animation-delay:.24s; }
  #editOverlay.show #editCard > *:nth-child(7){ animation-delay:.29s; }
  #editOverlay.show #editCard > *:nth-child(8){ animation-delay:.34s; }
  #editOverlay.closing{ animation:editOverlayOut .24s ease forwards; }
  #editOverlay.closing #editCard{ animation:editCardOut .24s cubic-bezier(.4,0,.7,1) forwards; }
  @keyframes editOverlayIn{ from{opacity:0;} to{opacity:1;} }
  @keyframes editOverlayOut{ from{opacity:1;} to{opacity:0;} }
  @keyframes editCardIn{
    0%{opacity:0; transform:translateY(16px) scale(.9); box-shadow:0 0 0 rgba(0,0,0,0);}
    55%{opacity:1; transform:translateY(-3px) scale(1.014); box-shadow:0 0 36px 2px var(--rank-soft);}
    100%{opacity:1; transform:translateY(0) scale(1); box-shadow:0 18px 50px rgba(0,0,0,.5);}
  }
  @keyframes editItemIn{
    0%{opacity:0; transform:translateY(10px);}
    100%{opacity:1; transform:translateY(0);}
  }
  @keyframes editCardOut{
    0%{opacity:1; transform:translateY(0) scale(1); filter:blur(0);}
    100%{opacity:0; transform:translateY(8px) scale(.93); filter:blur(1px);}
  }
  @media (prefers-reduced-motion: reduce){
    #editOverlay.show, #editOverlay.show #editCard,
    #editOverlay.show #editCard > *:not(.rank-info-close),
    #editOverlay.closing, #editOverlay.closing #editCard{ animation:none; }
  }

  /* WHAT'S NEW POPUP */
  #whatsNewOverlay.show{ animation:editOverlayIn .2s ease both; }
  #whatsNewOverlay.show .rank-info-card{ animation:editCardIn .36s cubic-bezier(.16,1.06,.3,1) both; }
  .wn-badge{font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin-top:2px;}
  .wn-list{list-style:none; padding:0; margin:16px 0 0; text-align:left;}
  .wn-list li{display:flex; gap:11px; align-items:flex-start; padding:10px 0; border-bottom:1px solid var(--line); font-size:13px; color:var(--text); line-height:1.45;}
  .wn-list li:last-child{border-bottom:none;}
  .wn-ic{flex-shrink:0; font-size:16px; width:22px; text-align:center; line-height:1.4;}
  .wn-list b{color:var(--rank); font-weight:600;}
  .wn-btn{margin-top:18px; width:100%; background:var(--rank); color:#0b0a10; border:none; border-radius:8px; padding:11px 0; font-family:'Sora',sans-serif; font-weight:600; font-size:14px; cursor:pointer; transition:filter .15s;}
  .wn-btn:hover{filter:brightness(1.1);}
  @media (prefers-reduced-motion: reduce){
    #whatsNewOverlay.show, #whatsNewOverlay.show .rank-info-card{ animation:none; }
  }
  .rank-info-card{
    background:var(--card-grad); border:1px solid var(--line2); border-radius:16px;
    box-shadow:0 24px 60px -24px rgba(0,0,0,.9);
    padding:30px 22px 24px; max-width:360px; width:100%; text-align:center;
    position:relative; max-height:85vh; overflow-y:auto;
  }
  .rank-info-close{
    position:absolute; top:10px; right:10px; width:28px; height:28px; border-radius:8px;
    background:var(--panel2); border:1px solid var(--line); color:var(--muted);
    cursor:pointer; font-size:16px; line-height:1;
  }
  .rank-info-close:hover{color:var(--rank); border-color:var(--rank);}
  .rank-info-badge{width:150px; height:163px; margin:0 auto 12px;}
  .rank-info-badge svg{width:100%; height:100%; filter:drop-shadow(0 0 20px var(--rank-soft));}
  .rank-info-name{
    font-family:'Cinzel',serif; font-weight:700; font-size:18px; letter-spacing:.1em;
    color:var(--rank);
  }
  .rank-info-title{
    font-size:12.5px; color:var(--muted); margin-top:3px; margin-bottom:20px; font-style:italic;
  }
  .rank-info-section{text-align:left; margin-top:16px;}
  .rank-info-heading{
    font-size:10.5px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted);
    margin-bottom:8px;
  }
  .rank-info-list{list-style:none; display:flex; flex-direction:column; gap:7px;}
  .rank-info-list li{
    font-size:12.5px; color:var(--text); padding-left:16px; position:relative; line-height:1.55;
  }
  .rank-info-list li::before{content:'—'; position:absolute; left:0; color:var(--rank);}

  /* Power Surge: the flash fires at the reveal (badge swap), not at open */
  .flash{
    position:absolute; inset:0; opacity:0;
    background:radial-gradient(circle at 50% 38%, #fff 0%, color-mix(in srgb, var(--rank) 55%, #fff) 34%, transparent 72%);
  }
  .overlay.reveal .flash{animation:flashHit .8s ease-out forwards;}
  @keyframes flashHit{0%{opacity:0;} 12%{opacity:1;} 100%{opacity:0;}}
  @keyframes flashPulse{0%{opacity:.55;}100%{opacity:0;}}

  .rays{
    position:absolute; width:900px; height:900px; pointer-events:none;
    top:110px; left:50%; margin:-450px 0 0 -450px; z-index:0; /* centered on the badge */
    background:conic-gradient(from 0deg,
      transparent 0deg, color-mix(in srgb, var(--rayC) 22%, transparent) 4deg, transparent 14deg,
      transparent 30deg, color-mix(in srgb, var(--rayC) 22%, transparent) 34deg, transparent 44deg,
      transparent 60deg, color-mix(in srgb, var(--rayC) 22%, transparent) 64deg, transparent 74deg,
      transparent 90deg, color-mix(in srgb, var(--rayC) 22%, transparent) 94deg, transparent 104deg,
      transparent 120deg, color-mix(in srgb, var(--rayC) 22%, transparent) 124deg, transparent 134deg,
      transparent 150deg, color-mix(in srgb, var(--rayC) 22%, transparent) 154deg, transparent 164deg,
      transparent 180deg, color-mix(in srgb, var(--rayC) 22%, transparent) 184deg, transparent 194deg,
      transparent 210deg, color-mix(in srgb, var(--rayC) 22%, transparent) 214deg, transparent 224deg,
      transparent 240deg, color-mix(in srgb, var(--rayC) 22%, transparent) 244deg, transparent 254deg,
      transparent 270deg, color-mix(in srgb, var(--rayC) 22%, transparent) 274deg, transparent 284deg,
      transparent 300deg, color-mix(in srgb, var(--rayC) 22%, transparent) 304deg, transparent 314deg,
      transparent 330deg, color-mix(in srgb, var(--rayC) 22%, transparent) 334deg, transparent 344deg, transparent 360deg);
    animation:raySpin 26s linear infinite, rayFade .8s ease-out;
    mask-image:radial-gradient(circle, black 0%, black 40%, transparent 72%);
  }
  @keyframes raySpin{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}
  @keyframes rayFade{from{opacity:0;}to{opacity:1;}}

  .ringburst{position:absolute; width:0; height:0; top:110px; left:50%; z-index:0;} /* centered on the badge */
  .ring{
    position:absolute; top:0; left:0; border-radius:50%; width:160px; height:160px;
    border:2px solid var(--rank); transform:translate(-50%,-50%) scale(.2);
    opacity:0;
  }
  .overlay.reveal .ring{animation:ringBurst 1.1s cubic-bezier(.15,.7,.3,1) forwards;}
  .overlay.reveal .ring:nth-child(1){animation-delay:.08s;}
  .overlay.reveal .ring:nth-child(2){animation-delay:.26s;}
  .overlay.reveal .ring:nth-child(3){animation-delay:.44s;}
  @keyframes ringBurst{
    0%{transform:translate(-50%,-50%) scale(.3); opacity:.8;}
    100%{transform:translate(-50%,-50%) scale(4.2); opacity:0;}
  }

  .stage{position:relative; display:flex; flex-direction:column; align-items:center; z-index:2;}

  #geassEyeRankup{
    /* Hidden in the rank-up overlay itself — this element only exists so its
       GIF data URI can be borrowed by #geassBindEye (see playGeassBindAnim). */
    display:none;
  }
  .overlay .badge{
    width:220px; height:220px; position:relative; z-index:1;
    filter:drop-shadow(0 0 0 var(--rank-soft));
  }
  /* phase 1 (.consume): the OLD badge charges with orbiting energy, glowing
     ever brighter in the OLD rank's color (--oldC, set inline by playRankUp) */
  .overlay.consume .badge{animation:surgeCharge var(--consumeDur) ease-in forwards;}
  @keyframes surgeCharge{
    0%{filter:brightness(1) drop-shadow(0 0 0 transparent); transform:scale(1);}
    30%{filter:brightness(1.06) drop-shadow(0 0 10px color-mix(in srgb, var(--oldC) 55%, transparent));}
    45%{filter:brightness(1.02) drop-shadow(0 0 7px color-mix(in srgb, var(--oldC) 40%, transparent));}
    62%{filter:brightness(1.14) drop-shadow(0 0 18px color-mix(in srgb, var(--oldC) 75%, transparent));}
    74%{filter:brightness(1.1) drop-shadow(0 0 14px color-mix(in srgb, var(--oldC) 60%, transparent));}
    88%{filter:brightness(1.3) drop-shadow(0 0 26px var(--oldC));}
    100%{filter:brightness(1.6) drop-shadow(0 0 34px #fff); transform:scale(1.04);}
  }
  /* phase 2 (.reveal): the nova fades and the NEW badge cools from white-hot
     into its own color */
  .overlay.reveal .badge{animation:surgeReveal var(--revealDur) ease-out both;}
  @keyframes surgeReveal{
    0%{opacity:1; transform:scale(1.06); filter:brightness(2.6) drop-shadow(0 0 40px #fff);}
    45%{opacity:1; filter:brightness(1.4) drop-shadow(0 0 26px var(--rank));}
    100%{opacity:1; transform:scale(1); filter:brightness(1) drop-shadow(0 0 18px color-mix(in srgb, var(--rank) 55%, transparent));}
  }
  .overlay .badge::after{
    content:''; position:absolute; inset:-14px; border-radius:50%;
    background:radial-gradient(circle, var(--rank-soft) 0%, transparent 68%);
    opacity:0; z-index:-1;
  }
  .overlay.reveal .badge::after{animation:glowBreathe 1.8s ease-in-out 1.1s infinite;}
  @keyframes glowBreathe{
    0%,100%{opacity:.55; transform:scale(1);}
    50%{opacity:1; transform:scale(1.12);}
  }

  .transchip{
    margin-top:8px; font-size:11.5px; letter-spacing:.14em; color:var(--muted);
    opacity:0;
  }
  .overlay.reveal .transchip{animation:chipIn .5s ease-out .85s forwards;}
  .transchip b{color:var(--rank); font-weight:600;}
  @keyframes chipIn{from{opacity:0; transform:translateY(-6px);} to{opacity:1; transform:translateY(0);}}

  .overlay .ru{
    position:relative; overflow:hidden;
    font-family:'Cinzel',serif; font-weight:700; font-size:22px;
    letter-spacing:.5em; margin-top:18px; color:var(--rank);
    opacity:0;
  }
  .overlay.reveal .ru{animation:ruIn .5s ease-out 1.0s forwards;}
  @keyframes ruIn{
    0%{opacity:0; letter-spacing:1em; transform:scale(.85);}
    100%{opacity:1; letter-spacing:.5em; transform:scale(1);}
  }
  .overlay .ru i{
    position:absolute; inset:0; pointer-events:none;
    background:linear-gradient(100deg, transparent 35%, color-mix(in srgb, var(--rank) 75%, white 15%) 50%, transparent 65%);
    transform:translateX(-140%);
  }
  .overlay.reveal .ru i{animation:shimmer 1s ease-out 1.5s;}
  @keyframes shimmer{to{transform:translateX(140%);}}

  .overlay .rn{
    font-family:'Cinzel',serif; font-weight:700; font-size:38px;
    letter-spacing:.16em; color:var(--text);
    opacity:0;
  }
  .overlay.reveal .rn{animation:rnIn .55s cubic-bezier(.25,.9,.3,1) 1.2s forwards;}
  @keyframes rnIn{
    0%{opacity:0; transform:translateY(14px) scale(.9);}
    100%{opacity:1; transform:translateY(0) scale(1);}
  }

  .overlay .tap{
    font-size:12px; color:var(--muted); margin-top:28px; letter-spacing:.1em;
    opacity:0;
  }
  /* badge ceremony: a tier name is shorter than a rank name, and it gains a
     flavor line underneath that the rank-up doesn't have */
  .badge-overlay .rn{font-size:30px; letter-spacing:.12em;}
  .badge-overlay .badge-flavor{
    font-size:12.5px; color:var(--muted); margin-top:10px;
    max-width:300px; line-height:1.5; opacity:0;
  }
  .badge-overlay.reveal .badge-flavor{animation:chipIn .5s ease-out 1.45s forwards;}
  .overlay.reveal .tap{animation:chipIn .5s ease-out 1.7s forwards, tapPulse 1.6s ease-in-out 2.2s infinite;}
  @keyframes tapPulse{0%,100%{opacity:.55;}50%{opacity:1;}}

  .shard{
    position:absolute; top:50%; left:50%; width:7px; height:7px; z-index:1;
    background:var(--rank); pointer-events:none;
    animation:shardFly var(--dur) cubic-bezier(.15,.6,.3,1) var(--del) forwards;
  }
  @keyframes shardFly{
    0%{transform:translate(-50%,-50%) rotate(0deg) scale(1); opacity:1;}
    100%{transform:translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) rotate(var(--rot)) scale(.2); opacity:0;}
  }

  @media (prefers-reduced-motion: reduce){
    .flash,.rays,.ring,.shard,.surge-mote,.novaCore,.novaFlash,.corona{display:none;}
    .overlay .badge,.overlay.consume .badge,.overlay.reveal .badge,
    .overlay .badge::after,.overlay.reveal .badge::after,
    .transchip,.overlay .ru,.overlay .ru i,.overlay .rn,.overlay .tap,
    .overlay.reveal .transchip,.overlay.reveal .ru,.overlay.reveal .ru i,
    .overlay.reveal .rn,.overlay.reveal .tap{
      animation:none; opacity:1;
    }
    .overlay .stage{animation:none !important;}
    .beam{display:none !important;}
  }

  /* ===== Rank-up tiers: E–C is the baseline above; tier-2 (B/A/S) and
     tier-3 (Monarch/Grand Monarch) layer extra intensity on top of it ===== */
  .beam{
    display:none; position:absolute; width:1100px; height:1100px; pointer-events:none;
    top:110px; left:50%; margin:-550px 0 0 -550px; z-index:0; /* centered on the badge */
    background:conic-gradient(from 0deg,
      transparent 0deg, color-mix(in srgb, var(--rayC) 38%, transparent) 8deg, transparent 24deg,
      transparent 176deg, color-mix(in srgb, var(--rayC) 38%, transparent) 184deg, transparent 200deg, transparent 360deg);
    mask-image:radial-gradient(circle, black 0%, black 22%, transparent 60%);
  }
  .overlay.tier-3 .beam{display:block; animation:raySpin 5s linear infinite, rayFade 1s ease-out;}

  @keyframes flashPulse2{0%{opacity:.4;}100%{opacity:0;}}
  .overlay.tier-2.reveal .flash{
    animation:flashHit .8s ease-out forwards, flashPulse2 .45s ease-out .6s forwards;
  }
  .overlay.tier-3.reveal .flash{
    animation:flashHit 1s ease-out forwards, flashPulse2 .5s ease-out .65s forwards, flashPulse2 .4s ease-out 1.25s forwards;
  }

  @keyframes stageShake{
    0%,100%{transform:translate(0,0);}
    20%{transform:translate(-6px,2px);} 40%{transform:translate(5px,-3px);}
    60%{transform:translate(-4px,3px);} 80%{transform:translate(3px,-2px);}
  }
  @keyframes stageShakeBig{
    0%,100%{transform:translate(0,0);}
    15%{transform:translate(-9px,3px);} 30%{transform:translate(8px,-5px);}
    45%{transform:translate(-7px,5px);} 60%{transform:translate(6px,-4px);}
    75%{transform:translate(-4px,3px);} 90%{transform:translate(3px,-2px);}
  }
  .overlay.tier-2.reveal .stage{animation:stageShake .5s ease-in-out .05s;}
  .overlay.tier-3.reveal .stage{animation:stageShakeBig .7s ease-in-out .05s;}

  .overlay.tier-2 .ring, .overlay.tier-3 .ring{border-width:3px;}
  .overlay.tier-3 .ring{border-width:3.5px;}

  .overlay.tier-2 .ru,.overlay.tier-3 .ru{letter-spacing:.5em;}
  .overlay.tier-3 .ru{font-size:24px;}

  /* ===== Power Surge: orbiting energy charges the old badge (.consume),
     then everything erupts in a nova flash and the new badge cools out of
     the burst (.reveal). Mote count, nova size and pacing escalate by tier;
     motes start in the OLD rank's color and run hot into the NEW one. ===== */
  .surgeFx{position:absolute; top:110px; left:50%; width:0; height:0; z-index:3; pointer-events:none;}
  .surge-mote{
    position:absolute; left:0; top:0; width:6px; height:6px; margin:-3px 0 0 -3px;
    border-radius:50%; background:var(--oldC); box-shadow:0 0 6px var(--oldC);
    opacity:0; pointer-events:none;
    animation-name:moteOrbit; animation-timing-function:linear; animation-iteration-count:infinite;
  }
  .overlay.consume .surge-mote{opacity:1;}
  .surge-mote.hot{background:var(--rank); box-shadow:0 0 9px var(--rank); transition:background .35s, box-shadow .35s;}
  @keyframes moteOrbit{
    from{transform:rotate(var(--ang)) translateX(var(--radius));}
    to{transform:rotate(calc(var(--ang) + 360deg)) translateX(var(--radius));}
  }
  .novaCore{
    position:absolute; left:-80px; top:-80px; width:160px; height:160px; border-radius:50%;
    opacity:0; pointer-events:none; filter:blur(7px);
    background:radial-gradient(circle, var(--oldC) 0%, transparent 70%);
  }
  .overlay.consume .novaCore{animation:corePulse 1.3s ease-in-out infinite;}
  /* after the reveal the same pulse continues, recoloured to the rank just reached */
  .overlay.reveal .novaCore{
    background:radial-gradient(circle, var(--rank) 0%, transparent 70%);
    animation:corePulse 1.3s ease-in-out infinite;
  }
  @keyframes corePulse{0%,100%{opacity:.3; transform:scale(1);}50%{opacity:.7; transform:scale(1.25);}}
  .novaFlash{
    position:absolute; left:-150px; top:-150px; width:300px; height:300px; border-radius:50%;
    opacity:0; transform:scale(.15); pointer-events:none; z-index:5;
    background:radial-gradient(circle, #fff 0%, var(--rayC) 38%, transparent 72%);
  }
  .overlay.reveal .novaFlash{animation:novaBurst 1.1s ease-out forwards;}
  @keyframes novaBurst{
    0%{opacity:0; transform:scale(.15);}
    18%{opacity:1; transform:scale(var(--novaScale,1.6));}
    100%{opacity:0; transform:scale(calc(var(--novaScale,1.6) * 1.5));}
  }
  .overlay.tier-2{--novaScale:2.1;}
  .overlay.tier-3{--novaScale:2.7;}
  .corona{
    position:absolute; left:50%; top:110px; width:460px; height:460px; margin:-230px 0 0 -230px;
    border-radius:50%; opacity:0; pointer-events:none; z-index:0;
    background:conic-gradient(from 0deg, transparent, color-mix(in srgb, var(--rank) 35%, transparent), transparent 60%);
  }
  .overlay.reveal .corona{animation:coronaSpin 1.6s ease-out forwards;}
  @keyframes coronaSpin{
    0%{opacity:0; transform:rotate(0deg) scale(.6);}
    25%{opacity:.9;}
    100%{opacity:0; transform:rotate(140deg) scale(1.15);}
  }

  /* ==================== LOOT CEREMONY — "FORGE BREACH" ====================
     Ported from the standalone design preview once the chosen animation and one
     box design per tier were picked:
       rare = Steel Case · legendary = Royal Vault · mythic = Void Heart.
     The visuals — geometry, timings, easings, every colour stop — are the
     preview's, unchanged. Two departures, both forced by THIS file rather than
     by taste:
       · every selector is scoped `#lootOverlay …` and every class carries an
         `lb-` prefix. The preview owned its whole document; here `.ring`,
         `.shard`, `.rays`, `.corona`, `.show`, `.hot` already mean something to
         the rank-up / badge ceremony (CLAUDE.md: the rank-up sheet is scoped
         `#overlay` for exactly this reason).
       · every @keyframes is renamed `lb*`. Keyframe names are global and cannot
         be scoped, and the preview's shardFly / ringBurst / novaBurst /
         flashHit / coronaSpin / raySpin / moteOrbit / corePulse / glowBreathe /
         tapPulse are ALL names this file already defines for the rank-up — the
         later definition would silently have won for both ceremonies.
     Specificity ladder (preserved from the preview, one id added to each rung):
       #lootOverlay .lb-face                  base shell
       #lootOverlay .lb-face.lb-blow          generic shrapnel fallback
       #lootOverlay.lb-d-X .lb-face           that design's material   (later in source, so it wins the tie)
       #lootOverlay.lb-d-X .lb-face.lb-blow   that design's OWN shrapnel
     ==================== */

  #lootOverlay{
    /* Tier accents. --lb-acc is the primary, --lb-acc2 the companion the tier is
       actually identified BY (mythic reads red/purple, legendary gilded rather
       than flat yellow). Repainted per tier by paintLootAccent(); every rule
       below reads them, never a literal. --loot is the plain public alias. */
    --loot:#3b82f6; --lb-acc:#3b82f6; --lb-acc2:#8ec5ff;
    --lb-a10:rgba(59,130,246,.10); --lb-a22:rgba(59,130,246,.22);
    --lb-a45:rgba(59,130,246,.45); --lb-a70:rgba(59,130,246,.70);
    --lb-b22:rgba(142,197,255,.22); --lb-b45:rgba(142,197,255,.45); --lb-b70:rgba(142,197,255,.70);
    --lb-ease:cubic-bezier(.4,0,.2,1);
    --lb-easeOut:cubic-bezier(.16,.84,.28,1);
    --lb-e1:0 2px 10px rgba(0,0,0,.45);
    --lb-e3:0 26px 60px rgba(0,0,0,.62);
    background:radial-gradient(circle at 50% 46%, #0d1220 0%, #06080f 62%, #04060b 100%);
    perspective:1300px; overflow:hidden;
  }
  /* fade the black in rather than cutting to it — the hard cut reads cheap */
  #lootOverlay.show{display:block; animation:lbOverlayIn .3s var(--lb-ease) both;}
  @keyframes lbOverlayIn{from{opacity:0;} to{opacity:1;}}
  /* THE ceremony-centring rule. The preview this was ported from lived on a page
     that could not scroll, so its overlay's `position:fixed; inset:0` box WAS the
     visual viewport. This app's document scrolls, and a fixed inset:0 box resolves
     against the layout viewport MINUS the classic scrollbar — so .lb-stage came out
     1265px wide in a 1280px window and the whole rift rendered off the visual
     centre (both axes once a horizontal scrollbar appeared), while the page behind
     stayed scrollable *under* a modal ceremony. Locking the document for the
     ceremony's lifetime restores the preview's own condition: no scrollbar, so
     inset:0 is the viewport, so the rift is dead centre at every size.
     Added in playLootReel, removed in closeLootCeremony — both paths, always. */
  html.lb-scroll-lock, html.lb-scroll-lock body{overflow:hidden;}
  #lootOverlay .lb-stage{
    position:absolute; inset:0;
    display:flex; align-items:center; justify-content:center;
    transform-style:preserve-3d;
  }
  #lootOverlay .lb-vig{
    position:absolute; inset:0; pointer-events:none; z-index:35;
    background:radial-gradient(circle at 50% 48%, transparent 40%, rgba(0,0,0,.55) 100%);
  }
  #lootOverlay .lb-chip{
    position:absolute; left:16px; top:16px; z-index:40;
    font-size:10px; letter-spacing:.2em; text-transform:uppercase;
    color:var(--muted); border:1px solid var(--line2); border-radius:20px;
    padding:6px 13px; background:rgba(12,16,26,.75);
    animation:lbFadeUp .5s ease-out both;
  }
  #lootOverlay .lb-chip b{color:var(--lb-acc); font-weight:600;}
  #lootOverlay .lb-taphint{
    position:absolute; left:0; right:0; bottom:30px; text-align:center; z-index:40;
    font-size:11px; letter-spacing:.13em; color:var(--muted); opacity:0;
    animation:lbFadeUp .6s ease-out .9s forwards, lbTapPulse 1.9s ease-in-out 1.6s infinite;
  }
  @keyframes lbFadeUp{from{opacity:0; transform:translateY(7px);} to{opacity:1; transform:none;}}
  @keyframes lbTapPulse{0%,100%{opacity:.45;} 50%{opacity:.95;}}

  /* ---- shared FX primitives (the preview's mirror of the Power Surge kit) ---- */
  #lootOverlay .lb-shard{
    position:absolute; left:50%; top:50%; width:7px; height:7px; z-index:12;
    background:var(--lb-acc); pointer-events:none; border-radius:1px;
    box-shadow:0 0 8px var(--lb-acc);
    animation:lbShardFly var(--dur) cubic-bezier(.12,.6,.28,1) var(--del) forwards;
  }
  @keyframes lbShardFly{
    0%{transform:translate(-50%,-50%) rotate(0deg) scale(1); opacity:1;}
    100%{transform:translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) rotate(var(--rot)) scale(.15); opacity:0;}
  }
  #lootOverlay .lb-ring{
    position:absolute; left:50%; top:50%; border-radius:50%; pointer-events:none; z-index:8;
    width:180px; height:180px; border:2px solid var(--lb-acc);
    box-shadow:0 0 22px var(--lb-a70), inset 0 0 22px var(--lb-a45);
    transform:translate(-50%,-50%) scale(.2); opacity:0;
    animation:lbRingBurst var(--rdur,1.1s) cubic-bezier(.14,.72,.28,1) var(--rdel,0s) forwards;
  }
  @keyframes lbRingBurst{
    0%{transform:translate(-50%,-50%) scale(.25); opacity:.9;}
    100%{transform:translate(-50%,-50%) scale(var(--rmax,4.4)); opacity:0;}
  }
  #lootOverlay .lb-novaflash{
    position:absolute; left:50%; top:50%; width:420px; height:420px; margin:-210px 0 0 -210px;
    border-radius:50%; pointer-events:none; z-index:14;
    background:radial-gradient(circle, #fff 0%, var(--lb-a70) 30%, transparent 70%);
    opacity:0; transform:scale(.15);
    animation:lbNovaBurst .85s cubic-bezier(.1,.75,.3,1) forwards;
  }
  @keyframes lbNovaBurst{
    0%{opacity:0; transform:scale(.12);}
    10%{opacity:1; transform:scale(1.15);}
    100%{opacity:0; transform:scale(2.2);}
  }
  /* Tight px stops on purpose: a %-based radial on a wide viewport washes the
     whole screen flat and swallows the shards it is supposed to be lighting. */
  #lootOverlay .lb-whiteout{
    position:absolute; inset:0; pointer-events:none; z-index:15; opacity:0;
    background:radial-gradient(circle at 50% 50%, #fff 0px, var(--lb-a70) 120px, transparent 400px);
    animation:lbFlashHit .55s ease-out forwards;
  }
  @keyframes lbFlashHit{0%{opacity:0;} 8%{opacity:1;} 55%{opacity:.35;} 100%{opacity:0;}}
  #lootOverlay .lb-corona{
    position:absolute; left:50%; top:50%; width:620px; height:620px; margin:-310px 0 0 -310px;
    border-radius:50%; pointer-events:none; z-index:2; opacity:0;
    background:conic-gradient(from 0deg, transparent, var(--lb-a45), transparent 58%);
    animation:lbCoronaSpin 1.8s ease-out forwards;
  }
  @keyframes lbCoronaSpin{
    0%{opacity:0; transform:rotate(0deg) scale(.55);}
    22%{opacity:.85;}
    100%{opacity:0; transform:rotate(155deg) scale(1.2);}
  }
  #lootOverlay .lb-rays{
    position:absolute; left:50%; top:50%; width:900px; height:900px; margin:-450px 0 0 -450px;
    pointer-events:none; z-index:1; opacity:0;
    background:conic-gradient(from 0deg,
      transparent 0deg, var(--lb-a22) 4deg, transparent 15deg,
      transparent 45deg, var(--lb-a22) 49deg, transparent 60deg,
      transparent 90deg, var(--lb-a22) 94deg, transparent 105deg,
      transparent 135deg, var(--lb-a22) 139deg, transparent 150deg,
      transparent 180deg, var(--lb-a22) 184deg, transparent 195deg,
      transparent 225deg, var(--lb-a22) 229deg, transparent 240deg,
      transparent 270deg, var(--lb-a22) 274deg, transparent 285deg,
      transparent 315deg, var(--lb-a22) 319deg, transparent 330deg, transparent 360deg);
    -webkit-mask-image:radial-gradient(circle, #000 0%, #000 30%, transparent 64%);
    mask-image:radial-gradient(circle, #000 0%, #000 30%, transparent 64%);
    filter:blur(6px);
    animation:lbRaySpin 30s linear infinite, lbRayIn 1.1s var(--lb-easeOut) .1s forwards;
  }
  /* linear here is deliberate: a continuous rotation with any easing visibly
     stutters every loop. Everything that starts and stops is eased. */
  @keyframes lbRaySpin{from{transform:rotate(0);} to{transform:rotate(360deg);}}
  /* stops short of full opacity — at 1 the spokes read as hot blobs, not light */
  @keyframes lbRayIn{to{opacity:.58;}}

  /* ---- the result / prize card ---- */
  #lootOverlay .lb-prize{
    position:relative; z-index:20;
    width:284px; max-width:calc(100vw - 40px); padding:30px 24px 28px;
    text-align:center; border-radius:20px;
    background:linear-gradient(180deg, #171e30 0%, #0d1220 100%);
    border:1px solid var(--lb-acc);
    box-shadow:0 0 0 1px var(--lb-a22), 0 0 70px var(--lb-a45), var(--lb-e3),
               inset 0 1px 0 rgba(255,255,255,.09);
    animation:lbPrizeIn .74s cubic-bezier(.18,.9,.24,1) both;
  }
  /* One settle, no bounce-back wobble; the brightness ramp tops out where a real
     bloom would — higher blew the card to a white rectangle for two frames. */
  @keyframes lbPrizeIn{
    0%{opacity:0; transform:translateY(18px) scale(.78); filter:brightness(1.9);}
    40%{opacity:1;}
    100%{opacity:1; transform:translateY(0) scale(1); filter:brightness(1);}
  }
  #lootOverlay .lb-prize::after{
    content:''; position:absolute; inset:-30px; border-radius:34px; z-index:-1;
    background:radial-gradient(circle, var(--lb-a22) 0%, transparent 70%);
    animation:lbGlowBreathe 3.4s ease-in-out 1s infinite;
  }
  @keyframes lbGlowBreathe{0%,100%{opacity:.6; transform:scale(1);} 50%{opacity:1; transform:scale(1.07);}}
  #lootOverlay .lb-plabel{
    font-size:9.5px; letter-spacing:.34em; text-transform:uppercase; color:#9aa3b8;
    margin-bottom:16px;
  }
  #lootOverlay .lb-pemo{
    font-size:62px; line-height:1; margin-bottom:16px;
    filter:drop-shadow(0 0 18px var(--lb-a70));
    animation:lbEmoPop .62s cubic-bezier(.2,1.2,.36,1) .14s both;
  }
  #lootOverlay .lb-pemo-img{
    width:84px; height:84px; object-fit:cover; border-radius:14px; margin-bottom:16px;
    filter:drop-shadow(0 0 18px var(--lb-a70));
    animation:lbEmoPop .62s cubic-bezier(.2,1.2,.36,1) .14s both;
  }
  @keyframes lbEmoPop{0%{transform:scale(.55) rotate(-8deg); opacity:0;} 100%{transform:none; opacity:1;}}
  #lootOverlay .lb-pnm{
    font-family:'Cinzel',Georgia,serif; font-weight:700; font-size:23px;
    letter-spacing:.02em; margin-bottom:16px; line-height:1.22;
    text-shadow:0 1px 14px rgba(0,0,0,.6); word-break:break-word;
  }
  #lootOverlay .lb-prize .lb-cr{
    position:static; width:64px; height:5px; margin:-6px auto 14px; border-radius:3px;
  }
  /* hairline rule above the tier pill — cheap, and it makes the block read composed */
  #lootOverlay .lb-prule{
    height:1px; margin:0 auto 16px; width:74px;
    background:linear-gradient(90deg, transparent, var(--lb-a70), transparent);
  }
  #lootOverlay .lb-ptier{
    display:inline-block; font-size:9.5px; font-weight:700; letter-spacing:.24em;
    color:var(--lb-acc); border:1px solid var(--lb-acc); border-radius:20px; padding:5px 15px;
    box-shadow:0 0 16px var(--lb-a22), inset 0 0 12px var(--lb-a10);
  }
  #lootOverlay .lb-shine{position:absolute; inset:0; border-radius:20px; overflow:hidden; pointer-events:none;}
  #lootOverlay .lb-shine i{
    position:absolute; top:-60%; left:-60%; width:52%; height:220%;
    background:linear-gradient(100deg, transparent, rgba(255,255,255,.22), transparent);
    transform:translateX(-120%) rotate(14deg);
    animation:lbShine 1.35s cubic-bezier(.3,.5,.2,1) .5s;
  }
  @keyframes lbShine{to{transform:translateX(430%) rotate(14deg);}}

  /* ---- the rift (phase 3's stage; also where the box is forged) ---- */
  #lootOverlay .lb-scene{
    position:relative; width:100%; height:100%;
    display:flex; align-items:center; justify-content:center;
    transform-style:preserve-3d;
  }
  #lootOverlay .lb-gate{
    /* max-width is 100% (the stage), never 100vw — vw counts the scrollbar, so a
       vw cap can never actually stop the gate overflowing the stage it lives in. */
    position:relative; width:330px; height:470px; max-width:100%;
    transform-origin:50% 50%;
    /* parked as a hairline slit exactly where the seam burns, until the tear runs */
    transform:scale(.005,.40); opacity:0;
    /* NOTE: no preserve-3d here on purpose — a 3D rendering context defeats the
       portal's overflow/border-radius clipping and the reel spills out of the oval. */
  }
  #lootOverlay .lb-gate.lb-tearing{animation:lbGateTear 1.15s cubic-bezier(.22,.85,.24,1) forwards;}
  /* NOTE: opacity MUST be restated on the 100% keyframe. A property that appears
     in only some keyframes takes the element's own value at the implicit 100% —
     so with the base rule's opacity:0 the gate faded straight back out again
     while its geometry animated open, leaving phase 3 on a black screen. */
  @keyframes lbGateTear{
    0%{transform:scale(.005,.40); opacity:0;}
    6%{opacity:1;}
    30%{transform:scale(.016,.86);}   /* unzips upward/downward, still a slit */
    42%{transform:scale(.028,1.06);}
    52%{transform:scale(.05,1.0);}
    80%{transform:scale(1.09,1.0);}   /* the rip: prised open sideways */
    91%{transform:scale(.96,1.016);}
    100%{transform:scale(1,1); opacity:1;}
  }
  #lootOverlay .lb-gate.lb-closing{animation:lbGateShut .5s cubic-bezier(.6,0,.85,.2) forwards;}
  @keyframes lbGateShut{
    0%{transform:scale(1,1); opacity:1;}
    55%{transform:scale(.05,1.06); opacity:1;}
    100%{transform:scale(.01,.004); opacity:0;}
  }
  /* The rift's outermost rim ring sits at inset:-26px, so an uncropped gate needs
     330 + 52 = 382px of stage. Narrower than that and #lootOverlay{overflow:hidden}
     shaves the rings off the sides. Narrow the rift rather than crop it.
     WIDTH ONLY — the height stays 470 because runLootForgeBreach reads that number
     back as `portH` to centre the reel on the scan line. */
  @media (max-width:400px){
    #lootOverlay .lb-gate{width:284px;}
  }
  #lootOverlay .lb-port{
    position:absolute; inset:0; border-radius:50%; overflow:hidden;
    /* belt-and-braces: clip-path is the reliable clipper here, border-radius alone
       is not once anything in the subtree touches a transform */
    clip-path:ellipse(50% 50% at 50% 50%);
    background:radial-gradient(ellipse at 50% 50%, #0a0d17 0%, #04060c 68%, #010205 100%);
    box-shadow:inset 0 0 90px rgba(0,0,0,.95), inset 0 0 40px var(--lb-a22);
  }
  #lootOverlay .lb-rim{
    position:absolute; inset:-3px; border-radius:50%; pointer-events:none;
    border:2px solid var(--lb-acc);
    box-shadow:0 0 34px var(--lb-acc), 0 0 90px var(--lb-a45), inset 0 0 60px var(--lb-a45);
  }
  #lootOverlay .lb-rim2{
    position:absolute; inset:-16px; border-radius:50%; pointer-events:none;
    background:conic-gradient(from 0deg, transparent 0deg, var(--lb-a70) 22deg, transparent 52deg,
      transparent 110deg, var(--lb-acc) 138deg, transparent 172deg,
      transparent 230deg, var(--lb-a70) 258deg, transparent 292deg, transparent 360deg);
    -webkit-mask-image:radial-gradient(ellipse, transparent 0%, transparent 88%, #000 94%, transparent 100%);
    mask-image:radial-gradient(ellipse, transparent 0%, transparent 88%, #000 94%, transparent 100%);
    filter:blur(1.2px);
    animation:lbRaySpin 7s linear infinite;
  }
  #lootOverlay .lb-rim3{
    position:absolute; inset:-26px; border-radius:50%; pointer-events:none;
    background:conic-gradient(from 180deg, transparent 0deg, var(--lb-a45) 40deg, transparent 90deg,
      transparent 180deg, var(--lb-a45) 220deg, transparent 268deg, transparent 360deg);
    -webkit-mask-image:radial-gradient(ellipse, transparent 0%, transparent 90%, #000 96%, transparent 100%);
    mask-image:radial-gradient(ellipse, transparent 0%, transparent 90%, #000 96%, transparent 100%);
    filter:blur(2.5px); opacity:.75;
    animation:lbRaySpin 11s linear infinite reverse;
  }
  #lootOverlay .lb-haze{
    position:absolute; inset:0; pointer-events:none; z-index:3;
    background:
      radial-gradient(ellipse 60% 30% at 50% 8%,  var(--lb-a45), transparent 70%),
      radial-gradient(ellipse 60% 30% at 50% 94%, var(--lb-a45), transparent 70%),
      radial-gradient(ellipse 70% 60% at 50% 50%, transparent 30%, rgba(3,5,10,.85) 100%);
  }
  #lootOverlay .lb-smoke{
    position:absolute; border-radius:50%; pointer-events:none; filter:blur(22px);
    background:radial-gradient(circle, var(--lb-a45), transparent 70%);
    animation:lbSmokeDrift var(--sdur) ease-in-out var(--sdel) infinite;
  }
  @keyframes lbSmokeDrift{
    0%,100%{transform:translate(0,0) scale(1); opacity:.28;}
    50%{transform:translate(var(--sx), var(--sy)) scale(1.5); opacity:.6;}
  }
  #lootOverlay .lb-reel{
    position:absolute; left:50%; top:0; width:150px; margin-left:-75px;
    display:flex; flex-direction:column; gap:10px; z-index:2;
    will-change:transform; opacity:0; transition:opacity .4s var(--lb-ease);
  }
  #lootOverlay .lb-reel.lb-shown{opacity:1;}
  #lootOverlay .lb-card{
    flex:0 0 96px; height:96px; border-radius:10px;
    border:1px solid rgba(255,255,255,.10);
    background:linear-gradient(180deg, rgba(22,28,44,.9), rgba(8,11,19,.9));
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:5px;
    /* NO backdrop-filter here — the preview this was ported from had
       `backdrop-filter:blur(1px)` on this rule and it is why the bottom of the
       rift came up empty. Every one of the strip's 46 cards asking for a
       backdrop sample forces a backdrop root over the whole 4866px-tall
       `will-change:transform` reel, inside a clip-path'd portal; the compositor
       only ever resolves it for part of that layer, so every card from roughly
       the scan line down simply never painted — the reel looked half-loaded,
       with 2-3 cards above the line and empty ellipse below. Layout was always
       correct (measured: cards span local -25..495 of a 470px port, identical
       to the preview) — the cards were laid out and never drawn, which is why
       forcing any repaint made them appear. The blur was 1px behind a card that
       is already 90% opaque, i.e. invisible; dropping it is a straight win.
       Verified live at 1280x800 / 900x720 / 430x900 on all three tiers. */
  }
  /* NO filter here: one drop-shadow x 46 strip cards is 46 blurred filters
     recomputed every frame of the spin, in the one layer that must not do any
     per-frame work. A text-shadow is a cheap raster effect and reads the same at
     this size. (.lb-bcard/.lb-prize below keep their real drop-shadows — there
     is only ever one of those on screen at a time.) */
  #lootOverlay .lb-card .lb-ce{font-size:27px; line-height:1; text-shadow:0 2px 6px rgba(0,0,0,.9);}
  #lootOverlay .lb-ce-img{width:34px; height:34px; object-fit:cover; border-radius:8px; box-shadow:0 2px 6px rgba(0,0,0,.9);}
  #lootOverlay .lb-bcard .lb-ce-img{width:56px; height:56px; border-radius:12px; filter:drop-shadow(0 0 18px var(--lb-a70));}
  #lootOverlay .lb-card .lb-cn{font-size:8.5px; color:#96a0b8; text-align:center; padding:0 6px; line-height:1.3;}
  #lootOverlay .lb-card .lb-cq{font-size:8px; color:#6b7690; text-align:center; line-height:1;}
  #lootOverlay .lb-card.lb-win{border-color:var(--lb-acc); box-shadow:0 0 0 1px var(--lb-a45), 0 0 26px var(--lb-a45);}
  #lootOverlay .lb-card.lb-win .lb-cn{color:var(--lb-acc);}
  #lootOverlay .lb-card.lb-win .lb-cq{color:var(--lb-a70,#6b7690);}
  /* The won item's own rarity, independent of the ceremony's tier theming (a
     rare box can drop a mythic-tagged item) — a small colored bar rather than
     recoloring .lb-cn/.lb-cq text, which the win-state rules above already
     claim at higher specificity. Same three-tier palette as .loot-tier-badge. */
  /* Rarity, CS:GO-style. The old 14x3px dash was too small to read on a moving
     strip — it was there, but you had to look for it. Now the rarity owns the
     card: a wash rising from the bottom, a matching edge, and a thick footer bar
     that is the actual "what did I just pull" signal. Same three-tier palette as
     everything else in the feature; --rc/--rc2 are the bar's gradient stops and
     --rcT the translucent wash. */
  #lootOverlay .rar-rare{--rc:#2563eb; --rc2:#7cb0ff; --rcT:rgba(59,130,246,.34);}
  #lootOverlay .rar-legendary{--rc:#d4a017; --rc2:#ffdc7a; --rcT:rgba(212,160,23,.34);}
  #lootOverlay .rar-mythic{--rc:#e0143c; --rc2:#a855f7; --rcT:rgba(200,30,120,.36);}
  /* eternal is an ITEM rarity, so a prize card can carry it even though no box
     tier does. Without its own entry here the card's rarity bar resolved to no
     gradient at all — five pixels of empty space under an eternal win. */
  #lootOverlay .rar-eternal{--rc:#0a0a0a; --rc2:#ffffff; --rcT:rgba(255,255,255,.30);}
  /* The wash is layered into the card's OWN background rather than a ::before —
     46 extra pseudo-elements inside the will-change:transform reel cost real
     frames. Same reason the bar carries no blurred glow: a box-shadow blur on
     every one of 46 cards is per-frame compositing work in the one layer that
     must not do any. (This is the same class of trap as the backdrop-filter
     that once left the bottom of the reel unpainted.) */
  #lootOverlay .lb-card{
    position:relative; overflow:hidden;
    background:linear-gradient(180deg, transparent 42%, var(--rcT, transparent)),
               linear-gradient(180deg, rgba(22,28,44,.9), rgba(8,11,19,.9));
    box-shadow:inset 0 0 0 1px var(--rcT, transparent);
  }
  /* the footer bar — replaces the old dash; full width, unmissable */
  #lootOverlay .lb-cr{
    position:absolute; left:0; right:0; bottom:0; width:auto; height:6px; margin:0;
    border-radius:0; z-index:2;
    background:linear-gradient(90deg, var(--rc), var(--rc2), var(--rc));
  }
  /* The winner keeps the ceremony's own tier accent on top of its rarity — the two
     are independent (a rare box can drop a mythic item) and both should read. */
  #lootOverlay .lb-card.lb-win{box-shadow:0 0 0 1px var(--lb-a45), 0 0 26px var(--lb-a45);}
  #lootOverlay .lb-scan{
    position:absolute; left:0; right:0; top:50%; height:2px; margin-top:-1px; z-index:4;
    background:linear-gradient(90deg, transparent, var(--lb-acc), transparent);
    box-shadow:0 0 16px var(--lb-acc); opacity:.85;
  }
  /* the card breaching OUT of the gate toward the viewer */
  #lootOverlay .lb-breach{
    position:absolute; left:50%; top:50%; width:190px; margin:-124px 0 0 -95px; z-index:22;
    transform-style:preserve-3d;
  }
  #lootOverlay .lb-bcard{
    width:190px; height:248px; border-radius:16px;
    border:1px solid var(--lb-acc);
    background:linear-gradient(180deg,#1a2236,#0c1120);
    box-shadow:0 0 0 1px var(--lb-a22), 0 0 60px var(--lb-a45), 0 30px 60px rgba(0,0,0,.7);
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px;
    animation:lbBreachOut 1.15s cubic-bezier(.14,.9,.26,1) forwards;
  }
  #lootOverlay .lb-bcard .lb-ce{font-size:56px; filter:drop-shadow(0 0 18px var(--lb-a70));}
  #lootOverlay .lb-bcard .lb-cn{
    font-family:'Cinzel',Georgia,serif; font-size:14px; letter-spacing:.05em;
    padding:0 12px; text-align:center; word-break:break-word;
  }
  #lootOverlay .lb-bcard{position:relative; overflow:hidden;}
  #lootOverlay .lb-bcard .lb-cr{
    position:absolute; left:0; right:0; bottom:0; width:auto; height:8px; margin:0; border-radius:0;
  }
  #lootOverlay .lb-bcard .lb-cq{font-size:12px; color:var(--lb-a70,#96a0b8); text-align:center;}
  @keyframes lbBreachOut{
    0%{transform:translateZ(-460px) rotateX(24deg) scale(.55); opacity:0; filter:brightness(4);}
    16%{opacity:1;}
    62%{transform:translateZ(300px) rotateX(-8deg) scale(1); filter:brightness(1.5);}
    100%{transform:translateZ(120px) rotateX(0deg) scale(1); filter:brightness(1);}
  }
  #lootOverlay .lb-shock{
    position:absolute; left:50%; top:50%; width:200px; height:200px;
    margin:-100px 0 0 -100px; border-radius:50%; pointer-events:none; z-index:18;
    border:3px solid var(--lb-acc); opacity:0;
    box-shadow:0 0 40px var(--lb-acc), inset 0 0 40px var(--lb-a45);
    animation:lbShockOut 1s cubic-bezier(.1,.75,.25,1) var(--sdel2,0s) forwards;
  }
  @keyframes lbShockOut{
    0%{transform:scale(.15) rotateX(66deg); opacity:1;}
    100%{transform:scale(7) rotateX(66deg); opacity:0;}
  }

  /* ---- phase 1: the forge charge (the box itself) ---- */
  /* `perspective` + the DEFAULT flat transform-style, never preserve-3d. The
     scene this lives in IS preserve-3d, so a 3D box here would sort against the
     cracks and the seam by real Z — and the cube's +85px faces would paint over
     both, hiding the one thing that has to be read. Flattening the box into its
     own plane puts it back under plain z-index rules: box 5, motes 6, cracks 9,
     seam 17. */
  #lootOverlay .lb-forge{
    position:absolute; left:50%; top:50%; width:170px; height:170px;
    margin:-85px 0 0 -85px; z-index:5; perspective:900px;
  }
  #lootOverlay .lb-shaker{position:relative; width:170px; height:170px; transform-style:preserve-3d;}
  #lootOverlay .lb-shaker.lb-charging{animation:lbShake 2s cubic-bezier(.4,0,.6,1) forwards;}
  @keyframes lbShake{
    0%{transform:translate(0,0);}
    10%{transform:translate(-1px,1px);}  20%{transform:translate(1px,-1px);}
    32%{transform:translate(-2px,1px);}  44%{transform:translate(3px,-2px);}
    56%{transform:translate(-4px,2px);}  66%{transform:translate(5px,-3px);}
    75%{transform:translate(-6px,3px);}  83%{transform:translate(7px,-4px);}
    90%{transform:translate(-8px,4px);}  95%{transform:translate(6px,-5px);}
    100%{transform:translate(0,0);}
  }
  #lootOverlay .lb-cube{
    position:absolute; inset:0; transform-style:preserve-3d;
    animation:lbCubeSpin 9s linear infinite;
    transition:filter .3s;
  }
  @keyframes lbCubeSpin{
    from{transform:rotateX(-20deg) rotateY(0deg);}
    to{transform:rotateX(-20deg) rotateY(360deg);}
  }
  #lootOverlay .lb-cube.lb-hot{animation:lbCubeSpin 2.2s linear infinite;}
  #lootOverlay .lb-fw{position:absolute; inset:0; transform-style:preserve-3d;}
  #lootOverlay .lb-fw.lb-f1{transform:translateZ(85px);}
  #lootOverlay .lb-fw.lb-f2{transform:rotateY(180deg) translateZ(85px);}
  #lootOverlay .lb-fw.lb-f3{transform:rotateY(90deg) translateZ(85px);}
  #lootOverlay .lb-fw.lb-f4{transform:rotateY(-90deg) translateZ(85px);}
  #lootOverlay .lb-fw.lb-f5{transform:rotateX(90deg) translateZ(85px);}
  #lootOverlay .lb-fw.lb-f6{transform:rotateX(-90deg) translateZ(85px);}
  #lootOverlay .lb-face{
    /* inset:-1px + a near-zero radius CLOSES the shell. With rounded corners the
       six faces stop short of each other at every edge, and the gap reads as a
       hole punched in the box (visible on every design; steel's 9px radius made
       it obvious, vault's 4px less so). The 1px overhang covers the hairline
       where perpendicular faces meet. */
    position:absolute; inset:-1px; border-radius:2px;
    background:linear-gradient(150deg, #1a2136 0%, #0d1220 60%, #090d17 100%);
    border:1px solid #2c3550;
    box-shadow:inset 0 0 32px rgba(0,0,0,.75), inset 0 1px 0 rgba(255,255,255,.06);
    display:flex; align-items:center; justify-content:center;
    font-size:34px; color:var(--lb-acc);
    text-shadow:0 0 12px var(--lb-a70);
  }
  #lootOverlay .lb-face .lb-fseam{
    position:absolute; left:8px; right:8px; top:50%; height:1px; margin-top:-.5px;
    background:linear-gradient(90deg, transparent, var(--lb-a45), transparent);
  }
  #lootOverlay .lb-face .lb-rune{
    opacity:.55; transition:opacity .4s, text-shadow .4s, transform .4s;
    animation:lbRuneBreathe 2s ease-in-out infinite;
  }
  @keyframes lbRuneBreathe{0%,100%{opacity:.4;} 50%{opacity:1;}}
  #lootOverlay .lb-cube.lb-hot .lb-rune{
    opacity:1; text-shadow:0 0 26px var(--lb-acc), 0 0 60px var(--lb-acc); animation-duration:.5s;
  }
  /* Edge-lit shrapnel: a dark panel tumbling across a bright nova reads as
     nothing, so the broken shell is painted white-hot statically and only
     transform/opacity animate. (Animating the `background` shorthand between
     gradients with differing stop counts interpolates discretely and the fill
     just never showed up.) */
  #lootOverlay .lb-face.lb-blow{
    animation:lbFaceFly 1.05s cubic-bezier(.16,.75,.3,1) forwards;
    background:linear-gradient(150deg,#ffffff 0%, var(--lb-acc) 45%, #1b2338 100%);
    border-color:#fff;
    box-shadow:0 0 30px var(--lb-acc), 0 0 75px var(--lb-a70), inset 0 0 26px rgba(255,255,255,.5);
  }
  @keyframes lbFaceFly{
    0%{transform:translateZ(0) rotate(0deg); opacity:1;}
    70%{opacity:.85;}
    100%{transform:translateZ(var(--fly)) translate(var(--fdx), var(--fdy)) rotate(var(--frot)) scale(.5);
         opacity:0;}
  }
  #lootOverlay .lb-core{
    position:absolute; left:50%; top:50%; width:200px; height:200px; margin:-100px 0 0 -100px;
    border-radius:50%; pointer-events:none; z-index:1; filter:blur(11px); opacity:0;
    background:radial-gradient(circle, var(--lb-acc) 0%, transparent 68%);
    animation:lbCorePulse 1.4s ease-in-out infinite;
  }
  @keyframes lbCorePulse{0%,100%{opacity:.22; transform:scale(.95);} 50%{opacity:.7; transform:scale(1.3);}}
  #lootOverlay .lb-core.lb-hot{animation-duration:.34s;}
  #lootOverlay .lb-motes{position:absolute; left:50%; top:50%; width:0; height:0; z-index:6; pointer-events:none;}
  #lootOverlay .lb-mote{
    position:absolute; left:0; top:0; width:6px; height:6px; margin:-3px 0 0 -3px;
    border-radius:50%; background:var(--lb-acc); box-shadow:0 0 9px var(--lb-acc), 0 0 20px var(--lb-a70);
    animation:lbMoteOrbit var(--mdur) linear infinite, lbMoteIn .5s ease-out both;
  }
  @keyframes lbMoteOrbit{
    from{transform:rotate(var(--ang)) translateX(var(--rad)) rotate(calc(var(--ang) * -1));}
    to{transform:rotate(calc(var(--ang) + 360deg)) translateX(var(--rad)) rotate(calc((var(--ang) + 360deg) * -1));}
  }
  @keyframes lbMoteIn{from{opacity:0;} to{opacity:1;}}
  #lootOverlay .lb-crack{
    position:absolute; left:50%; top:50%; width:3px; height:190px; margin:-95px 0 0 -1.5px;
    z-index:9; pointer-events:none; transform-origin:50% 50%;
    background:linear-gradient(180deg, transparent, #fff 22%, var(--lb-acc) 50%, #fff 78%, transparent);
    box-shadow:0 0 16px var(--lb-acc), 0 0 40px var(--lb-a70);
    transform:rotate(var(--crot)) scaleY(0); opacity:0;
    animation:lbCrackOpen .5s cubic-bezier(.2,.9,.3,1) var(--cdel) forwards;
  }
  @keyframes lbCrackOpen{
    0%{transform:rotate(var(--crot)) scaleY(0) scaleX(.4); opacity:0;}
    40%{opacity:1;}
    100%{transform:rotate(var(--crot)) scaleY(1) scaleX(1); opacity:1;}
  }
  #lootOverlay .lb-label{
    position:absolute; left:0; right:0; top:calc(50% + 190px); text-align:center; z-index:20;
    font-family:'Cinzel',Georgia,serif; font-size:13px; letter-spacing:.45em; color:var(--lb-acc);
    opacity:0; text-shadow:0 0 20px var(--lb-a70);
    animation:lbLabel 2s ease-in-out forwards;
  }
  @keyframes lbLabel{0%{opacity:0; letter-spacing:.2em;} 30%{opacity:.9;} 100%{opacity:0; letter-spacing:.7em;}}

  /* ---- phase 2: the detonation IS the gate tearing open ----
     Everything here exists to make the hand-off read as ONE event: the box's own
     cracks swing to vertical, fuse into a single white seam, and that seam is
     literally the slit the portal widens out of. */
  /* The 6 radial forge cracks stop being radial: they swing to vertical and
     overlap, so six separate cuts read as one ragged tear. */
  #lootOverlay .lb-crack.lb-converge{animation:lbConverge .30s cubic-bezier(.25,.9,.25,1) forwards;}
  @keyframes lbConverge{
    0%{transform:rotate(var(--crot)) scaleY(1) scaleX(1); opacity:1;}
    100%{transform:rotate(0deg) scaleY(1.5) scaleX(2.4); opacity:1;}
  }
  /* ...then they hand their light to the gate rim and blow apart sideways. */
  #lootOverlay .lb-crack.lb-handoff{animation:lbHandoff .42s ease-out forwards;}
  @keyframes lbHandoff{
    0%{transform:rotate(0deg) scaleY(1.5) scaleX(2.4); opacity:1;}
    100%{transform:rotate(0deg) scaleY(2.5) scaleX(7); opacity:0;}
  }
  /* The seam: the single white-hot cut the box splits along. Its height (200px)
     and the gate's opening slit (.40 x 470px = 188px) are matched on purpose —
     the portal starts exactly where the seam is brightest. */
  #lootOverlay .lb-seam{
    position:absolute; left:50%; top:50%; width:14px; height:200px; margin:-100px 0 0 -7px;
    z-index:17; pointer-events:none; border-radius:9px; opacity:0; filter:blur(1px);
    background:linear-gradient(180deg, transparent, #fff 16%, #fff 84%, transparent);
    box-shadow:0 0 26px var(--lb-acc), 0 0 70px var(--lb-a70), 0 0 140px var(--lb-a45);
    animation:lbSeam .66s cubic-bezier(.2,.85,.25,1) forwards;
  }
  @keyframes lbSeam{
    0%{opacity:0; transform:scale(.15,.14);}
    16%{opacity:1; transform:scale(1.15,.72);}
    46%{opacity:1; transform:scale(.5,1.3);}
    100%{opacity:0; transform:scale(3.2,2.2);}
  }
  /* the sideways-expanding flare that sells the prising-open */
  #lootOverlay .lb-ripflash{
    position:absolute; left:50%; top:50%; width:340px; height:490px; margin:-245px 0 0 -170px;
    border-radius:50%; z-index:16; pointer-events:none; opacity:0;
    background:radial-gradient(ellipse at 50% 50%, #fff 0%, var(--lb-a70) 32%, transparent 72%);
    animation:lbRip .55s cubic-bezier(.15,.8,.3,1) forwards;
  }
  @keyframes lbRip{
    0%{opacity:0; transform:scale(.04,1);}
    20%{opacity:1; transform:scale(.5,1.04);}
    100%{opacity:0; transform:scale(1.6,1.16);}
  }

  /* ---- per-tier identity ----
     One tier class on #lootOverlay drives every per-tier look by descendant
     selector — no per-element tagging, no JS branching on colour. The BOX SHELL
     is design-driven instead (below); what stays keyed to the tier is everything
     the box is NOT: the portal rim, the reel cards and the prize card. */
  #lootOverlay.lb-legendary .lb-core{background:radial-gradient(circle, var(--lb-acc2) 0%, var(--lb-acc) 34%, transparent 68%);}
  #lootOverlay.lb-mythic    .lb-core{background:radial-gradient(circle, var(--lb-acc) 0%, var(--lb-acc2) 44%, transparent 70%);}
  #lootOverlay.lb-rare .lb-rim{border-width:2px;}
  #lootOverlay.lb-legendary .lb-rim{
    border:3px solid var(--lb-acc);
    box-shadow:0 0 34px var(--lb-acc), 0 0 96px var(--lb-b45),
               inset 0 0 70px var(--lb-a45), inset 0 0 0 1px var(--lb-b70);
  }
  #lootOverlay.lb-legendary .lb-rim2{filter:blur(1px); opacity:1;}
  #lootOverlay.lb-legendary .lb-port{box-shadow:inset 0 0 90px rgba(0,0,0,.95), inset 0 0 46px var(--lb-a45);}
  #lootOverlay.lb-mythic .lb-rim{
    border:2px solid var(--lb-acc);
    box-shadow:0 0 34px var(--lb-acc), 0 0 110px var(--lb-b70), inset 0 0 64px var(--lb-a45);
  }
  #lootOverlay.lb-mythic .lb-rim3{
    background:conic-gradient(from 180deg, transparent 0deg, var(--lb-b70) 40deg, transparent 90deg,
      transparent 180deg, var(--lb-b70) 220deg, transparent 268deg, transparent 360deg);
    opacity:.95;
  }
  #lootOverlay.lb-mythic .lb-port{box-shadow:inset 0 0 90px rgba(0,0,0,.96), inset 0 0 52px var(--lb-b45);}
  #lootOverlay.lb-legendary .lb-card,#lootOverlay.lb-legendary .lb-bcard{
    background:linear-gradient(180deg, rgba(40,29,9,.92), rgba(10,7,3,.93));
  }
  #lootOverlay.lb-mythic .lb-card,#lootOverlay.lb-mythic .lb-bcard{
    background:linear-gradient(180deg, rgba(27,10,32,.92), rgba(7,3,11,.95));
  }
  #lootOverlay.lb-mythic .lb-bcard{
    box-shadow:0 0 0 1px var(--lb-a22), 0 0 60px var(--lb-a45), 0 0 110px var(--lb-b45), var(--lb-e3);
  }
  #lootOverlay.lb-legendary .lb-prize{
    box-shadow:0 0 0 1px var(--lb-b22), 0 0 70px var(--lb-a45), 0 0 130px rgba(212,160,23,.22),
               var(--lb-e3), inset 0 1px 0 rgba(255,223,138,.16);
  }
  #lootOverlay.lb-legendary .lb-ptier{
    color:#ffe6a8; border-color:var(--lb-acc);
    background:linear-gradient(90deg, rgba(212,160,23,.20), rgba(255,223,138,.08));
  }
  #lootOverlay.lb-legendary .lb-pemo{filter:drop-shadow(0 0 16px var(--lb-a70)) drop-shadow(0 0 34px var(--lb-b45));}
  #lootOverlay.lb-mythic .lb-prize{
    box-shadow:0 0 0 1px var(--lb-b45), 0 0 70px var(--lb-a45), 0 0 130px var(--lb-b45),
               var(--lb-e3), inset 0 1px 0 rgba(255,255,255,.08);
  }
  #lootOverlay.lb-mythic .lb-prize::after{
    background:radial-gradient(circle, var(--lb-b45) 0%, var(--lb-a22) 42%, transparent 72%);
  }
  #lootOverlay.lb-mythic .lb-ptier{
    color:#ffd3dd; border-color:var(--lb-acc);
    background:linear-gradient(90deg, rgba(224,20,60,.22), rgba(147,51,234,.24));
  }
  #lootOverlay.lb-mythic .lb-pemo{filter:drop-shadow(0 0 16px var(--lb-a70)) drop-shadow(0 0 36px var(--lb-b70));}
  #lootOverlay.lb-mythic .lb-prule{
    background:linear-gradient(90deg, transparent, var(--lb-acc) 35%, var(--lb-acc2) 65%, transparent);
  }

  /* ==================== THE THREE BOX DESIGNS ====================
     One per tier, each carried through idle -> charge -> shrapnel. This section
     is LAST in the sheet on purpose: each design's rules deliberately outrank
     the generic `.lb-face.lb-blow` fallback of equal specificity by source
     order, so the debris still reads as the box that broke rather than as six
     identical glowing rectangles. ==================== */

  /* ---------- RARE — "Steel Case": brushed service case, rivets, blue seam ---------- */
  #lootOverlay.lb-d-steel .lb-face{
    background:
      repeating-linear-gradient(90deg, rgba(255,255,255,.05) 0 1px, transparent 1px 4px),
      linear-gradient(155deg,#333d52 0%,#1e2637 54%,#141b29 100%);
    border:1px solid #4d5871;
    /* INSET only. var(--lb-e1) is an outward drop-shadow, and on a 3D shell each
       REAR face's halo spills past the near face's silhouette with nothing to
       occlude it — that spill is what reads as gaps around the edges. The faces
       themselves were never transparent: force them solid and the cube renders
       perfectly closed. */
    box-shadow:inset 0 1px 0 rgba(255,255,255,.18), inset 0 0 26px rgba(0,0,0,.68);
  }
  #lootOverlay.lb-d-steel .lb-face::before{        /* inner panel + corner rivets */
    content:''; position:absolute; inset:6px; border-radius:5px; pointer-events:none;
    border:1px solid rgba(255,255,255,.07);
    background:
      radial-gradient(circle 2.4px at 7px 7px, rgba(255,255,255,.55), transparent),
      radial-gradient(circle 2.4px at calc(100% - 7px) 7px, rgba(255,255,255,.55), transparent),
      radial-gradient(circle 2.4px at 7px calc(100% - 7px), rgba(255,255,255,.55), transparent),
      radial-gradient(circle 2.4px at calc(100% - 7px) calc(100% - 7px), rgba(255,255,255,.55), transparent);
  }
  #lootOverlay.lb-d-steel .lb-face .lb-fseam{
    left:0; right:0; height:2px; margin-top:-1px;
    background:linear-gradient(90deg, transparent, var(--lb-acc) 20%, var(--lb-acc2) 50%, var(--lb-acc) 80%, transparent);
    box-shadow:0 0 12px var(--lb-a70), 0 0 26px var(--lb-a45);
  }
  #lootOverlay.lb-d-steel .lb-face .lb-rune{font-size:26px; color:var(--lb-acc2);}
  #lootOverlay.lb-d-steel .lb-face.lb-blow{       /* white-hot plate, striations survive */
    background:
      repeating-linear-gradient(90deg, rgba(255,255,255,.55) 0 1px, transparent 1px 4px),
      linear-gradient(150deg,#fff 0%, var(--lb-acc2) 34%, #63739a 66%, #10182a 100%);
    border-color:#fff;
    box-shadow:0 0 28px var(--lb-acc), 0 0 72px var(--lb-a70), inset 0 0 24px rgba(255,255,255,.55);
  }
  #lootOverlay.lb-d-steel .lb-face.lb-blow::before{opacity:1; border-color:rgba(255,255,255,.6);}

  /* ---------- LEGENDARY — "Royal Vault": bolted door, gold locking wheel ---------- */
  #lootOverlay.lb-d-vault .lb-face{
    border-radius:2px;
    border:2px solid #8a6412;
    background:
      linear-gradient(180deg, rgba(255,223,138,.10) 0 50%, rgba(0,0,0,.18) 50% 100%),
      linear-gradient(150deg,#241a08 0%,#120c03 60%,#0a0702 100%);
    box-shadow:inset 0 0 0 3px rgba(255,223,138,.10), inset 0 0 28px rgba(0,0,0,.86),
               0 0 20px rgba(212,160,23,.2);
  }
  #lootOverlay.lb-d-vault .lb-face::before{       /* the locking wheel, always turning */
    content:''; position:absolute; left:50%; top:50%; width:64%; height:64%;
    margin:-32% 0 0 -32%; border-radius:50%; pointer-events:none;
    border:2px solid var(--lb-acc);
    background:
      conic-gradient(from 0deg,
        var(--lb-b70) 0 5deg, transparent 5deg 85deg,
        var(--lb-b70) 85deg 95deg, transparent 95deg 175deg,
        var(--lb-b70) 175deg 185deg, transparent 185deg 265deg,
        var(--lb-b70) 265deg 275deg, transparent 275deg 355deg,
        var(--lb-b70) 355deg 360deg),
      radial-gradient(circle, rgba(255,223,138,.20) 0 27%, transparent 29%);
    box-shadow:0 0 14px rgba(212,160,23,.45), inset 0 0 14px rgba(0,0,0,.75);
    animation:lbVaultSpin 15s linear infinite;
  }
  @keyframes lbVaultSpin{from{transform:rotate(0deg);} to{transform:rotate(360deg);}}
  #lootOverlay.lb-d-vault .lb-cube.lb-hot .lb-face::before{animation-duration:1.6s;}
  #lootOverlay.lb-d-vault .lb-face::after{        /* bolted door frame */
    content:''; position:absolute; inset:5px; border-radius:3px; pointer-events:none;
    border:1px solid rgba(212,160,23,.5);
    background:
      radial-gradient(circle 2.6px at 9px 9px, var(--lb-b70), transparent 62%),
      radial-gradient(circle 2.6px at calc(100% - 9px) 9px, var(--lb-b70), transparent 62%),
      radial-gradient(circle 2.6px at 9px calc(100% - 9px), var(--lb-b70), transparent 62%),
      radial-gradient(circle 2.6px at calc(100% - 9px) calc(100% - 9px), var(--lb-b70), transparent 62%),
      radial-gradient(circle 2.2px at 50% 7px, var(--lb-b45), transparent 62%),
      radial-gradient(circle 2.2px at 50% calc(100% - 7px), var(--lb-b45), transparent 62%),
      radial-gradient(circle 2.2px at 7px 50%, var(--lb-b45), transparent 62%),
      radial-gradient(circle 2.2px at calc(100% - 7px) 50%, var(--lb-b45), transparent 62%);
  }
  #lootOverlay.lb-d-vault .lb-face .lb-fseam{     /* the door split */
    left:-2px; right:-2px; height:3px; margin-top:-1.5px;
    background:linear-gradient(180deg,#000,#6b4d10 38%,#ffe6a8 52%,#000);
    box-shadow:0 0 10px rgba(212,160,23,.5);
  }
  #lootOverlay.lb-d-vault .lb-face .lb-rune{font-size:15px; color:#ffe6a8; opacity:.5;}
  #lootOverlay.lb-d-vault .lb-face.lb-blow{       /* torn door plate, bolts and wheel arc lit */
    background:linear-gradient(150deg,#fff 0%, #ffe6a8 28%, var(--lb-acc) 60%, #241a08 100%);
    border-color:#fff;
    box-shadow:0 0 30px var(--lb-acc), 0 0 78px var(--lb-a70), inset 0 0 0 3px rgba(255,255,255,.4);
  }
  #lootOverlay.lb-d-vault .lb-face.lb-blow::before{opacity:1; animation:none; border-color:#fff;}
  #lootOverlay.lb-d-vault .lb-face.lb-blow::after{opacity:1; border-color:#fff;}

  /* ---------- MYTHIC — "Void Heart": solid octahedron, red/purple plasma ----------
     Diamond-clipped faces turn the cube into an octahedron. The facets are SOLID
     plasma: an opaque red->violet body with off-centre marbling veils and a
     molten bloom at the middle. There is deliberately NO opening anywhere in
     this design — the body layer is fully opaque and every layer painted over it
     either lightens (mix-blend-mode:screen) or is a transparent-to-light
     gradient, so nothing — not stacked facets, not the blur, not the charge
     brightening — can produce a dark eye at a centre. Rule for anyone editing
     this: never introduce a stop darker than the body gradient, and never place
     one at 50% 50%. */
  #lootOverlay.lb-d-void .lb-face{
    clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);
    border:none; border-radius:0;
    background:
      /* marbling — three veils, none concentric with the facet, so the surface
         reads as swirled plasma rather than as a target */
      radial-gradient(ellipse 70% 56% at 31% 27%, rgba(255,104,150,.52) 0%, rgba(224,20,60,.40) 44%, transparent 76%),
      radial-gradient(ellipse 66% 58% at 75% 73%, rgba(158,72,250,.54) 0%, rgba(147,51,234,.34) 46%, transparent 78%),
      radial-gradient(ellipse 50% 42% at 63% 31%, rgba(255,206,228,.22), transparent 70%),
      /* centre kept BRIGHTER than the body, never darker */
      radial-gradient(circle at 50% 50%, rgba(255,178,208,.26) 0%, rgba(224,20,60,.16) 32%, transparent 64%),
      /* the solid body itself — opaque, so a facet behind can never read
         through as a hole */
      linear-gradient(152deg,#b41d4c 0%,#801a55 33%,#52156c 65%,#2f0e47 100%);
    box-shadow:none;
    filter:drop-shadow(0 0 12px var(--lb-a70)) drop-shadow(0 0 28px var(--lb-b70));
  }
  #lootOverlay.lb-d-void .lb-face::before{        /* plasma arcs raking the facet */
    content:''; position:absolute; inset:0; clip-path:inherit; pointer-events:none;
    background:conic-gradient(from 0deg,
      transparent 0 16deg, var(--lb-acc) 30deg, transparent 46deg,
      transparent 118deg, var(--lb-acc2) 150deg, transparent 178deg,
      transparent 248deg, var(--lb-acc) 276deg, transparent 302deg);
    filter:blur(1.6px); opacity:.9; mix-blend-mode:screen;
    animation:lbVoidArc 3.6s linear infinite;
  }
  @keyframes lbVoidArc{from{transform:rotate(0deg);} to{transform:rotate(360deg);}}
  #lootOverlay.lb-d-void .lb-cube.lb-hot .lb-face::before{animation-duration:.6s;}
  /* The molten heart. An additive bloom where a dark void would have been:
     heavily blurred and screen-blended so that on the assembled octahedron —
     where three or four facets are visible at once — it reads as one glowing
     heart under the surface instead of a row of bright discs. */
  #lootOverlay.lb-d-void .lb-face::after{
    content:''; position:absolute; left:50%; top:50%; width:58%; height:58%;
    margin:-29% 0 0 -29%; border-radius:50%; pointer-events:none;
    background:radial-gradient(circle, rgba(255,226,240,.52) 0%, rgba(255,96,152,.42) 34%,
               rgba(172,64,238,.26) 62%, transparent 84%);
    filter:blur(7px); mix-blend-mode:screen;
    animation:lbHeartBloom 2.5s ease-in-out infinite;
  }
  @keyframes lbHeartBloom{0%,100%{transform:scale(.88); opacity:.72;} 50%{transform:scale(1.14); opacity:1;}}
  #lootOverlay.lb-d-void .lb-cube.lb-hot .lb-face::after{animation-duration:.5s;}
  /* ===== the shell's own open corners =====
     Diamond-clipping the six cube faces gives the octahedral silhouette, but
     what it actually builds is a CUBOCTAHEDRON with its 8 triangular faces
     missing: at most spin angles you could see straight THROUGH the shell to the
     black backdrop, and the gaps read as exactly the kind of perforation this
     design is meant not to have. So the hull gets closed.

     THE FIX — the 8 missing triangles, one per free pseudo on the f1..f4
     wrappers, no extra markup (buildLootCube is shared). Each child transform is
       <undo parent> rotateY(a) rotateX(b) translateZ(98.15px) translateY(±17.9px)
     where a = atan2(sx,sz) and b = ∓35.264° (= atan(1/√2)) put the plane's normal
     on the cube diagonal (sx,sy,sz), and 98.15px (= 170/√3) is that plane's
     distance from the centre. The triangle's corners are the three cube
     edge-midpoints around that corner, which project into the plane's own axes
     at (±60.1, 34.7) and (0, -69.4) — so it is NOT centred on the plane's
     centre, hence the final in-plane translateY.
     ::before are the +y corners (tip up), ::after the -y corners (tip down).

     The boxes are sized to the triangle's own bounding box (130 x 112.5, +8% for
     seam overlap) rather than left at the wrapper's 170x170, and that is
     load-bearing, not tidiness: Chromium sorts a preserve-3d scene by each
     element's FULL quad, not by its clipped shape, so oversized boxes made back
     facets paint over front ones and the box rendered as a jumble of
     intersecting panels. Same reason there is no inner core here: any plane
     through the centre interpenetrates the others and sorts unpredictably. The
     hull is convex and closed, so it doesn't need one. */
  #lootOverlay.lb-d-void .lb-fw.lb-f1::before,#lootOverlay.lb-d-void .lb-fw.lb-f1::after,
  #lootOverlay.lb-d-void .lb-fw.lb-f2::before,#lootOverlay.lb-d-void .lb-fw.lb-f2::after,
  #lootOverlay.lb-d-void .lb-fw.lb-f3::before,#lootOverlay.lb-d-void .lb-fw.lb-f3::after,
  #lootOverlay.lb-d-void .lb-fw.lb-f4::before,#lootOverlay.lb-d-void .lb-fw.lb-f4::after{
    content:''; position:absolute; left:50%; top:50%;
    width:124px; height:107.2px; margin:-53.6px 0 0 -62px;
    pointer-events:none; transition:opacity .12s linear;
    background:
      radial-gradient(ellipse 68% 56% at 35% 31%, rgba(240,92,142,.42), transparent 74%),
      radial-gradient(ellipse 64% 58% at 73% 71%, rgba(150,66,242,.42), transparent 76%),
      linear-gradient(152deg,#951a43 0%,#6c1857 40%,#44125f 72%,#2b0d41 100%);
  }
  #lootOverlay.lb-d-void .lb-fw.lb-f1::before,#lootOverlay.lb-d-void .lb-fw.lb-f2::before,
  #lootOverlay.lb-d-void .lb-fw.lb-f3::before,#lootOverlay.lb-d-void .lb-fw.lb-f4::before{clip-path:polygon(50% 0,100% 100%,0 100%);}
  #lootOverlay.lb-d-void .lb-fw.lb-f1::after,#lootOverlay.lb-d-void .lb-fw.lb-f2::after,
  #lootOverlay.lb-d-void .lb-fw.lb-f3::after,#lootOverlay.lb-d-void .lb-fw.lb-f4::after{clip-path:polygon(50% 100%,100% 0,0 0);}
  #lootOverlay.lb-d-void .lb-fw.lb-f1::before{transform:translateZ(-85px) rotateY(45deg)   rotateX(-35.264deg) translateZ(98.15px) translateY(-17.9px);}
  #lootOverlay.lb-d-void .lb-fw.lb-f1::after {transform:translateZ(-85px) rotateY(45deg)   rotateX(35.264deg)  translateZ(98.15px) translateY(17.9px);}
  #lootOverlay.lb-d-void .lb-fw.lb-f2::before{transform:translateZ(-85px) rotateY(45deg)   rotateX(-35.264deg) translateZ(98.15px) translateY(-17.9px);}
  #lootOverlay.lb-d-void .lb-fw.lb-f2::after {transform:translateZ(-85px) rotateY(45deg)   rotateX(35.264deg)  translateZ(98.15px) translateY(17.9px);}
  #lootOverlay.lb-d-void .lb-fw.lb-f3::before{transform:translateZ(-85px) rotateY(-135deg) rotateX(-35.264deg) translateZ(98.15px) translateY(-17.9px);}
  #lootOverlay.lb-d-void .lb-fw.lb-f3::after {transform:translateZ(-85px) rotateY(-135deg) rotateX(35.264deg)  translateZ(98.15px) translateY(17.9px);}
  #lootOverlay.lb-d-void .lb-fw.lb-f4::before{transform:translateZ(-85px) rotateY(225deg)  rotateX(-35.264deg) translateZ(98.15px) translateY(-17.9px);}
  #lootOverlay.lb-d-void .lb-fw.lb-f4::after {transform:translateZ(-85px) rotateY(225deg)  rotateX(35.264deg)  translateZ(98.15px) translateY(17.9px);}
  /* ...and the whole solid leaves with the shell: once the faces blow there is
     no shell left for an interior to be inside of. */
  #lootOverlay.lb-d-void .lb-cube:has(.lb-face.lb-blow) .lb-fw::before,
  #lootOverlay.lb-d-void .lb-cube:has(.lb-face.lb-blow) .lb-fw::after{opacity:0;}
  #lootOverlay.lb-d-void .lb-face .lb-fseam{
    left:12%; right:12%; height:1px; opacity:.6;
    background:linear-gradient(90deg, transparent, var(--lb-acc2), transparent);
    box-shadow:0 0 10px var(--lb-b70);
  }
  #lootOverlay.lb-d-void .lb-face .lb-rune{font-size:17px; color:#fff; opacity:.55;}
  #lootOverlay.lb-d-void .lb-face.lb-blow{        /* diamond splinters — solid, white-hot */
    background:
      radial-gradient(ellipse 72% 58% at 33% 27%, #fff 0%, rgba(255,166,204,.85) 40%, transparent 74%),
      linear-gradient(152deg,#ffffff 0%, #ffa8cc 22%, var(--lb-acc) 50%, var(--lb-acc2) 78%, #7d219f 100%);
    filter:drop-shadow(0 0 16px var(--lb-acc)) drop-shadow(0 0 40px var(--lb-b70));
    box-shadow:none;
  }
  #lootOverlay.lb-d-void .lb-face.lb-blow::before{opacity:1; animation:none;}
  #lootOverlay.lb-d-void .lb-face.lb-blow::after{ /* the heart survives the break as a hot spot */
    opacity:1; animation:none; filter:blur(4px);
    width:52%; height:52%; margin:-26% 0 0 -26%;
    background:radial-gradient(circle, #fff 0 30%, rgba(255,154,200,.78) 62%, transparent 84%);
  }

  /* Reduced motion: playLootReel skips the charge and the reel entirely and
     presents the result card directly (see the JS) — this stills what is left. */
  @media (prefers-reduced-motion: reduce){
    #lootOverlay.show, #lootOverlay .lb-prize, #lootOverlay .lb-prize::after,
    #lootOverlay .lb-pemo, #lootOverlay .lb-taphint, #lootOverlay .lb-chip{animation:none;}
    #lootOverlay .lb-taphint{opacity:.6;}
    #lootOverlay .lb-shine, #lootOverlay .lb-rays{display:none;}
  }

/* ---- Weekly spin ceremony (v2.2; rebuilt as the CS:GO-style roulette, 1.6 2026-09-05) ----
   Geometry is shared with WS in app.js: .ws-sq is 76px with an 8px gap (STEP 84).
   Solid tier fills, a centre marker with triangles, no @keyframes — the reel
   moves on an inline transform transition, exactly like the loot reel. */
#weekSpinOverlay .ws-card{max-width:420px; text-align:center;}
.ws-title{font-family:'Cinzel',serif; font-size:20px; letter-spacing:.24em; color:var(--gold); margin-bottom:4px;}
.ws-sub{font-size:12px; color:var(--muted); margin-bottom:6px;}
.ws-caption{font:700 11px 'Sora',sans-serif; letter-spacing:.18em; text-transform:uppercase; color:var(--muted); margin:0 0 10px;}
.ws-port{position:relative; overflow:hidden; height:96px; border:1px solid var(--line2); border-radius:10px; background:var(--panel); cursor:pointer;}
.ws-reel{display:flex; gap:8px; height:100%; align-items:center; will-change:transform;}
.ws-sq{flex:0 0 76px; height:76px; display:flex; align-items:center; justify-content:center; border-radius:6px; font:800 22px 'Sora',sans-serif; color:#fff; letter-spacing:-.02em;}
.ws-sq.ws-rare, .ws-dot.ws-rare{background:#2563eb;}
.ws-sq.ws-legendary, .ws-dot.ws-legendary{background:#d4a017; color:#1a1200;}
.ws-sq.ws-mythic, .ws-dot.ws-mythic{background:linear-gradient(135deg,#e0143c,#a855f7);}
.ws-marker{position:absolute; left:50%; top:0; bottom:0; width:2px; margin-left:-1px; background:rgba(255,255,255,.9); box-shadow:0 0 8px rgba(255,255,255,.5); pointer-events:none;}
.ws-tri{position:absolute; left:50%; margin-left:-7px; width:0; height:0; border-left:7px solid transparent; border-right:7px solid transparent;}
.ws-tri-top{top:0; border-top:9px solid var(--gold);}
.ws-tri-bot{bottom:0; border-bottom:9px solid var(--gold);}
.ws-result{min-height:26px; margin:14px 0 6px; font-size:15px; color:var(--text); opacity:0; transform:translateY(6px); transition:opacity .4s, transform .4s;}
.ws-result.on{opacity:1; transform:none;}
.ws-result b{color:var(--gold);}
.ws-last{margin-top:10px;}
.ws-last-title{font-size:9.5px; letter-spacing:.2em; color:var(--muted); margin-bottom:5px;}
.ws-last-row{display:flex; gap:5px; justify-content:center;}
.ws-dot{display:inline-block; width:16px; height:16px; border-radius:3px; flex:0 0 16px; vertical-align:middle;}
.ws-dot.ws-empty{border:1px dashed var(--line2); box-sizing:border-box;}
/* a scoreboard week that spun carries its tier square and replays on tap */
.sb-row[data-spin]{cursor:pointer;}
.sb-row .ws-dot{width:12px; height:12px; flex:0 0 12px; margin-right:6px;}

/* ---- 2.19: emergency day from Status (always shown; Today + Yesterday since 2026-09-05) ---- */
.status-emergency{margin-top:6px;}
.se-head{font-size:12px; font-weight:600; color:var(--text);}
.se-hint{font-size:10.5px; color:var(--muted); margin:4px 0 8px; line-height:1.4;}
.se-pick{display:flex; gap:8px;}
.se-pick button{
  flex:1; min-width:0; background:transparent; border:1px solid rgba(239,68,68,.4); border-radius:9px;
  color:var(--loss); font:600 12px 'Sora',sans-serif; padding:9px 6px; cursor:pointer; overflow-wrap:anywhere;
}
.se-pick button.armed{background:rgba(239,68,68,.14);}
.se-pick button.se-attn{box-shadow:0 0 0 1px rgba(239,68,68,.5) inset;}
.se-pick button:disabled{opacity:.45; cursor:default;}

/* ---- 2.9 level card + 2.6 version history ---- */
.lvl-ring{cursor:pointer;}
.li-big{font-family:'Cinzel',serif; font-size:34px; color:var(--gold); text-align:center; margin:6px 0 2px;}
.li-sub{text-align:center; font-size:12px; color:var(--muted); margin-bottom:10px;}
.li-bar{height:8px; border-radius:4px; background:var(--panel3); overflow:hidden; margin:0 8px 6px;}
.li-bar i{display:block; height:100%; background:linear-gradient(90deg,#a16207,#d4a017); border-radius:4px;}
.li-line{display:flex; justify-content:space-between; font-size:12px; color:var(--text); padding:5px 8px; border-bottom:1px solid var(--line);}
.li-line:last-child{border-bottom:none;}
.li-line .d{color:var(--muted);}
.li-note{font-size:10.5px; color:var(--muted); text-align:center; margin-top:8px;}
.version-line{text-align:center; font-size:11px; color:var(--muted); margin-top:10px; cursor:pointer;
  text-decoration:underline dotted; text-underline-offset:3px;}
/* 2.6 (2026-09-05): collapsible release headers over What's New-style bullet lists */
#versionOverlay .rank-info-card{max-width:420px;}
.vh-rel{border-bottom:1px solid var(--line);}
.vh-rel:last-child{border-bottom:none;}
.vh-head{width:100%; display:flex; align-items:center; gap:8px; background:transparent; border:none; padding:10px 4px;
  color:inherit; font-family:inherit; cursor:pointer; text-align:left;}
.vh-v{font-weight:800; font-size:12px; color:var(--gold); background:rgba(212,160,23,.14); border:1px solid rgba(212,160,23,.4);
  border-radius:999px; padding:1px 8px; white-space:nowrap;}
.vh-d{font-size:10.5px; color:var(--muted); white-space:nowrap;}
.vh-cur{font-size:9.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--gold); white-space:nowrap;}
.vh-tag{flex:1; font-size:12px; color:var(--text); min-width:0;}
.vh-arrow{font-size:12px; color:var(--muted); transition:transform .2s;}
.vh-rel.open .vh-arrow{transform:rotate(180deg);}
.vh-list{margin:0 0 6px; padding:0 2px;}
.vh-list li{font-size:12.5px; padding:7px 0;}
@media (prefers-reduced-motion: reduce){ .vh-arrow{transition:none;} }

/* ---- 2.18 post-rankup diff ---- */
.rc-row{display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:8px 6px; border-bottom:1px solid var(--line); font-size:12.5px;}
.rc-row:last-child{border-bottom:none;}
.rc-row .lbl{color:var(--muted);}
.rc-row .vals{white-space:nowrap;}
.rc-row .old{color:var(--muted); text-decoration:line-through; margin-right:7px;}
.rc-row .new{color:var(--gold); font-weight:800;}
.rc-row.rc-same .new{color:var(--text); font-weight:600;}
.rc-note{font-size:10.5px; color:var(--muted); text-align:center; margin-top:8px;}
.rc-continue{margin-top:8px; background:var(--panel3); color:var(--text);}

/* ---- 3.3 week-over-week + 3.4 scoreboard ---- */
.wow-card{margin-top:14px; padding:10px 12px; border:1px solid var(--line); border-radius:10px; background:var(--panel2);}
.wow-line{display:flex; justify-content:space-between; font-size:12px; color:var(--muted); padding:2px 0;}
.wow-line b{color:var(--text);}
.wow-verdict{font-size:12px; margin-top:6px; text-align:center; color:var(--muted);}
.wow-card.ahead .wow-verdict b{color:var(--gain);}
.wow-card.behind .wow-verdict b{color:var(--loss);}
.sb-card{margin-top:12px; padding:10px 12px; border:1px solid var(--line); border-radius:10px; background:var(--panel2);}
.sb-title{font-size:10.5px; font-weight:800; letter-spacing:.2em; text-transform:uppercase; color:var(--muted); margin-bottom:6px;}
.sb-row{display:flex; align-items:center; gap:8px; padding:4px 0; font-size:12px; border-bottom:1px solid var(--line);}
.sb-row:last-child{border-bottom:none;}
.sb-rank{width:26px; text-align:center;}
.sb-week{flex:1; color:var(--muted);}
.sb-medal .sb-week{color:var(--text);}
.sb-xp{color:var(--gold);}
.sb-now{opacity:.85;}
.sb-now .sb-week{font-style:italic;}

/* ---- 3.2 calendar Elo fold ---- */
.cal-elo{margin-top:12px;}
.cal-elo-toggle{background:transparent; border:1px solid var(--line); border-radius:8px;
  color:var(--muted); font:600 11px 'Sora',sans-serif; padding:7px 12px; cursor:pointer; width:100%;}
.cal-elo-host{margin-top:8px; overflow-x:auto;}
.wow-sub{font-size:10px; color:var(--muted); text-align:center; margin-top:2px; letter-spacing:.04em;}

/* ---- 3.3 (2026-09-05, Tin): the week race. Cars are placed by a transform attribute
   written from JS (trackPoint), never by CSS. Lap colour = the lap the car is on;
   this week is the rank colour with a white stroke and an opacity-only ring pulse
   (raceRingPulse — `ra` prefix, so AURA-3 audits it). Focus On 1's dash flow and
   glow are deliberately NOT ported: they animate stroke-dashoffset and filter. ---- */
.week-race{margin-top:12px; padding:10px 12px; border:1px solid var(--line); border-radius:10px; background:var(--panel2);}
.race-head{display:flex; align-items:center; justify-content:space-between; gap:8px;}
.race-head .sb-title{margin-bottom:0; min-width:0;}
.race-mode{margin:0; display:flex; gap:4px; flex:0 0 auto;}
.race-mode button{flex:0 0 auto; padding:4px 10px; font-size:10.5px; letter-spacing:.06em;}
.race-svg{display:block; width:100%; height:auto; margin-top:6px;}
.race-bed{stroke:var(--panel3); stroke-width:18;}
.race-mid{stroke:var(--line2); stroke-width:1;}
.race-line{stroke:var(--muted); stroke-width:2;}
.car-body, .race-chip{fill:#46527f; background:#46527f;}
.lap-2 .car-body, .race-chip.lap-2{fill:#5aa9ff; background:#5aa9ff;}
.lap-3 .car-body, .race-chip.lap-3{fill:#ff5c5c; background:#ff5c5c;}
.lap-4 .car-body, .race-chip.lap-4{fill:#edf0fa; background:#edf0fa;}
.car .car-cabin{fill:#0d0b12;}
.car.car-current .car-body, .race-chip.current{fill:var(--rank); background:var(--rank); stroke:#fff; stroke-width:1.5px;}
.race-chip.current{box-shadow:0 0 0 1.5px #fff;}
.car.car-current .car-ring{stroke:#fff; stroke-width:1.2; animation:raceRingPulse 1.6s ease-in-out infinite alternate;}
@keyframes raceRingPulse{ from{opacity:.15;} to{opacity:.9;} }
.car-label{font-size:7px; fill:var(--muted); font-weight:700; transition:opacity .35s ease;}
.car-label.current{fill:var(--rank);}
.race-legend{display:flex; flex-wrap:wrap; gap:3px 14px; font-size:10.5px; color:var(--muted); margin-top:6px;}
.race-key{display:flex; align-items:center; gap:5px; white-space:nowrap;}
.race-key b{color:var(--text); font-weight:600;}
.race-chip{width:8px; height:8px; border-radius:2px; flex:0 0 auto; display:inline-block;}
.race-lapkey{display:flex; flex-wrap:wrap; gap:3px 12px; font-size:9px; color:var(--muted); margin-top:8px; text-transform:uppercase; letter-spacing:.5px;}
.race-lapkey span{display:flex; align-items:center; gap:4px;}
@media (prefers-reduced-motion: reduce){ .car.car-current .car-ring{animation:none; opacity:.6;} .car-label{transition:none;} }

/* ---- 1.4 Wall of Fails ---- */
.wall-count{font-size:11px; color:var(--muted); margin-bottom:10px;}
.wall-day{margin-bottom:14px;}
.wall-date{font-size:10.5px; font-weight:800; letter-spacing:.16em; color:var(--muted);
  border-bottom:1px solid var(--line); padding-bottom:4px; margin-bottom:6px;}
.wall-row{padding:6px 8px; border-left:3px solid rgba(239,68,68,.5); margin-bottom:6px;
  background:var(--panel2); border-radius:0 8px 8px 0; font-size:12.5px;}
.wall-row.geass{border-left-color:#9333ea;}
.wall-row.geass .wall-name{color:#c084fc;}
.wall-name{font-weight:600;}
.wall-charge{float:right; color:var(--loss); font-weight:700; font-size:12px;}
.wall-reason{font-size:11.5px; color:var(--muted); font-style:italic; margin-top:3px;}

/* ---- 4.3 Y axis + 4.4 rank shifts + 4.5 rules ---- */
.prog-grid{position:absolute; left:0; right:0; height:1px; background:rgba(255,255,255,.06); pointer-events:none;}
.prog-grid-lbl{position:absolute; right:2px; transform:translateY(-105%); font-size:8.5px; color:var(--muted); pointer-events:none; opacity:.8;}
/* Scroll mode: the axis numbers ride the VIEWPORT, not the content. Pinned to
   the plot's right edge they scroll away with the newest columns — measured
   1,242px outside the visible strip on a phone — so the labelled Y axis
   silently did nothing past 31 days. A zero-width sticky flex item holds the
   left edge of .prog-scroll at every scroll position and is the containing
   block for the labels inside it. At or below the threshold the labels stay
   exactly where they were (byte-identity contract). */
.prog-gridlbls{position:sticky; left:0; width:0; flex:0 0 0; align-self:stretch; z-index:4; pointer-events:none;}
.prog-gridlbls .prog-grid-lbl{left:1px; right:auto;}
.prog-rank.rank-shift{font-weight:900; text-decoration:underline; text-underline-offset:2px;}
.prog-rank.rank-shift.shift-up::before{content:'▲'; font-size:8px; margin-right:1px;}
.prog-rank.rank-shift.shift-down::before{content:'▼'; font-size:8px; margin-right:1px;}
.cstat-rules{flex:0 0 100%; order:10; padding:2px 0 4px 4px;}
.cstat-rule{font-size:10.5px; color:var(--muted); line-height:1.5;}
.cstat-row{flex-wrap:wrap;}

/* ---- 5.7 tier off toggle ---- */
.loot-tier-off{font:700 9px 'Sora',sans-serif; letter-spacing:.08em; padding:3px 8px;
  border-radius:6px; border:1px solid rgba(34,197,94,.5); color:var(--gain);
  background:transparent; cursor:pointer; margin-left:6px;}
.loot-tier-off.off{border-color:rgba(239,68,68,.5); color:var(--loss);}

/* ---- 5.5 eternal rarity: black-and-white, beyond mythic ---- */
.rar-eternal-grad{background:linear-gradient(90deg,#0a0a0a,#f8fafc 55%,#0a0a0a);
  -webkit-background-clip:text; background-clip:text; color:transparent; font-weight:800;}
.loot-item-rarity-pill.rar-eternal{color:#f8fafc; border-color:#f8fafc;
  background:linear-gradient(90deg,rgba(10,10,10,.9),rgba(90,90,95,.5));}
.rar-eternal{color:#f8fafc;}
#lootOverlay .lb-card.rar-eternal{border-color:#f8fafc;
  box-shadow:0 0 14px rgba(248,250,252,.35);}
#lootOverlay .lb-card.rar-eternal .lb-cr{background:linear-gradient(90deg,#0a0a0a,#f8fafc);}

/* ---- 5.8 tier showcase: bob + turn; silhouette until earned ---- */
.lb-showcase{display:flex; justify-content:center; align-items:center; height:74px;
  margin:6px 0 2px; position:relative;}
/* the bob lives on a wrapper holding BOTH the chest and the "?" — as a sibling
   the hint used to stand still while the silhouette moved under it (5.8) */
.lb-showcase-bob{display:inline-block; position:relative; transform-origin:50% 60%;
  animation:showcaseBob 5.5s ease-in-out infinite;}
.lb-showcase .lb-chest{display:inline-block;}
.lb-showcase.locked .lb-chest{filter:brightness(0) contrast(1); opacity:.85;}
.lb-showcase-hint{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
  font:800 18px 'Cinzel',serif; color:var(--muted);
  text-shadow:0 0 8px rgba(255,255,255,.25); pointer-events:none;}
@keyframes showcaseBob{
  0%,100%{ transform:translateY(0) rotate(-6deg); }
  25%   { transform:translateY(-5px) rotate(0deg); }
  50%   { transform:translateY(0) rotate(6deg); }
  75%   { transform:translateY(-5px) rotate(0deg); }
}
@media (prefers-reduced-motion: reduce){ .lb-showcase-bob{ animation:none; } }
