/* Modal shell plus the codex, monster shop, talent tree, forge, rebirth and
   result screens. */
.modal {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: center; justify-content: center;
  /* Padded by the safe area so the dialog clears a notch and a home indicator
     when the portal runs the game fullscreen on a phone. */
  padding: calc(6px + var(--sa-t)) calc(6px + var(--sa-r))
           calc(6px + var(--sa-b)) calc(6px + var(--sa-l));
  background: rgba(6, 4, 10, .72);
  backdrop-filter: blur(3px);
}
/* A backdrop blur is a full-screen read-back of whatever is behind it, redone
   every single frame — over a live WebGL canvas that is one of the most
   expensive things a phone can be asked to composite, and it buys nothing that
   a slightly darker scrim does not. Dropped on touch devices, where it was a
   measurable share of why the handset got hot with a panel open. */
@media (hover: none), (max-width: 760px) {
  .modal { backdrop-filter: none; background: rgba(6, 4, 10, .82); }
}
/* A locked dialog (the defeat screen) has no ✕ and ignores the backdrop, so the
   backdrop must not advertise itself as a way out. */
.modal.locked { cursor: default; background: rgba(6, 4, 10, .88); }
/* The dialog frame is `panel-frame`, 9-sliced. Its top slice is deep because the
   art carries a raised header tab above the frame line — the slice has to clear
   the whole tab or the corner filigree gets cut through the middle. That tab is
   what `.modal-head` sits in, which is why the head's padding is asymmetric.
   The scroll motif along each edge is the only part that stretches. */
.modal-box {
  width: min(880px, 94vw); max-height: 86vh; display: flex; flex-direction: column;
  background: linear-gradient(180deg, #4a4162, #241d33 18%, #17111f);
  background-clip: padding-box;
  border: 34px solid transparent;
  border-width: 92px 34px 36px;
  border-image: url('../assets/ui/panel-frame.webp') 170 60 62 60 fill / 92px 34px 36px / 0 stretch;
  border-radius: 0;
  box-shadow: none;
  filter: drop-shadow(0 14px 26px rgba(0,0,0,.75));
  animation: modalIn .18s ease-out;
}
@keyframes modalIn { from { opacity: 0; transform: translateY(18px) scale(.97); } }

/* The frame's crown is drawn in the top border, so the head starts inside the
   violet field just under it — no top padding of its own to add. */
.modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 4px 4px 12px; border-bottom: 3px solid rgba(0,0,0,.45);
}
.modal-head h2 {
  font-size: 22px; font-weight: 900; letter-spacing: .06em; color: var(--gold);
  text-shadow: 0 3px 0 #000, 2px 0 0 #000, -2px 0 0 #000, 0 -2px 0 #000;
}
.modal-close {
  width: 34px; height: 34px; font-size: 16px; font-weight: 900; cursor: pointer;
  font-family: var(--font); color: var(--text);
  background: linear-gradient(180deg, #4a4162, #241d33);
  border: 3px solid var(--edge); border-radius: 10px;
}
.modal-close:hover { filter: brightness(1.2); }
/* `touch-action` is the whole reason a phone can read these panels at all. The
   body sets `touch-action: none` so the canvas owns every gesture, and that
   disallows panning for descendants too — which silently made the Shop (3600px
   of content in a 530px window), the 50-tier Codex and the Forge unscrollable on
   touch. `pan-y` opts this one scroller back in; `overscroll-behavior: contain`
   stops a flick that reaches the end from bouncing the page behind it. */
.modal-body {
  padding: 14px 4px 4px;
  overflow-y: auto;
  touch-action: pan-y;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.modal-body::-webkit-scrollbar { width: 10px; }
.modal-body::-webkit-scrollbar-thumb { background: #4a4162; border-radius: 6px; border: 2px solid #17111f; }

.panel-note { color: var(--dim); font-size: 13px; line-height: 1.55; margin-bottom: 14px; }
.panel-note b { color: var(--gold); }

/* ------------------------------------------------------- panel explainer */
/* The collapsed rules block (see `explainer()` in ui/Panels.js). Closed, it is
   one line carrying the panel's live numbers; open, it is the paragraph it
   replaced. The summary row doubles as the panel's status strip, which is why
   it is styled as a plate rather than as a link. */
.panel-info { margin-bottom: 12px; }
.panel-info > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 9px 12px; cursor: pointer; list-style: none;
  font-size: 12.5px; font-weight: 700; color: var(--dim);
  background: linear-gradient(180deg, #332a48, #221b31);
  border: 3px solid var(--edge); border-radius: 12px;
}
.panel-info > summary::-webkit-details-marker { display: none; }
.panel-info > summary:hover { filter: brightness(1.12); }
.pi-sum b { color: var(--gold); font-weight: 900; }
.pi-more {
  flex: none; padding: 2px 9px 3px; border-radius: 7px;
  font-size: 10px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase;
  color: #cbbdf0; background: rgba(255,255,255,.07); border: 2px solid rgba(255,255,255,.09);
}
.panel-info[open] > summary { border-radius: 12px 12px 0 0; }
.panel-info[open] .pi-more::after { content: ' ▴'; }
.panel-info:not([open]) .pi-more::after { content: ' ▾'; }
.panel-info .panel-note {
  margin: 0; padding: 11px 12px;
  background: rgba(0,0,0,.28);
  border: 3px solid var(--edge); border-top: 0; border-radius: 0 0 12px 12px;
}

/* An actionable warning, not prose — the grid being full is the reason a buy
   button is dead, so it gets its own strip instead of a clause in a paragraph. */
.panel-warn {
  margin-bottom: 12px; padding: 9px 12px;
  font-size: 12px; font-weight: 700; color: #ffc0c8;
  background: rgba(255, 74, 106, .12);
  border: 3px solid rgba(255, 74, 106, .38); border-radius: 12px;
}

/* Sub-heading under a forge section head. */
.forge-sub { margin: -2px 0 9px; font-size: 11.5px; line-height: 1.45; color: var(--dim); }

/* ------------------------------------------------------------- codex */
.codex-bands { display: flex; flex-direction: column; gap: 14px; }
.band-head {
  display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px;
  font-size: 14px; font-weight: 900; letter-spacing: .05em;
}
.band-el { font-size: 11px; padding: 2px 8px; border-radius: 7px; border: 2px solid; }
.band-ability { font-size: 11.5px; color: var(--dim); font-weight: 700; letter-spacing: 0; }
/* `grid-auto-rows: 1fr` is what keeps a wrapped band tidy. A row of five cells
   whose contents differ in height — a discovered monster has a portrait, an
   undiscovered one has a stub — otherwise sizes each ROW to its own tallest
   cell, so on a phone (two columns, three rows per band) the last row came out
   visibly squat and its card art was stretched to a different aspect than the
   rows above it. Equal tracks make every cell in a band the same box. */
.codex-row { display: grid; grid-template-columns: repeat(5, 1fr); grid-auto-rows: 1fr; gap: 8px; }

/* The card slot art. `cell-monster` and `cell-locked` are one pair forced to the
   same 400×548 box by the cutout tool, so a discovered and an undiscovered cell
   sitting side by side put their recesses on the same line.
 *
 * Stretched rather than 9-sliced: the recess in the upper half is a fixed share
 * of the card, and a 9-slice would hold the frame steady while sliding that
 * recess around as the row height changes. The cells are near enough to the
 * art's own aspect that the stretch does not read. */
.codex-cell, .shop-cell {
  background: url('../assets/ui/cell-monster.webp') center / 100% 100% no-repeat;
  border: 0; border-radius: 0; box-shadow: none;
}
.codex-cell {
  position: relative; padding: 8px 6px 9px; text-align: center;
}
/* Undiscovered cells get the padlock plate rather than a dimmed copy of the
   unlocked one. The plate's own padlock ends up behind `.cc-portrait`, which
   is drawn over the recess — the portrait is the more useful of the two. So
   the plate's grey-violet cast does the work and the opacity keeps a run of
   49 locked cells from shouting over the one the player has actually
   reached. */
.codex-cell.locked {
  opacity: .6; filter: none;
  background-image: url('../assets/ui/cell-locked.webp');
}
/* Owned and live states used to be a border-colour swap; with the frame in the
   art they are a halo instead, which still reads at a glance in a grid of 100. */
.codex-cell.owned { box-shadow: 0 0 0 2px rgba(255,210,74,.75), 0 0 14px rgba(255,210,74,.35); }
.cc-tier { font-size: 11px; font-weight: 900; color: var(--dim); }
/* The portrait fills the card's recess where the 34px colour swatch used to
   sit. All fifty faces come from one atlas; the inline background-position
   picks the cell and background-size does the scaling. */
.cc-portrait, .sc-portrait {
  background: url('../assets/ui/monsters.webp') no-repeat;
  background-size: 1000% 500%;
  filter: drop-shadow(0 3px 3px rgba(0,0,0,.45));
}
.cc-portrait { width: 64px; height: 64px; margin: 3px auto 4px; }
.cc-name { font-size: 10.5px; font-weight: 900; line-height: 1.2; min-height: 26px; }
.cc-dps { margin-top: 3px; font-size: 10.5px; font-weight: 900; color: var(--gold); }

/* -------------------------------------------------------- talent tree */
.talent-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.talent {
  display: flex; gap: 12px; padding: 12px;
  background: linear-gradient(180deg, #332a48, #1d1729);
  border: 3px solid var(--edge); border-radius: 14px;
}
.talent-ico {
  width: 52px; height: 52px; flex: 0 0 52px; display: flex; align-items: center; justify-content: center;
  font-size: 26px; background: linear-gradient(180deg, #4a4162, #241d33);
  border: 3px solid var(--edge); border-radius: 12px;
}
.talent-main { flex: 1; min-width: 0; }
.talent-name { font-size: 14px; font-weight: 900; letter-spacing: .03em; }
.talent-lvl { font-size: 11px; font-weight: 900; color: var(--gold); }
.talent-desc { margin: 4px 0 7px; font-size: 11.5px; line-height: 1.45; color: var(--dim); }
.talent-bar { height: 8px; background: #141020; border: 2px solid var(--edge); border-radius: 6px; overflow: hidden; }
.talent-bar i { display: block; height: 100%; background: linear-gradient(180deg, #9dff96, var(--green-2)); }
.buy-btn {
  margin-top: 8px; width: 100%; padding: 8px; cursor: pointer;
  font-family: var(--font); font-size: 13px; font-weight: 900; letter-spacing: .04em; color: #2a1b06;
  background: linear-gradient(180deg, #fff4c2, #ffd24a 45%, #e8901c);
  border: 3px solid var(--edge); border-radius: 10px;
  box-shadow: inset 0 3px 0 rgba(255,255,255,.6), 0 4px 0 #8a5410;
}
/* Gem prices take the amethyst of the gem sprite rather than the old cyan, so
   the button and the currency it spends are visibly the same thing. */
.buy-btn.gem { background: linear-gradient(180deg, #f0c8ff, #c14ae8 45%, #7a1fa8); color: #2a0640; box-shadow: inset 0 3px 0 rgba(255,255,255,.55), 0 4px 0 #4d1170; }
.buy-btn:disabled { filter: grayscale(.7) brightness(.75); cursor: not-allowed; }
.buy-btn:active:not(:disabled) { transform: translateY(3px); box-shadow: inset 0 3px 0 rgba(255,255,255,.5), 0 1px 0 #8a5410; }
.buy-btn .cost-ico { width: 15px; height: 15px; vertical-align: -3px; }

/* Single buy plus MAX. The bulk button is the narrow one — it is the faster
   play deep in a run, but it should never be the one hit by accident. */
.buy-row { display: flex; gap: 6px; }
.buy-row .buy-btn { flex: 1; }
.buy-row .buy-btn.alt {
  flex: 0 0 62px; font-size: 12px; color: #ffe9b0;
  background: linear-gradient(180deg, #5a4a78, #34284c);
  box-shadow: inset 0 3px 0 rgba(255,255,255,.18), 0 4px 0 #1d1430;
}

/* ---------------------------------------------------------- dark forge */
.forge-head {
  margin: 20px 0 8px; font-size: 14px; letter-spacing: .09em; color: var(--gold);
  text-shadow: 0 2px 0 #000;
}
.forge-head.gem { color: #d98aff; }
.forge-head.warn { color: #ff9a6a; }
.talent-now {
  float: right; font-size: 11px; font-weight: 900; color: var(--green);
  padding: 1px 7px; background: rgba(125,255,138,.12); border-radius: 6px;
}

/* The Forge line — a compact row, not a talent card.
 *
 * Fourteen talent cards stacked two-across came to well over 1800px of content
 * inside a dialog capped at 86vh, so the panel was permanently a long scroll and
 * read as oversized whatever the screen. The row below is a single 52px band:
 * icon, name and current bonus on one line, the level bar with its count baked
 * in on the next, and the two buy buttons pinned to the right rather than
 * stacked underneath. The rules sentence moved to the row's `title`, which is
 * where it costs nothing. Same information, roughly a third of the height. */
/* `minmax(0, 1fr)`, not `1fr`. A grid track's automatic floor is its content's
   min-content width, and a `.fline` cannot go below its 34px icon plus its
   104px buy column plus the *untruncated* width of a nowrap name — which on
   three columns overflowed the dialog and put a horizontal scrollbar under it.
   A zero floor lets the track shrink and hands the overflow to the ellipsis
   that `.fl-name` already carries. */
.forge-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.fline {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  background: linear-gradient(180deg, #332a48, #1d1729);
  border: 3px solid var(--edge); border-radius: 12px;
}
.fline.maxed { opacity: .7; }
.fl-ico {
  width: 34px; height: 34px; flex: 0 0 34px;
  display: flex; align-items: center; justify-content: center; font-size: 18px;
  background: linear-gradient(180deg, #4a4162, #241d33);
  border: 2px solid var(--edge); border-radius: 9px;
}
.fl-main { flex: 1; min-width: 0; }
.fl-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
/* The name is the one part allowed to be clipped — the number beside it is what
   the player is reading, and a wrapped name would take the row to three lines. */
.fl-name {
  font-size: 12.5px; font-weight: 900; letter-spacing: .02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fl-now {
  flex: none; font-size: 11px; font-weight: 900; color: var(--green);
  padding: 1px 6px; background: rgba(125,255,138,.12); border-radius: 6px;
}
/* The level count rides inside the bar rather than above it — a `Lv 42/100`
   chip on its own line was a whole extra row of height per upgrade. */
.fl-bar {
  position: relative; height: 13px; margin-top: 5px; overflow: hidden;
  background: #141020; border: 2px solid var(--edge); border-radius: 6px;
}
.fl-bar i { display: block; height: 100%; background: linear-gradient(180deg, #9dff96, var(--green-2)); }
.fl-bar b {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 900; letter-spacing: .04em; color: #fff;
  text-shadow: 0 1px 0 #000, 0 0 3px #000;
}
.fl-buy { flex: 0 0 auto; display: flex; flex-direction: column; gap: 4px; width: 104px; }
.fl-buy .buy-btn { margin-top: 0; padding: 6px 4px; font-size: 11.5px; }
.fl-buy .buy-btn.alt {
  padding: 3px 4px; font-size: 10px; color: #ffe9b0;
  background: linear-gradient(180deg, #5a4a78, #34284c);
  box-shadow: inset 0 3px 0 rgba(255,255,255,.18), 0 3px 0 #1d1430;
}
/* Fourteen short rows want the width more than they want a wide dialog. */
@media (min-width: 1000px) {
  .modal[data-panel="forge"] .modal-box { width: min(1020px, 94vw); }
  .forge-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.panel-cta { display: flex; gap: 10px; margin-top: 18px; }
.panel-cta .mbtn { flex: 1; }

/* ------------------------------------------------------------ rebirth */
/* The four things a rebirth pays, stated before any prose. They are a flat
   promise now — +10% damage, +10% attack speed, one arena, gems for depth — so
   they get plates at the top of the panel rather than three rows buried in a
   comparison table the player has to read to work out what they are buying. */
.rebirth-rewards {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 14px;
}
.rw {
  padding: 11px 8px 10px; text-align: center;
  background: linear-gradient(180deg, #3a2447, #1e1430);
  border: 3px solid #6b3d7a; border-radius: 14px;
}
.rw-ico { font-size: 22px; line-height: 1.1; }
.rw-ico .cost-ico { width: 22px; height: 22px; vertical-align: -3px; }
.rw-big {
  margin-top: 3px; font-size: 18px; font-weight: 900; color: var(--gold);
  text-shadow: 0 2px 0 #000;
}
.rw-sub {
  margin-top: 2px; font-size: 10.5px; font-weight: 700; line-height: 1.3; color: var(--dim);
}

.rebirth-gate {
  display: flex; align-items: center; gap: 11px; padding: 12px 14px; margin-bottom: 14px;
  font-size: 13px; line-height: 1.5; color: var(--dim);
  background: linear-gradient(180deg, #2c2140, #1a1229);
  border: 3px solid var(--edge); border-radius: 14px;
}
.rebirth-gate.open {
  color: #ffd9e6;
  background: linear-gradient(180deg, #4a1f3a, #2a1024);
  border-color: #a33a68;
  box-shadow: 0 0 20px rgba(255,90,140,.28);
}
.rebirth-gate b { color: var(--gold); }
.rg-ico { font-size: 26px; }

.rebirth-table { width: 100%; border-collapse: collapse; margin-bottom: 14px; font-size: 13px; }
.rebirth-table th {
  padding: 7px 10px; text-align: left; font-size: 11px; letter-spacing: .07em;
  color: var(--dim); text-transform: uppercase; border-bottom: 2px solid var(--edge);
}
.rebirth-table td { padding: 7px 10px; border-bottom: 2px solid rgba(255,255,255,.05); }
.rebirth-table td:first-child { color: var(--dim); }
.rebirth-table td:nth-child(2) { color: #fff; font-weight: 800; }
.rebirth-table td.gain { color: var(--green); font-weight: 900; }

.rebirth-warn {
  padding: 10px 12px; margin-bottom: 14px; font-size: 12px; color: #ffb7a0;
  background: rgba(255,90,60,.09); border: 2px solid rgba(255,120,80,.3); border-radius: 11px;
}
.rebirth .mbtn { width: 100%; }

/* --------------------------------------------------------- monster shop */
/* Same five-across band rhythm as the codex, so the two panels read as two
   views of one collection: the codex is what a monster *is*, the shop is what
   it costs. An undiscovered row keeps its slot rather than collapsing — the gap
   in the ladder is the point. */
.shop-row { display: grid; grid-template-columns: repeat(5, 1fr); grid-auto-rows: 1fr; gap: 8px; }
.shop-cell {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 9px 7px 8px; text-align: center;
}
/* The price button and the MERGE ONLY / UNDISCOVERED plate both sit on the
   bottom edge, so the middle of the card absorbs the slack instead of the row
   heights disagreeing. */
.shop-cell .sc-dps { margin-top: auto; }
.shop-cell.sale { box-shadow: 0 0 0 2px rgba(140,110,220,.7); }
.shop-cell.live { box-shadow: 0 0 0 2px rgba(255,210,74,.8), 0 0 14px rgba(255,210,74,.35); }
.shop-cell.merge { opacity: .72; }
/* An undiscovered shop row is the same "not yours yet" idea as a locked codex
   cell, so it wears the same plate. */
.shop-cell.unknown {
  opacity: .72; filter: none;
  background-image: url('../assets/ui/cell-locked.webp');
}
.sc-tier { font-size: 11px; font-weight: 900; color: var(--dim); }
.sc-portrait { width: 64px; height: 64px; }
/* The undiscovered stand-in. Sized to the portrait it replaces, not to a thin
   bar: a 26px strip where a 64px face belongs left the rest of the card sliding
   up into the art's recess, which is what made undiscovered tiers read as
   broken next to discovered ones. */
.sc-swatch {
  width: 64px; height: 64px; border-radius: 12px; border: 2px solid rgba(0,0,0,.55);
  background: #241d33;
  display: flex; align-items: center; justify-content: center;
  font-size: 26px; font-weight: 900; color: #6c5f88;
}
.sc-name { font-size: 10.5px; font-weight: 900; line-height: 1.2; min-height: 26px; }
.sc-dps { font-size: 10.5px; font-weight: 900; color: var(--gold); }
.shop-cell .buy-btn { width: 100%; margin-top: 4px; padding: 5px 4px; font-size: 11px; }
.sc-locked {
  width: 100%; margin-top: 4px; padding: 5px 4px; border-radius: 8px;
  font-size: 9.5px; font-weight: 900; letter-spacing: .06em; color: var(--dim);
  background: #1a1426; border: 2px solid #120c1c;
}

/* -------------------------------------------------------- how to play */
.howto ol { margin: 0 0 14px 20px; }
.howto li { font-size: 13px; line-height: 1.7; color: #ded5f2; }
.howto li b { color: var(--gold); }
.legend { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-top: 10px; }
.legend div { font-size: 12px; color: var(--dim); padding: 7px 10px;
  background: rgba(255,255,255,.04); border-radius: 9px; border: 2px solid rgba(255,255,255,.07); }
.legend b { color: #fff; }

/* ------------------------------------------------------------ result */
/* The defeat screen is the one dialog a player cannot dismiss, so it has to
   carry its own summary rather than lean on the HUD behind it: what happened,
   where the run lands, what it cost, and what survived — then the single button
   that is the only way out. Read top to bottom, it answers the three questions
   a player actually has before they can press it. */
.result { text-align: center; }
.result-crest {
  font-size: 44px; line-height: 1; margin-bottom: 6px;
  filter: drop-shadow(0 4px 8px rgba(0,0,0,.6));
}
.result h3 { font-size: 30px; letter-spacing: .07em; margin-bottom: 8px;
  text-shadow: 0 3px 0 #000, 2px 0 0 #000, -2px 0 0 #000, 0 -2px 0 #000; }
.result.lose h3 { color: var(--red); }
.result.win h3 { color: var(--green); }
.result p { font-size: 14px; color: var(--dim); line-height: 1.6; margin-bottom: 14px; }

/* The consequence table. Rows rather than a paragraph because these are the
   three numbers the player is looking for, and a sentence buries them. */
.result-lines {
  display: flex; flex-direction: column; gap: 1px;
  margin: 0 auto 16px; max-width: 340px; text-align: left;
  background: var(--edge); border: 3px solid var(--edge); border-radius: 12px;
  overflow: hidden;
}
.result-lines div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 9px 12px; background: linear-gradient(180deg, #332a48, #241d33);
}
.result-lines span { font-size: 12px; color: var(--dim); }
.result-lines b { font-size: 14px; font-weight: 900; color: #fff; white-space: nowrap; }
.result-lines b.bad { color: #ff8a9a; }
.result-lines b.good { color: var(--green); }
.result .mbtn { width: 100%; max-width: 340px; }

/* The "more maps coming soon" note on the run-complete screen. Boxed and tinted
   so it reads as a message from the developers rather than as another run stat,
   which is what it looked like as a bare paragraph between the two. */
.result-more {
  margin: 14px auto 16px; max-width: 380px;
  padding: 11px 14px;
  background: rgba(95, 216, 255, 0.07);
  border: 2px solid rgba(95, 216, 255, 0.30);
  border-radius: 10px;
  font-size: 13px; line-height: 1.65; color: var(--dim);
}
.result-more b { color: #9fe8ff; font-weight: 900; }
.result .mbtn:disabled { opacity: .5; cursor: not-allowed; filter: grayscale(.6); }

@media (max-width: 620px) {
  .result-more { font-size: 12px; padding: 9px 11px; margin: 11px auto 13px; }
}
@media (max-height: 520px) {
  .result-more { font-size: 11.5px; padding: 8px 10px; margin: 9px auto 11px; }
}

/* ------------------------------------------------------- ad placeholder */
/* Shown only off-portal, where there is no ad network. Styled plainly and
   labelled as a placeholder so it can never be mistaken for a real ad — its
   whole job is to let the reward timing and pause behaviour be exercised. */
.ad-stub {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6, 4, 12, .88);
}
.ad-box {
  width: min(420px, 88vw); padding: 26px 22px; text-align: center;
  background: linear-gradient(180deg, #2a2140, #150e24);
  border: 3px solid var(--edge); border-radius: 18px;
  box-shadow: 0 12px 0 rgba(0,0,0,.5);
}
.ad-tag {
  font-size: 10px; font-weight: 900; letter-spacing: .18em; color: var(--dim);
}
.ad-what {
  margin: 8px 0 16px; font-size: 17px; font-weight: 900; color: var(--gold);
  text-shadow: 0 2px 0 #000;
}
.ad-bar { height: 10px; background: #100b1c; border: 2px solid var(--edge); border-radius: 7px; overflow: hidden; }
.ad-bar i { display: block; width: 0; height: 100%; background: linear-gradient(180deg, #9dff96, var(--green-2)); }
.ad-note { margin-top: 12px; font-size: 11px; color: var(--dim); }
.ad-skip {
  margin-top: 14px; padding: 8px 22px; cursor: pointer;
  font-family: var(--font); font-size: 12px; font-weight: 900; letter-spacing: .08em;
  color: #ded5f2; background: #2e2344;
  border: 2px solid var(--edge); border-radius: 10px;
}
.ad-skip:hover { background: #3c2f58; color: #fff; }

@media (max-width: 760px) {
  .codex-row, .shop-row { grid-template-columns: repeat(3, 1fr); }
  .talent-grid, .legend { grid-template-columns: 1fr; }
  .forge-grid { grid-template-columns: minmax(0, 1fr); }
  .rebirth-rewards { grid-template-columns: repeat(2, 1fr); }
  .rebirth-table { font-size: 12px; }
  .panel-cta { flex-direction: column; }
}

/* ------------------------------------------------------- phone portrait
 *
 * The four content panels are the densest screens in the game — the Codex and
 * the Shop are fifty cards each, the Forge is fourteen stat lines — and on a
 * 390px phone they were being asked to render at desktop proportions inside a
 * third of the width. Three things had to change, and they are the same three
 * for all four panels:
 *
 *   1. The dialog takes the screen. `max-height: 90vh` on a browser whose
 *      address bar comes and goes measures the WRONG viewport, and the 9-slice
 *      frame's crown alone was eating 66px of it. `dvh` tracks the live
 *      viewport, and the chrome shrinks to leave the content the room.
 *   2. Cards go to two columns and shed their desktop padding, so a card is a
 *      readable object rather than a clipped one.
 *   3. Every control that can be tapped gets a real target. A 30px ✕ and a 24px
 *      buy button are both under the 44px a thumb actually needs.
 */
@media (max-width: 620px) {
  .modal { padding: calc(4px + var(--sa-t)) calc(4px + var(--sa-r))
                    calc(4px + var(--sa-b)) calc(4px + var(--sa-l)); }
  .modal-box {
    width: 100%; max-height: 92vh; max-height: 92dvh;
    border-width: 58px 20px 22px;
    border-image: url('../assets/ui/panel-frame.webp') 170 60 62 60 fill / 58px 20px 22px / 0 stretch;
  }
  .modal-head { padding: 2px 2px 9px; }
  .modal-head h2 { font-size: 16px; letter-spacing: .04em; }
  /* Bigger than the desktop button, not smaller: this is the one control on the
     panel that is pressed with a thumb reaching across the screen. */
  .modal-close { width: 40px; height: 40px; font-size: 17px; border-radius: 12px; }
  .modal-body { padding: 12px 2px 4px; }
  .modal-body::-webkit-scrollbar { width: 6px; }

  .panel-note { font-size: 11.5px; line-height: 1.5; margin-bottom: 11px; }
  .panel-info { margin-bottom: 10px; }
  .panel-info > summary { min-height: 42px; padding: 8px 11px; font-size: 12px; }
  .panel-warn { font-size: 11.5px; padding: 8px 11px; }

  /* Codex + Shop */
  .codex-row, .shop-row { grid-template-columns: repeat(2, 1fr); gap: 7px; }
  .codex-cell { padding: 7px 4px 8px; }
  .shop-cell { padding: 7px 5px 7px; gap: 2px; }
  .cc-portrait, .sc-portrait { width: 58px; height: 58px; }
  .sc-swatch { width: 58px; height: 58px; font-size: 24px; }
  .cc-name, .sc-name { font-size: 11px; min-height: 0; }
  .cc-dps, .sc-dps { font-size: 11px; }
  .sc-tier, .cc-tier { font-size: 10px; }
  .shop-cell .buy-btn { min-height: 38px; font-size: 12px; padding: 6px 4px; }
  .sc-locked { min-height: 38px; display: flex; align-items: center; justify-content: center; }
  /* The band header is three unrelated things on one baseline — the class name,
     its element badge and a sentence of ability text. At this width the flex row
     squeezed the title to two words a line and the ability text to a four-line
     column. Stacked, each one gets the full width for as long as it needs it. */
  .band-head {
    display: grid; grid-template-columns: auto 1fr; align-items: center;
    gap: 4px 8px; margin-bottom: 5px; font-size: 13px;
  }
  .band-ability { grid-column: 1 / -1; font-size: 11px; line-height: 1.4; }
  .band-el { justify-self: start; }

  /* Forge + talents */
  .talent { padding: 10px; gap: 10px; }
  .talent-ico { width: 42px; height: 42px; flex-basis: 42px; font-size: 21px; border-radius: 10px; }
  .talent-name { font-size: 13px; }
  .talent-desc { font-size: 11px; margin: 3px 0 6px; }
  /* `float: right` puts the current bonus on the name's line, which wraps into
     the level chip once the name is long. On a phone it gets its own line. */
  .talent-now { float: none; display: inline-block; margin-left: 6px; }
  .buy-btn { min-height: 40px; font-size: 12.5px; }
  .buy-row .buy-btn.alt { flex-basis: 56px; }

  /* The Forge row survives the phone intact — it is already one band. The buy
     column just has to hit a thumb target, and `min-height` on `.buy-btn` above
     would otherwise stack two 40px buttons into an 84px row. */
  .fline { padding: 7px 8px; gap: 8px; }
  .fl-ico { width: 30px; height: 30px; flex-basis: 30px; font-size: 16px; }
  .fl-name { font-size: 12px; }
  .fl-buy { width: 92px; }
  .fl-buy .buy-btn { min-height: 30px; font-size: 11px; }
  .fl-buy .buy-btn.alt { min-height: 24px; font-size: 9.5px; }

  .rw { padding: 9px 6px 8px; border-radius: 12px; }
  .rw-ico { font-size: 19px; }
  .rw-big { font-size: 16px; }
  .rw-sub { font-size: 10px; }

  .forge-head { margin: 16px 0 6px; font-size: 13px; }
  .rebirth-table { font-size: 11.5px; }
  .rebirth-table th, .rebirth-table td { padding: 6px 7px; }
  .rebirth-gate { padding: 10px 11px; font-size: 12px; }

  .mbtn { width: 100%; max-width: 340px; font-size: 15px; padding: 13px 14px; }
  .mbtn.primary { font-size: 17px; padding: 16px 14px; }
  .mbtn.ghost { width: 100%; }

  .result-crest { font-size: 38px; }
  .result h3 { font-size: 23px; }
  .result p { font-size: 13px; }
  .result-lines span { font-size: 11.5px; }
  .result-lines b { font-size: 13px; }
}

/* Landscape phone: the frame's crown alone is 92px of a 360px-tall screen, so
   the whole 9-slice steps down and the dialog is allowed nearly the full height.
   Content still scrolls — this only stops the chrome eating the window. */
@media (max-height: 520px) {
  .modal-box {
    max-height: 96vh; max-height: 96dvh;
    border-width: 50px 20px 20px;
    border-image: url('../assets/ui/panel-frame.webp') 170 60 62 60 fill / 50px 20px 20px / 0 stretch;
  }
  .modal-head { padding: 2px 4px 7px; }
  .modal-head h2 { font-size: 15px; }
  .modal-close { width: 34px; height: 34px; font-size: 15px; }
  .modal-body { padding: 9px 4px 4px; }
  /* Landscape has width to spare and no height at all, so the cards go wide and
     the result screen loses the vertical padding rather than the content. */
  .codex-row, .shop-row { grid-template-columns: repeat(4, 1fr); }
  .talent-grid, .forge-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .rebirth-rewards { grid-template-columns: repeat(4, 1fr); }
  .result-crest { font-size: 30px; margin-bottom: 2px; }
  .result h3 { font-size: 21px; }
  .result-lines div { padding: 6px 11px; }
  .ad-box { padding: 16px 22px; }
  .ad-skip { margin-top: 10px; padding: 6px 18px; }
}

/* Narrow landscape (568x320): four across leaves ~120px a card, which clips the
   longer monster names, so it steps back to three. */
@media (max-height: 520px) and (max-width: 620px) {
  .codex-row, .shop-row { grid-template-columns: repeat(3, 1fr); }
  .talent-grid { grid-template-columns: 1fr; }
  .forge-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
