/* height:100dvh for the same reason as the select screen: a fixed inset:0
   element sizes to iOS's large viewport, which put the hotbar and the quick-eat
   button behind Safari's toolbar on a phone. */
#hud { position: fixed; inset: 0; height: 100dvh; pointer-events: none;
       color: #e8e2f7; font-family: Georgia, serif; }
#hud.hidden { display: none; }
#hud > * { pointer-events: auto; }

/* character panel: portrait + name + hp bar + levels */
@property --xp { syntax: '<number>'; inherits: true; initial-value: 0; }
/* THE RING MUST STAY A CIRCLE. portrait-wrap is a block, so it stretched to
   whatever the panel was wide - and once the HP bar took a 76px minimum, the
   panel grew past the portrait and the ring's box became an ELLIPSE. A conic
   gradient under a circular mask on an oval box is what produced the gold hook
   flying off the side (Alan: "why does the portrait ring always bugs?").
   Centring the column keeps the wrap at the portrait's own size. */
#char-panel { position: absolute; top: 16px; left: 16px; }
/* xp progress wraps the portrait as a golden ring */
#portrait-wrap::before { content: ''; position: absolute; inset: -6px; border-radius: 50%;
  background: conic-gradient(from 0turn,
    #8a6410 0,
    #d4a94f calc(var(--xp, 0) * 0.45turn),
    #ffd166 calc(var(--xp, 0) * 0.8turn),
    #ffe9a0 calc(var(--xp, 0) * 1turn),
    rgba(58, 44, 18, .55) 0);
  -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%);
  mask: radial-gradient(circle, transparent 61%, #000 62%);
  transition: --xp .5s ease; pointer-events: none;
  animation: ring-breathe 2.8s ease-in-out infinite; }
@keyframes ring-breathe {
  0%, 100% { box-shadow: 0 0 8px rgba(255, 209, 102, .3); filter: brightness(1); }
  50% { box-shadow: 0 0 16px rgba(255, 209, 102, .6); filter: brightness(1.15); }
}
/* a soft glint sweeping around the ring */
#portrait-wrap::after { content: ''; position: absolute; inset: -6px; border-radius: 50%;
  background: conic-gradient(transparent 0 86%, rgba(255, 250, 225, .9) 93%, transparent 100%);
  -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%);
  mask: radial-gradient(circle, transparent 61%, #000 62%);
  animation: ring-glint 3.4s linear infinite; opacity: .45; pointer-events: none; }
@keyframes ring-glint { to { transform: rotate(1turn); } }
/* charging spark riding the arc's tip */
#xp-spark { position: absolute; inset: -6px; pointer-events: none; display: none;
  transform: rotate(calc(var(--xp, 0) * 1turn)); transition: transform .5s ease; }
#portrait-wrap.charged #xp-spark { display: block; }
#xp-spark::before { content: ''; position: absolute; top: -3px; left: 50%; margin-left: -4px;
  width: 8px; height: 8px; border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, #ffd166 55%, transparent 78%);
  box-shadow: 0 0 8px #ffd166, 0 0 18px rgba(255, 184, 77, .8);
  animation: spark-twinkle .85s ease-in-out infinite; }
@keyframes spark-twinkle {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.55); opacity: .75; }
}
#portrait-wrap { position: relative; flex-shrink: 0; width: max-content; }
/* LEVEL BADGE: a small dark disc with a gold rim, sitting on the portrait's
   lower right. Same gold vocabulary as the ring and the panel's level dial,
   but small and quiet enough not to compete with the XP arc it sits on. */
#lvl-badge { position: absolute; right: -4px; bottom: -4px; z-index: 3;
  min-width: 24px; height: 24px; padding: 0 4px; box-sizing: border-box;
  border-radius: 999px; border: 2px solid #cfa54e;
  background: radial-gradient(circle at 50% 35%, #241a3f, #120b22 70%);
  color: #ffd98a; font-size: 12px; font-weight: 700; line-height: 20px;
  text-align: center; letter-spacing: 0;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
  box-shadow: 0 0 8px rgba(212, 169, 79, .45), inset 0 0 6px rgba(0, 0, 0, .6);
  pointer-events: none; }
/* it flashes gold for a beat when the number changes */
#lvl-badge.bump { animation: lvl-bump .7s ease-out; }
@keyframes lvl-bump {
  0% { transform: scale(1); box-shadow: 0 0 8px rgba(212, 169, 79, .45); }
  35% { transform: scale(1.35); box-shadow: 0 0 18px rgba(255, 216, 138, .95); }
  100% { transform: scale(1); box-shadow: 0 0 8px rgba(212, 169, 79, .45); }
}
#portrait { width: 66px; height: 66px; border-radius: 50%; border: 2px solid #6a5acd;
  box-shadow: 0 0 10px rgba(106, 90, 205, .4); display: block; object-fit: cover; }

/* portrait panel bars: HP loud, XP quiet */
/* CENTRED UNDER THE PORTRAIT, whatever size the portrait is. A hand-tuned
   negative margin was 14px off, because the portrait is 66px in the base rule
   and 96px once the responsive override lands (Alan: "shoud the hp bar
   cenetered to the portrait?"). auto margins centre it on the panel, which is
   exactly as wide as the portrait, so it stays centred at any size. */
/* A MINIMUM WIDTH, so the label is not hostage to the portrait. On a phone the
   portrait is only 54px, which left "1234/1500" with 1.5px of clearance each
   side - technically fitting, visibly jammed against the border. The space
   beside the portrait is empty, so the bar may reach into it. */
#hp-bar2 { position: absolute; top: 100%; left: -6px; right: -6px; margin-top: 10px; height: 16px;
  border-radius: 7px; background: #0c1710; border: 1px solid #2a5c3a;
  overflow: hidden; box-shadow: 0 2px 6px rgba(0, 0, 0, .5); pointer-events: none; }
/* YOURS IS GREEN, THEIRS IS RED. Red is the enemy colour everywhere else in
   the game - the target frame, the bars over monsters - so a red bar by your
   own portrait read as a mob's (Alan: "own hp bar should be green, not red
   perhaps? otherwise looks like a mob?"). It still turns hot when you are low,
   which is the moment the alarm colour is worth spending. */
#hp-fill2 { height: 100%; width: 100%; border-radius: 5px;
  background: linear-gradient(90deg, #7ce8a4, #35c46d 60%, #1d8f4c);
  transition: width .25s ease; }
/* under a third it goes hot and pulses: a number is slow to read mid-fight */
#hp-fill2.low { background: linear-gradient(90deg, #ff8a4a, #ff3b3b 60%, #b3121e);
  animation: hp-throb 1.1s ease-in-out infinite; }
@keyframes hp-throb { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.45); } }
/* THE NUMBER HAS TO READ ON BOTH HALVES OF THE BAR - the light green fill and
   the dark empty track behind it - so it is white with a hard dark outline
   rather than a tint with a soft shadow, which vanished on a phone (Alan: "u
   can barely see the hp text in the hp bar in mobile"). */
#hp-num { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-size: 10.5px; font-weight: 800; color: #fff;
  letter-spacing: .2px; font-variant-numeric: tabular-nums;
  /* "132 / 132" wrapped to two lines inside the bar and clipped */
  white-space: nowrap; line-height: 1; }
/* four digits each side: same bar, denser label */
#hp-num.tight { font-size: 9.5px; letter-spacing: 0;
  text-shadow: 0 0 2px #04120a, 1px 0 1.5px #04120a, -1px 0 1.5px #04120a,
               0 1px 1.5px #04120a, 0 -1px 1.5px #04120a; }

/* stats popup (portrait tap) */
#char-panel { cursor: pointer; }
#stats-panel { position: absolute; top: 128px; left: 14px; width: 292px; z-index: 5;
  max-height: calc(100dvh - 150px); overflow-y: auto;
  background: linear-gradient(160deg, rgba(18, 10, 40, .94), rgba(8, 5, 22, .94));
  border: 1px solid #4b3d7a; border-radius: 14px; padding: 12px 14px;
  box-shadow: 0 6px 22px rgba(0, 0, 0, .5);
  transform-origin: top left; transition: transform .18s ease, opacity .18s ease;
  font-size: 12.5px; }
#stats-panel.closed { transform: scale(.7); opacity: 0; pointer-events: none; }
/* No backdrop-filter anywhere. These panels are already ~94% opaque, so the
   blur had nothing to reveal - all it did was smear a soft halo of the scene
   just outside each rounded corner, which read as a dirty translucent border
   around every popup. */
#stats-header { display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 9px; }
#stats-name { font-size: 14px; color: #fff; }
#stats-rarity { font-size: 11px; color: #ffd166; margin-left: 8px; line-height: 16px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 3px 8px; }
#stats-rarity .ident { white-space: nowrap; }
/* The duel rating, in the identity line beside class and rank. Given its own
   colour and a chip so it reads as a STANDING, not another trait: it is the
   one number on this panel that another player can take off you.
   Inline-flex and centred on the text line: as an inline span its border
   box hung below the baseline and the chip sat half a line under the words
   beside it (Alan: "the unrated badge is not fitting well"). */
.duel-rank { color: #d8a7ff; font-weight: 700; white-space: nowrap;
  display: inline-flex; align-items: center; height: 16px; line-height: 16px;
  padding: 0 7px; border-radius: 999px; font-size: 10.5px;
  background: rgba(120, 70, 180, .22); border: 1px solid rgba(190, 140, 255, .35); }
#stats-panel[data-cls] #stats-rarity .duel-rank { color: #d8a7ff; }
#stats-panel[data-cls="warrior"] #stats-rarity { color: #ff9d5c; }
#stats-panel[data-cls="mystic"] #stats-rarity { color: #7f9dff; }
#stats-panel[data-cls="ranger"] #stats-rarity { color: #5cbf7a; }
.skill-row { margin-bottom: 8px; }
.skill-row .skill-line { display: flex; justify-content: space-between;
  color: #cabffa; margin-bottom: 3px; }
.skill-row .skill-xp { color: #8f83bd; font-size: 10.5px; }
.skill-bar { height: 6px; border-radius: 4px; background: #170d24;
  border: 1px solid #2c2250; overflow: hidden; }
.skill-fill { height: 100%; border-radius: 4px;
  background: linear-gradient(90deg, #8a6410, #d4a94f 45%, #ffd166 75%, #ffe9a0);
  box-shadow: 0 0 6px rgba(255, 209, 102, .45), inset 0 1px 1px rgba(255, 255, 255, .25); }
/* character sheet: sprite + equipment side by side (WoW-style) */
#stats-close { margin-left: auto; background: none; border: none; color: #8f83bd;
  font-size: 14px; cursor: pointer; padding: 0 0 0 8px; }
#stats-body { display: flex; align-items: center; margin-bottom: 9px;
  justify-content: space-evenly; gap: 0; }
#stats-sprite { height: 118px; width: auto; flex-shrink: 0;
  animation: breathe-sheet 2.6s ease-in-out infinite; transform-origin: bottom center; }
@keyframes breathe-sheet {
  0%, 100% { transform: scaleY(1); }
  50% { transform: scaleY(1.03) scaleX(0.99); }
}
#stats-eq { display: flex; flex-direction: column; gap: 7px; min-width: 0; flex: 1; }
.stats-eq-slot { display: flex; gap: 8px; align-items: center;
  background: #17102e; border: 1px dashed #3d3166; border-radius: 9px;
  padding: 6px 8px; color: #6f639c; }
.stats-eq-slot.filled { border-style: solid; border-color: #6a5acd; color: #e8e2f7;
  cursor: pointer; background: linear-gradient(160deg, #241a4a, #17102e);
  box-shadow: 0 0 8px rgba(106, 90, 205, .35); }
.stats-eq-slot.filled:hover { background: #221740; }
.seq-ico { font-size: 16px; }
.seq-txt { min-width: 0; }
.seq-txt b { display: block; font-size: 10px; letter-spacing: 1px; color: #8f83bd;
  text-transform: uppercase; }
.seq-txt span { font-size: 11.5px; display: block; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
#stats-grid { border-top: 1px solid #2c2250; margin-top: 9px; padding-top: 7px; }
.stat-row { display: flex; justify-content: space-between; padding: 2.5px 0;
  color: #a99ed4; }
.stat-row b { font-weight: 700; }
#stats-traits { border-top: 1px solid #2c2250; margin-top: 8px; padding-top: 8px;
  display: flex; flex-wrap: wrap; gap: 4px; }
.trait-chip { background: #221741; border: 1px solid #3a2d6b; border-radius: 8px;
  padding: 2px 8px; font-size: 10.5px; color: #b7aede; }


/* floating xp drop chips */
#xp-drops { position: absolute; top: 142px; right: 16px; display: flex;
  flex-direction: column; align-items: flex-end; gap: 3px; pointer-events: none; }
.xp-drop { background: rgba(34, 23, 65, .92); border: 1px solid #6a5acd;
  border-radius: 8px; padding: 2px 10px; font-size: 12px; color: #cabffa;
  animation: xpdrop 1.6s ease forwards; }
@keyframes xpdrop { 0% { opacity: 0; transform: translateY(6px); }
  15% { opacity: 1; transform: none; } 70% { opacity: 1; }
  100% { opacity: 0; transform: translateY(-14px); } }

/* docked minimap: always visible, tap to expand */
#mini-dock { position: absolute; top: 14px; right: 14px; width: 118px; height: 118px;
  border-radius: 12px; overflow: hidden; border: 1px solid #4b3d7a; cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .5); }
#mini-img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* blocked tiles, drawn 1px-per-tile and scaled up: crisp edges, no blur */
#mini-walls { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; image-rendering: pixelated; pointer-events: none; }
#mini-dots { position: absolute; inset: 0; }
#inv-gold { float: right; color: #ffd166; font-size: 12px; letter-spacing: 0; text-transform: none; }

/* bag button: right-edge utility column with minimap + mute */
#bag-count { position: absolute; top: -6px; right: -6px; min-width: 16px; height: 16px;
  border-radius: 8px; background: #d92645; color: #fff; font-size: 9.5px;
  line-height: 16px; padding: 0 3px; font-family: Georgia, serif;
  border: 1px solid #ff8a9a; }
#inv-panel { position: absolute; top: 174px; right: 18px;
  background: linear-gradient(160deg, rgba(18, 10, 40, .92), rgba(8, 5, 22, .92));
  border: 1px solid #4b3d7a; border-radius: 14px; padding: 10px 12px 12px;
  box-shadow: 0 6px 22px rgba(0, 0, 0, .5);
  transform-origin: bottom right; transition: transform .18s ease, opacity .18s ease; }
#inv-panel.closed { transform: scale(.6); opacity: 0; pointer-events: none; }
#inv-panel { transform-origin: top right; }
#inv-title { font-size: 12px; color: #a99ed4; letter-spacing: 1.5px;
  text-transform: uppercase; margin-bottom: 8px; }
/* THE BAG'S SIZE COMES FROM THE SLOT, NOT A PERCENTAGE. Two goes at this:
   five RIGID 46px columns overflowed the painted frame (Alan: "the squares
   bleed to the righ?"), and the fluid `1fr` tracks with `width: min(254px,
   100%)` that replaced them COLLAPSED instead - #inv-panel is absolutely
   positioned with no width, so it sizes to its content while the grid sizes to
   the panel, which is circular, and the browser resolves minmax(0, 1fr) to
   zero. Slots came out 12px wide (Alan: "why are the backpack icons still
   overlapping?").
   One definite number fixes both directions: the track IS the slot, so the
   grid is exactly 5*slot + 4*gap and the panel shrink-wraps to it. Nothing to
   resolve circularly, nothing to overflow. Narrow phones get a smaller slot. */
#inventory { display: grid; gap: 6px;
  grid-template-columns: repeat(5, var(--slot, 46px)); }
.inv-slot { width: var(--slot, 46px); height: var(--slot, 46px);
  border-radius: 9px; background: #17102e;
  border: 1px solid #2c2250; display: flex; flex-direction: column; align-items: center;
  justify-content: center; font-size: 20px; cursor: default; color: #cabffa; }
.inv-slot.food { cursor: pointer; border-color: #6a5acd; }
.inv-slot.food:hover { background: #251a4a; box-shadow: 0 0 8px rgba(106, 90, 205, .4); }

#log { position: absolute; bottom: 82px; left: 50%; transform: translateX(-50%);
  width: min(460px, 60vw); overflow: hidden; display: flex;
  flex-direction: column-reverse; font-size: 13px; color: #cfc6ee;
  text-shadow: 0 1px 2px #000, 0 0 4px rgba(0, 0, 0, .85); pointer-events: none;
  /* The old scrim was a big soft radial ellipse, which over lit ground read
     as a dirty smear behind the text rather than as anything deliberate.
     A mask does the same job from the other side: the oldest line FADES OUT
     as it leaves instead of being sliced through, so there is nothing to
     hide and no scrim needed. */
  -webkit-mask-image: linear-gradient(to top, #000 58%, rgba(0, 0, 0, 0));
  mask-image: linear-gradient(to top, #000 58%, rgba(0, 0, 0, 0));
  padding: 0 14px;
  /* max-height MUST be an exact multiple of line-height or the top line is
     cut through the middle of the glyphs - 96 / 18 was 5.33 lines */
  line-height: 17px; max-height: 102px; }   /* whole lines only — 6 of them */
#log div { padding: 0; }
#toasts { position: absolute; bottom: 150px; left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column-reverse; gap: 6px; align-items: center;
  pointer-events: none; }
.toast { background: rgba(20, 12, 44, .9); border: 1px solid #6a5acd; border-radius: 10px;
  padding: 6px 16px; font-size: 14px; animation: toast 2.6s forwards;
  box-shadow: 0 2px 12px rgba(0, 0, 0, .4); }
@keyframes toast { 0% { opacity: 0; transform: translateY(-8px); }
  12% { opacity: 1; transform: none; } 80% { opacity: 1; } 100% { opacity: 0; } }

/* quest tracker */
/* the tracker fills the gap between the portrait and the minimap, so it can
   never grow under the minimap or off the right edge of a phone */
#quest-tracker { position: absolute; top: 20px; left: 102px; right: 146px; min-width: 170px;
  background: rgba(10, 6, 26, .78); border: 1px solid #4b3d7a; border-radius: 12px;
  padding: 8px 13px; }
#qt-title { font-size: 9px; letter-spacing: 2.5px; color: #6f639c; }
#qt-name { font-size: 13px; color: #ffd166; margin: 3px 0 2px; }
#qt-progress { font-size: 12px; color: #b7aede; }

/* npc dialogue */
#dialogue[hidden] { display: none !important; }
#dialogue { position: absolute; bottom: 96px; left: 50%; transform: translateX(-50%);
  width: min(430px, 88vw); background: linear-gradient(160deg, rgba(18, 10, 40, .95), rgba(8, 5, 22, .95));
  border: 1px solid #6a5acd; border-radius: 14px; padding: 14px 16px;
  box-shadow: 0 8px 26px rgba(0, 0, 0, .6); }
#dlg-name { font-size: 14px; color: #ffd166; margin-bottom: 7px; letter-spacing: .5px; }
#dlg-lines { font-size: 13.5px; color: #d9d2f0; line-height: 1.55; font-style: italic; }
#dlg-lines p { margin: 0 0 7px; }
/* EVERY dialogue button, not just Close: "Browse wares" wore the browser's
   own grey until the forge button stood next to it and Alan asked why they
   were "ugly and unstyled". The keeper's own actions read as the offer, in
   gold; Close stays the quiet one. */
#dialogue button { margin-top: 6px; background: #2c2250; color: #e8e2f7; border: 1px solid #6a5acd;
  border-radius: 9px; padding: 6px 18px; font-size: 13px; cursor: pointer; font-family: inherit; }
#dialogue button:hover { background: #3a2d6b; }
#dlg-shop, #dlg-bank, #dlg-forge { background: #3a2a10; border-color: #cfa54e; color: #ffe2b8; }
#dlg-shop:hover, #dlg-bank:hover, #dlg-forge:hover { background: #4d3814; }

/* minimap */

#map-panel { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(340px, 88vw); background: linear-gradient(160deg, rgba(18, 10, 40, .96), rgba(8, 5, 22, .96));
  border: 1px solid #6a5acd; border-radius: 16px; padding: 12px;
  box-shadow: 0 10px 34px rgba(0, 0, 0, .65); transition: transform .18s ease, opacity .18s ease; }
#map-panel.closed { transform: translate(-50%, -50%) scale(.75); opacity: 0; pointer-events: none; }
#map-title { font-size: 13px; letter-spacing: 2.5px; color: #fff; margin: 2px 2px 9px;
  text-shadow: 0 0 10px rgba(138, 108, 255, .6); }
#map-title span { color: #8f83bd; letter-spacing: 1px; font-size: 11px; }
#map-view { position: relative; border-radius: 10px; overflow: hidden;
  border: 1px solid #3a2d6b; }
#map-img { display: block; width: 100%; height: auto; }
#map-walls { position: absolute; inset: 0; width: 100%; height: 100%;
  image-rendering: pixelated; pointer-events: none; }
#map-dots { position: absolute; inset: 0; }
.map-dot { position: absolute; border-radius: 50%; transform: translate(-50%, -50%); }
.map-dot.you { width: 11px; height: 11px; background: #fff;
  box-shadow: 0 0 8px #fff, 0 0 16px #8a6cff; animation: youpulse 1.4s ease-in-out infinite; }
.map-dot.npc { width: 8px; height: 8px; background: #ffd166; box-shadow: 0 0 6px #ffd166; }
.map-dot.mob { width: 7px; height: 7px; background: #ff5d73; box-shadow: 0 0 5px #ff5d73; }
@keyframes youpulse { 0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50% { transform: translate(-50%, -50%) scale(1.35); } }
/* Seven entries do not fit one line in a 302px panel - it used to run off the
   edge and break "hand in" across two rows mid-phrase. */
#map-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; justify-content: center;
  margin-top: 9px; font-size: 11px; color: #a99ed4; align-items: center; }
#map-legend span { display: inline-flex; align-items: center; white-space: nowrap; }
#map-legend .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 4px; }
#map-legend .you { background: #fff; }
#map-legend .npc { background: #ffd166; }
#map-legend .mob { background: #ff5d73; }

/* phones: compact panels, no collisions */
@media (max-width: 600px) {
  #char-panel { gap: 8px; padding: 8px 12px 8px 8px; border-radius: 12px; }
  #portrait { width: 48px; height: 48px; }
        #hp-bar { height: 12px; margin-top: 4px; }
  #hp-text { font-size: 9px; line-height: 12px; }
  #xp-bar { height: 8px; }
  #xp-label { font-size: 7.5px; line-height: 8px; }
  #quest-tracker { top: 18px; left: 96px; right: 116px; min-width: 0; padding: 6px 10px; }
  #qt-name { font-size: 11.5px; }
  #qt-progress { font-size: 10.5px; }
  /* The character panel ends ABOVE the WALK ring and scrolls inside. It ran
     461px tall from 104px and sat on the stick (Alan: "the character button
     is overlapping with the walk button"). The ring's zone is 100px + 172px
     from the bottom; 8px of air above it. */
  #stats-panel { top: 104px; width: min(250px, 78vw);
                 max-height: calc(100dvh - 104px - 280px); overflow-y: auto;
                 -webkit-overflow-scrolling: touch; }
  .tmap-node { width: 74px; }
  .tmap-node .tmn-name { font-size: 8.5px; }
  #gear-grid { grid-template-columns: 1fr; }
  #gold-panel { padding: 5px 10px; font-size: 13px; }
  #xp-drops { top: 120px; }
  #mini-dock { width: 92px; height: 92px; }
  .xp-drop { font-size: 10.5px; padding: 1px 8px; }
  #log { width: min(340px, calc(100vw - 130px)); font-size: 11.5px;
    line-height: 16px; max-height: 96px; }   /* 6 whole lines */
  .toast { font-size: 12px; padding: 4px 12px; }
  #toasts { bottom: 130px; }
  #dialogue { bottom: 84px; }
}

/* low-hp warning vignette */
#low-vignette { position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 55%, rgba(180, 20, 40, .38) 100%);
  animation: lowpulse 1.1s ease-in-out infinite; }
@keyframes lowpulse { 0%, 100% { opacity: .35; } 50% { opacity: .9; } }

/* death flash */
#death-flash[hidden] { display: none !important; } /* author display:flex would beat the hidden attr */
#death-flash { position: absolute; inset: 0; pointer-events: auto; display: flex;
  flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  background: radial-gradient(ellipse at center, rgba(70, 0, 14, .55) 0%, rgba(12, 0, 4, .94) 100%);
  animation: deathin 2.2s ease forwards; }
#death-flash span { font-size: clamp(34px, 9vw, 58px); letter-spacing: 12px; color: #ffb3c0;
  font-weight: bold; text-shadow: 0 0 30px rgba(255, 40, 70, .7), 0 2px 5px #000;
  animation: death-title 2.4s ease both; }
@keyframes death-title { 0% { opacity: 0; transform: scale(1.12); letter-spacing: 22px; }
  100% { opacity: 1; transform: scale(1); letter-spacing: 12px; } }
#death-flash small { font-size: 14px; color: #d8a9b6; font-style: italic;
  text-shadow: 0 1px 3px #000; }
@keyframes deathin { 0% { opacity: 0; } 100% { opacity: 1; } }
#respawn-btn { margin-top: 16px; background: rgba(20, 10, 16, .6); color: #ffd166;
  border: 1.5px solid #cfa54e; border-radius: 12px; padding: 12px 34px; font-size: 16px;
  font-family: Georgia, serif; letter-spacing: 1px; cursor: pointer;
  box-shadow: 0 0 16px rgba(212, 169, 79, .35); animation: deathin 2.8s ease both; }
#respawn-btn:hover { background: rgba(60, 34, 20, .75); box-shadow: 0 0 24px rgba(212, 169, 79, .6); }

.map-dot.small.you { width: 7px; height: 7px; }
.map-dot.small.npc { width: 5px; height: 5px; }
.map-dot.small.mob { width: 5px; height: 5px; }
.map-dot.small.quest-mob { width: 7px; height: 7px; }   /* the hunted one reads bigger */

/* --- equipment row (in the pack panel) --- */
#equip-row { display: flex; gap: 8px; padding: 6px 0 8px; }
.equip-slot { flex: 1; display: flex; align-items: center; gap: 7px;
  background: #17102e; border: 1px dashed #3d3166; border-radius: 9px;
  padding: 6px 9px; color: #6f639c; font-size: 12px; cursor: default; }
.equip-slot.filled { border-style: solid; border-color: #6a5acd; color: #e8e2f7;
  cursor: pointer; }
.equip-slot.filled:hover { background: #221740; }
.eq-icon { font-size: 15px; }
.eq-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.inv-slot.gear { border-color: #7a5acd; }

/* --- shop panel --- */
#shop-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(360px, calc(100vw - 24px)); max-height: min(520px, 80vh); overflow-y: auto;
  background: rgba(12, 7, 28, .96); border: 1px solid #6a5acd; border-radius: 14px;
  padding: 12px 14px; z-index: 40; color: #e8e2f7; font-family: Georgia, serif; }
#shop-panel.closed { display: none; }
#shop-title { font-size: 14px; letter-spacing: 2px; color: #ffd166;
  display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
#shop-gold { margin-left: auto; color: #ffd166; font-size: 13px; }
#shop-close { background: none; border: none; color: #8f83bd; font-size: 15px;
  cursor: pointer; padding: 2px 4px; }
/* ROOM TO READ. The icon sat in a 22px slot with the stat line jammed under
   the name at 11px (Alan, on the desktop shop and forge: "cramped and badly
   aligned"). The art gets its own 36px column, the text its own line-height,
   and the row enough padding that one does not bleed into the next. */
.shop-row { display: flex; align-items: center; gap: 12px; padding: 8px 6px;
  border-bottom: 1px solid rgba(106, 90, 205, .15); }
.shop-ico { font-size: 17px; width: 36px; min-width: 36px; text-align: center; justify-content: center; }
.shop-name { flex: 1; font-size: 13.5px; min-width: 0; line-height: 1.35; }
.shop-name small { display: block; color: #a99ed4; font-size: 11.5px; margin-top: 2px; }
.shop-buy { background: #2c2250; color: #ffd166; border: 1px solid #6a5acd;
  border-radius: 8px; padding: 4px 10px; font-size: 12.5px; cursor: pointer;
  font-family: inherit; white-space: nowrap; }
.shop-buy:hover { background: #3a2d6b; }
.shop-buy.cant { opacity: .45; }
#shop-sell-title { margin: 10px 0 4px; font-size: 12px; color: #8f83bd;
  letter-spacing: 1px; }
#shop-sell-title small { color: #6f639c; }
.shop-empty { color: #6f639c; font-size: 12px; padding: 4px 2px; }

/* --- ability hotbar: arcane orbs --- */
@property --cd { syntax: '<number>'; inherits: true; initial-value: 0; }
#hotbar { position: fixed; bottom: max(12px, env(safe-area-inset-bottom));
  left: 50%; transform: translateX(-50%); display: flex; gap: 13px; z-index: 30;
  transition: opacity .35s ease, transform .35s ease;
  --hb-rim: #6a5acd; --hb-glow: rgba(138, 108, 255, .6); --hb-core: rgba(84, 58, 178, .5); }
#hotbar[data-cls="warrior"] { --hb-rim: #d98a4a; --hb-glow: rgba(255, 150, 70, .6); --hb-core: rgba(170, 78, 26, .48); }
#hotbar[data-cls="mystic"]  { --hb-rim: #7f9dff; --hb-glow: rgba(120, 160, 255, .65); --hb-core: rgba(64, 88, 200, .5); }
#hotbar[data-cls="ranger"]  { --hb-rim: #5cbf7a; --hb-glow: rgba(96, 214, 138, .6); --hb-core: rgba(28, 130, 70, .48); }
.hb-slot { position: relative; width: 56px; height: 56px; border-radius: 50%;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--hb-rim);
  background:
    radial-gradient(circle at 32% 26%, rgba(255, 255, 255, .18), transparent 44%),
    radial-gradient(circle at 50% 68%, var(--hb-core), rgba(12, 7, 28, .96) 80%);
  box-shadow: 0 3px 10px rgba(0, 0, 0, .55), inset 0 0 14px rgba(0, 0, 0, .5);
  transition: transform .09s ease, --cd .32s linear; }
.hb-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  border-radius: 50%; z-index: 1; pointer-events: none;
  box-shadow: inset 0 0 16px rgba(0, 0, 0, .65); }
.hb-slot.has-art .hb-icon { display: none; }
.hb-slot.has-art { background: #0c071c; }
/* slow-turning rune ring around ready skills */
.hb-slot.ready::after { content: ''; position: absolute; inset: -7px; border-radius: 50%;
  border: 1.5px dashed var(--hb-rim); opacity: .75; pointer-events: none;
  animation: hb-runering 10s linear infinite; }
@keyframes hb-runering { to { transform: rotate(1turn); } }
.hb-slot:active { transform: scale(.88); filter: brightness(1.5); }
/* ready to fire: soft breathing glow */
.hb-slot.ready { animation: hb-breathe 3.6s ease-in-out infinite; }
@keyframes hb-breathe {
  0%, 100% { box-shadow: 0 0 5px var(--hb-glow), 0 3px 10px rgba(0, 0, 0, .55), inset 0 0 14px rgba(0, 0, 0, .5); }
  50% { box-shadow: 0 0 15px var(--hb-glow), 0 0 32px var(--hb-glow), 0 3px 10px rgba(0, 0, 0, .55), inset 0 0 10px rgba(0, 0, 0, .35); }
}
/* the moment a cooldown finishes: bright ignition ring */
.hb-slot.ignite { animation: hb-ignite .5s ease-out; }
@keyframes hb-ignite {
  0% { box-shadow: 0 0 0 0 var(--hb-glow), 0 0 24px var(--hb-glow); filter: brightness(2); }
  100% { box-shadow: 0 0 0 14px rgba(0, 0, 0, 0), 0 0 8px var(--hb-glow); filter: brightness(1); }
}
.hb-icon { font-size: 23px; filter: drop-shadow(0 0 4px rgba(0, 0, 0, .8)); z-index: 2; }
.hb-key { position: absolute; top: 4px; left: 9px; font-size: 9px; color: #b9aee6;
  z-index: 4; text-shadow: 0 1px 2px #000; }
/* pressing the key should look like pressing the button, or the keyboard
   feels like it is doing nothing */
.hb-slot.keyed, #attack-btn.keyed { animation: key-press .18s ease-out; }
@keyframes key-press {
  0% { transform: scale(.86); filter: brightness(1.6); }
  100% { transform: scale(1); filter: none; }
}
#atk-key { position: absolute; bottom: 6px; left: 50%; transform: translateX(-50%);
  font-size: 9px; letter-spacing: .08em; color: #ffd9c2; z-index: 4;
  text-shadow: 0 1px 2px #000; pointer-events: none; }
.hb-cd { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-size: 15px; font-weight: 700; color: #ffd166;
  pointer-events: none; z-index: 3; text-shadow: 0 1px 3px #000; }
/* radial cooldown sweep - --cd runs 1 (just used) down to 0 (ready) */
/* channeling: every other skill is refused by the sim - dim them so the
   lock reads as a rule, not a bug. Moving is the cancel, not another button. */
.hb-slot.chan-locked { opacity: .38; filter: grayscale(.75); transform: scale(.94);
  transition: opacity .18s ease, transform .18s ease; }
.hb-slot.chan-locked .hb-icon, .hb-slot.chan-locked .hb-art { opacity: .6; }
.hb-slot.cooling::before { content: ''; position: absolute; inset: -1px; z-index: 2;
  background: conic-gradient(rgba(4, 2, 12, .82) calc(var(--cd, 0) * 1turn), transparent 0);
  border-radius: 50%; }
.hb-slot.cooling .hb-icon { opacity: .45; }
.hb-slot.casting { border-color: #baf0ff; animation: hb-cast .7s ease-in-out infinite; }
@keyframes hb-cast {
  0%, 100% { box-shadow: 0 0 8px rgba(154, 216, 255, .5); }
  50% { box-shadow: 0 0 22px rgba(154, 216, 255, .95); }
}
.hb-slot.locked { opacity: .45; filter: grayscale(.9); }
.hb-slot.locked .hb-cd { font-size: 10px; color: #cbc2ee; background: rgba(5, 3, 15, .55); border-radius: 50%; }
/* THE TOUCH LAYOUT IS A GUESS; A KEYPRESS IS PROOF.
   Every touch-UI rule below is gated on body:not(.kb): main.js adds .kb on the
   first trusted keydown and remembers it, because media queries lie - Alan's
   laptop reports (pointer: coarse) and got the thumb fan instead of the 12345
   hotbar. A phone never presses a key, so it keeps the fan; anything with a
   keyboard proves itself out of it forever. */
/* COARSE ONLY, since v2.99.13. This fan hard-codes five absolute slot
   positions, so the sixth slot had nowhere to stand - and on a narrow MOUSE
   window (<=700px, no keypress yet) the fan was still what you got. Touch
   devices never see it any more either: the right-hand column further down
   replaces it wholesale, per Alan's "no centred fan anywhere any more". So a
   narrow mouse window now keeps the ordinary centred row, which grows to six
   on its own, and this block survives only as the thing the column overrides. */
@media ((hover: none) and (pointer: coarse)) {
  /* centered thumb fan: primary in the middle where the thumb rests,
     skills curving up either side - no corner reach after tapping a mob */
  body:not(.kb) #hotbar { left: 50%; transform: translateX(-50%); right: auto; width: 330px;
    height: 108px; bottom: max(20vh, env(safe-area-inset-bottom)); display: block; }
  body:not(.kb) .hb-slot { position: absolute; width: 50px; height: 50px; }
  body:not(.kb) .hb-slot:nth-child(1) { width: 64px; height: 64px; left: 133px; bottom: 2px; }
  body:not(.kb) .hb-slot:nth-child(1) .hb-icon { font-size: 30px; }
  body:not(.kb) .hb-slot:nth-child(2) { left: 72px; bottom: 12px; }
  body:not(.kb) .hb-slot:nth-child(3) { left: 12px; bottom: 34px; }
  body:not(.kb) .hb-slot:nth-child(4) { left: 208px; bottom: 12px; }
  body:not(.kb) .hb-slot:nth-child(5) { left: 268px; bottom: 34px; }
  body:not(.kb) .hb-key { display: none; }
}

/* no double-tap zoom anywhere in the game UI */
html, body { touch-action: manipulation; }

/* sound toggle: under the minimap dock */
#quest-dot { position: absolute; top: -3px; right: -3px; width: 10px; height: 10px;
  border-radius: 50%; background: #ffd166; border: 1.5px solid #2a1c07;
  box-shadow: 0 0 8px rgba(255, 209, 102, .8); }

/* --- item menu: tap a bag slot, choose what happens to the item --- */
#item-menu { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 250px; z-index: 60;
  border: 18px solid transparent; border-image: url('../assets/ui/panel_frame.png') 100 stretch;
  background: linear-gradient(160deg, rgba(13, 9, 30, .97), rgba(6, 4, 18, .97)) padding-box;
  padding: 12px 14px; font-family: Georgia, serif; text-align: center;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .6); }
/* drag-to-rotate 3D weapon preview above the item name. Square so the model can be spun to
   any orientation without clipping. Only the four weapons with procedural models get one;
   everything else keeps the flat icon and this stays hidden. */
#im-preview { width: 190px; height: 190px; margin: -2px auto 8px; border-radius: 10px;
  background: radial-gradient(ellipse at 50% 42%, rgba(90,70,130,.30), rgba(8,6,20,.55) 70%);
  box-shadow: inset 0 0 22px rgba(0,0,0,.55); overflow: hidden; position: relative; }
#im-preview[hidden] { display: none; }
#im-preview::after { content: 'drag to turn'; position: absolute; left: 0; right: 0; bottom: 5px;
  font-size: 10px; letter-spacing: .08em; color: #7d739c; pointer-events: none; }
#im-canvas { display: block; width: 190px; height: 190px; cursor: grab;
  touch-action: none; -webkit-user-select: none; user-select: none; }
#im-canvas.dragging { cursor: grabbing; }
#im-name { font-size: 15px; color: #f0e9ff; letter-spacing: .03em; margin-bottom: 4px; }
#im-name[data-rarity="uncommon"] { color: #8ee6a1; }
#im-name[data-rarity="rare"] { color: #7f9dff; }
#im-name[data-rarity="epic"] { color: #c99aff; }
#im-name[data-rarity="astral"] { color: #c9a0ff; }
#im-name[data-rarity="legendary"] { color: #ffd166; }
/* a rolled piece's mods, under its base line (docs/AFFIX_SPEC.md) */
i.mods { font-style: normal; color: #c9bfff; }
/* the WORN menu hides Throw away (hud.js openWornMenu sets hidden) but a
   display rule on the button outranked the attribute, so it showed anyway */
#im-drop[hidden] { display: none; }
/* WHAT IT IS - one quiet line under the name. Rarity and slot only; the
   numbers moved out of it and into rows of their own. */
#im-meta { display: flex; align-items: center; justify-content: center; gap: 6px;
  font-size: 10.5px; letter-spacing: .13em; text-transform: uppercase;
  color: #8f83bd; margin: 0 0 9px; }
#im-meta:empty { display: none; }

/* WHAT IT DOES - a label column and a value column, so four stats read as
   four things. They used to be joined with spaces into one run of text. */
#im-stats, #im-diff { text-align: left; margin: 0 0 9px; }
#im-stats:empty, #im-diff:empty { display: none; }
.im-row { display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: 10px; padding: 3.5px 0; font-size: 12.5px; color: #b7aede;
  border-bottom: 1px solid rgba(122, 106, 190, .13); }
.im-row:last-child { border-bottom: none; }
.im-row b { font-weight: normal; font-variant-numeric: tabular-nums; color: #efe9ff; }
.im-row.up b { color: #8ee6a1; }
.im-row.down b { color: #ff9d9d; }
.im-mod { font-size: 11.5px; color: #c9bfff; padding: 3px 0 0; line-height: 1.45; }
.im-note { font-size: 11px; color: #8f83bd; line-height: 1.5; padding: 5px 0 0; }
.im-note.warn { color: #ffb066; }
.im-note.bad { color: #ff8a9c; }

/* CONDITION, always present on a wearable piece - a bar between the label and
   the reading, so the row lines up with the stat rows above it. */
.im-row.im-cond { grid-template-columns: auto 1fr auto; }
.im-cond-bar { height: 5px; border-radius: 3px; background: rgba(122, 106, 190, .22);
  overflow: hidden; }
.im-cond-bar i { display: block; height: 5px; border-radius: 3px; background: #7ce8a4; }
.im-cond.low .im-cond-bar i { background: #ffb066; }
.im-cond.low b { color: #ffb066; }
.im-cond.broken .im-cond-bar i { background: #ff5d73; }
.im-cond.broken b { color: #ff5d73; letter-spacing: .06em; }

/* the comparison's own heading, so "Replaces X" is not mistaken for a stat */
.im-cmp { font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
  color: #7d739c; margin: 10px 0 4px; padding-top: 9px;
  border-top: 1px solid rgba(122, 106, 190, .2); }
#item-menu .up { color: #8ee6a1; }
#item-menu .down { color: #ff9d9d; }
/* the slot the open menu belongs to keeps a gold ring */
.im-target { outline: 2px solid #ffd166; outline-offset: 1px; border-radius: 10px; }
#item-menu button { display: block; width: 100%; margin: 5px 0 0; padding: 8px 0;
  border-radius: 9px; font-family: inherit; font-size: 13.5px; cursor: pointer; }
#im-act { margin-top: 11px; }
#im-act { background: #2c2250; color: #ffd166; border: 1px solid #6a5acd; }
#im-act:hover { background: #3a2d6b; }
/* quieter than Eat: pinning is a preference, not the thing you came for */
#im-pin { background: #1d1738; color: #b7aede; border: 1px solid #3b2f66; font-size: 12.5px; }
#im-pin:hover { background: #271f4a; color: #efe9ff; }
#im-pin[hidden] { display: none; }
#im-drop { background: rgba(90, 24, 34, .55); color: #ff9d9d; border: 1px solid #8a3a48; }
#im-drop:hover { background: rgba(122, 30, 44, .7); }
#im-cancel { background: transparent; color: #8f83bd; border: 1px solid #3a3060; }
#im-cancel:hover { color: #cfc3f2; }

#settings-panel { position: absolute; top: 184px; right: 14px; width: 224px; z-index: 8;
  border: 18px solid transparent; border-image: url('../assets/ui/panel_frame.png') 100 stretch;
  background: linear-gradient(160deg, rgba(13, 9, 30, .96), rgba(6, 4, 18, .96)) padding-box;
  padding: 10px 12px; font-family: Georgia, serif;
  transform-origin: top right; transition: transform .18s ease, opacity .18s ease;
  box-shadow: 0 8px 26px rgba(0, 0, 0, .55); }
#settings-panel.closed { transform: scale(.7); opacity: 0; pointer-events: none; }
#set-title { display: flex; justify-content: space-between; align-items: center;
  font-size: 12px; letter-spacing: 1.5px; text-transform: uppercase; color: #a99ed4;
  margin-bottom: 9px; }
#set-close { background: none; border: none; color: #8f83bd; font-size: 14px; cursor: pointer; }
.set-row { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  padding: 9px 10px; margin-bottom: 6px; border-radius: 10px; cursor: pointer;
  background: #17102e; border: 1px solid #2c2250; color: #e8e2f7;
  font-family: Georgia, serif; font-size: 13px; }
.set-row:hover { border-color: #6a5acd; }
.set-row.off { opacity: .5; }
.set-row.off::after { content: 'off'; margin-left: auto; color: #8f83bd; font-size: 11px; }
.set-row:not(.off)::after { content: 'on'; margin-left: auto; color: #7fbf7f; font-size: 11px; }
#set-switch::after { content: '' !important; }
/* the contract address sits directly above the version line: quiet, mono, and
   wrapping anywhere, because it is 42 characters of noise on a narrow panel */
#set-ca-wrap { margin-top: 10px; padding-top: 9px; border-top: 1px solid #2a2450; text-align: center; }
.set-ca-label { color: #6f639c; font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; }
.set-ca { display: block; width: 100%; margin-top: 4px; padding: 5px 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9.5px; line-height: 1.45;
  letter-spacing: 0; word-break: break-all; text-align: center;
  color: #9d93cf; background: #15122c; border: 1px solid #2a2450; border-radius: 6px; cursor: pointer; }
.set-ca:hover { color: #cfc8ff; border-color: #4a4180; }
.set-ca.copied { color: #9fe8b5; border-color: #3f7d55; }
#set-version { text-align: center; color: #6f639c; font-size: 10.5px; margin-top: 4px; }
@media (max-width: 540px) {
  #hp-bar2 { height: 16px; margin-top: 8px; }
}

/* game surfaces are not a document: no tap flash, no text/image selection,
   no long-press callout menus */
html, body { -webkit-tap-highlight-color: transparent; }
#game, #hud, #select-screen {
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
#hud img, #select-screen img {
  -webkit-user-drag: none; user-drag: none; pointer-events: none;
}
#select-controls input { user-select: text; -webkit-user-select: text; }

#inv-close { margin-left: auto; background: none; border: none; color: #8f83bd;
  font-size: 15px; cursor: pointer; padding: 2px 6px; }
#inv-close:hover { color: #e8e2f7; }

/* bank panel: same skin as the shop */
/* WIDE ENOUGH FOR FIVE SLOTS AT EITHER SIZE. The two grids are five columns of
   var(--slot), which is 58px on a wide desktop - 360px was cut for a list of
   rows and left the last column short. */
#bank-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(calc(var(--slot, 46px) * 5 + 24px + 36px), calc(100vw - 24px));
  max-height: min(620px, 86vh); overflow-y: auto;
  background: rgba(12, 7, 28, .96); border: 1px solid #6a5acd; border-radius: 14px;
  padding: 12px 14px; z-index: 40; color: #e8e2f7; font-family: Georgia, serif; }
/* the title stays put while the two containers scroll under it, so the close
   button is never the thing you have to scroll back up to find */
#bank-title { position: sticky; top: -12px; z-index: 1; margin: -12px -14px 8px;
  padding: 12px 14px 8px; background: rgba(12, 7, 28, .98); }
#bank-panel.closed { display: none; }
#bank-title { font-size: 14px; letter-spacing: 2px; color: #9ad8ff;
  display: flex; align-items: center; gap: 10px; }
#bank-count2 { margin-left: auto; color: #8f83bd; font-size: 12px; }
#bank-close { background: none; border: none; color: #8f83bd; font-size: 15px;
  cursor: pointer; padding: 2px 4px; }
/* THE VAULT IS A GRID, like the pack it trades with. Both sides use .inv-slot,
   so a piece looks the same wherever it is sitting and the two halves read as
   two containers rather than a table and a form. */
.vault-sub { display: flex; align-items: baseline; justify-content: space-between;
  margin: 12px 0 6px; font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
  color: #8f83bd; }
.vault-sub span { letter-spacing: .04em; text-transform: none; font-size: 11px; color: #6b6191; }
#bank-items, #bank-deposit { display: grid; gap: 6px;
  grid-template-columns: repeat(5, var(--slot, 46px)); justify-content: center; }
#bank-items .inv-slot, #bank-deposit .inv-slot { cursor: pointer; }
#bank-items .inv-slot:not([data-rarity]), #bank-deposit .inv-slot:not([data-rarity]) { cursor: default; }
#bank-items .inv-slot[data-rarity]:hover, #bank-deposit .inv-slot[data-rarity]:hover {
  filter: brightness(1.25); }
/* an empty vault says so, in the space the grid leaves */
#bank-items[data-empty]:not([data-empty=""])::before {
  content: attr(data-empty); grid-column: 1 / -1; text-align: center;
  font-size: 12px; color: #6b6191; padding: 6px 0 2px; }

/* --- THE FORGE (craftData.js) - the vault's shape, an ember palette ------ */
/* A COLUMN WITH ONE SCROLLER, NOT ONE LONG SCROLL. The whole panel used to
   scroll, so #forge-detail - the Reroll and Ascend buttons for the piece you
   just clicked - sat below a twenty-row list and off the bottom of the box:
   "u click and u dont know what to do unless u scroll down". Now the list is
   the only thing that scrolls and the actions are pinned under it. */
#forge-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(380px, calc(100vw - 24px)); max-height: min(560px, 82vh);
  display: flex; flex-direction: column; overflow: hidden;
  background: rgba(20, 9, 8, .96); border: 1px solid #b8622e; border-radius: 14px;
  padding: 12px 14px; z-index: 40; color: #f3e6d8; font-family: Georgia, serif; }
#forge-panel.closed { display: none !important; }   /* beats the flex above */
#forge-title, #forge-pouch, #forge-tabs { flex: 0 0 auto; }
#forge-repairs, #forge-items { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
#forge-detail { flex: 0 0 auto; max-height: 46%; overflow-y: auto; }
/* the tabs swap these two, and a flex child still obeys [hidden] - but this
   panel has been bitten by an author display rule beating the attribute
   before (see #death-flash), so both are pinned down explicitly */
#forge-items[hidden], #forge-detail[hidden] { display: none !important; }
#forge-tabs { display: flex; gap: 6px; margin: 2px 0 8px; }
.fgtab { flex: 1; padding: 6px 0; border-radius: 9px; font-family: Georgia, serif;
  font-size: 12px; letter-spacing: 1.4px; text-transform: uppercase; cursor: pointer;
  background: rgba(58, 28, 16, .55); border: 1px solid #7a4326; color: #c9a58a; }
.fgtab.active { color: #ffd166; border-color: #ffb066; background: #3a1c10; }
.fg-none { font-size: 12px; color: #c9a58a; line-height: 1.6; padding: 4px 2px 2px; }
#forge-title { font-size: 14px; letter-spacing: 2px; color: #ffb066;
  display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
#forge-gold { margin-left: auto; color: #ffd166; font-size: 12px; }
#forge-close { background: none; border: none; color: #c9a58a; font-size: 15px; cursor: pointer; padding: 2px 4px; }
#forge-pouch { font-size: 11.5px; color: #c9a58a; margin-bottom: 8px; line-height: 1.5; }
#forge-pouch b { color: #ffd166; }
#forge-items .shop-row { cursor: pointer; align-items: flex-start; gap: 10px; padding: 8px 6px; }
#forge-items .shop-row.on { outline: 1px solid #ffb066; }
#forge-items .shop-name { line-height: 1.4; }
#forge-items .fg-title { font-size: 13.5px; }
#forge-items .fg-worn { font-size: 9.5px; letter-spacing: 1.2px; text-transform: uppercase;
  color: #ffb066; margin-left: 7px; vertical-align: 1px; }
#forge-items .shop-name small { display: block; margin-top: 3px; color: #b9a48d; }
#forge-items .shop-name small.fg-mods { color: #c9bfff; }
#forge-detail { margin-top: 12px; padding-top: 10px; border-top: 1px solid #5a2e1a; }
.fg-name { font-size: 15px; margin: 0 0 8px; }
/* one column of text, one column of buttons - every Reroll lines up */
.fg-mod { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center;
  padding: 7px 0; font-size: 12.5px; border-bottom: 1px solid rgba(184, 98, 46, .18); }
.fg-mod span { color: #e8d8c8; line-height: 1.35; }
.fg-btn { background: #3a1c10; border: 1px solid #b8622e; color: #ffd7b0; border-radius: 8px;
  padding: 6px 10px; font: inherit; font-size: 11.5px; cursor: pointer; white-space: nowrap;
  min-width: 172px; text-align: center; }
.fg-btn.cant { opacity: .45; cursor: default; }
.fg-btn.ascend { display: block; width: 100%; margin-top: 10px; padding: 8px 10px; font-size: 12.5px;
  background: #4a2a10; border-color: #ffb066; color: #ffe2b8; }
.fg-hint { font-size: 11px; color: #b09880; margin-top: 10px; line-height: 1.55; }
/* the pouch strip under the bag grid */
#pouch { margin-top: 8px; font-size: 11px; color: #a99ed4; line-height: 1.5; }
#pouch b { color: #ffd166; }

/* cast bar above the hotbar */
#cast-bar { position: fixed; bottom: 74px; left: 50%; transform: translateX(-50%);
  width: 200px; height: 17px; border-radius: 9px; background: #12091e;
  border: 1.5px solid #cfa54e; overflow: hidden; z-index: 31;
  box-shadow: 0 0 12px rgba(212, 169, 79, .35), inset 0 1px 4px rgba(0,0,0,.7); }
#cast-fill { height: 100%; width: 0; transition: width .28s linear;
  background: linear-gradient(90deg, #8a6cff, #9ad8ff 60%, #d8f4ff);
  box-shadow: 0 0 10px rgba(154, 216, 255, .8); }
#cast-label { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-size: 10.5px; color: #fff; letter-spacing: 1px;
  font-family: Georgia, serif; text-shadow: 0 1px 3px rgba(0,0,0,.8); }
.hb-slot.casting { border-color: #9ad8ff; box-shadow: 0 0 12px rgba(154, 216, 255, .5); }
@media (max-width: 540px) { #cast-bar { bottom: 66px; } }

/* loading screen between select and world */
#loading { position: fixed; inset: 0; z-index: 60; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 18px;
  background: radial-gradient(ellipse at 50% 35%, #1a1040 0%, #05030f 70%);
  font-family: Georgia, serif; }
#loading[hidden] { display: none; }
#load-emblem { width: 84px; height: 84px; border-radius: 22%;
  box-shadow: 0 0 26px rgba(122, 160, 255, .55);
  animation: load-pulse 1.8s ease-in-out infinite; }
@keyframes load-pulse { 0%, 100% { transform: scale(1); box-shadow: 0 0 18px rgba(122, 160, 255, .4); }
  50% { transform: scale(1.06); box-shadow: 0 0 38px rgba(122, 160, 255, .75); } }
#load-title { font-size: 24px; letter-spacing: 8px; text-indent: 8px;
  background: linear-gradient(180deg, #fff8e9 20%, #ffd166 55%, #b8860b 90%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 0 14px rgba(255, 209, 102, .3)); }
#load-bar { width: min(300px, 70vw); height: 9px; border-radius: 6px;
  background: #170d24; border: 1px solid #7a6430; overflow: hidden; }
#load-fill { height: 100%; width: 0; transition: width .2s ease;
  background: linear-gradient(90deg, #ffd166, #fff3c9 60%, #ffd166); }
#load-tip { color: #8f83bd; font-size: 13px; font-style: italic; }

/* --- cinematic dialogue (FF-style portrait scenes) --- */
#cine {
  position: fixed; inset: 0; z-index: 60; cursor: pointer;
  -webkit-user-select: none; user-select: none; overflow: hidden;
  background: linear-gradient(180deg, rgba(2,4,10,.25), rgba(2,4,10,0) 30%,
              rgba(2,4,10,0) 42%, rgba(2,4,10,.72) 68%, rgba(2,4,10,.96) 88%);
  animation: cine-in .28s ease-out;
}
@keyframes cine-in { from { opacity: 0; } to { opacity: 1; } }
#cine-portrait {
  /* Ordinary speakers get a BUST: object-fit cover, with the box aspect set
     per sprite in hud.js so every character crops to the same share of its own
     body. The boss keeps object-fit contain below - his whole figure and that
     greatsword are the point of his scene. */
  position: absolute; right: 1vw; bottom: 0; height: 46vh; max-width: 46vw;
  object-fit: cover; object-position: 50% 0%;
  /* A rim, not a border: stacked drop-shadows trace the alpha silhouette so
     the outline follows the character instead of boxing the image. */
  filter: drop-shadow(1.5px 0 0 #140f26) drop-shadow(-1.5px 0 0 #140f26)
          drop-shadow(0 1.5px 0 #140f26) drop-shadow(0 -1.5px 0 #140f26);
}
#cine-portrait.enter { animation: cine-enter .34s cubic-bezier(.2, .8, .3, 1) both; }
/* wide bust crops (the boss): the 70vw cap shrinks a square image to a
   fraction of a tall one's height, so the wide lane lets him tower. Hung
   well off the right edge so the dialogue text keeps its column. */
/* the boss keeps his full figure - the greatsword and the silhouette are the
   whole reason that scene lands */
#cine-portrait.boss { max-width: 96vw; right: -6vw; height: 62vh; bottom: 0;
  aspect-ratio: auto; object-fit: contain; object-position: bottom right; }
@keyframes cine-enter {
  0% { opacity: 0; transform: translateY(34px) scale(.97); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
#cine-band {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 14px 18px calc(20px + env(safe-area-inset-bottom)) 22px;
  min-height: 21vh; display: flex; flex-direction: column; gap: 8px;
}
#cine-name {
  display: flex; align-items: center; gap: 12px;
  color: #6fd7ff; font-size: 15px; letter-spacing: .06em;
}
#cine-name::after {
  content: ''; flex: 1; height: 1px; max-width: 46vw;
  background: linear-gradient(90deg, rgba(111,215,255,.85), rgba(111,215,255,0));
}
#cine-text {
  color: #f2f6ff; font-size: 17px; line-height: 1.5; max-width: 66vw;
  padding-left: 26px; min-height: 3em;
  text-shadow: 0 1px 3px rgba(0,0,0,.9);
}
#cine-next {
  align-self: center; color: #6fd7ff; font-size: 13px;
  animation: cine-bob 1.1s ease-in-out infinite;
}
#cine-next[hidden] { display: none; }
@keyframes cine-bob {
  0%, 100% { transform: translateY(0); opacity: .95; }
  50% { transform: translateY(4px); opacity: .45; }
}
@media (max-width: 700px) {
  #cine-portrait { right: 4px; bottom: 0; height: 30vh; max-width: 52vw; }
  #cine-text { max-width: 60vw; font-size: 15px; padding-left: 8px; }
  #cine-name::after { max-width: 30vw; }
  #cine-next { align-self: flex-start; margin-left: 24vw; }
}

/* --- quest journal (tap the tracker chip) --- */
#quest-tracker { cursor: pointer; }
#quest-log { position: absolute; top: 128px; left: 14px; width: 302px; z-index: 6;
  max-height: calc(100dvh - 150px); overflow-y: auto;
  background: linear-gradient(160deg, rgba(18, 10, 40, .95), rgba(8, 5, 22, .95));
  border: 1px solid #4b3d7a; border-radius: 14px; padding: 12px 14px;
  box-shadow: 0 6px 22px rgba(0, 0, 0, .5);
  transform-origin: top left; transition: transform .18s ease, opacity .18s ease;
  font-size: 12.5px; }
#quest-log.closed { transform: scale(.7); opacity: 0; pointer-events: none; }
#ql-head { display: flex; justify-content: space-between; align-items: center;
  font-size: 12px; color: #a99ed4; letter-spacing: 1.5px; text-transform: uppercase;
  margin-bottom: 9px; }
#ql-close { background: none; border: none; color: #8f83bd; font-size: 14px;
  cursor: pointer; padding: 0 0 0 8px; }
.ql-row { display: flex; gap: 9px; align-items: baseline; padding: 6px 0;
  border-top: 1px solid #241a45; }
.ql-row:first-child { border-top: none; }
.ql-ico { flex-shrink: 0; font-size: 13px; width: 18px; text-align: center; }
/* the seal carries the state, so it is coloured rather than pictorial */
.ql-row.done .ql-ico { color: #7fbf7f; }
.ql-row.active .ql-ico { color: #ffd166; text-shadow: 0 0 7px rgba(255, 209, 102, .7); }
.ql-row.ready .ql-ico { color: #9ad8ff; text-shadow: 0 0 7px rgba(154, 216, 255, .6); }
.ql-row.locked .ql-ico { color: #6b6190; }
.ql-txt { min-width: 0; flex: 1; }
.ql-txt b { display: block; font-size: 12.5px; color: #e8e2f7; }
.ql-row.done .ql-txt b { color: #7fbf7f; }
.ql-row.locked { opacity: .5; }
.ql-txt small { display: block; color: #8f83bd; font-size: 11px; margin-top: 1px; }
.ql-row.active .ql-txt b { color: #ffd166; }

/* --- the current objective, on every map surface --- */
/* A quest target reads as a mob you WANT: the mob red, ringed in the quest
   gold the tracker and the giver dots already use, and breathing so the eye
   finds it among a dozen ordinary dots. */
.map-dot.quest-mob { background: #ff7a6b; box-shadow: 0 0 0 2px #ffd166,
  0 0 8px rgba(255, 209, 102, .9); animation: qmob 1.5s ease-in-out infinite; }
@keyframes qmob { 0%, 100% { box-shadow: 0 0 0 2px #ffd166, 0 0 7px rgba(255, 209, 102, .8); }
  50% { box-shadow: 0 0 0 3px #ffd166, 0 0 13px rgba(255, 209, 102, 1); } }
.w-node.objective i { box-shadow: 0 0 0 2px #ffd166, 0 0 14px rgba(255, 209, 102, .85); }
.w-node .w-seal { position: absolute; top: -13px; left: 50%; transform: translateX(-50%);
  font-size: 12px; font-style: normal; color: #ffd166;
  text-shadow: 0 0 8px rgba(255, 209, 102, .95); animation: qmob 1.5s ease-in-out infinite; }
.w-node.objective span { color: #ffd166; }


/* --- quest journal: acts, and the fog over what is ahead --- */
.ql-act { display: flex; align-items: baseline; gap: 8px; cursor: pointer;
  margin: 10px 0 2px; padding: 5px 7px; border-radius: 7px;
  background: rgba(75, 61, 122, .22); border: 1px solid #3a2f63; }
.ql-act:first-child { margin-top: 0; }
.ql-act-n { flex-shrink: 0; width: 22px; font-size: 10.5px; letter-spacing: 1px;
  color: #8f83bd; text-align: center; }
.ql-act-t { flex: 1; min-width: 0; font-size: 12px; color: #e8e2f7;
  letter-spacing: .6px; }
.ql-act-c { flex-shrink: 0; font-size: 10.5px; color: #8f83bd; }
.ql-act.done { background: rgba(60, 100, 60, .16); border-color: #33532f; }
.ql-act.done .ql-act-t { color: #7fbf7f; }
.ql-act.done .ql-act-n::after { content: ' \2713'; }
/* a sealed act names nothing - it only says the road goes on, and how far */
.ql-act.sealed { background: rgba(20, 14, 40, .5); border-color: #2b2350;
  cursor: default; opacity: .72; }
.ql-act.sealed .ql-act-t { color: #6b6190; font-style: italic; }
.ql-act.open { border-color: #6b5aa8; }
.ql-row.fog { opacity: .42; }
.ql-row.fog .ql-ico { color: #6b6190; }
.ql-row.fog .ql-txt b { color: #8f83bd; letter-spacing: 2px; }
/* --- daily bounties ---
   The board is the one block in the journal that changes on its own, so it
   gets the one colour nothing else in here uses. Everything structural is
   borrowed: .ql-act for the header, .ql-row for the rows. The only new
   FURNITURE is the claim button, and its size is set by the thumb - 44px is
   the floor everywhere in this game, and a row in a scrolling list is exactly
   where a smaller one gets mis-tapped. */
.ql-act.ql-bounty { background: rgba(120, 80, 40, .2); border-color: #6a4a2a; }
.ql-act.ql-bounty .ql-act-n { color: #ffb347; }
.ql-act.ql-bounty.sealed { background: rgba(20, 14, 40, .5); border-color: #2b2350; }
.ql-act.ql-bounty.sealed .ql-act-n { color: #6b6190; }
.ql-brow { align-items: center; }
.ql-brow.ready .ql-txt b { color: #9ad8ff; }
.ql-claim { flex-shrink: 0; min-height: 44px; padding: 0 14px; border-radius: 8px;
  border: 1px solid #6a4a2a; background: rgba(255, 179, 71, .16); color: #ffb347;
  font: inherit; font-size: 12px; letter-spacing: .6px; cursor: pointer; }
.ql-claim:active { background: rgba(255, 179, 71, .3); }
.ql-pay { color: #ffb347 !important; }
.ql-bnote { margin-top: 7px; font-size: 10.5px; color: #8f83bd; letter-spacing: .5px; }

.ql-foot { margin-top: 11px; padding-top: 8px; border-top: 1px solid #241a45;
  font-size: 10.5px; color: #8f83bd; text-align: center; letter-spacing: .8px; }



/* touch layout: lift the cast bar and battle log above the fan */
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb) #inv-panel { right: 12px; }
  body:not(.kb) #cast-bar { bottom: calc(20vh + 118px); }
  /* The log lives at the bottom centre, ALWAYS - it never moves. The skill
     fan ends ~150px higher and the attack button ends above this band too,
     so the low lane is permanently clear; the earlier lift-in-combat made
     the whole HUD look like it was shuffling. */
  body:not(.kb) #log { left: 50%; transform: translateX(-50%); text-align: center;
    width: min(360px, calc(100vw - 44px));
    bottom: max(22px, calc(env(safe-area-inset-bottom) + 16px));
    font-size: 11px; line-height: 15px; max-height: 45px; }
  body:not(.kb) #log div { padding: 0; }
  body:not(.kb) #toasts { bottom: 186px; }
}


/* --- target frame: what you're fighting, top centre --- */
/* Docked under the player portrait (Alan's call): your target's name and
   health live where every MMO puts them, not floating centre-screen. */
#target-frame { position: absolute; top: 96px; left: 14px; transform: none;
  min-width: 190px; max-width: 40vw; z-index: 7; padding: 8px 14px 9px;
  text-align: left;
  background: linear-gradient(160deg, rgba(40, 14, 22, .9), rgba(16, 6, 14, .9));
  border: 1px solid #a04858; border-radius: 12px; text-align: center;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .5); pointer-events: none; }
#tf-name { font-size: 13px; color: #ffd7dd; letter-spacing: .5px; margin-bottom: 5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#tf-hp { position: relative; height: 10px; border-radius: 5px; background: #170d14;
  border: 1px solid #5c2a36; overflow: hidden; }
#tf-fill { height: 100%; width: 100%; border-radius: 4px;
  background: linear-gradient(90deg, #ff5d73, #d92645 60%, #a3122e);
  transition: width .22s ease; }
#tf-num { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-size: 8.5px; font-weight: 700; color: #ffe3e8;
  text-shadow: 0 1px 2px #000; letter-spacing: .5px; }
@media (max-width: 700px) { #target-frame { top: 108px; left: 10px; min-width: 170px; } }
/* DESKTOP: TOP CENTRE. Docked under the portrait it had nothing to align
   to - its left edge off the portrait's, its top on the portrait's bottom,
   the ELITE seal at -9px colliding with it (Alan: "why on desktop the mob
   hp bar still on the left and unaligned properly?"). With a keyboard the
   eye is on the fight mid-screen; the frame goes where every MMO puts it.
   The phone keeps the dock - its centre top is the quest banner.
   Keyed on the POINTER, not body.kb: kb only flips on a keypress, and a
   mouse player (double-click to move) never presses one - 3.0.13 shipped
   this rule and Alan's frame never moved. */
@media (hover: hover) and (pointer: fine) {
  #target-frame { top: 14px; left: 50%; transform: translateX(-50%);
    min-width: 230px; max-width: 34vw; }
}
/* ROOM ON A DESKTOP (Alan: "why do they need to scroll when they have so
   much desktop space"). The character and pack panels were cut to a phone's
   width and kept that width on a 27-inch monitor, so the skill tree and the
   passives scrolled inside a 292px column. With a mouse and a wide window
   they take the space: a wider panel, larger type, passives two abreast,
   bigger pack slots, and the height the window actually has. */
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) {
  #stats-panel { width: 540px; font-size: 14px; max-height: calc(100dvh - 150px); padding: 14px 18px; }
  #stats-panel #pv-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; }   /* outranks the flex column rule below */
  #stats-panel .pv-node { margin: 0; }
  .tmap-node { width: 100px; }
  .tmap-node .tmn-name { font-size: 10px; }
  #inv-panel { max-height: min(84vh, 920px); }
  /* the pack's slot size for this layout lives at the END of the file with
     the other --slot values - see THE SLOT AND ITS TRACK ARE ONE NUMBER */
}
/* THE ELITE FRAME (monsterData.ELITE). Silver where a boss is gold: a double
   rim, a cold glow, and an ELITE seal over the top edge - the same sign the
   body wears in the world (1.3x, silver aura), said again where you read the
   fight. Alan's reference was WoW's dragon frame; ours is drawn, not pasted,
   so it scales to a phone. */
#target-frame.elite { border: 2px solid #d5dcea;
  box-shadow: 0 0 0 2px #5b6478, 0 0 18px rgba(213, 220, 234, .5), inset 0 0 12px rgba(213, 220, 234, .12);
  background: linear-gradient(160deg, rgba(34, 36, 52, .94), rgba(14, 12, 24, .94)); }
#target-frame.elite #tf-name { color: #eef2ff; letter-spacing: .8px; }
#target-frame.elite #tf-hp { border-color: #7c8699; }
#target-frame.elite::before { content: 'ELITE'; position: absolute; top: -9px; left: 50%;
  transform: translateX(-50%); background: #1a1c26; border: 1px solid #d5dcea; color: #eef2ff;
  font-size: 9px; letter-spacing: 2.2px; padding: 1px 8px; border-radius: 6px; line-height: 1.3; }
.threat.elite { border-color: #d5dcea; box-shadow: 0 0 8px rgba(213, 220, 234, .35); }
.threat.elite .th-name { color: #eef2ff; }

/* --- WHO ELSE IS HITTING YOU ---------------------------------------------
   A stack of pills under the target frame, one per aggroed monster that is
   not your current target. Alan: "if i get attacked by 2 mobs can it show
   multiple health bars of who is attacking me and i can maybe tap on either
   one to focus on them". Tappable - this is the ONE part of the combat HUD
   that takes a touch, so it carries pointer-events while its parents do not. */
#threat-list { position: absolute; top: 152px; left: 14px; z-index: 7;
  display: flex; flex-direction: column; gap: 5px; max-width: 44vw;
  pointer-events: none; }
.threat {
  display: flex; align-items: center; gap: 7px; padding: 4px 9px 5px;
  background: linear-gradient(160deg, rgba(38, 16, 12, .86), rgba(14, 6, 10, .86));
  border: 1px solid #8d5230; border-radius: 10px; cursor: pointer;
  pointer-events: auto; -webkit-tap-highlight-color: transparent;
  box-shadow: 0 3px 10px rgba(0, 0, 0, .45); }
.threat:active { transform: scale(.97); border-color: #e2a05e; }
/* the one you are hitting: a steady gold rule and a word, never a blink */
.threat.is-target { border-color: #ffd166; background:
  linear-gradient(160deg, rgba(58, 34, 12, .9), rgba(20, 10, 6, .9)); }
.threat.is-target .th-fill { background: linear-gradient(90deg, #ffd166, #e08a2c); }
.th-tag { font-size: 8px; font-weight: 800; letter-spacing: 1px; color: #16100a;
  background: #ffd166; border-radius: 4px; padding: 1px 4px 2px; flex: none; }
/* the one that is hitting you HARDEST reads first - it is the one to answer */
.threat .th-name { font-size: 11px; color: #ffdcc4; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 26vw; }
.threat .th-hp { position: relative; width: 46px; height: 6px; flex: none;
  border-radius: 3px; background: #1a0e0b; border: 1px solid #59301d;
  overflow: hidden; }
.threat .th-fill { height: 100%; border-radius: 2px;
  background: linear-gradient(90deg, #ff8a5d, #e0492c); transition: width .22s ease; }
@media (max-width: 700px) {
  /* thumb-sized rows on a phone, and clear of the target frame above them */
  #threat-list { top: 168px; left: 10px; max-width: 58vw; gap: 6px; }
  /* ~44px tall: a thumb target, not a mouse one */
  .threat { padding: 11px 12px 12px; border-radius: 12px; }
  .threat .th-name { font-size: 12px; max-width: 30vw; }
  .th-tag { font-size: 9px; padding: 2px 5px 3px; }
  .threat .th-hp { width: 54px; height: 7px; }
}

/* --- duel: your hp, the opponent's, and the 3-2-1 countdown --- */
#duel-frame { position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
  min-width: 220px; max-width: 64vw; z-index: 7; padding: 10px 14px 9px;
  background: linear-gradient(160deg, rgba(32, 12, 46, .92), rgba(13, 5, 22, .92));
  border: 1px solid #6a3fae; border-radius: 12px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .5); pointer-events: none; }
/* The bell, centre screen. It used to be a 22px line inside the duel panel,
   which is the same place the game puts a target's name - so the moment the
   fight actually starts read as a HUD label rather than an event. It is over
   in three seconds and nothing else is happening yet, so it can afford the
   whole screen. pointer-events: none throughout - the arena is live
   underneath it and a click must never land on the number. */
#duel-countdown { position: fixed; inset: 0; z-index: 40;
  display: grid; place-items: center; pointer-events: none; }
#duel-countdown[hidden] { display: none; }
.dc-beat { font-weight: 900; line-height: 1; letter-spacing: 2px;
  color: #ffd166; text-shadow: 0 0 26px rgba(255, 180, 60, .85),
  0 6px 18px rgba(0, 0, 0, .75); will-change: transform, opacity; }
/* a struck number: lands hard, then falls away while the next one lands */
.dc-beat.num { font-size: clamp(88px, 22vw, 200px);
  animation: dc-punch 1s cubic-bezier(.2, .9, .25, 1) both; }
/* FIGHT! is wider than it is tall, so it gets its own smaller scale and a
   held finish rather than a fade to nothing */
.dc-beat.go { font-size: clamp(44px, 11vw, 104px); color: #ff5f52;
  text-shadow: 0 0 30px rgba(255, 70, 50, .9), 0 6px 18px rgba(0, 0, 0, .8);
  animation: dc-go .9s cubic-bezier(.15, .95, .2, 1) both; }
@keyframes dc-punch {
  0%   { transform: scale(2.1); opacity: 0; }
  14%  { transform: scale(1); opacity: 1; }
  70%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(.72); opacity: 0; }
}
@keyframes dc-go {
  0%   { transform: scale(.4) rotate(-6deg); opacity: 0; }
  18%  { transform: scale(1.18) rotate(0deg); opacity: 1; }
  32%  { transform: scale(1); }
  75%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(1.5); opacity: 0; }
}
/* THE RESULT, at the size of the bell. A duel used to end in a line of chat -
   the one moment the whole thing exists for was the quietest thing on screen.
   Wider than a number, so it takes the same treatment at a smaller scale. */
.dc-beat.result { font-size: clamp(40px, 10vw, 96px); letter-spacing: 6px;
  animation: dc-result 2.6s cubic-bezier(.12, .9, .2, 1) both; }
.dc-beat.result.win  { color: #ffd97a;
  text-shadow: 0 0 34px rgba(255, 200, 90, .9), 0 6px 20px rgba(0, 0, 0, .8); }
.dc-beat.result.loss { color: #8fa2c4;
  text-shadow: 0 0 26px rgba(120, 150, 200, .6), 0 6px 20px rgba(0, 0, 0, .85); }
.dc-beat.result.draw { color: #cdc3e6;
  text-shadow: 0 0 24px rgba(190, 175, 230, .55), 0 6px 20px rgba(0, 0, 0, .8); }
/* the rating line, under the word - the number people actually came for */
.dc-sub { margin-top: 10px; font-size: clamp(14px, 3.4vw, 22px); font-weight: 700;
  letter-spacing: 2px; color: #e8e2f7; text-align: center;
  text-shadow: 0 2px 10px rgba(0, 0, 0, .9);
  animation: dc-sub-in 2.6s ease both; }
@keyframes dc-result {
  0%   { transform: scale(.55); opacity: 0; }
  10%  { transform: scale(1.06); opacity: 1; }
  20%  { transform: scale(1); }
  82%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(1.12); opacity: 0; }
}
@keyframes dc-sub-in {
  0%, 12% { opacity: 0; transform: translateY(6px); }
  24%     { opacity: 1; transform: translateY(0); }
  82%     { opacity: 1; }
  100%    { opacity: 0; }
}
/* Forfeit: deliberately the quietest thing in the duel panel until it is
   armed, and unmistakable once it is. It is the only control here, so it also
   has to take pointer events back - #duel-frame turns them off. */
#duel-quit { display: block; margin: 8px auto 0; pointer-events: auto;
  padding: 4px 14px; font: inherit; font-size: 11px; letter-spacing: 1px;
  color: #b9a8cf; background: rgba(20, 10, 32, .8);
  border: 1px solid #4b3570; border-radius: 8px; cursor: pointer; }
#duel-quit:hover { color: #e8e2f7; border-color: #6a3fae; }
#duel-quit.armed { color: #23121a; background: #e06a5a; border-color: #ff8f7d;
  font-weight: 700; animation: dq-pulse .9s ease-in-out infinite; }
@keyframes dq-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
@media (prefers-reduced-motion: reduce) {
  .dc-beat.num, .dc-beat.go, .dc-beat.result { animation: dc-fade .9s ease both; }
  #duel-quit.armed { animation: none; }
  @keyframes dc-fade { 0% { opacity: 0; } 12% { opacity: 1; }
                       75% { opacity: 1; } 100% { opacity: 0; } }
}
.duel-row { display: flex; align-items: center; gap: 8px; margin-top: 5px; }
.duel-row:first-of-type { margin-top: 0; }
.duel-label { font-size: 11px; color: #e8e2f7; min-width: 44px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.duel-hp { position: relative; flex: 1; height: 10px; border-radius: 5px;
  background: #150d24; border: 1px solid #4a3178; overflow: hidden; }
.duel-hp > div { height: 100%; width: 100%; border-radius: 4px;
  transition: width .18s ease; }
.duel-mine .duel-hp > div { background: linear-gradient(90deg, #6bdc7c, #2ecc40 60%, #1c9e30); }
.duel-foe .duel-hp > div { background: linear-gradient(90deg, #ff5d73, #d92645 60%, #a3122e); }
.duel-hp span { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-size: 8.5px; font-weight: 700; color: #fff;
  text-shadow: 0 1px 2px #000; letter-spacing: .5px; }
@media (max-width: 700px) { #duel-frame { top: 108px; min-width: 190px; } }

/* --- first-minutes hint bubble --- */
/* ABOVE the hotbar (bottom 12px + a 64px slot): at 58px the bubble lay across
   the skill icons the moment it had two lines (Alan: "ensure it doesnt hide
   the bars"). Phones keep their own offset below - the hotbar is a column
   on the right there. */
#hint-bubble { position: fixed; bottom: max(116px, calc(env(safe-area-inset-bottom) + 104px)); left: 50%;
  transform: translateX(-50%); max-width: 74vw; z-index: 45; padding: 8px 30px 8px 14px;
  text-align: center; background: rgba(14, 8, 32, .94); border: 1px solid #8a6cff;
  border-radius: 14px; color: #e8e2f7; font-family: Georgia, serif; font-size: 12.5px;
  cursor: pointer; box-shadow: 0 0 18px rgba(138, 108, 255, .35);
  animation: hint-pulse 2.2s ease-in-out infinite; }
#hint-bubble::after { content: '✕'; position: absolute; top: 50%; right: 10px;
  transform: translateY(-50%); color: #8f83bd; font-size: 12px; }
@keyframes hint-pulse { 0%, 100% { box-shadow: 0 0 10px rgba(138, 108, 255, .25); }
  50% { box-shadow: 0 0 24px rgba(138, 108, 255, .55); } }
/* clears the battle log's idle stack (22px up, 45px tall) instead of sitting on it */
@media (max-width: 700px) { #hint-bubble { bottom: max(78px, calc(env(safe-area-inset-bottom) + 72px)); } }


/* --- tutorial bubble: the skip has to be right there --- */
/* The bubble used to be dismiss-on-tap with an X. That clears ONE tip; a
   player who does not want tips wants them all gone, in one action. */
#hint-bubble .hint-text { display: block; }
#hint-bubble .hint-skip { display: block; margin: 6px auto 0; padding: 3px 12px;
  background: rgba(138, 108, 255, .18); border: 1px solid #6b5aa8; border-radius: 9px;
  color: #c9c0ec; font-family: Georgia, serif; font-size: 11px; cursor: pointer; }
#hint-bubble .hint-skip:hover { background: rgba(138, 108, 255, .34); color: #fff; }

/* --- how to play --- */
#help-panel { position: absolute; top: 90px; left: 50%; transform: translateX(-50%);
  width: min(430px, 92vw); max-height: min(70dvh, 560px); overflow-y: auto; z-index: 30;
  background: linear-gradient(160deg, rgba(18, 10, 40, .97), rgba(8, 5, 22, .97));
  border: 1px solid #4b3d7a; border-radius: 14px; padding: 14px 16px;
  box-shadow: 0 10px 34px rgba(0, 0, 0, .6); font-size: 12.5px; color: #e8e2f7; }
#help-panel.closed { display: none; }
#help-title { display: flex; justify-content: space-between; align-items: center;
  font-size: 12px; color: #a99ed4; letter-spacing: 1.6px; text-transform: uppercase;
  margin-bottom: 10px; }
#help-close { background: none; border: none; color: #8f83bd; font-size: 14px; cursor: pointer; }
.help-h { margin: 12px 0 5px; color: #ffd166; font-size: 11px; letter-spacing: 1.3px;
  text-transform: uppercase; }
.help-h:first-child { margin-top: 0; }
.help-row { display: flex; gap: 10px; padding: 4px 0; border-top: 1px solid #241a45;
  align-items: baseline; }
.help-row b { flex: 0 0 40%; color: #e8e2f7; font-weight: 600; }
.help-row span { flex: 1; color: #a99ed4; }
.set-row.off { opacity: .45; }

#world-note { margin-top: 8px; text-align: center; font-size: 11.5px; color: #8f83bd; }
#world-note.warn { color: #ffd166; }
/* an unreachable destination still shows, but reads as out of reach */
.w-node.far { opacity: .45; cursor: default; }

/* --- the waypoint prompt --- */
#wp-prompt { position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 172px; z-index: 30; padding: 9px 20px; border-radius: 13px;
  border: 1px solid #ffd166; background: rgba(30, 20, 8, .88); color: #ffe9a8;
  font-family: Georgia, serif; font-size: 14px; cursor: pointer;
  box-shadow: 0 0 18px rgba(255, 209, 102, .45);
  animation: wp-breathe 2.1s ease-in-out infinite; }
#wp-prompt[hidden] { display: none; }
#door-prompt { position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 172px; z-index: 30; padding: 9px 20px; border-radius: 13px;
  border: 1px solid #8a6cff; background: rgba(14, 10, 30, .9); color: #dcd3ff;
  font-family: Georgia, serif; font-size: 14px; cursor: pointer;
  box-shadow: 0 0 16px rgba(138, 108, 255, .35); }
#door-prompt[hidden] { display: none; }
#door-prompt:hover { background: rgba(30, 22, 56, .96); color: #fff; }
/* if a crystal and a door are both in reach, they stack rather than overlap */
body:has(#wp-prompt:not([hidden])) #door-prompt { bottom: 218px; }
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb) #door-prompt { bottom: 210px; font-size: 13px; }
  body:not(.kb):has(#wp-prompt:not([hidden])) #door-prompt { bottom: 254px; }
}
#wp-prompt:hover { background: rgba(52, 36, 12, .95); color: #fff; }
@keyframes wp-breathe { 0%, 100% { box-shadow: 0 0 12px rgba(255, 209, 102, .35); }
  50% { box-shadow: 0 0 26px rgba(255, 209, 102, .7); } }
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb) #wp-prompt { bottom: 210px; font-size: 13px; }
}

/* --- loot flying into the bag --- */
.loot-fly { position: fixed; z-index: 80; font-size: 22px; pointer-events: none;
  transition: transform .55s cubic-bezier(.5, -0.2, .9, .6), opacity .55s ease; }

/* quest-giver dot on the maps */
.map-dot.quest { background: #ffd166; box-shadow: 0 0 6px #ffd166; }
.map-dot.portal { background: #7fd0ff; box-shadow: 0 0 5px #7fd0ff; }
.map-dot.party { width: 9px; height: 9px; background: #6ef29b;
  border: 1.5px solid #1f7a44; box-shadow: 0 0 6px #6ef29b; }
.map-dot.small.party { width: 7px; height: 7px; }
#map-legend .dot.quest { background: #ffd166; }
#map-legend .dot.portal { background: #7fd0ff; }
/* the legend swatches are scoped to #map-legend, so the live marker's own
   rule does not reach them - the target swatch needs saying twice */
#map-legend .dot.quest-mob { background: #ff7a6b; box-shadow: 0 0 0 2px #ffd166;
  margin-left: 3px; }

/* --- the quest pin --- */
/* First attempt was a 13px badge with an 11px "!" in it. On a 300px map that
   is a speck: Alan could not see it AT ALL. A marker you have to hunt for is
   not a marker - this is the thing you opened the map to find, so it is drawn
   as a proper pin, sized to be spotted at a glance and anchored by a point at
   the position it marks. */
.map-dot.glyph { width: 22px; height: 22px; border-radius: 50% 50% 50% 0;
  transform: translate(-50%, -100%) rotate(-45deg);
  background: linear-gradient(145deg, #ffe9a8, #ffc42e);
  border: 2px solid #4a3208; box-sizing: border-box;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .65), 0 0 12px rgba(255, 209, 102, .8);
  z-index: 6; animation: qpin 1.7s ease-in-out infinite; }
/* the glyph rides upright inside a pin that is itself rotated 45deg */
.map-dot.glyph::before { content: attr(data-mark); position: absolute;
  inset: 0; transform: rotate(45deg); display: flex;
  align-items: center; justify-content: center;
  font: 800 14px/1 Georgia, serif; color: #2a1c05; }
/* The dock's dots are at NATIVE size (add() maps them into the zoom window
   itself - "dots at native size"), so this IS the on-screen size. The old
   6.5px was written when the dots sat inside a 2.4x-scaled container and it
   drew a 9px speck once they no longer did (Alan: "too small and hard to
   see"). Measured with getBoundingClientRect on the phone viewport. */
.map-dot.small.glyph { width: 16px; height: 16px; border-width: 1.5px; }
.map-dot.small.glyph::before { font-size: 10px; }
@keyframes qpin {
  0%, 100% { box-shadow: 0 2px 6px rgba(0,0,0,.65), 0 0 9px rgba(255,209,102,.7); }
  50% { box-shadow: 0 2px 6px rgba(0,0,0,.65), 0 0 18px rgba(255,209,102,1); } }
#map-legend .dot.glyph { background: linear-gradient(145deg, #ffe9a8, #ffc42e);
  color: #2a1c05; width: 14px; height: 14px; font: 800 11px/14px Georgia, serif;
  text-align: center; border-radius: 50%; border: 1px solid #4a3208;
  box-sizing: border-box; }




/* ONE CLUSTER, ONE STYLE. The four HUD buttons had each been hand-styled where
   it was added: three sizes, two border colours, two hover treatments and
   spacing that did not match (Alan: "why are their styles all different and
   size and spacing is different?"). They share this now, and the only thing
   each id still says is WHERE it sits. */
.hud-btn { position: absolute; z-index: 6; box-sizing: border-box;
  width: var(--hud-btn, 36px); height: var(--hud-btn, 36px); padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; cursor: pointer; font-size: 16px;
  background: radial-gradient(circle at 34% 30%, rgba(255,255,255,.1), transparent 45%), rgba(16, 10, 34, .92);
  color: #ffd166; border: 1.5px solid #cfa54e;
  box-shadow: 0 0 8px rgba(212, 169, 79, .3), 0 2px 8px rgba(0, 0, 0, .5);
  transition: transform .12s ease, border-color .12s ease, box-shadow .12s ease; }
.hud-btn:hover { border-color: #8a6cff; box-shadow: 0 0 10px rgba(138, 108, 255, .4);
  transform: scale(1.08); }
.hud-btn:active { transform: scale(.94); }
.hud-btn .btn-ico { width: 19px; height: 19px; display: block; }
/* the right-hand column: one pitch, derived from the button size so the two
   never drift apart again */
/* ONE RIGHT RAIL. The cluster used to sit on its own inset (22px) while the
   minimap sat on 14px and the bag panel on 18px, so three things stacked down
   the same edge and none of their edges met. --hud-rail is now the single
   number all three read, and --hud-row is DERIVED from the minimap instead of
   being a guess: map top + map size + the outer ring + a real gap, so the
   buttons can never creep under the dial again.
   3*36 + 2*10 = 128 wide, against a 126px dial - the column reads as one
   object under the map. */
body { --hud-btn: 36px; --hud-gap: 10px; --hud-rail: 14px;
  --map-top: 14px; --map-size: 118px;
  --hud-row: calc(var(--map-top) + var(--map-size) + 4px + 12px); }
#settings-btn { top: var(--hud-row); right: var(--hud-rail); }
#bag-btn     { top: var(--hud-row); right: calc(var(--hud-rail) + (var(--hud-btn) + var(--hud-gap))); }
#quest-btn   { top: var(--hud-row); right: calc(var(--hud-rail) + 2 * (var(--hud-btn) + var(--hud-gap))); }
/* the bag hangs off the same rail and the same row - one source of truth */
#inv-panel { top: calc(var(--hud-row) + var(--hud-btn) + 12px); right: var(--hud-rail); }
/* first person rides the same column on a pointer device */
#mini-fpv    { top: 92px; right: 96px; display: none; }
@media (min-width: 701px) { #mini-fpv { top: 118px; right: 122px; } }
@media (pointer: coarse) { body:not(.kb) #mini-fpv { display: flex; } }
/* ON TOUCH IT BELONGS BY THE THUMB, not on the far side of the screen from it
   (Alan: "maybe the FPS icon should go above the walk joystick instead of the
   left on mobile actually"). The walk zone is 150px wide at the left edge and
   starts 100px up, so this sits just above its ring. */
@media (pointer: coarse) {
  body:not(.kb) #mini-fpv { top: auto; right: auto; left: 26px; bottom: 286px; }
}
body.fpv #mini-fpv { border-color: #ffd98a; color: #fff2cc;
  box-shadow: 0 0 12px rgba(212, 169, 79, .55), 0 2px 8px rgba(0, 0, 0, .5); }

/* --- ornate celestial UI kit (Kling-painted) --- */
#stats-panel, #quest-log, #inv-panel, #bank-panel, #shop-panel, #map-panel, #dialogue {
  border: 18px solid transparent;
  border-image: url('../assets/ui/panel_frame.png') 100 stretch;
  border-radius: 0;
  background:
    linear-gradient(160deg, rgba(13, 9, 30, .96), rgba(6, 4, 18, .96)) padding-box;
}
/* slim gold rings - elegant, no bulk */
#portrait { border: 2px solid #cfa54e; box-shadow: 0 0 8px rgba(212, 169, 79, .45),
  inset 0 0 6px rgba(0, 0, 0, .5); }
#mini-dock { border: none; overflow: hidden; background: transparent; box-shadow: none;
  border-radius: 50%; }
/* THE MINIMAP FILLS ITS RING. Fitting the whole square map inside the circle
   left dead corners and the square's own edge floating in a round frame
   (Alan: "does that look bad?" - it did). The dock now shows a ZOOMED view
   CENTRED ON THE PLAYER, the way an MMO minimap works, so the disc is full
   at any position and no marker has to be faked onto the rim. The whole
   square still lives in the map panel, one tap away. hud.js writes the
   transform on each repaint. */
#mini-img, #mini-walls, #mini-dots { transform-origin: 0 0; will-change: transform; }
#mini-dock { border-radius: 50%; }   /* the dock clips; children must NOT,
     because clip-path resolves before their transform and a scaled clip
     leaves the circle oversized and off-centre (the truncated minimap) */
#mini-dock::after { content: ''; position: absolute; inset: 1px; border-radius: 50%;
  border: 2px solid #cfa54e; pointer-events: none;
  box-shadow: 0 0 10px rgba(212, 169, 79, .4), inset 0 0 8px rgba(0, 0, 0, .55); }
#mini-dock::before { content: ''; position: absolute; inset: -4px; border-radius: 50%;
  border: 1px solid rgba(212, 169, 79, .35); pointer-events: none; }
/* framed panels: kill the blurred halo under the transparent border zone */
#stats-panel, #quest-log, #inv-panel, #bank-panel, #shop-panel, #map-panel, #dialogue {
  box-shadow: 0 8px 26px rgba(0, 0, 0, .55);
}


/* painted item art */
.item-art { width: 32px; height: 32px; border-radius: 8px; object-fit: cover;
  display: block; box-shadow: 0 1px 4px rgba(0, 0, 0, .5); }
/* THE ART IS THE LABEL. With the 8px name gone the icon gets the whole slot,
   which is the only thing that actually made this bag readable - a potion is
   recognised as a shape long before anyone reads a word under it. Sized off
   --slot so it grows with the 58px desktop slots too. */
.inv-art { width: calc(var(--slot, 46px) - 9px); height: calc(var(--slot, 46px) - 9px);
  max-width: 100%; border-radius: 8px; object-fit: contain;
  pointer-events: none; filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .65)); }
.shop-ico { display: flex; align-items: center; }


/* out of combat the skills rest out of sight */
#hotbar.resting { opacity: 0; transform: translateX(-50%) translateY(26px); pointer-events: none; }
#cast-bar.resting { opacity: 0; }
#cast-bar { transition: opacity .3s ease; }


/* ability compendium rows in the character sheet */
.ab-head { font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
  color: #8f83bd; margin: 10px 0 6px; border-top: 1px solid #2c2250; padding-top: 9px; }
.ab-row { display: flex; gap: 10px; align-items: flex-start; padding: 5px 0; }
.ab-row.locked { opacity: .45; filter: grayscale(.7); }
.ab-art { width: 38px; height: 38px; border-radius: 50%; object-fit: cover;
  border: 1.5px solid #cfa54e; flex-shrink: 0;
  box-shadow: 0 0 6px rgba(212, 169, 79, .3); }
.ab-mark { width: 38px; height: 38px; display: flex; align-items: center;
  justify-content: center; font-size: 20px; }
.ab-txt { min-width: 0; }
.ab-txt b { display: block; font-size: 12.5px; color: #e8e2f7; }
.ab-txt small { display: block; font-size: 10px; color: #ffd166; margin: 1px 0 2px; }
.ab-txt span { display: block; font-size: 11px; color: #a99ed4; line-height: 1.35; }


/* character sheet tabs */
#stats-tabs { display: flex; gap: 6px; margin-bottom: 10px; }
.stab { flex: 1; padding: 6px 0; border-radius: 9px; font-family: Georgia, serif;
  font-size: 12px; letter-spacing: 1px; cursor: pointer; color: #8f83bd;
  background: #17102e; border: 1px solid #2c2250; }
.stab.active { color: #ffd166; border-color: #cfa54e; background: #221741;
  box-shadow: 0 0 8px rgba(212, 169, 79, .25); }
#stats-tree, #stats-gear { display: none; overflow-x: hidden; }
#stats-panel[data-tab="skills"] #stats-body,
#stats-panel[data-tab="skills"] #stats-grid,
#stats-panel[data-tab="skills"] #stats-traits-box { display: none; }
#stats-panel[data-tab="skills"] #stats-tree { display: block; }
#stats-panel[data-tab="gear"] #stats-body,
#stats-panel[data-tab="gear"] #stats-grid,
#stats-panel[data-tab="gear"] #stats-traits-box { display: none; }
#stats-panel[data-tab="gear"] #stats-gear { display: block; }

/* the Gear tab: seven slots, rarity-coloured names, a running total */
#gear-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 6px; }
.gear-slot { display: flex; gap: 8px; align-items: center; padding: 8px 9px;
  border: 1.5px dashed #3a2f5e; border-radius: 11px; color: #6f639c;
  background: rgba(14, 9, 30, .6); min-width: 0; }
.gear-slot.filled { border-style: solid; border-color: #6a5acd; cursor: pointer;
  background: linear-gradient(170deg, #221741 0%, #120b28 60%, #0d0720 100%);
  box-shadow: inset 0 1px 0 rgba(202, 191, 250, .1), 0 2px 6px rgba(0, 0, 0, .4); }
.gear-slot.filled:hover { background: #221740; }
.gear-slot .gs-ico, .bag-gear .gs-ico { font-size: 18px; flex: none; }
.gear-slot .gs-txt, .bag-gear .gs-txt { min-width: 0; }
.gear-slot .gs-txt small, .bag-gear .gs-txt small { display: block; font-size: 9px; letter-spacing: 1px;
  text-transform: uppercase; color: #6f639c; }
.gear-slot .gs-txt b, .bag-gear .gs-txt b { display: block; font-size: 11px; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gear-slot .gs-txt b.gs-none { color: #4d4374; font-weight: normal; }
.gear-slot .gs-txt span, .bag-gear .gs-txt span { display: block; font-size: 9.5px; color: #8f83bd;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#gear-total { margin-top: 9px; font-size: 11px; color: #ffd166;
  border-top: 1px solid #2c2250; padding-top: 8px; }

/* --- armour sets ---
   The set colour is teal and nothing else in the HUD uses it: gold is the
   quest/level colour, amber is bounties, purple is the panel itself. A player
   glancing at the gear tab should be able to tell what is set-related without
   reading, and reusing an existing colour would have made that a guess.
   The ring goes OUTSIDE the tile with an outline rather than a border, so a
   set piece and a selected piece can both be shown at once - selection is a
   border, membership is the ring, and neither has to win. */
.gear-slot.in-set { outline: 1.5px solid #57d3c0; outline-offset: -1.5px; }
.set-box { margin-top: 9px; padding: 8px 10px; border-radius: 10px;
  border: 1px solid #2c5a55; background: rgba(30, 78, 72, .18); }
.set-box.none { border-color: #2c2250; background: rgba(20, 14, 40, .5); opacity: .78; }
.set-head { display: flex; align-items: baseline; gap: 8px; }
.set-head b { flex: 1; min-width: 0; font-size: 12px; color: #7fe3d2; letter-spacing: .5px; }
.set-box.none .set-head b { color: #8f83bd; }
.set-head span { flex: none; font-size: 10.5px; color: #8f83bd; }
.set-line { display: flex; gap: 8px; align-items: baseline; margin-top: 5px;
  font-size: 10.5px; color: #6f639c; }
.set-line i { flex: none; width: 15px; text-align: center; font-style: normal;
  font-size: 9.5px; border-radius: 4px; padding: 1px 0;
  background: rgba(120, 110, 170, .18); color: #8f83bd; }
.set-line span { min-width: 0; }
.set-line.on { color: #cfe9e4; }
.set-line.on i { background: rgba(87, 211, 192, .22); color: #7fe3d2; }
.im-set { color: #7fe3d2; }
.set-need { margin-top: 6px; font-size: 10px; color: #8f83bd; letter-spacing: .3px; }

/* the bag as square cells, like the inventory - a tapped slot filters them */
.gear-slot.selected { border-color: #ffd166;
  box-shadow: 0 0 8px rgba(255, 209, 102, .4); }
#bag-gear-grid { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 7px; }
.gear-cell { width: 44px; height: 44px; border-radius: 10px; cursor: pointer;
  display: flex; align-items: center; justify-content: center; font-size: 20px;
  border: 1.5px solid #3a2f5e; background: rgba(10, 6, 26, .7); }
.gear-cell:hover { background: #1a1133; }
.gear-cell.r-rare { border-color: #4a6da8; }
.gear-cell.r-epic, .gear-cell.r-astral { border-color: #8a5cb8; }
.gear-cell.r-legendary { border-color: #cfa54e; }
.gear-cell.selected { border-color: #ffd166;
  box-shadow: 0 0 8px rgba(255, 209, 102, .5); }
.gear-worn-row { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-top: 6px; font-size: 11px; color: #8f83bd; }
.gear-worn-row b { font-size: 11.5px; }
.gear-worn-row .tree-learn { margin-top: 0; padding: 4px 10px; }
.gc-name { font-size: 12px; margin-bottom: 3px; }
.gc-name small { color: #8f83bd; font-size: 10px; margin-left: 4px; }
.gear-compare { border: 1px solid #3a2f5e; border-left: 3px solid #ffd166;
  border-radius: 0 10px 10px 10px; padding: 8px 10px; margin: 2px 0 2px 14px;
  background: rgba(16, 10, 34, .8); }
.gc-vs { font-size: 10.5px; color: #8f83bd; }
.gc-vs b { color: #e8e2f7; }
.gc-diff { display: flex; gap: 9px; flex-wrap: wrap; font-size: 11px; margin-top: 4px; }
.gc-diff .up { color: #7fd0a0; }
.gc-diff .down { color: #e0705c; }
.gc-diff .same { color: #8f83bd; }

/* the level dial: a celestial astrolabe - ticked gold bezel, starfield core,
   a gradient xp arc with a burning tip */
#level-orb { position: relative; width: 118px; flex: none; text-align: center;
  align-self: center; isolation: isolate; }
#level-orb svg { width: 118px; height: 118px; display: block; }
.dial-rim { fill: none; stroke: #cfa54e; stroke-opacity: .38; stroke-width: 1.2; }
.dial-tick { stroke: #cfa54e; stroke-opacity: .35; stroke-width: 1; }
.dial-tick.major { stroke-opacity: .8; stroke-width: 1.6; }
.dial-core { fill: url(#dial-core-g); stroke: #3a2f5e; stroke-width: 1;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, .55)); }
.dial-star { fill: #cabffa; opacity: .5;
  animation: dial-twinkle 3.4s ease-in-out infinite; }
.dial-star:nth-of-type(2n) { animation-duration: 4.7s; animation-delay: -1.1s; }
.dial-star:nth-of-type(3n) { animation-duration: 2.6s; animation-delay: -2.3s; }
.dial-star:nth-of-type(5n) { animation-duration: 5.8s; animation-delay: -.7s; }
.dial-track { fill: none; stroke: rgba(36, 26, 69, .9); stroke-width: 6.5; }
.dial-arc { fill: none; stroke: url(#dial-arc-g); stroke-width: 6.5;
  stroke-linecap: round;
  filter: drop-shadow(0 0 4px rgba(255, 209, 102, .55));
  animation: dial-arc-glow 3.6s ease-in-out infinite; }
.dial-tip { fill: #fff6d9;
  filter: drop-shadow(0 0 5px #ffd166) drop-shadow(0 0 2px #fff);
  animation: dial-tip-flare 1.7s ease-in-out infinite; }
.dial-inner { fill: none; stroke: #cfa54e; stroke-opacity: .22; stroke-width: .8;
  stroke-dasharray: 2 3;
  transform-box: fill-box; transform-origin: center;
  animation: dial-spin-rev 26s linear infinite; }

/* THE DIAL TURNS. It was a still picture of an astrolabe; an astrolabe that
   does not move is a coaster. Two dashed halos counter-rotate at different
   speeds, the xp arc breathes its own glow, the burning tip flares, and the
   starfield behind it twinkles - all of it on the ring itself, nothing added
   around the outside of the panel. */
.dial-halo { fill: none; stroke: #ffd166; stroke-opacity: .78; stroke-width: 1.5;
  stroke-dasharray: 3 10; stroke-linecap: round;
  transform-box: fill-box; transform-origin: center;
  animation: dial-spin 34s linear infinite, dial-breathe 4.6s ease-in-out infinite;
  filter: drop-shadow(0 0 4px rgba(255, 209, 102, .65)); }
.dial-halo.b { stroke-opacity: .3; stroke-width: .9; stroke-dasharray: 1.5 7;
  animation: dial-spin-rev 19s linear infinite, dial-breathe 3.1s ease-in-out infinite; }
/* a soft gold bloom UNDER the whole dial, so the ring reads as lit rather
   than drawn - sized to the dial, never to the panel */
#level-orb::before { content: ''; position: absolute; inset: -6px; border-radius: 50%;
  pointer-events: none; z-index: -1;
  background: radial-gradient(circle at 50% 50%,
    rgba(255, 209, 102, .32) 0%, rgba(255, 209, 102, .14) 52%,
    rgba(255, 209, 102, 0) 72%);
  animation: dial-breathe 4.6s ease-in-out infinite; }
@keyframes dial-spin { to { transform: rotate(360deg); } }
@keyframes dial-spin-rev { to { transform: rotate(-360deg); } }
@keyframes dial-breathe { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
@keyframes dial-arc-glow {
  0%, 100% { filter: drop-shadow(0 0 4px rgba(255, 209, 102, .55)); }
  50% { filter: drop-shadow(0 0 10px rgba(255, 209, 102, .95))
               drop-shadow(0 0 20px rgba(255, 209, 102, .38)); } }
@keyframes dial-tip-flare {
  0%, 100% { filter: drop-shadow(0 0 5px #ffd166) drop-shadow(0 0 2px #fff); }
  50% { filter: drop-shadow(0 0 13px #ffd166) drop-shadow(0 0 5px #fff); } }
@keyframes dial-twinkle { 0%, 100% { opacity: .22; } 50% { opacity: .85; } }
@media (prefers-reduced-motion: reduce) {
  .dial-halo, .dial-inner, .dial-arc, .dial-tip, .dial-star, #level-orb::before {
    animation: none !important; }
}
.orb-mid { position: absolute; top: 0; left: 0; width: 118px; height: 118px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; }
.orb-mid small { font-size: 8.5px; letter-spacing: 2.5px; color: #8f83bd; }
.orb-mid b { font-family: Georgia, serif; font-size: 31px; line-height: 1.05;
  background: linear-gradient(180deg, #fff2c9 15%, #ffd166 55%, #b8862e 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .6)); }
.orb-xp { font-size: 9.5px; color: #8f83bd; margin-top: 3px; }

/* stat captions fold away behind the ? until tapped (or hovered) */
.stat-row.has-cap { cursor: pointer; flex-wrap: wrap; }
.cap-mark { font-style: normal; font-size: 8.5px; color: #6f639c;
  border: 1px solid #3a2f5e; border-radius: 50%; padding: 0 4px; margin-left: 3px; }
.stat-row .cap { display: none; width: 100%; font-size: 9.5px; color: #6f639c;
  margin-top: 2px; }
.stat-row.open .cap { display: block; }
@media (hover: hover) { .stat-row.has-cap:hover .cap { display: block; } }

/* stat allocation, living with the numbers it grows */
.build-pts { font-size: 12.5px; color: #ffd166; margin: 8px 0 4px;
  letter-spacing: .4px; }
.stat-row.build-row small { color: #6f639c; font-size: 9.5px; margin-left: 4px; }
.build-val { color: #ffd166; }
.build-add { width: 26px; height: 26px; margin-left: 8px; border-radius: 8px;
  cursor: pointer; flex: none;
  border: 1px solid #cfa54e; background: rgba(46, 30, 8, .8); color: #ffd166;
  font-size: 15px; line-height: 1; }
.stat-row span small { color: #6f639c; font-size: 9.5px; }

/* collapsed traits, a fold instead of a wall of chips */
#stats-traits-box { margin-top: 8px; border-top: 1px solid #2c2250; padding-top: 6px; }
#stats-traits-box summary { font-size: 11.5px; color: #8f83bd; cursor: pointer;
  letter-spacing: .6px; }
#stats-traits-box[open] summary { color: #cabffa; margin-bottom: 5px; }

/* the skill map: tiers joined by drawn lines, every branch visible */
.tree-pts { font-size: 11px; color: #ffd166; float: right; letter-spacing: 0; }
#tree-map { position: relative; margin: 4px 0 6px; }
#tree-lines { position: absolute; inset: 0; width: 100%; height: 100%;
  overflow: visible; }
.tree-line-glow { fill: none; stroke: #ffd166; stroke-width: 7;
  stroke-opacity: .16; stroke-linecap: round; }
.tree-line-under { fill: none; stroke: #191033; stroke-width: 4.5;
  stroke-linecap: round; }
.tree-line { fill: none; stroke: #453a6e; stroke-width: 2;
  stroke-linecap: round; stroke-dasharray: 5 4; }
.tree-line.lit { stroke: #d9b25e; stroke-dasharray: none; }
.tree-joint { fill: #453a6e; stroke: #191033; stroke-width: 1; }
.tree-joint.lit { fill: #ffd166; }
.tmap-node { position: absolute; width: 86px; transform: translateX(-50%);
  text-align: center; cursor: pointer; padding: 5px 2px 4px; border-radius: 11px;
  border: 1.5px solid #3a2f5e; margin-top: -22px;
  background: linear-gradient(170deg, #221741 0%, #120b28 55%, #0d0720 100%);
  box-shadow: inset 0 1px 0 rgba(202, 191, 250, .1), 0 3px 8px rgba(0, 0, 0, .45); }
.tmap-node.s-known, .tmap-node.s-learned {
  background: linear-gradient(170deg, #2e2148 0%, #1a1030 55%, #140a26 100%);
  box-shadow: inset 0 1px 0 rgba(255, 209, 102, .18), 0 3px 8px rgba(0, 0, 0, .45),
    0 0 9px rgba(207, 165, 78, .18); }
.tmap-node .tmn-ico { display: block; font-size: 17px; line-height: 1.2; color: #cabffa; }
/* painted skill art on the tree, dimmed while the node is locked */
.tmap-node .tmn-art { display: block; width: 30px; height: 30px; border-radius: 8px;
  margin: 0 auto 2px; object-fit: cover; box-shadow: 0 0 0 1px rgba(0, 0, 0, .45); }
.tmap-node.s-level .tmn-art, .tmap-node.s-parent .tmn-art {
  filter: grayscale(.85) brightness(.55); }
.lo-slot .lo-art { width: 34px; height: 34px; border-radius: 8px; object-fit: cover; }
.tc-head .tn-art { width: 34px; height: 34px; border-radius: 9px; object-fit: cover;
  flex: none; }
.gear-slot .gs-art, .bag-gear .gs-art, .gear-cell .gs-art { width: 28px; height: 28px;
  border-radius: 7px; object-fit: cover; flex: none; }
.gear-slot .gs-empty { width: 22px; height: 22px; border-radius: 7px; flex: none;
  border: 1.5px dashed #3a3060; }
.tmap-node .tmn-name { display: block; font-size: 9px; color: #a99ed4;
  line-height: 1.15; letter-spacing: .2px; }
/* state is worn, not written: a corner badge at most, the rest is light */
.tmn-badge { position: absolute; top: -7px; right: -1px; font-size: 8px;
  line-height: 1; padding: 2px 5px; border-radius: 7px; color: #8f83bd;
  background: #17102e; border: 1px solid #3a2f5e; letter-spacing: .3px; }
.tmn-badge.can { color: #0d2a18; background: #7fd0a0; border-color: #a5e8c2;
  font-weight: bold; }
.tmn-badge.pad { width: 8px; height: 7px; padding: 0; top: -6px;
  background: #4d4374; border: none; border-radius: 2px; }
.tmn-badge.pad::before { content: ''; position: absolute; top: -4px; left: 1px;
  width: 4px; height: 4px; border: 1.5px solid #4d4374; border-bottom: none;
  border-radius: 4px 4px 0 0; }
.tmap-node.s-level, .tmap-node.s-parent { opacity: 1;
  filter: grayscale(.85) brightness(.62); }
.tmap-node.s-level .tmn-name, .tmap-node.s-parent .tmn-name { color: #6f639c; }
.tmap-node.s-learnable { border-color: #7fd0a0;
  animation: node-breathe 2.2s ease-in-out infinite; }
@keyframes node-breathe {
  0%, 100% { box-shadow: 0 0 5px rgba(127, 208, 160, .25); }
  50% { box-shadow: 0 0 12px rgba(127, 208, 160, .6); }
}
.tc-got { font-size: 10.5px; color: #7fd0a0; margin-top: 5px; }
.tmap-node.s-known, .tmap-node.s-learned { border-color: #cfa54e; }
.tmap-node.s-known .tmn-ico, .tmap-node.s-learned .tmn-ico { color: #ffd166; }
.tmap-node.on-bar::after { content: '●'; position: absolute; top: 1px; right: 4px;
  font-size: 7px; color: #7fd0a0; }
.tmap-node.selected { border-color: #ffd166;
  box-shadow: 0 0 9px rgba(255, 209, 102, .5); }

/* the card: one tapped skill, all its truth, and what you can do about it */
.tree-card { border: 1px solid #3a2f5e; border-radius: 12px; padding: 9px 10px;
  background: rgba(16, 10, 34, .7); margin-top: 4px; }
.tree-card .tc-head { display: flex; gap: 9px; align-items: center; }
.tree-card .tn-icon { font-size: 19px; width: 26px; text-align: center; color: #ffd166; }
.tree-card .tn-txt b { font-size: 13px; color: #e8e2f7; display: block; }
.tree-card .tn-txt small { font-size: 10px; color: #ffd166; }
.tc-desc { font-size: 11px; color: #a99ed4; line-height: 1.4; margin: 5px 0 2px; }
.tc-lock { font-size: 10.5px; color: #c98a7a; margin-top: 5px; }
.tc-slots { display: flex; align-items: center; gap: 6px; margin-top: 7px; }
.tc-slots span { font-size: 10.5px; color: #8f83bd; }
.tc-slot { width: 30px; height: 30px; border-radius: 8px; cursor: pointer;
  border: 1px solid #3a2f5e; background: rgba(10, 6, 26, .7); color: #cabffa;
  font-size: 12px; }
.tc-slot.here { border-color: #7fd0a0; color: #7fd0a0; }
.tree-learn { padding: 6px 12px; border-radius: 8px; cursor: pointer;
  border: 1px solid #cfa54e; background: rgba(46, 30, 8, .8); color: #ffd166;
  font-size: 11.5px; margin-top: 7px; }
.tree-learn:disabled { opacity: .35; cursor: default; }
.lo-title { font-size: 12px; color: #cabffa; margin: 12px 0 6px;
  letter-spacing: .5px; }
#lo-slots { display: flex; gap: 7px; margin-bottom: 8px; }
.lo-slot { width: 44px; height: 44px; border-radius: 10px; cursor: pointer;
  border: 1.5px solid #3a2f5e; background: rgba(10, 6, 26, .7);
  display: flex; align-items: center; justify-content: center;
  font-size: 19px; color: #cabffa; position: relative; }
.lo-slot .lo-key { position: absolute; top: 1px; left: 4px; font-size: 9px;
  color: #6f639c; }
.lo-slot.picking { border-color: #ffd166;
  box-shadow: 0 0 8px rgba(255, 209, 102, .5); }
.lo-hint { font-size: 10.5px; color: #8f83bd; margin-bottom: 6px; }

/* --- PASSIVES: the cheap half of the build choice ------------------------
   MOBILE FIRST. A phone panel is ~314px wide, so these are full-width rows
   stacked down the page - never a grid that has to be squinted at. Each row
   is one tap target with the price on its own button, so the cost of the
   choice is never hidden behind a tooltip (there is no hover on a phone).
   ONE COLUMN ALWAYS: the character sheet is a fixed ~300px panel on every
   screen, so a viewport-width media query says nothing about the room these
   rows actually have - two columns inside it just reproduced "cramped and not
   aligned well". */
#pv-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.pv-node { display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: 6px 10px; padding: 8px 10px; border-radius: 10px;
  border: 1px solid #2f2757; background: rgba(22, 16, 46, .55); }
.pv-node.owned { border-color: #4f7f5c; background: rgba(24, 44, 30, .5); }
.pv-node.locked { opacity: .5; }
.pv-name { grid-column: 1; font-size: 13px; color: #f0e9ff; letter-spacing: .02em; }
.pv-desc { grid-column: 1; font-size: 11px; color: #a99cd6; line-height: 1.35; }
.pv-buy { grid-column: 2; grid-row: 1 / span 2; min-width: 92px; min-height: 40px;
  padding: 8px 10px; border-radius: 9px; font-family: inherit; font-size: 12px;
  cursor: pointer; background: #2c2250; color: #ffd166; border: 1px solid #6a5acd; }
.pv-buy:disabled { cursor: default; background: rgba(30, 24, 56, .5);
  color: #8f83bd; border-color: #3a3060; }
.pv-node.owned .pv-buy:disabled { color: #8ee6a1; border-color: #4f7f5c; }



/* day cycle: an 8-minute warm/cool breath over the whole scene */
#day-tint { position: fixed; inset: 0; pointer-events: none; z-index: 1;
  animation: daycycle 480s ease-in-out infinite; }
@keyframes daycycle {
  0%, 100% { background: rgba(255, 178, 96, .045); }
  50% { background: rgba(96, 128, 255, .055); }
}


/* quick-eat orb beside the combat fan */
/* -268, not -190: the desktop hotbar is a 332px row centred on the screen, so
   its left end is 166px out and a 50px orb needs its centre past ~205 to
   clear it. -190 sat 26px under the first skill on any window between 701
   and 899px wide (Alan: "the food icon is covered by hotbar") - the fix
   below at min-width 900 never reached that band. The phone layout overrides
   the transform entirely, so this only ever governs the row. */
#quick-eat { position: fixed; bottom: max(18px, env(safe-area-inset-bottom)); left: 50%;
  transform: translateX(-268px); width: 50px; height: 50px; border-radius: 50%;
  border: 2px solid #7fbf7f; cursor: pointer; z-index: 30; padding: 0;
  background: #0c1c0e; overflow: visible;
  box-shadow: 0 0 10px rgba(127, 191, 127, .4), 0 3px 10px rgba(0, 0, 0, .5);
  transition: opacity .35s ease, transform .35s ease; }
#quick-eat.resting { opacity: 0; pointer-events: none; transform: translateX(-268px) translateY(26px); }
/* The orb stays up when you are hurt so you can eat out of a fight - but
   with a panel open it stood alone at the foot of the screen under the shop
   (Alan: "why is the food button still appearing by itself"). A panel is
   not a fight: it steps back with the rest of the bar. */
body.panel-open #quick-eat { opacity: 0; pointer-events: none; }
#qe-art { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; pointer-events: none; }
#qe-count { position: absolute; top: -5px; right: -5px; min-width: 17px; height: 17px;
  border-radius: 9px; background: #2c5a2e; border: 1px solid #7fbf7f; color: #d9ffd9;
  font-size: 10px; line-height: 16px; }
#qe-heal { position: absolute; bottom: -7px; left: 50%; transform: translateX(-50%);
  font-size: 10px; color: #8fe89b; text-shadow: 0 1px 2px #000; }
/* the key hint is a KEYCAP in the corner, exactly where the hotbar slots
   wear theirs - its only rule used to live in the mobile block, so on a
   desktop it rendered inline against the heal figure and read as "+30F"
   (Alan: "whats that? and why is it misaligned looking?") */
#qe-key { position: absolute; top: 4px; left: 9px; bottom: auto; transform: none;
  font-size: 9px; color: #b9aee6; z-index: 4; text-shadow: 0 1px 2px #000;
  pointer-events: none; }
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  /* DOCKED to the combat fan - crowning its left column, not marooned in a
     corner. It rests and wakes with the same combat state as the fan. */
  body:not(.kb) #quick-eat { bottom: calc(max(20vh, env(safe-area-inset-bottom)) + 96px);
    left: calc(50% - 157px); right: auto; transform: none; }
  body:not(.kb) #quick-eat.resting { transform: translateY(26px); }
}

/* gear delta badges */
.inv-delta { position: absolute; top: 2px; right: 3px; font-size: 10px; }
.inv-delta.up { color: #7fe87f; text-shadow: 0 0 4px rgba(90, 230, 90, .8); }
.inv-delta.down { color: #ff7f8a; text-shadow: 0 0 4px rgba(255, 90, 110, .7); }
.inv-slot { position: relative; }

/* gold pop when the count changes */
#inv-gold.pop { animation: gold-pop .45s ease; }
@keyframes gold-pop { 0% { transform: scale(1); } 30% { transform: scale(1.35); color: #fff3c9; } 100% { transform: scale(1); } }
#inv-gold { display: inline-block; }

/* low-hp vignette pulses with the heartbeat */
#low-vignette { animation: vignette-pulse 1s ease-in-out infinite !important; }
@keyframes vignette-pulse {
  0%, 100% { opacity: .75; } 45% { opacity: 1; } 60% { opacity: .8; }
}


/* cinematic zone title card */
#zone-title { position: fixed; top: 22vh; left: 50%;
  z-index: 46; text-align: center; pointer-events: none; width: max-content; max-width: 92vw; }
#zt-name { font-family: Georgia, serif; font-size: clamp(24px, 6vw, 42px);
  background: linear-gradient(180deg, #fff8e9 20%, #ffd166 55%, #b8860b 90%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 0 16px rgba(255, 209, 102, .35)) drop-shadow(0 2px 4px rgba(0,0,0,.8)); }
#zt-sub { font-family: Georgia, serif; font-style: italic; font-size: clamp(11px, 2.6vw, 15px);
  color: #cabffa; margin-top: 2px; text-shadow: 0 1px 3px #000; letter-spacing: 2px; }
.zt-rule { height: 1px; max-width: 60vw; margin: 6px auto;
  background: linear-gradient(90deg, transparent, rgba(255, 209, 102, .8), transparent); }
#zone-title { opacity: 0; transform: translateY(10px) translateX(-50%);
  transition: opacity .5s ease, transform .8s ease; will-change: opacity, transform; }
#zone-title.on { opacity: 1; transform: translateY(0) translateX(-50%); }
#zt-name { letter-spacing: 6px; }


/* boss cast bar: sits under the target frame, unmistakably hostile */
#boss-cast { position: absolute; top: 92px; left: 50%; transform: translateX(-50%);
  width: min(280px, 62vw); height: 20px; border-radius: 10px; z-index: 8;
  background: #1a0a08; border: 1.5px solid #ff7a3a; overflow: hidden;
  box-shadow: 0 0 14px rgba(255, 90, 42, .45), inset 0 1px 4px rgba(0,0,0,.7); }
#bc-fill { height: 100%; width: 0; border-radius: 9px;
  background: linear-gradient(90deg, #7a1d0a, #ff5a2a 60%, #ffb14a);
  /* no width transition: the fill is interpolated per frame in JS, and CSS
     easing on top is what made it stall-then-teleport between sim ticks */
  box-shadow: 0 0 12px rgba(255, 122, 58, .9); }
#bc-label { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-family: Georgia, serif; font-size: 11.5px;
  letter-spacing: 1.5px; color: #fff; text-shadow: 0 1px 3px #000, 0 0 8px rgba(255,90,42,.9); }
@media (max-width: 700px) { #boss-cast { top: 186px; } }

/* berserk: the screen itself catches fire at the edges */
#hud.berserk::after { content: ''; position: fixed; inset: 0; pointer-events: none;
  z-index: 2; box-shadow: inset 0 0 120px rgba(255, 70, 20, .55),
  inset 0 0 40px rgba(255, 140, 60, .35);
  animation: berserk-breathe 1.1s ease-in-out infinite; }
@keyframes berserk-breathe {
  0%, 100% { opacity: .55; } 50% { opacity: 1; }
}


/* burning: standing inside the Hollow King's fire scorches the screen edges */
#hud::before { content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 3;
  opacity: var(--burn, 0); transition: opacity .12s linear;
  box-shadow: inset 0 0 90px rgba(255, 110, 30, .85),
              inset 0 0 200px rgba(255, 60, 10, .55); }


/* primary attack: the button that means you never tap the middle of the
   screen. Sits under the resting thumb, right of the skill fan. */
#attack-btn { position: fixed; bottom: max(16px, env(safe-area-inset-bottom));
  right: 18px; width: 84px; height: 84px; border-radius: 50%; z-index: 31;
  cursor: pointer; padding: 0; border: none; background: transparent;
  filter: drop-shadow(0 0 14px rgba(255, 132, 60, .5)) drop-shadow(0 4px 10px rgba(0,0,0,.65));
  transition: opacity .3s ease, transform .12s ease; }
#atk-icon { display: block; width: 100%; height: 100%;
  background: url('../assets/ui/attack_btn.png') center / contain no-repeat;
  font-size: 0; }
#attack-btn:active { transform: scale(.9); filter: brightness(1.45); }
#attack-btn.resting { opacity: 0; pointer-events: none; transform: translateY(24px); }
/* ready-to-swing pulse so the rhythm is visible without watching numbers */
#atk-ring { position: absolute; inset: -4px; border-radius: 50%;
  border: 2px solid rgba(255, 205, 140, .95); pointer-events: none; opacity: 0;
  box-shadow: 0 0 12px rgba(255, 170, 90, .8); }
#attack-btn.ready #atk-ring { animation: atk-ready 1.1s ease-out infinite; }
@keyframes atk-ready {
  0% { transform: scale(.92); opacity: .9; }
  100% { transform: scale(1.18); opacity: 0; }
}
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb) #attack-btn { width: 82px; height: 82px; bottom: 74px; right: 14px; }
}


/* --- item rarity: a legendary should never look like a snack --- */
.inv-slot[data-rarity], .shop-row[data-rarity] { --rar: #6f7ba0; --rar-glow: rgba(111,123,160,.25); }
.inv-slot[data-rarity="rare"], .shop-row[data-rarity="rare"] { --rar: #7fd0ff; --rar-glow: rgba(127,208,255,.4); }
.inv-slot[data-rarity="epic"], .shop-row[data-rarity="epic"],
.inv-slot[data-rarity="astral"], .shop-row[data-rarity="astral"] { --rar: #c9a5ff; --rar-glow: rgba(201,165,255,.45); }
.inv-slot[data-rarity="legendary"], .shop-row[data-rarity="legendary"] { --rar: #ffd166; --rar-glow: rgba(255,209,102,.55); }
/* THE CHASE TIER sits above legendary gold: a pale burning white, because
   at one in twenty thousand to one in a million the frame is the whole
   reward - a curio does nothing else. */
.inv-slot[data-rarity="curio"], .shop-row[data-rarity="curio"] { --rar: #fff6d0; --rar-glow: rgba(255,246,208,.85); }

.inv-slot[data-rarity] { border-color: var(--rar);
  background: radial-gradient(circle at 50% 118%, var(--rar-glow), #17102e 72%);
  box-shadow: 0 0 8px var(--rar-glow); }
.inv-slot[data-rarity="legendary"] { animation: rar-pulse 2.6s ease-in-out infinite; }
.inv-slot[data-rarity="curio"] { animation: rar-pulse 1.7s ease-in-out infinite; }
@keyframes rar-pulse {
  0%, 100% { box-shadow: 0 0 8px rgba(255, 209, 102, .45); }
  50% { box-shadow: 0 0 18px rgba(255, 209, 102, .85); }
}
.shop-row[data-rarity] .shop-name { color: var(--rar); }
.shop-row[data-rarity] { border-left: 2px solid var(--rar); padding-left: 6px; }

/* empty pack slots read as forged sockets rather than dead boxes */
.inv-slot:not([data-rarity]) {
  background:
    radial-gradient(circle at 50% 42%, rgba(106, 90, 205, .10), transparent 62%),
    linear-gradient(160deg, #150e2a, #0d0819);
  border-color: #241a45;
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, .65), inset 0 -1px 0 rgba(120, 100, 200, .12); }
.inv-slot:not([data-rarity])::after {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: rgba(138, 108, 255, .16);
  box-shadow: 0 0 6px rgba(138, 108, 255, .12); }

/* empty equipment sockets show the shape of what belongs there */
.equip-slot:not(.filled) .eq-icon, .stats-eq-slot:not(.filled) .seq-ico { opacity: .28; }


/* --- quest tracker: a sealed scroll, not a grey box --- */
#quest-tracker { display: flex; align-items: flex-start; gap: 11px;
  overflow: hidden;   /* position stays absolute: it already anchors the spine */
  background:
    linear-gradient(180deg, rgba(30, 20, 10, .55), rgba(10, 6, 26, .82)) padding-box,
    rgba(10, 6, 26, .82);
  border: 1px solid #6a5730 !important;
  box-shadow: 0 4px 16px rgba(0,0,0,.5), inset 0 1px 0 rgba(255, 214, 130, .12); }
#qt-seal { color: #cfa54e; font-size: 13px; line-height: 1.1; margin-top: 2px;
  filter: drop-shadow(0 0 5px rgba(212, 165, 78, .55)); }
#qt-body { min-width: 0; flex: 1; }
#qt-title { color: #8a7440 !important; }
/* long objectives clip rather than stretching the chip past the minimap */
#qt-name, #qt-progress { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* a thin thread of progress under the objective */
#qt-bar { height: 3px; margin-top: 5px; border-radius: 2px;
  background: rgba(90, 74, 40, .5); overflow: hidden; }
#qt-fill { height: 100%; width: 0; border-radius: 2px;
  background: linear-gradient(90deg, #8a6410, #ffd166);
  box-shadow: 0 0 6px rgba(255, 209, 102, .6); transition: width .35s ease; }
/* objective complete: the seal lights and the whole chip breathes */
#quest-tracker.turnin { animation: qt-turnin 2.2s ease-in-out infinite; }
#quest-tracker.turnin #qt-seal { color: #fff3c9; }
#quest-tracker.turnin #qt-progress { color: #ffd166; }
@keyframes qt-turnin {
  0%, 100% { box-shadow: 0 4px 16px rgba(0,0,0,.5), 0 0 0 rgba(255, 209, 102, 0); }
  50% { box-shadow: 0 4px 16px rgba(0,0,0,.5), 0 0 18px rgba(255, 209, 102, .55); }
}
#quest-tracker.ready #qt-seal { color: #9ad8ff; }
#quest-tracker.ready #qt-bar { display: none; }

/* --- map dots: shape carries meaning, not just colour --- */
.map-dot.you { border-radius: 50%; }
.map-dot.npc { border-radius: 2px; transform: translate(-50%, -50%) rotate(45deg);
  width: 7px !important; height: 7px !important; }   /* folk: a diamond */
.map-dot.quest { border-radius: 2px !important;
  transform: translate(-50%, -50%) rotate(45deg);
  width: 9px !important; height: 9px !important;
  animation: dot-quest 1.6s ease-in-out infinite; }  /* business waiting: it pulses */
@keyframes dot-quest {
  0%, 100% { box-shadow: 0 0 5px #ffd166; }
  50% { box-shadow: 0 0 13px #ffd166, 0 0 22px rgba(255, 209, 102, .6); }
}
.map-dot.portal { border-radius: 50% !important; width: 9px !important; height: 9px !important;
  background: transparent !important;
  border: 2px solid #7fd0ff;                          /* gates: a ring you step through */
  box-shadow: 0 0 7px #7fd0ff, inset 0 0 5px rgba(127, 208, 255, .8) !important; }
.map-dot.mob { border-radius: 1px; transform: translate(-50%, -50%) rotate(45deg); }
#map-legend .dot.npc { transform: rotate(45deg); border-radius: 2px; }
#map-legend .dot.portal { background: transparent; border: 2px solid #7fd0ff; }
.map-dot.small.quest { width: 7px !important; height: 7px !important; }
/* NOTE: the .quest rules above are !important, so the quest PIN deliberately
   does not wear that class - see the add() call in hud.js. */
.map-dot.small.portal { width: 7px !important; height: 7px !important; border-width: 1.5px; }


/* --- character sheet polish --- */
/* name over its rarity line, close button held to the right — the two never
   compete for the same row, so neither phrase breaks mid-word */
#stats-header { border-bottom: 1px solid rgba(207, 165, 78, .28); padding-bottom: 8px;
  display: grid; grid-template-columns: 1fr auto; align-items: start;
  column-gap: 8px; row-gap: 1px; }
#stats-name { white-space: nowrap; grid-column: 1; }
#stats-rarity { grid-column: 1; margin-left: 0; line-height: 1.35; }
#stats-close { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
#stats-name { font-size: 15px;
  background: linear-gradient(180deg, #fff8e9 25%, #ffd166 70%, #c8951f 95%);
  -webkit-background-clip: text; background-clip: text; color: transparent; }
/* the portrait sprite gets a lit plinth so it reads as a display, not a cutout */
#stats-body { position: relative; padding: 10px 4px 12px; }
#stats-body::before { content: ''; position: absolute; left: 6px; bottom: 6px;
  width: 118px; height: 26px; border-radius: 50%;
  background: radial-gradient(ellipse, rgba(138, 108, 255, .35), transparent 70%);
  filter: blur(1px); }
#stats-sprite { position: relative; }
/* stat rows alternate and gain a gold key column */
.stat-row { padding: 4px 8px; border-radius: 6px; }
.stat-row:nth-child(odd) { background: rgba(138, 108, 255, .07); }
.stat-row b { color: #ffd166; }
#stats-grid { border-top: 1px solid rgba(207, 165, 78, .28); padding-top: 9px; }
/* the ability compendium reads as a list of cards */
.ab-row { padding: 7px 8px; border-radius: 10px; background: rgba(23, 16, 46, .55);
  border: 1px solid #2c2250; margin-bottom: 6px; }
.ab-row:not(.locked) { border-color: rgba(207, 165, 78, .35); }
.ab-head { color: #cfa54e !important; border-top-color: rgba(207, 165, 78, .28) !important; }
/* traits become chips with a little more presence */
.trait-chip { border-color: #4b3d7a; background: rgba(34, 23, 65, .8); }
#stats-traits { border-top-color: rgba(207, 165, 78, .28) !important; }

/* --- painted UI icons (no emoji anywhere) --- */
.btn-ico { width: 21px; height: 21px; object-fit: contain; display: block;
  filter: drop-shadow(0 0 4px rgba(255, 209, 102, .35)); }
#settings-btn .btn-ico { transition: transform .5s ease; }
#settings-btn:hover .btn-ico { transform: rotate(60deg); }
.set-ico { width: 22px; height: 22px; object-fit: contain; flex-shrink: 0;
  filter: drop-shadow(0 0 4px rgba(255, 209, 102, .3)); }
.set-row.off .set-ico { filter: grayscale(1); }
.eq-icon, .seq-ico { width: 20px; height: 20px; object-fit: contain; flex-shrink: 0; }
.seq-ico { width: 22px; height: 22px; }

/* ---------- desktop / wide-screen layout ----------
   The HUD was tuned on a 390px phone; on a wide desktop the same rules leave
   panels running the full window height and the cinematic band scaled to a
   fraction of a 1700px-wide screen. */
@media (min-width: 900px) {
  /* quest log panel: a readable card, not a full-height column */
  #quest-log { width: 340px; max-height: min(52vh, 520px); }
  #ql-list { max-height: none; }

  /* the always-on quest tracker was stretched between two pixel offsets, so
     on a 1700px screen it ran nearly the full width. Cap it. */
  #quest-tracker { right: auto; width: 340px; }

  /* cinematic: the portrait is anchored to the right and the text column has
     room, instead of the band being sized in vw on a very wide viewport */
  #cine-band { min-height: 200px; padding-bottom: 26px; }
  #cine-text { max-width: 780px; font-size: 19px; min-height: 3.2em; }
  #cine-name::after { max-width: 420px; }
  #cine-portrait { height: 48vh; max-width: 38vw; right: 2vw; bottom: 0; }
  /* the boss bust is wide; keep it FULLY on screen with object-fit contain so
     nothing is cut at the right edge or the floor */
  #cine-portrait.boss { height: 94vh; max-width: 54vw; right: 1vw; bottom: 0;
    aspect-ratio: auto; object-fit: contain; object-position: bottom right; }

  /* The log sits bottom-LEFT on a desktop. Centre-bottom is where it goes on
     a phone because that is the only clear lane; on a wide screen it lands in
     the middle of the play space, over the character you are looking at. */
  #log { left: auto; right: 64px; bottom: 96px; transform: none; text-align: right;
    width: min(430px, 32vw); }

  /* Food sat at translateX(-190px) from centre, which on the desktop row
     layout is directly under the leftmost skill orb - the hotbar covered it.
     Park it clear of the row's left end and label its key. */
  #quick-eat { transform: translateX(-268px); }
  #quick-eat.resting { transform: translateX(-268px) translateY(26px); }
  #qe-key { position: absolute; bottom: -16px; left: 50%; transform: translateX(-50%);
    font-size: 9px; letter-spacing: .08em; color: #b7e8bd;
    text-shadow: 0 1px 2px #000; pointer-events: none; }

  /* the other panels get sane caps too */
  #inv-panel, #bank-panel, #shop-panel { max-height: min(64vh, 640px); }
  /* never taller than the window leaves it - 780px on a laptop overshot the
     app's short embedded viewport and the tree spilled over the chat bar */
  #stats-panel { max-height: min(calc(100dvh - 148px), 780px); }

  /* Settings is the one panel reached by a KEY as well as a button, and Escape
     has no on-screen anchor to fly out of. On a phone the panel hanging off
     the cogwheel is fine because the screen is a hand's width; on a desktop
     the eye is at the middle of the window, so that is where it opens - with a
     dimmed backdrop, since it is the one panel that is modal in feel. */
  #settings-panel { top: 50%; left: 50%; right: auto;
    width: 300px; transform: translate(-50%, -50%);
    transform-origin: center; padding: 14px 16px; }
  #settings-panel.closed { transform: translate(-50%, -50%) scale(.88); }
  #settings-panel::before { content: ''; position: fixed; inset: 0; z-index: -1;
    background: rgba(4, 2, 12, .45); }
  #settings-panel.closed::before { display: none; }
  #set-title { font-size: 13px; margin-bottom: 12px; }
  .set-row { font-size: 14px; padding: 11px 12px; }

  /* the portrait was sized for a 390px phone and reads as a thumbnail on a
     1700px screen; the name and bars beside it already have the room */
  #portrait { width: 92px; height: 92px; }
  /* ...and the tracker's left offset was measured against the 66px portrait,
     so it now starts underneath it. The panel ends at x=112 and its xp ring
     overhangs to 118, hence 132. */
  #quest-tracker { left: 132px; }
}

/* --- multiplayer chat ---------------------------------------------------
   Bottom-LEFT, above the log, and deliberately narrow: chat must never grow
   into the space the hotbar and the character occupy, and on a phone the
   bottom-centre is where the thumb lives. */
#chat { position: fixed; left: 14px; bottom: 92px; width: min(340px, 46vw);
  z-index: 7; display: flex; flex-direction: column; gap: 6px;
  font-family: Georgia, serif; }
#chat[hidden] { display: none; }
/* NO BED - the words carry themselves (Alan: "dont like the backgrounded
   chat box, remove background, u just need to make text more readable").
   Readability comes from an OUTLINE on the type, not a box behind it; the
   side padding stays because overflow-x:hidden (needed for the scroll) was
   clipping the old 8px shadow glow at the edges, which read as truncated. */
#chat-log { display: flex; flex-direction: column; gap: 3px;
  max-height: 26vh; overflow-y: auto; overflow-x: hidden; padding: 0 6px;
  overscroll-behavior: contain; touch-action: pan-y;
  scrollbar-width: thin; scrollbar-color: #6b58b0 transparent; }
/* The scrollbar was thin, default-coloured and drawn over a dark game, so it
   was invisible - a log that scrolls but shows no sign of it reads as a log
   that does not scroll. */
#chat-log::-webkit-scrollbar { width: 8px; }
#chat-log::-webkit-scrollbar-thumb { background: #6b58b0; border-radius: 4px; }
#chat-log::-webkit-scrollbar-track { background: rgba(8, 5, 20, .5); }
/* HISTORY: the log shows about six lines and keeps three hundred. This is the
   only way to reach the rest of them. */
#chat-hist { align-self: flex-start; border: 1px solid #4b3d7a;
  background: rgba(8, 5, 20, .82); color: #9d86ff; border-radius: 8px;
  padding: 2px 9px; font-family: Georgia, serif; font-size: 11px;
  cursor: pointer; opacity: .6; }
#chat-hist:hover { opacity: 1; }
#chat-hist.unread { opacity: 1; border-color: #8a6cff; color: #ffd166; }
/* Read mode. Over a 3D world, unbacked text is unreadable at this height, so
   the tall log takes a panel of its own. */
#chat.tall #chat-log { max-height: 58vh; background: rgba(8, 5, 20, .88);
  border: 1px solid #4b3d7a; border-radius: 10px; padding: 8px 10px; }
#chat.tall #chat-hist { opacity: 1; }
.chat-line { font-size: 12.5px; line-height: 1.4; color: #f6f2ff; font-weight: 600;
  /* a true outline: four hard 1px offsets close the ring, the 3px blur lifts
     it off bright ground; a hairline stroke keeps the letterforms crisp */
  text-shadow: 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
               0 0 3px #000, 0 2px 4px rgba(0, 0, 0, .9);
  -webkit-text-stroke: 0.35px rgba(0, 0, 0, .8); paint-order: stroke fill;
  overflow-wrap: break-word; }
.chat-line .zone { color: #6f639c; font-size: 11px; }
.chat-line .who { color: #ffd166; }
.chat-line.party { color: #8fd8ff; }
.chat-line .party-tag { color: #5db8e8; font-size: 11px; }
.chat-line.sys { color: #8f83bd; font-style: italic; }
#chat-form { display: flex; }
#chat-input { flex: 1; min-width: 0; background: rgba(8, 5, 20, .82);
  border: 1px solid #4b3d7a; border-radius: 9px; padding: 7px 10px;
  color: #e8e2f7; font-family: Georgia, serif; font-size: 13px; }
#chat-input:focus { outline: none; border-color: #8a6cff; }
/* the input only takes the keyboard while it has focus - otherwise WASD would
   type instead of walking */
#chat-input:not(:focus) { opacity: .55; }
/* a visible send key - the phone keyboard's tick is not reliably a submit,
   and on desktop it doubles as the discoverable "this box sends" affordance */
#chat-send { margin-left: 5px; width: 36px; flex: none; border-radius: 9px;
  border: 1px solid #4b3d7a; background: rgba(8, 5, 20, .82); color: #9d86ff;
  font-size: 14px; cursor: pointer; }
#chat-send:active { background: rgba(138, 108, 255, .25); }

#net-status { position: fixed; top: 8px; left: 50%; transform: translateX(-50%);
  z-index: 9; font-family: Georgia, serif; font-size: 11px; letter-spacing: 1px;
  color: #b7aede; background: rgba(8, 5, 20, .8); border: 1px solid #4b3d7a;
  border-radius: 999px; padding: 4px 12px; pointer-events: none; }
#net-status[hidden] { display: none; }
/* The idle notice is the one the player is meant to ACT on, so it takes
   pointer events back (the banner is inert otherwise, to stay out of the way
   of the world underneath it) and looks like something you can press. */
#net-status.clickable { pointer-events: auto; cursor: pointer;
  color: #ffe9a8; border-color: #8a6f2e; background: rgba(28, 18, 6, .92); }
#net-status.clickable:hover { background: rgba(48, 32, 10, .95); }

@media (max-width: 540px) {
  #chat-log { max-height: 18vh; }
}

/* --- mobile chat: out of the way until asked for ------------------------
   The input spanned the bottom of a phone screen ON TOP of the game, and
   tapping anywhere near it dragged up the keyboard. On narrow screens the
   box now lives behind a toggle: the log shows the latest lines faintly,
   the input only exists while the player wants it. */
#chat-toggle { position: fixed; left: 12px; bottom: 96px; z-index: 8;
  width: 40px; height: 40px; border-radius: 50%; font-size: 17px;
  background: rgba(16, 10, 34, .92); border: 1.5px solid #4b3d7a;
  color: #e8e2f7; cursor: pointer; }
#chat-toggle.unread { border-color: #ffd166;
  box-shadow: 0 0 10px rgba(212, 169, 79, .45); }
#chat-toggle[hidden] { display: none; }

@media (max-width: 700px) {
  /* THE PHONE'S BOTTOM IS LANES, and chat gets the one above the skill fan.
     From the floor up: battle log (0-60px), the thumb row - chat toggle
     left, food centre, attack right (74-156px), the skill fan (20vh to
     20vh+108px), then chat. At bottom:148px the chat box sat INSIDE the fan
     and the toggle sat on the input (Alan: "the chat box etc all seem to be
     in odd spots"). A keyboard user on a narrow window has the plain hotbar
     row instead of the fan, so chat drops back to the desktop height. */
  /* half the screen, hugging the left: full width ran under the jump button
     and the attack column on the right (Alan; 75% was still "too long") */
  /* THE CHAT NEVER MOVES ON ITS OWN. It used to lift above the fan when a
     fight started (Alan: "annoying that it keeps moving"). Now bubble and
     box are one draggable unit: main.js sets --chat-x / --chat-b on <body>
     when the bubble is dragged, the box rides 52px above the bubble, and
     the spot is remembered. Nothing else touches its position. */
  /* v2.91.10-11 (Alan): the chat lives UNDER the walk ring - bubble in the
     bottom-left corner, the box just above it (ring starts at 140px, the
     box ends under that). Drag the bubble, the box follows. */
  #chat { left: var(--chat-x, 12px); right: auto; width: 56vw;
    bottom: calc(var(--chat-b, 24px) + 48px); }
  body.kb #chat { left: 14px; bottom: 92px; }
  #chat-toggle { left: var(--chat-x, 12px); bottom: var(--chat-b, 24px); }
  /* HISTORY RIDES BESIDE THE BUBBLE ON A PHONE. In the box it sat over the
     joystick (Alan: "its too close to the joystick and it overlaps"), and the
     box is draggable, so it has to travel with the bubble rather than with
     the log. Same lane, same size, one bubble to the right. */
  #chat-hist { position: fixed; z-index: 8; pointer-events: auto;
    left: calc(var(--chat-x, 12px) + 48px); bottom: var(--chat-b, 24px);
    width: 40px; height: 40px; border-radius: 50%; padding: 0;
    line-height: 1; opacity: 1;
    /* OPAQUE. At .92 the world showed through the glyph's own bubble, so the
       icon read as sitting on a hole rather than on a button (Alan: "does not
       cover the button behind it so u can still see a bit of it"). */
    background: #100a22; border: 1.5px solid #4b3d7a;
    color: #e8e2f7;
    /* the word does not fit a 40px bubble, and the label is set by script:
       hide the text and draw a glyph for whichever state it is in. FLEX, not
       a bare inline ::after - an inline box inside a font-size:0 button sits
       on the baseline near the bottom of the circle, which is what made the
       glyph look half-off its own button. */
    display: flex; align-items: center; justify-content: center;
    font-size: 0; color: transparent; }
  /* the literal characters, NOT CSS unicode escapes: `content: 'F553'`
     needs a terminating space or the parser eats the next token, and mine
     rendered as the text "1 F553" */
  #chat-hist::after { font-size: 17px; line-height: 1; color: #e8e2f7;
    content: '◴'; }
  #chat.tall #chat-hist::after { content: '✕'; font-size: 15px; }
  /* the collapsed box swallows nothing, but its History bubble must still
     take a tap - it is the only way back into the log */
  #chat.collapsed { pointer-events: none; }
  #chat.collapsed #chat-hist { pointer-events: auto; }
  /* THREE LINES WHEN IT IS NOT BEING READ (Alan: "when the history isnt
     active maybe just show like 3 lines? otherwise it blocks the joystick").
     A px cap, not vh: three lines is three lines on every phone, and 16vh on
     a tall screen was seven of them sitting across the stick. 3 x 16px of
     line plus the two 2px gaps. The cap itself is at the END of this file,
     where it can outrank the collapsed rules below. */
  .chat-line { font-size: 11px; }
  /* the battle log stays at the very bottom (Alan), clear of the bubble */
  body:not(.kb) #log { left: 60px; right: 60px; width: auto; transform: none;
    bottom: 20px; text-align: center; }
  #chat-log { max-height: 16vh; }
  /* collapsed: a whisper of recent chat, no input, taps fall through */
  #chat.collapsed { pointer-events: none; }
  #chat.collapsed #chat-form { display: none; }
  #chat.collapsed #chat-log { max-height: 12vh; opacity: .55; }
}

/* --- party ---------------------------------------------------------------
   Under the portrait, where the eye already goes for vital signs. Rows are
   name plus a thin hp bar; a dead member's bar sits empty, which says it. */
#party-panel { position: fixed; left: 14px; top: 132px; z-index: 6;
  width: 168px; background: rgba(10, 6, 26, .82); border: 1px solid #4b3d7a;
  border-radius: 12px; padding: 8px 10px; font-family: Georgia, serif; }
#party-panel[hidden] { display: none; }
.party-row { margin-bottom: 6px; }
.party-name { font-size: 11.5px; color: #e8e2f7; letter-spacing: .4px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.party-name .pzone { color: #6f639c; font-size: 10px; }
.party-hp { height: 4px; border-radius: 2px; background: #170d14;
  border: 1px solid #3a2530; overflow: hidden; margin-top: 2px; }
/* healthy green, worried amber under 60, screaming red under 30 - the class
   is set alongside the width wherever the bar is written */
.party-hp i { display: block; height: 100%;
  background: linear-gradient(90deg, #6ef29b, #2f9e57);
  transition: width .3s ease, background .3s ease; }
.party-hp i.mid { background: linear-gradient(90deg, #ffd166, #d99b2b); }
.party-hp i.low { background: linear-gradient(90deg, #ff5d73, #d92645); }
#party-leave { width: 100%; margin-top: 2px; padding: 4px; font-size: 10.5px;
  background: none; border: 1px solid #3a2f5e; border-radius: 8px;
  color: #8f83bd; cursor: pointer; font-family: Georgia, serif; }
#party-leave:hover { border-color: #a4614f; color: #d79b8a; }

/* the player menu: same gold-on-midnight kit as the invite banner */
#player-menu { position: fixed; z-index: 10; min-width: 150px;
  background: rgba(16, 10, 34, .96); border: 1.5px solid #cfa54e;
  border-radius: 12px; padding: 5px; font-family: Georgia, serif;
  box-shadow: 0 8px 22px rgba(0, 0, 0, .55), 0 0 14px rgba(212, 169, 79, .25); }
#player-menu[hidden] { display: none; }
.pm-name { font-size: 11.5px; color: #ffd166; letter-spacing: .6px;
  padding: 5px 10px 5px; border-bottom: 1px solid rgba(207, 165, 78, .25);
  margin-bottom: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pm-item { display: block; width: 100%; text-align: left; background: none;
  border: 0; color: #e8e2f7; font-family: Georgia, serif; font-size: 13px;
  padding: 8px 10px; border-radius: 8px; cursor: pointer; }
.pm-item:hover { background: rgba(207, 165, 78, .16); }
.pm-item.off { color: #6f639c; cursor: default; }

#party-invite { position: fixed; top: 54px; left: 50%; transform: translateX(-50%);
  z-index: 9; display: flex; gap: 10px; align-items: center;
  background: rgba(16, 10, 34, .95); border: 1.5px solid #cfa54e;
  border-radius: 12px; padding: 9px 14px; font-family: Georgia, serif;
  box-shadow: 0 0 18px rgba(212, 169, 79, .35); }
#party-invite[hidden] { display: none; }
#pi-text { font-size: 13px; color: #e8e2f7; }
#party-invite button { padding: 5px 14px; border-radius: 8px; cursor: pointer;
  font-family: Georgia, serif; font-size: 12.5px; }
#pi-yes { background: linear-gradient(180deg, rgba(46,30,8,.9), rgba(24,14,4,.95));
  border: 1px solid #cfa54e; color: #ffd166; }
#pi-no { background: none; border: 1px solid #3a2f5e; color: #8f83bd; }

/* the duel challenge banner: same kit as the party invite above, offset a
   little lower so the two can never overlap if both happen to be up */
#duel-invite { position: fixed; top: 94px; left: 50%; transform: translateX(-50%);
  z-index: 9; display: flex; gap: 10px; align-items: center;
  background: rgba(16, 10, 34, .95); border: 1.5px solid #a04858;
  border-radius: 12px; padding: 9px 14px; font-family: Georgia, serif;
  box-shadow: 0 0 18px rgba(160, 72, 88, .35); }
#duel-invite[hidden] { display: none; }
#di-text { font-size: 13px; color: #e8e2f7; }
#duel-invite button { padding: 5px 14px; border-radius: 8px; cursor: pointer;
  font-family: Georgia, serif; font-size: 12.5px; }
#di-yes { background: linear-gradient(180deg, rgba(46,12,16,.9), rgba(24,6,8,.95));
  border: 1px solid #a04858; color: #ff9daa; }
#di-no { background: none; border: 1px solid #3a2f5e; color: #8f83bd; }

@media (max-width: 700px) {
  /* under the portrait like on desktop, but SLIM: one line per member -
     name and a short hp bar side by side, zone dropped, leave key small */
  #party-panel { top: 96px; left: 8px; right: auto; width: 118px; padding: 5px 7px; }
  .party-row { display: flex; align-items: center; gap: 5px; margin-bottom: 3px; }
  .party-name { flex: 1; font-size: 10px; letter-spacing: .2px; }
  .party-name .pzone { display: none; }
  .party-hp { flex: none; width: 34px; margin-top: 0; }
  #party-leave { margin-top: 1px; padding: 2px 4px; font-size: 9px; border-radius: 6px; }
}

/* An open NPC dialogue owns the bottom of the screen; the chat column under
   it read as clutter, so it steps aside until the conversation closes. */
body:has(#dialogue:not([hidden])) #chat,
body:has(#dialogue:not([hidden])) #chat-toggle { display: none; }
/* the character sheet is tall; chat stamping over its lower rows reads as
   broken at ANY viewport, not just phones - it steps aside while it's open */
body:has(#stats-panel:not(.closed)) #chat,
body:has(#stats-panel:not(.closed)) #chat-toggle { display: none; }

@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  /* an open panel owns a phone's screen - the combat fan and attack key
     were stamping over the character sheet's lower rows */
  body:has(#stats-panel:not(.closed)) #hotbar,
  body:has(#stats-panel:not(.closed)) #attack-btn,
  body:has(#stats-panel:not(.closed)) #chat,
  body:has(#stats-panel:not(.closed)) #chat-toggle,
  body:has(#stats-panel:not(.closed)) #quick-eat,
  body:has(#inv-panel:not(.closed)) #hotbar,
  body:has(#inv-panel:not(.closed)) #attack-btn,
  body:has(#inv-panel:not(.closed)) #quick-eat,
  body:has(#quest-log:not(.closed)) #hotbar,
  body:has(#quest-log:not(.closed)) #attack-btn,
  body:not(.kb) body:has(#quest-log:not(.closed)) #quick-eat { display: none; }
}

.bv-split { font-size: 9px; color: #6f639c; font-weight: normal; }

/* the world map: tabs, the continent, and its waypoint pins */
#map-tabs { display: flex; gap: 6px; margin: 6px 0; }
.mtab { flex: 1; padding: 5px 0; border-radius: 8px; font-family: Georgia, serif;
  font-size: 11.5px; letter-spacing: 1px; cursor: pointer; color: #8f83bd;
  background: #17102e; border: 1px solid #2c2250; }
.mtab.active { color: #ffd166; border-color: #cfa54e; background: #221741; }
#world-view { position: relative; border-radius: 12px; overflow: hidden;
  border: 1px solid #3a2f5e; }
#world-img { display: block; width: 100%; height: auto; }
.w-node { position: absolute; transform: translate(-50%, -50%); text-align: center;
  cursor: default; }
.w-node i { display: block; width: 13px; height: 13px; border-radius: 50%;
  margin: 0 auto; border: 2px solid #6f639c; background: rgba(10, 6, 26, .8); }
.w-node span { display: block; font-size: 9.5px; color: #cabffa; margin-top: 2px;
  text-shadow: 0 1px 3px #000, 0 0 6px #000; letter-spacing: .4px; white-space: nowrap; }
.w-node.open { cursor: pointer; }
.w-node.open i { border-color: #ffd166; background: rgba(60, 44, 10, .9);
  box-shadow: 0 0 8px rgba(255, 209, 102, .6); }
.w-node.open:hover i { background: #ffd166; }
.w-node.here i { border-color: #8fe6a1; background: rgba(20, 50, 26, .9);
  animation: here-pulse 1.6s ease-in-out infinite; }
@keyframes here-pulse {
  0%, 100% { box-shadow: 0 0 5px rgba(143, 230, 161, .5); }
  50% { box-shadow: 0 0 13px rgba(143, 230, 161, .95); }
}
.w-node.locked i { opacity: .55; }
.w-node.locked span { color: #6f639c; }
#map-legend[hidden], #map-view[hidden] { display: none; }


/* --- draggable HUD panels (WoW-style) -----------------------------------
   Chat and the battle log are the two panels whose ideal spot depends on
   the screen and the player, not on any breakpoint I can guess (Alan: they
   overlap at some widths). Each carries a grip; drag it and the position is
   remembered per panel. Settings has a reset row for when it goes wrong. */
.pan-grip { position: absolute; top: -2px; right: 2px; z-index: 3;
  font-size: 13px; line-height: 1; padding: 2px 4px; border-radius: 5px;
  color: #6f639c; background: rgba(10, 6, 26, .55); cursor: grab;
  opacity: 0; transition: opacity .15s ease; pointer-events: auto;
  touch-action: none; user-select: none; }
#log { pointer-events: none; }          /* text stays click-through... */
#log .pan-grip { pointer-events: auto; } /* ...the grip does not */
#log:hover .pan-grip, #chat:hover .pan-grip, .pan-grip.dragging { opacity: 1; }
.pan-grip.dragging { cursor: grabbing; color: #ffd166; }
/* a dragged panel is positioned from the top-left and must drop its
   centring transform, or it jumps by half its width on the first move */
#log.pan-moved, #chat.pan-moved { transform: none; right: auto; bottom: auto; }
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb) .pan-grip { font-size: 15px; padding: 4px 6px; }
}
/* On a phone there is no hover, so the grips sat there permanently - a
   drag handle advertising itself over the battle log at all times (Alan).
   Show them only while the chat panel is open, which is when someone is
   actually arranging their panels; they vanish again when it closes. */
@media ((hover: none) and (pointer: coarse)) {
  /* The hover rule above wins on specificity (0,1,1 beats 0,1,0), and a TAP
     on a phone leaves :hover stuck on - so the grip lit up and stayed lit,
     which is why hiding `.pan-grip` alone did nothing (Alan saw it again).
     Kill the hover rules at equal specificity here, then re-show only while
     the chat panel is actually open. */
  #log:hover .pan-grip, #chat:hover .pan-grip { opacity: 0; pointer-events: none; }
  .pan-grip { opacity: 0; pointer-events: none; }
  body:has(#chat:not([hidden])) .pan-grip,
  body:has(#chat:not([hidden])) #log:hover .pan-grip,
  body:has(#chat:not([hidden])) #chat:hover .pan-grip {
    opacity: .55; pointer-events: auto; }
  /* The grip sits INSIDE #log, which is overflow:hidden - so it cannot be
     lifted above the box without being clipped. Instead the text gets out of
     its way, and only while the grip is actually on screen: the last word of
     "You are in the world." was disappearing under it (Alan). Full width
     returns the moment chat closes and the grip goes. */
  body:has(#chat:not([hidden])) #log { padding-right: 32px; box-sizing: border-box; }
  .pan-grip.dragging { opacity: 1; pointer-events: auto; }
}

/* Odd Jobs: the side-quest block. Diamond rather than a numeral because it is
   NOT act seven - it sits beside the chain, not after it. Violet keeps it
   visually separate from the gold of the main story. */
.ql-act.ql-side .ql-act-n { color: #d8a7ff; }
.ql-act.ql-side .ql-act-t { color: #e8dcff; }
.ql-row.side-locked .ql-ico { color: #8a7aa8; }


/* a primed chain: the orb holds a bonus blow and says so */
.hb-slot.chain-primed { border-color: #7fe0d4;
  animation: chain-hum .8s ease-in-out infinite; }
@keyframes chain-hum {
  0%, 100% { box-shadow: 0 0 8px rgba(127, 224, 212, .5); }
  50% { box-shadow: 0 0 20px rgba(127, 224, 212, .95); }
}


/* --- LIMIT BREAK - a blade of light on the right edge -------------------- */
/* No box, no border: a slim skewed track that FILLS with light and ignites
   when ready. It used to lie centred above the hotbar, but the open-world
   camera stands the character at bottom-centre of the frame and the bar sat
   straight across him (Alan: "in the way"). Touch already solved this years
   of bug reports ago: VERTICAL on the right edge, owning empty screen in
   every zone. Desktop now uses the same answer - rotate(-90deg) turns the
   track's x-axis into "up", so the width-percent fill from hud.js climbs
   the bar unchanged. */
#limit-btn { position: fixed; width: 220px; height: 14px;
  left: auto; right: -78px;       /* centre lands 32px in from the right edge */
  top: 44%; bottom: auto;
  transform: rotate(-90deg) skewX(-14deg);
  padding: 0; border: 0; background: none;
  cursor: pointer; z-index: 22; transition: opacity .3s, transform .3s; }
#limit-btn.resting { opacity: 0; pointer-events: none;
  transform: rotate(-90deg) skewX(-14deg) translateY(14px); }
#limit-track { position: absolute; inset: 0; background: rgba(12, 8, 28, .72);
  box-shadow: inset 0 0 6px rgba(0, 0, 0, .8), 0 0 8px rgba(90, 70, 160, .35);
  overflow: hidden; }
#limit-fill { position: absolute; top: 0; bottom: 0; left: 0; width: 0%;
  background: linear-gradient(90deg, #3d2a6e, #7a5cff 70%, #b9a4ff);
  transition: width .25s ease-out; }
#limit-tip { position: absolute; top: -3px; bottom: -3px; width: 10px;
  transform: translateX(-5px); background: radial-gradient(closest-side, #fff, transparent);
  opacity: .0; transition: left .25s ease-out, opacity .3s; pointer-events: none; }
#limit-btn.ready #limit-fill { background: linear-gradient(90deg, #b3541e, #ff8547 55%, #ffd166); }
#limit-btn.ready #limit-track { animation: limit-blaze .9s ease-in-out infinite; }
@keyframes limit-blaze {
  0%, 100% { box-shadow: inset 0 0 6px rgba(0, 0, 0, .8), 0 0 10px rgba(255, 160, 70, .55); }
  50% { box-shadow: inset 0 0 4px rgba(60, 20, 0, .6), 0 0 26px rgba(255, 209, 102, .95); }
}
#limit-label { position: absolute; left: 50%; top: -16px;
  transform: translateX(-50%) skewX(14deg);
  font: 700 10px/1 'Cinzel', serif;
  letter-spacing: .22em; color: #ffd166; opacity: 0; transition: opacity .3s;
  text-shadow: 0 0 8px rgba(255, 180, 80, .8); white-space: nowrap; }
#limit-btn.ready #limit-label { opacity: 1; }
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  /* ON TOUCH THE BAR STANDS UP. Horizontal, it sat in the traffic between
     the fan, quick-eat and the target frame (Alan: "so hard to see with
     stuff in between"). Vertical on the LEFT EDGE it owns empty screen:
     rotate(-90deg) turns the track's x-axis into "up", so the same
     width-percent fill from hud.js climbs the bar unchanged. */
  /* THE LIMIT ORB (Alan: "maybe it can just be a button as well and it
     charges up?"). A round button on top of the skill column, right edge.
     The track is turned -90deg inside it so hud.js's width-% fill rises
     from the bottom like a filling glass; the circle clips it. Ready = the
     same blaze as the bar had. */
  body:not(.kb) #limit-btn { width: 56px; height: 56px; right: 14px; left: auto; top: auto;
    bottom: 530px; transform: none; border-radius: 50%; overflow: hidden;
    border: 2px solid #4b3d7a; box-shadow: 0 3px 10px rgba(0, 0, 0, .55); }
  body:not(.kb) #limit-btn.resting { transform: translateY(12px); }
  body:not(.kb) #limit-btn.ready { border-color: #ffd166; }
  body:not(.kb) #limit-track { inset: 0; border-radius: 50%; transform: rotate(-90deg); }
  body:not(.kb) #limit-tip { display: none; }
  body:not(.kb) #limit-label { top: 50%; left: 50%; transform: translate(-50%, -50%); opacity: 1;
    font-size: 8px; letter-spacing: .12em; }
}

/* --- COMBO METER - the chain, written across the screen ------------------ */
/* Fighting-game grammar: a number that PUNCHES bigger with every hit, climbs
   through heat colours as the chain grows, and falls away when it breaks. */
#combo-meter { position: fixed; right: 26px; top: 30%; z-index: 24;
  pointer-events: none; text-align: right;
  transform: rotate(4deg); transition: opacity .45s ease; }
#combo-meter.resting { opacity: 0; }
#combo-n { display: block; font: 900 58px/0.9 'Cinzel', serif;
  color: var(--combo-col, #dfe6ff);
  text-shadow: 0 0 14px var(--combo-glow, rgba(160, 180, 255, .7)),
               2px 3px 0 rgba(10, 5, 25, .9); }
#combo-word { display: block; font: 700 13px/1 'Cinzel', serif;
  letter-spacing: .34em; color: var(--combo-col, #dfe6ff); opacity: .85;
  text-shadow: 1px 2px 0 rgba(10, 5, 25, .9); }
#combo-meter.pop #combo-n { animation: combo-pop .28s cubic-bezier(.2, 2.2, .4, 1); }
@keyframes combo-pop {
  0% { transform: scale(1.55); } 100% { transform: scale(1); }
}
/* heat tiers */
#combo-meter[data-tier="1"] { --combo-col: #dfe6ff; --combo-glow: rgba(160, 180, 255, .7); }
#combo-meter[data-tier="2"] { --combo-col: #ffd166; --combo-glow: rgba(255, 200, 90, .8); }
#combo-meter[data-tier="3"] { --combo-col: #ff8547; --combo-glow: rgba(255, 130, 60, .85); }
#combo-meter[data-tier="4"] { --combo-col: #c9a2ff; --combo-glow: rgba(190, 140, 255, .95); }
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb) #combo-meter { right: 14px; top: 24%; }
  body:not(.kb) #combo-n { font-size: 44px; }
}


/* compass: the minimap stays north-up while the camera orbits, so a needle
   says which way the LENS points. Hidden until the view actually rotates. */
#mini-north { position: absolute; left: 50%; top: 50%; width: 0; height: 0;
  pointer-events: none; opacity: 0; transition: opacity .3s; z-index: 3; }
#mini-north::before { content: 'N'; position: absolute; left: -6px; top: -47px;
  font: 700 10px/1 'Cinzel', serif; color: #ffd166;
  text-shadow: 0 0 6px rgba(0,0,0,.9), 0 0 10px rgba(255,190,80,.6); }
#mini-dock.rotated #mini-north { opacity: 1; }


/* mid-fight boss lines: a banner over the action, never a modal. The fight
   stays fully playable underneath - that is the whole point. */
#boss-say { position: fixed; left: 50%; top: 16%; transform: translateX(-50%);
  max-width: min(560px, 86vw); text-align: center; pointer-events: none;
  font: 600 17px/1.45 'Cinzel', serif; color: #e8dcff; opacity: 0; z-index: 26;
  text-shadow: 0 2px 8px rgba(5, 3, 15, .95), 0 0 22px rgba(120, 70, 200, .5);
  transition: opacity .35s ease; }
#boss-say.on { opacity: 1; animation: boss-say-in .45s ease-out; }
#boss-say[data-who="you"] { color: #ffe9b3;
  text-shadow: 0 2px 8px rgba(5, 3, 15, .95), 0 0 22px rgba(255, 190, 80, .45); }
@keyframes boss-say-in {
  from { transform: translateX(-50%) translateY(10px); opacity: 0; }
  to { transform: translateX(-50%) translateY(0); opacity: 1; }
}

/* --- mobile camera cluster: eye = first person, arrow = reset ---------- */
/* Touch only: desktop has the wheel, right-drag and V, and needs no help. */
/* LEFT edge: the right edge mid-height belongs to the LIMIT BAR (fixed,
   vertical, centred ~42-44%), which only appears charged - the first
   placement sat straight under it and shipped overlapping (Alan). The
   left edge at this height is empty on every HUD state. */
#cam-btns { display: none; position: absolute; left: 14px; top: 46%;
  transform: translateY(-50%); flex-direction: column; gap: 10px; z-index: 6; }
@media (pointer: coarse) { #cam-btns { display: flex; } }
#cam-btns button { width: 40px; height: 40px; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, rgba(255,255,255,.1), transparent 45%), rgba(16, 10, 34, .82);
  color: #cabffa; border: 1.5px solid #4b3d7a; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .5); }
#cam-btns button:active { transform: scale(.92); }
/* lit while the lens is inside first person, so the mode reads as ON */
#cam-fpv.on { border-color: #cfa54e; color: #ffd98a;
  box-shadow: 0 0 10px rgba(212, 169, 79, .4), 0 2px 8px rgba(0, 0, 0, .5); }
/* autorun toggle joins the camera cluster; lit while running */
#cam-run.on { border-color: #7ec48c; color: #8fe0a4;
  box-shadow: 0 0 10px rgba(99, 176, 119, .45), 0 2px 8px rgba(0, 0, 0, .5); }

/* --- trading -------------------------------------------------------------
   The invite banner reuses the duel challenge's shape on purpose: answering
   one is answering any, and a player should not have to learn a second
   vocabulary of buttons under time pressure. */
#trade-invite { position: fixed; top: 94px; left: 50%; transform: translateX(-50%);
  z-index: 9; display: flex; gap: 10px; align-items: center;
  background: rgba(16, 10, 34, .95); border: 1.5px solid #4b7f6a;
  border-radius: 12px; padding: 9px 14px; font-family: Georgia, serif;
  box-shadow: 0 0 18px rgba(75, 127, 106, .35); }
#trade-invite[hidden] { display: none; }
#ti-text { font-size: 13px; color: #e8e2f7; }
#trade-invite button { padding: 5px 14px; border-radius: 8px; cursor: pointer;
  font-family: Georgia, serif; font-size: 12.5px; }
#tti-yes { background: linear-gradient(180deg, rgba(14,40,30,.9), rgba(6,20,15,.95));
  border: 1px solid #4b7f6a; color: #9fe3c4; }
#tti-no { background: none; border: 1px solid #3a2f5e; color: #8f83bd; }

/* THE WINDOW ITSELF. Sized for a phone first and merely allowed to be wider:
   two people run this at once, standing in a square, on whatever they have in
   their hands. Nothing here needs hover and nothing needs a right-click. */
#trade-window { position: fixed; inset: 0; z-index: 30; display: flex;
  align-items: center; justify-content: center;
  background: rgba(6, 4, 14, .62); }
#trade-window[hidden] { display: none; }
.tw-card { width: min(94vw, 460px); max-height: 88vh; overflow-y: auto;
  background: rgba(16, 10, 34, .98); border: 1.5px solid #4b7f6a;
  border-radius: 14px; padding: 12px 14px 10px; font-family: Georgia, serif;
  color: #e8e2f7; box-shadow: 0 0 26px rgba(75, 127, 106, .3); }
.tw-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
.tw-title { font-size: 16px; letter-spacing: .4px; }
.tw-foe { font-size: 12px; color: #9d92c6; }
.tw-cols { display: flex; gap: 10px; }
.tw-side { flex: 1 1 0; min-width: 0; background: rgba(255,255,255,.03);
  border: 1px solid #2e2650; border-radius: 10px; padding: 7px 8px; }
.tw-label { font-size: 11px; color: #9d92c6; margin-bottom: 5px;
  display: flex; align-items: center; gap: 6px; }
/* the confirm state, said out loud on both sides - a confirm you no longer
   hold is what the old swap scam relied on nobody noticing */
.tw-ok { color: #9fe3c4; border: 1px solid #4b7f6a; border-radius: 6px;
  padding: 0 5px; font-size: 10px; }
.tw-items { min-height: 46px; display: flex; flex-wrap: wrap; gap: 4px; }
.tw-bag { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; }
.tw-bagline { margin-top: 10px; }
.tw-chip { font-family: Georgia, serif; font-size: 11.5px; color: #d9d2f0;
  background: rgba(255,255,255,.05); border: 1px solid #3a2f5e;
  border-radius: 8px; padding: 5px 9px; cursor: pointer;
  /* a real tap target on a phone, not a link-sized sliver */
  min-height: 30px; }
.tw-chip.on { border-color: #4b7f6a; color: #9fe3c4;
  background: rgba(75, 127, 106, .16); }
.tw-chip:disabled { cursor: default; opacity: .85; }
.tw-empty { font-size: 11px; color: #6f659a; padding: 4px 2px; }
.tw-gold { display: block; margin-top: 6px; font-size: 11px; color: #9d92c6; }
.tw-gold input { width: 100%; margin-top: 3px; padding: 5px 7px; font-size: 13px;
  font-family: Georgia, serif; color: #ffd166; background: rgba(0,0,0,.35);
  border: 1px solid #3a2f5e; border-radius: 7px; }
.tw-gold-read b { color: #ffd166; font-weight: normal; }
.tw-foot { display: flex; gap: 8px; margin-top: 10px; }
.tw-foot button { flex: 1; padding: 9px 0; border-radius: 9px; cursor: pointer;
  font-family: Georgia, serif; font-size: 13px; min-height: 40px; }
.tw-cancel { background: none; border: 1px solid #3a2f5e; color: #8f83bd; }
.tw-confirm { background: linear-gradient(180deg, rgba(14,40,30,.9), rgba(6,20,15,.95));
  border: 1px solid #4b7f6a; color: #9fe3c4; }
.tw-confirm.on { background: linear-gradient(180deg, rgba(28,74,56,.95), rgba(12,38,28,.98));
  color: #cdf5e2; }
/* the two-second hold before the goods move: the window says so and stops
   taking edits, because a pause nobody explains reads as a bug */
#trade-window.locked .tw-chip,
#trade-window.locked .tw-gold input,
#trade-window.locked .tw-confirm { pointer-events: none; opacity: .55; }
.tw-note { margin-top: 7px; min-height: 15px; font-size: 11.5px; color: #9fe3c4; }

/* Adrift from the world: items live on the server now, so a dropped socket
   is a pause rather than a single-player game. */
body.offline #hotbar, body.offline #inv-panel { filter: grayscale(.5); opacity: .75; }

@media (max-width: 700px) {
  .tw-card { width: 96vw; padding: 10px 11px 9px; }
  .tw-cols { gap: 7px; }
  .tw-chip { font-size: 11px; padding: 5px 8px; }
  .tw-title { font-size: 15px; }
}

/* --- FPV THUMBSTICK (src/touchStick.js) ---------------------------------
   First person on a phone: the left half of the screen is a floating
   joystick, the right half a look-drag. Shown only while the lens is in
   first person (body.fpv, set by main.js from the real zoom), only on coarse
   pointers, and never for anyone who has proved a keyboard (body.kb).
   z-index sits ABOVE the canvas and BELOW every button - the camera
   cluster (6), the skill fan (30) and the attack button (31) keep their
   taps; everything else on the left half is stick. */
/* v2.91.11: the zone is the RING plus a little leeway, not half the screen
   - a thumb reaching across for a camera swipe kept landing in it (Alan:
   "the movement should be mostly contained in that button"). The ring's
   centre sits at (70px, 186px from the bottom); the zone is 150x172 around
   it, and the floating base is clamped inside that box. */
#stick-zone { display: none; position: fixed; left: 0; bottom: 100px; width: 150px; height: 172px;
  z-index: 5; touch-action: none; -webkit-user-select: none; user-select: none; }
/* v2.91.9: the stick is there in THIRD PERSON too (Alan: "i have to like
   move across the whole screen which makes it hard to control, rather than
   just contained into the joystick"). Tap-to-move still works anywhere
   outside the zone; a swipe still turns the camera. */
@media (pointer: coarse) { body:not(.kb) #stick-zone { display: block; } }
/* A panel is the screen while it is open. On a phone the WALK ring and the
   jump button step aside (Alan: "should there even be the walk button when
   char screen is open?") and the character panel may use the height they
   were reserving. main.js sets body.panel-open from the panels' own state. */
@media (pointer: coarse) {
  body.panel-open:not(.kb) #stick-zone, body.panel-open:not(.kb) #jump-btn { display: none !important; }
  body.panel-open:not(.kb) #stats-panel { max-height: calc(100dvh - 104px - 96px); }
  /* THE CHAT CLUSTER STANDS DOWN TOO. #chat-toggle sits at z-index 8, above
     every panel, so it and the message log drew straight over an open journal
     on a phone (Alan: "when quest journal opens the chat log is still over it
     on mobile"). None of it is usable while you are reading a panel, and a
     phone has no room to show both - so they step aside exactly as the stick
     and the jump button do. The chat panel itself is exempt: if it is open,
     typing is what you are doing. */
  body.panel-open:not(.kb) #log,
  body.panel-open:not(.kb) #chat-toggle,
  body.panel-open:not(.kb) #chat-hist { display: none !important; }
}
#stick-base { position: absolute; width: 112px; height: 112px; margin: -56px 0 0 -56px;
  border-radius: 50%; display: none; pointer-events: none;
  border: 1.5px solid rgba(203, 191, 250, .45);
  background: radial-gradient(circle, rgba(16, 10, 34, .55), rgba(16, 10, 34, .25) 68%, transparent 72%);
  box-shadow: 0 0 18px rgba(0, 0, 0, .35); }
#stick-base.on { display: block; }
/* RESTING HINT. A floating stick has nothing on screen until a thumb lands,
   so nobody knows it is there (Alan: "i dont see the joystick on mobile?").
   A faint ring with a walk glyph sits where the thumb naturally rests and
   fades out while the stick is in use. */
#stick-zone::before { content: ''; position: absolute; left: 70px; bottom: 86px;
  width: 92px; height: 92px; margin: 0 0 -46px -46px; border-radius: 50%;
  border: 2px dashed rgba(215, 205, 255, .85);
  background: radial-gradient(circle, rgba(16, 10, 34, .55), rgba(16, 10, 34, .25) 60%, transparent 72%);
  box-shadow: 0 0 14px rgba(0, 0, 0, .5), inset 0 0 18px rgba(0, 0, 0, .35);
  pointer-events: none; opacity: .9; transition: opacity .25s ease; }
#stick-zone::after { content: 'WALK'; position: absolute; left: 70px; bottom: 86px;
  transform: translate(-50%, 50%); font-size: 12px; font-weight: 700; letter-spacing: .2em;
  color: #e8e2f7; text-shadow: 0 1px 3px #000, 0 0 8px rgba(0, 0, 0, .8); pointer-events: none;
  opacity: .9; transition: opacity .25s ease; }
#stick-zone:has(#stick-base.on)::before, #stick-zone:has(#stick-base.on)::after { opacity: 0; }
/* the hint also steps aside for an OPEN chat box and for the combat fan -
   it is a hint, not furniture, and the stick itself keeps working */
body:has(#chat:not(.collapsed):not([hidden])) #stick-zone::before,
body:has(#chat:not(.collapsed):not([hidden])) #stick-zone::after { opacity: 0; }
/* IN A FIGHT THE STICK STAYS. It used to hide its ring the moment combat
   started (the fan owned that band) and Alan read that as the walk going
   away: "what if u need to dodge stuff like in the boss battle?" So in first
   person the skill fan leaves the left half entirely and stands as a column
   on the right edge above the attack button; the ring stays lit. */
/* THE SAME CONDITION AS THE SIZES BELOW, and it has to be. This block turns
   the hotbar into a COLUMN; the block near the end of this file gives it
   `width: 46px` under `(max-width: 700px), (coarse)`. A narrow DESKTOP window
   matches that one and not this one, so the container became 46px wide while
   the slots were still laid out in a row - six of them squeezed to 16px each,
   floating a third of the way up the screen. Measured at 414x861 with a fine
   pointer before the fix: slots 16x46 at y=541.
   Anyone who narrows their browser is on this path, not just a recording. */
@media (pointer: coarse), (max-width: 700px) {
  /* ...and in THIRD PERSON TOO: one right-hand cluster in both modes -
     attack, jump, the skill column, the limit orb on top. */
  body:not(.kb) #hotbar { left: auto; right: 14px; transform: none; width: 56px; height: auto;
    bottom: 246px; display: flex; flex-direction: column-reverse; gap: 8px; align-items: center; }
  /* RELATIVE, never static: the slot's art (.hb-art, inset 0) and the
     rotating ready-ring (::after) are absolutely positioned INSIDE it. Static
     handed them the whole column as their box - a 280px slab of skill art
     with a giant spinning ring, "the green thing" (Alan). */
  body:not(.kb) #hotbar .hb-slot { position: relative; width: 46px; height: 46px; left: auto; bottom: auto; }
  body:not(.kb) #hotbar .hb-slot:nth-child(1) { width: 52px; height: 52px; }
  body:not(.kb) #hotbar .hb-slot:nth-child(1) .hb-icon { font-size: 24px; }
  /* the orb is one more button in the column, so it is the column's size
     (Alan: "make the limit break button same size as the combat buttons") */
  body:not(.kb) #limit-btn { width: 46px; height: 46px; right: 19px; }
}
#stick-knob { position: absolute; left: 50%; top: 50%; width: 52px; height: 52px; margin: -26px 0 0 -26px;
  border-radius: 50%; border: 1.5px solid #cfa54e;
  background: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, .22), transparent 45%), rgba(84, 58, 178, .78);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .6), 0 0 10px rgba(212, 169, 79, .35); }

/* --- JUMP BUTTON (touch) ------------------------------------------------
   Space jumps on a keyboard; a phone had no way to (Alan: "is it possible
   to add the jump button as well?"). It sits on the right edge above the
   attack button, in the thumb's reach, and steps aside during a fight - the
   skill fan owns that band then, and nobody jumps mid-swing. Coarse
   pointers only, never for a proven keyboard. */
#jump-btn { display: none; position: fixed; right: 16px; bottom: 172px; width: 58px; height: 58px;
  border-radius: 50%; z-index: 31; cursor: pointer; padding: 0;
  /* MATCHES THE WALK RING (Alan: "more transparent like the walk joystick").
     It was an opaque plum disc with a hard border - furniture, sitting over
     the world at the same weight as a panel. Same dashed ring, same
     see-through fill, same soft glow as the joystick hint opposite it. */
  background: radial-gradient(circle, rgba(16, 10, 34, .55), rgba(16, 10, 34, .25) 60%, transparent 72%);
  color: #e8e2f7; border: 2px dashed rgba(215, 205, 255, .85);
  box-shadow: 0 0 14px rgba(0, 0, 0, .5), inset 0 0 18px rgba(0, 0, 0, .35);
  opacity: .9; align-items: center; justify-content: center;
  transition: transform .1s ease, opacity .25s ease; }
#jump-btn:active { transform: scale(.9); opacity: 1; color: #fff;
  background: radial-gradient(circle, rgba(84, 58, 178, .6), rgba(84, 58, 178, .3) 60%, transparent 72%);
  border-color: rgba(255, 255, 255, .9); }
/* shown only in first person - see the FEWER BUTTONS block below */
body:has(#stats-panel:not(.closed)) #jump-btn,
body:has(#inv-panel:not(.closed)) #jump-btn,
body:has(#dialogue:not([hidden])) #jump-btn { display: none !important; }

/* --- FEWER BUTTONS (v2.91.0) --------------------------------------------
   Alan, on a phone with ten controls in view: "thats alot of buttons
   around... is that the best way?" No. The left-edge camera cluster and the
   jump button are gone on touch; their jobs moved onto things already there:
     first person  -> the eye on the minimap's rim (#mini-fpv)
     reset camera  -> tap the N that appears on the minimap when it is turned
     autorun       -> push the thumbstick past its rim and let go
     jump          -> flick the thumbstick up
   The old elements stay in the DOM (main.js still reads #cam-fpv's state)
   but never draw on a touch screen. */
#cam-btns { display: none !important; }
/* ...except JUMP, which came back within the hour: "u cant have it tap on
   the button because it stops the walking... it needs to move and jump
   together" - the left thumb is on the stick, so jump belongs under the
   RIGHT thumb. First person only (that is where the stick is), right edge
   above the attack button; the skill column starts above it. */
@media (pointer: coarse) {
  body:not(.kb) #jump-btn { display: flex; }                                   /* third person too (Alan) */
}

/* the N is a button once the camera has turned: tap it to face north again */
#mini-dock.rotated #mini-north { pointer-events: auto; cursor: pointer; }
#mini-dock.rotated #mini-north::before { padding: 6px 8px; margin: -6px -8px; }
/* thumbstick cues: past the rim = "let go to run"; running = gold ring */
/* Past the rim the ring warms; the LOCK METER then fills over the dwell, so
   autorun visibly arms instead of firing by surprise. A conic gradient is the
   whole cue - no prompt to read, no toast to dismiss. */
#stick-base.past { border-color: #cfa54e; box-shadow: 0 0 16px rgba(212, 169, 79, .45); }
#stick-base.past::after {
  content: ''; position: absolute; inset: -5px; border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(rgba(212, 169, 79, .85) calc(var(--lock, 0) * 360deg),
                             rgba(212, 169, 79, 0) 0);
  -webkit-mask: radial-gradient(circle, transparent 0 calc(100% - 4px), #000 calc(100% - 4px));
          mask: radial-gradient(circle, transparent 0 calc(100% - 4px), #000 calc(100% - 4px)); }
#stick-base.armed { border-color: #ffd98a;
  box-shadow: 0 0 22px rgba(255, 217, 138, .65); }
body.autorun #stick-zone::before { border-style: solid; border-color: #cfa54e;
  box-shadow: 0 0 16px rgba(212, 169, 79, .5); opacity: .95 !important; }
body.autorun #stick-zone::after { content: 'AUTO-WALK'; font-size: 9.5px; letter-spacing: .1em; color: #ffd98a; opacity: .95 !important; }
/* the idle notice sat on the quest banner on phones */
@media (max-width: 700px) { #net-status { top: 84px; } }

/* autorun in third person: nothing on screen said you were running and there
   was no way to stop (Alan). A pill above the log; any tap on the ground ends
   the run (renderer3d pointerdown -> onCancelRun). Touch screens only. */
#run-pill { display: none; position: fixed; left: 50%; bottom: 66px; transform: translateX(-50%);
  z-index: 8; padding: 5px 14px; border-radius: 999px; font-family: Georgia, serif; font-size: 11px;
  letter-spacing: .14em; color: #ffd98a; background: rgba(16, 10, 34, .88); border: 1.5px solid #cfa54e;
  box-shadow: 0 0 12px rgba(212, 169, 79, .35); pointer-events: none; }
/* the stick's own ring says AUTO-WALK in both modes now; the pill retired in v2.91.9 */
/* jump: a touch smaller, the thumb still lands on it */
#jump-btn { width: 52px; height: 52px; right: 19px; bottom: 176px; }

/* phones never drag panels (main.js PHONE_LAYOUT), so the drag handle is
   noise - Alan: "the moving button on mobile should only appear after..." -
   there is nothing for it to do; gone on touch layouts */
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb) .pan-grip, body:not(.kb):has(#chat:not([hidden])) .pan-grip { display: none; }
}
/* the keyboard layout keeps its full banner and key hint on the bar */
body.kb #limit-label::after { content: ' BREAK \00b7 R'; }

/* --- READABLE CHAT, FOOD IN THE CLUSTER, NO KEY HINTS ON TOUCH (v2.91.8) ---
   Alan: "the text is quite hard to read in the chat dialogue, especially
   'you are in the world'" - lines sat bare on bright ground. Each line gets
   a dark pill; the collapsed whisper is no longer half-transparent; system
   lines lose the dim purple italic. */
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  /* NO BOXES (Alan: "they look very blocky and in the way"). Legibility
     comes from the letterforms: a dark stroke painted UNDER the fill
     (paint-order) plus a soft shadow reads over lit cobbles and dark
     night alike, and costs no rectangle. */
  /* CLEAN, not outlined: the heavy stroke read as smeared (Alan: "the
     shadowing looks kind of ugly... more precise and clean"). White,
     semi-bold, one crisp 1px drop plus a tight 2px halo - the same recipe
     as subtitles. The battle log keeps its original look. */
  body:not(.kb) #chat-log { gap: 2px; }
  body:not(.kb) .chat-line { font-size: 12.5px; line-height: 1.35; font-weight: 600; color: #fff;
    text-shadow: none; paint-order: stroke fill; -webkit-text-stroke: 1.4px #000;   /* a thin border, no glow (Alan) */
    overflow-wrap: anywhere; }
  body:not(.kb) .chat-line.sys { color: #e6dfff; font-style: normal; }
  body:not(.kb) #chat.collapsed #chat-log { opacity: .95; max-height: 8vh; }
  /* "the food button is also just randomly floating on one side" - it joins
     the right-hand cluster, beside the attack button, and moves WITH it */
  body:not(.kb) #quick-eat { left: auto; right: 104px; bottom: 90px; transform: none;
    width: 52px; height: 52px; }
  body:not(.kb) #quick-eat.resting { transform: translateY(26px); }
  /* keyboard hints mean nothing to a thumb */
  body:not(.kb) #atk-key, body:not(.kb) #qe-key, body:not(.kb) .hb-key { display: none; }
}

/* --- the you-dot points where you are going (v2.91.8) --------------------
   A small triangle riding the dot, turned to the travel heading hud.js sets
   in --heading. The N compass badge is gone (Alan: "some random N"); the
   map is north-up anyway. The pulse animation owns the dot's transform, so
   the pointer lives on ::after with its own. */
#mini-north { display: none !important; }
.map-dot.you.heading::after { content: ''; position: absolute; left: 50%; top: 50%;
  width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-bottom: 9px solid #fff; filter: drop-shadow(0 0 3px #8a6cff);
  transform: translate(-50%, -50%) rotate(var(--heading, 0deg)) translateY(-11px); }
.map-dot.small.you.heading::after { border-left-width: 4px; border-right-width: 4px; border-bottom-width: 7px;
  transform: translate(-50%, -50%) rotate(var(--heading, 0deg)) translateY(-9px); }
#mini-fpv.held { border-color: #ffd166; }

/* --- ONE COLUMN, ONE AXIS (v2.91.8) ---------------------------------------
   Alan: "really annoying having to track all these buttons and them not
   aligning with each other." Every right-hand control now shares the same
   centre line, 44px in from the right edge, stacked with 14px gaps:
     attack 74-146 | jump 160-212 | food 226-278 | skills 292-590 | limit 598-650

   SIX SKILLS, SAME ENVELOPE (v2.99.13). The bar went 5 -> 6 slots. Growing the
   column upward at 52px would have pushed the limit orb to 686px, off a short
   phone entirely and into the minimap on a tall one, so the SLOT shrinks to
   46px instead (still over the 44px touch minimum) and six of them occupy
   almost exactly the five's old height. The orb lands back on its documented
   line. Alan's rule holds: new combat controls go INTO this column, never
   beside it. */
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  /* one 8px gap everywhere, the same as between skill slots (Alan: "should
     it not be the same so spacing etc is the same?") */
  body:not(.kb) #attack-btn { width: 72px; height: 72px; right: 8px; bottom: 74px; }
  body:not(.kb) #jump-btn { width: 52px; height: 52px; right: 18px; bottom: 154px; }
  body:not(.kb) #quick-eat { width: 52px; height: 52px; right: 18px; bottom: 214px; left: auto; transform: none; }
  body:not(.kb) #quick-eat.resting { transform: translateY(26px); }
  body:not(.kb) #hotbar { width: 46px; right: 21px; bottom: 274px; gap: 8px; }
  body:not(.kb) #hotbar .hb-slot, body:not(.kb) #hotbar .hb-slot:nth-child(1) { width: 46px; height: 46px; }
  body:not(.kb) #hotbar .hb-slot:nth-child(1) .hb-icon { font-size: 22px; }
  body:not(.kb) #limit-btn { width: 46px; height: 46px; right: 21px; bottom: 598px; }
}

/* SHORT PHONES. The column is a fixed stack measured from the bottom, so on a
   667px-tall screen (iPhone SE and friends) the limit orb's top lands 51px
   down - close to the quest banner. Six slots at 42px with 6px gaps pull the
   whole stack back in without dropping any control below the 44px... except
   the slots themselves, which is why this only applies where the alternative
   is a control off the screen entirely. Portrait only, per Alan. */
@media ((max-width: 700px) or ((hover: none) and (pointer: coarse))) and (max-height: 700px) {
  body:not(.kb) #hotbar { width: 42px; right: 23px; gap: 6px; }
  body:not(.kb) #hotbar .hb-slot, body:not(.kb) #hotbar .hb-slot:nth-child(1) { width: 42px; height: 42px; }
  body:not(.kb) #hotbar .hb-slot:nth-child(1) .hb-icon { font-size: 20px; }
  body:not(.kb) #limit-btn { bottom: 564px; }
}

/* food at full health: still there, shaded and inert (Alan) */
#quick-eat.full { opacity: .4; filter: grayscale(.7); pointer-events: none; }

/* out of combat the column is attack + jump only: food and the limit orb
   follow the attack button's own resting state (Alan: "hide the food and
   limit break icon when not in combat as well") */
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb):has(#attack-btn.resting) #quick-eat,
  body:not(.kb):has(#attack-btn.resting) #limit-btn { opacity: 0; pointer-events: none; transform: translateY(26px); }
}

/* ONE COMBAT FLAG FOR THE WHOLE COLUMN (Alan: "the skill buttons should all
   appear when the attack button appears"). Each control used to rest on its
   own clock - skills on a hot timer, food on hunger, the orb on charge - so
   the column assembled itself piece by piece. On touch the attack button's
   resting state is the only switch: attack out = everything out. */
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb):has(#attack-btn:not(.resting)) #hotbar.resting,
  body:not(.kb):has(#attack-btn:not(.resting)) #quick-eat.resting:not(.nofood),
  body:not(.kb):has(#attack-btn:not(.resting)) #limit-btn.resting { opacity: 1; pointer-events: auto; transform: none; }
  body:not(.kb):has(#attack-btn:not(.resting)) #quick-eat.full { opacity: .4; pointer-events: none; }
  body:not(.kb):has(#attack-btn.resting) #hotbar { opacity: 0; pointer-events: none; }
  body:not(.kb) #quick-eat.nofood { opacity: 0 !important; pointer-events: none !important; }
}

/* an open panel owns the phone: the limit orb and jump step aside with the
   rest of the column (Alan: "limit break button is still sitting on top of
   z index when bag and settings are open") */
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:has(#inv-panel:not(.closed)) #limit-btn, body:has(#inv-panel:not(.closed)) #jump-btn,
  body:has(#stats-panel:not(.closed)) #limit-btn, body:has(#stats-panel:not(.closed)) #jump-btn,
  body:has(#quest-log:not(.closed)) #limit-btn, body:has(#quest-log:not(.closed)) #jump-btn,
  body:has(#settings-panel:not(.closed)) #limit-btn, body:has(#settings-panel:not(.closed)) #jump-btn,
  body:has(#settings-panel:not(.closed)) #hotbar, body:has(#settings-panel:not(.closed)) #attack-btn,
  body:has(#settings-panel:not(.closed)) #quick-eat,
  body:has(#help-panel:not(.closed)) #limit-btn, body:has(#help-panel:not(.closed)) #jump-btn { display: none !important; }
}

/* CHAT READ MODE WINS OVER THE COLLAPSED WHISPER, and it has to be said down
   here: the phone block caps #chat.collapsed #chat-log at 12vh and the
   no-keyboard block at 8vh, both at least as specific as .tall, so a History
   tap opened a panel that was still eight lines high. Tapping History IS the
   request to read back, whether the box is collapsed or not. */
#chat.tall #chat-log, body:not(.kb) #chat.tall #chat-log {
  max-height: 46vh; opacity: 1; pointer-events: auto;
  background: rgba(8, 5, 20, .92); border: 1px solid #4b3d7a;
  border-radius: 10px; padding: 8px 10px; }

/* ...and the other half: three lines when nobody is reading. The phone block
   caps the log at 16vh (12vh collapsed, 8vh with no keyboard), which on a
   tall screen is six or seven lines lying across the joystick. Three is
   enough to see that someone spoke; History is how you read the rest. */
@media (max-width: 700px) {
  #chat:not(.tall) #chat-log,
  body:not(.kb) #chat:not(.tall) #chat-log { max-height: 56px; }
}

/* THE STICK WINS UNDER THE CHAT. The log sits over the joystick's lane and
   there is nowhere else on a phone to put it (Alan: "if it overlaps it should
   still let ppl run"). Chat lines are something you READ, not something you
   press, so while the log is not in read mode it takes no touches at all and
   the thumb falls straight through to the stick underneath. The input row and
   the History bubble stay pressable; read mode takes its touches back so the
   log can be scrolled. */
@media (max-width: 700px) {
  #chat:not(.tall) #chat-log { pointer-events: none; }
  #chat.tall #chat-log { pointer-events: auto; }
}

/* --- taken over on another device -----------------------------------------
   One code stands in the world once (World.onJoin), so a login elsewhere
   ends this session. This is the only screen in the game that appears
   without being asked for, so it says plainly what happened, offers the way
   back, and leaves on its own if nobody answers. */
#replaced-out { position: fixed; inset: 0; z-index: 60; display: flex;
  align-items: center; justify-content: center; padding: 20px;
  background: rgba(6, 4, 16, .82); backdrop-filter: blur(3px); }
#replaced-out .ro-card { width: min(380px, 92vw); text-align: center;
  background: linear-gradient(160deg, rgba(20, 12, 44, .98), rgba(9, 6, 24, .98));
  border: 1px solid #6a5acd; border-radius: 16px; padding: 20px 22px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .65); }
#replaced-out h2 { margin: 0 0 10px; font-size: 16px; color: #ffd166;
  letter-spacing: 1px; }
#replaced-out p { margin: 0 0 10px; font-size: 13px; color: #cfc7ea;
  line-height: 1.5; }
#replaced-out .ro-count b { color: #ffd166; }
#replaced-out button { display: block; width: 100%; margin-top: 8px;
  padding: 10px 12px; font: inherit; font-size: 13.5px; cursor: pointer;
  background: #2c2250; color: #e8e2f7; border: 1px solid #6a5acd;
  border-radius: 10px; }
#replaced-out #ro-back { background: #3a2d6b; color: #ffd166; }
#replaced-out button:hover { background: #45356f; }

/* --- the consignment board -------------------------------------------------
   Same skin as the shop and the vault, because it IS a counter: a player who
   can use those can use this without being taught anything. Gold on the right
   of every row, the seller's name under the item, and your own listings in
   their own block so cancelling is never a mis-tap on someone else's. */
#mkt-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(380px, calc(100vw - 24px)); max-height: min(560px, 82vh); overflow-y: auto;
  background: rgba(12, 7, 28, .96); border: 1px solid #6a5acd; border-radius: 14px;
  padding: 12px 14px; z-index: 40; color: #e8e2f7; font-family: Georgia, serif; }
#mkt-panel.closed { display: none; }
#mkt-title { font-size: 14px; letter-spacing: 2px; color: #ffd166;
  display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
#mkt-gold { margin-left: auto; color: #ffd166; font-size: 13px; }
#mkt-close { background: none; border: none; color: #8f83bd; font-size: 15px;
  cursor: pointer; padding: 2px 4px; }
#mkt-mine-title, #mkt-sell-title { margin: 10px 0 4px; font-size: 12px;
  color: #8f83bd; letter-spacing: 1px; }
#mkt-mine-title small, #mkt-sell-title small { color: #6f639c; letter-spacing: 0; }
.mkt-row { display: flex; align-items: center; gap: 9px; padding: 5px 2px;
  border-bottom: 1px solid rgba(106, 90, 205, .18); }
.mkt-ico { width: 26px; height: 26px; object-fit: contain; }
.mkt-name { flex: 1; font-size: 13.5px; min-width: 0; }
.mkt-name small { display: block; color: #8f83bd; font-size: 11px; }
.mkt-price { color: #ffd166; font-size: 12.5px; white-space: nowrap; }
.mkt-btn { background: #2c2250; color: #ffd166; border: 1px solid #6a5acd;
  border-radius: 8px; padding: 4px 10px; font: inherit; font-size: 12.5px;
  cursor: pointer; }
.mkt-btn:hover { background: #3a2d6b; }
.mkt-btn.cant { opacity: .45; }
.mkt-btn.off { color: #cfc7ea; }
.mkt-empty { color: #6f639c; font-size: 12px; padding: 4px 2px; }

/* THE BOARD PROMPT wears the door prompt's clothes - same shape, same place,
   same "you are close enough to act" meaning - and stacks above it if a door
   and the board are somehow both in reach. */
#mkt-prompt { position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 172px; z-index: 30; padding: 9px 20px; border-radius: 13px;
  border: 1px solid #cfa54e; background: rgba(20, 14, 30, .9); color: #ffd166;
  font-family: Georgia, serif; font-size: 14px; cursor: pointer;
  box-shadow: 0 0 16px rgba(207, 165, 78, .35); }
#mkt-prompt[hidden] { display: none; }
#mkt-prompt:hover { background: rgba(40, 28, 56, .96); color: #fff; }
body:has(#wp-prompt:not([hidden])) #mkt-prompt { bottom: 218px; }
body:has(#door-prompt:not([hidden])) #mkt-prompt { bottom: 218px; }
@media (max-width: 700px), ((hover: none) and (pointer: coarse)) {
  body:not(.kb) #mkt-prompt { bottom: 210px; font-size: 13px; }
}

/* the board's shelves: which kind of thing you are looking at */
#mkt-tabs { display: flex; flex-wrap: wrap; gap: 5px; margin: 2px 0 8px; }
.mkt-tab { background: #1a1030; color: #a99cd8; border: 1px solid #3b2d63;
  border-radius: 999px; padding: 3px 10px; font: inherit; font-size: 11.5px;
  cursor: pointer; }
.mkt-tab:hover { border-color: #6a5acd; color: #e8e2f7; }
.mkt-tab.on { background: #3a2d6b; color: #ffd166; border-color: #6a5acd; }
/* a shelf with nothing on it is still worth showing - it says what the market
   deals in - but it should not compete with the ones that have goods */
.mkt-tab.bare { opacity: .5; }
.mkt-tab.bare.on { opacity: 1; }

/* the two sides of the counter: what is for sale, and what you are selling */
#mkt-modes { display: flex; gap: 6px; margin: 2px 0 8px; }
.mkt-mode { flex: 1; background: #160d2b; color: #a99cd8; border: 1px solid #3b2d63;
  border-radius: 10px; padding: 6px 10px; font: inherit; font-size: 12.5px;
  letter-spacing: .5px; cursor: pointer; }
.mkt-mode:hover { border-color: #6a5acd; color: #e8e2f7; }
.mkt-mode.on { background: #3a2d6b; color: #ffd166; border-color: #8a6cff; }
#mkt-rows[hidden], #mkt-mine[hidden], #mkt-tabs[hidden] { display: none; }

/* THE PATH MAP (Skill Tree Arc, step 2). Three columns, five tiers, the
   price on every node always. Nodes are the old .tmap-node skin laid out
   statically in a grid instead of absolutely on a canvas. */
#tree-paths { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin: 6px 0 8px; }
.tp-root { display: flex; justify-content: center; margin: 8px 0 2px; }
.tp-root .tp-node { width: 96px; }
.tp-col { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.tp-head { text-align: center; padding: 5px 2px 6px; border-bottom: 1px solid #3a2f5e; margin-bottom: 2px; }
.tp-head b { display: block; font-size: 12px; color: #ffd166; letter-spacing: .04em; }
.tp-head small { display: block; font-size: 9px; color: #8f83bd; line-height: 1.2; }
.tp-head em { display: block; font-style: normal; font-size: 9px; color: #cabffa; margin-top: 2px; }
.tp-tier { display: flex; flex-direction: column; gap: 4px; padding: 4px 3px 5px; border-radius: 9px;
  background: rgba(20, 12, 44, .45); border: 1px solid rgba(58, 47, 94, .5); }
.tp-tierlbl { font-size: 8px; color: #6f639c; letter-spacing: .08em; text-transform: uppercase; }
.tp-tierlbl.open { color: #a99ed4; }
#stats-tree .tp-node { position: static; width: auto; transform: none; margin: 0; padding: 5px 3px 5px; }
.tp-node .tp-price { display: block; margin-top: 3px; font-size: 8.5px; line-height: 1.2; color: #8f83bd;
  padding: 1px 4px; border-radius: 6px; background: #17102e; border: 1px solid #3a2f5e; }
.tp-node .tp-price.can { color: #0d2a18; background: #7fd0a0; border-color: #a5e8c2; font-weight: bold; }
.tp-node .tp-price.got { color: #ffd166; border-color: #6a5533; }
.tp-passive { cursor: default; }
.tp-passive.s-learnable { cursor: pointer; }
.tp-passive .tp-pips { display: block; font-size: 10px; letter-spacing: 2px; color: #ffd166; line-height: 1; margin-bottom: 2px; }
.tp-passive .tp-desc { display: none; }
.tp-empty { opacity: .35; border-style: dashed; cursor: default; }
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) {
  #tree-paths { gap: 10px; }
  .tp-node .tmn-name { font-size: 11px; }
  .tp-node .tp-price { font-size: 10px; }
  .tp-passive .tp-desc { display: block; font-size: 9.5px; color: #a99cd6; line-height: 1.3; margin-top: 2px; }
  .tp-head b { font-size: 14px; } .tp-head small { font-size: 10.5px; }
}

/* the connectors drawn over the path map; the passives leave room for them */
#tree-paths { position: relative; }
#tree-paths .tp-root { grid-column: 1 / -1; margin: 6px 0 14px; }
#tree-arrows { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; z-index: 0; }
#tree-paths .tp-col, #tree-paths .tp-root { position: relative; z-index: 1; }
#tree-paths .tp-tier { background: none; border: none; padding: 10px 3px 6px; gap: 10px; }
#tree-paths .tp-tierlbl { position: absolute; margin-top: -2px; }
#tree-paths .tp-tier { position: relative; }
#tree-paths .tp-tier .tp-node:first-of-type { margin-top: 8px; }

/* THE TALENT-TREE LOOK (v3.1.20): icons on a grid, words in the card. */
#tree-paths { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 4px; position: relative; padding-bottom: 6px; }
#tree-paths .tp-root { grid-column: 1 / -1; display: flex; justify-content: center; margin: 4px 0 18px; position: relative; z-index: 1; }
#tree-paths .tp-col { display: flex; flex-direction: column; align-items: center; gap: 0; min-width: 0; position: relative; z-index: 1; }
#tree-paths .tp-head { text-align: center; padding: 3px 8px; border: 1px solid #4b3d7a; border-radius: 8px; background: #17102e; margin-bottom: 22px; }
#tree-paths .tp-head b { display: block; font-size: 11px; color: #ffd166; letter-spacing: .06em; text-transform: uppercase; }
#tree-paths .tp-head em { display: block; font-style: normal; font-size: 9px; color: #a99ed4; }
#tree-paths .tp-tier { position: relative; display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 12px 0 12px; background: none; border: none; }
#tree-paths .tp-gate { position: absolute; top: -7px; left: 50%; transform: translateX(-50%); font-size: 8px; padding: 1px 5px; border-radius: 6px;
  background: #17102e; border: 1px solid #453a6e; color: #6f639c; letter-spacing: .04em; white-space: nowrap; z-index: 2; }
#tree-paths .tp-gate.open { color: #ffd166; border-color: #8a6a20; }
#stats-tree .tp-node { position: static; width: 64px; transform: none; margin: 0; padding: 0; border: none; background: none; box-shadow: none; text-align: center; cursor: pointer; }
#stats-tree .tp-node .tp-ico { position: relative; width: 48px; height: 48px; margin: 0 auto 3px; border-radius: 10px; border: 1.5px solid #3a2f5e;
  background: linear-gradient(170deg, #221741 0%, #120b28 55%, #0d0720 100%); box-shadow: 0 3px 8px rgba(0,0,0,.45); overflow: visible; }
#stats-tree .tp-node .tp-ico .tmn-art { width: 44px; height: 44px; margin: 1px; border-radius: 8px; }
#stats-tree .tp-node .tmn-name { font-size: 8.5px; line-height: 1.1; display: block; color: #a99ed4; }
#stats-tree .tp-node.s-learnable .tp-ico { border-color: #7fd0a0; animation: node-breathe 2.2s ease-in-out infinite; }
#stats-tree .tp-node.s-known .tp-ico, #stats-tree .tp-node.s-learned .tp-ico { border-color: #cfa54e; box-shadow: 0 0 9px rgba(207,165,78,.35); }
#stats-tree .tp-node.s-known .tmn-name, #stats-tree .tp-node.s-learned .tmn-name { color: #ffd166; }
#stats-tree .tp-node.selected .tp-ico { border-color: #ffffff; box-shadow: 0 0 10px rgba(255,255,255,.5); }
#stats-tree .tp-node.s-level, #stats-tree .tp-node.s-parent { filter: grayscale(.85) brightness(.62); }
#stats-tree .tp-node.on-bar::after { content: '●'; position: absolute; top: -4px; right: 6px; font-size: 8px; color: #7fd0a0; }
#stats-tree .tp-node .tp-chip { position: absolute; bottom: -7px; left: 50%; transform: translateX(-50%); font-size: 8px; line-height: 1; padding: 2px 5px; border-radius: 6px;
  color: #8f83bd; background: #17102e; border: 1px solid #3a2f5e; white-space: nowrap; }
#stats-tree .tp-node .tp-chip.can { color: #0d2a18; background: #7fd0a0; border-color: #a5e8c2; font-weight: bold; }
#stats-tree .tp-node .tp-chip.got { color: #ffd166; border-color: #6a5533; }
#stats-tree .tp-empty { opacity: .3; cursor: default; }
#stats-tree .tp-empty .tp-ico { border-style: dashed; }
#stats-tree .tp-pass { position: relative; width: 30px; height: 30px; border-radius: 50%; border: 1.5px solid #3a2f5e; background: #120b28; cursor: pointer;
  display: flex; align-items: center; justify-content: center; flex: none; }
#stats-tree .tp-pass .tp-rune { font-size: 13px; color: #a99ed4; line-height: 1; }
#stats-tree .tp-pass .tp-rank { position: absolute; bottom: -8px; left: 50%; transform: translateX(-50%); font-size: 7.5px; color: #8f83bd; background: #17102e; border: 1px solid #3a2f5e; border-radius: 5px; padding: 0 3px; line-height: 1.3; }
#stats-tree .tp-pass.s-learnable { border-color: #7fd0a0; }
#stats-tree .tp-pass.s-learned { border-color: #cfa54e; } #stats-tree .tp-pass.s-learned .tp-rune { color: #ffd166; }
#stats-tree .tp-pass.s-parent { opacity: .45; }
#stats-tree .tp-pass.selected { border-color: #fff; }
#tree-arrows { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; z-index: 0; }
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) {
  #tree-paths { gap: 0 12px; }
  #stats-tree .tp-node { width: 84px; }
  #stats-tree .tp-node .tp-ico { width: 56px; height: 56px; } #stats-tree .tp-node .tp-ico .tmn-art { width: 52px; height: 52px; }
  #stats-tree .tp-node .tmn-name { font-size: 10px; }
  #stats-tree .tp-pass { width: 34px; height: 34px; }
  #tree-paths .tp-head b { font-size: 12px; }
}
/* the skill icon stays on the column's centre line; the rune hangs off to
   the right of it, so the spine runs straight tier to tier */
#tree-paths .tp-tier .tp-pass { position: absolute; left: calc(50% + 30px); top: 50%; transform: translateY(-60%); }
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) { #tree-paths .tp-tier .tp-pass { left: calc(50% + 36px); } }
@media (max-width: 700px) {
  #stats-tree .tp-node { width: 56px; }
  #stats-tree .tp-node .tp-ico { width: 42px; height: 42px; } #stats-tree .tp-node .tp-ico .tmn-art { width: 38px; height: 38px; }
  #stats-tree .tp-pass { width: 24px; height: 24px; } #stats-tree .tp-pass .tp-rune { font-size: 11px; }
  #tree-paths .tp-tier .tp-pass { left: calc(50% + 22px); }
  #tree-paths .tp-col:last-child .tp-tier .tp-pass { left: auto; right: 0; }
}
/* ROOM FOR THE TREE (v3.1.21). Three columns of icon + rune need ~100px
   each; the Skills tab widens the panel to hold them on every screen, and
   each tier is a fixed two-column grid (icon | rune slot, empty or not) so
   the spine runs straight and nothing hangs outside its column. */
#stats-panel[data-tab="skills"] { width: 440px; max-width: calc(100vw - 28px); }
#tree-paths { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 6px; overflow: hidden; }
#tree-paths .tp-tier { display: grid; grid-template-columns: 64px 30px; column-gap: 8px; justify-content: center; align-items: center; padding: 14px 0; }
#tree-paths .tp-tier .tp-pass { position: static; transform: none; grid-column: 2; justify-self: start; }
#tree-paths .tp-tier .tp-node { grid-column: 1; }
#stats-tree .tp-node { width: 64px; }
#stats-tree .tp-node .tmn-name { white-space: normal; overflow-wrap: anywhere; max-width: 64px; margin: 0 auto; }
#stats-tree .tp-pass .tp-rank { bottom: -9px; }
@media (max-width: 700px) {
  #stats-panel[data-tab="skills"] { width: min(96vw, 420px); left: 2vw; max-height: calc(100dvh - 104px - 200px); }
  #tree-paths { gap: 0 2px; }
  #tree-paths .tp-tier { grid-template-columns: 56px 26px; column-gap: 5px; padding: 12px 0; }
  #tree-paths .tp-tier .tp-pass { position: static; left: auto; right: auto; transform: none; }
  #tree-paths .tp-col:last-child .tp-tier .tp-pass { left: auto; right: auto; }
}
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) {
  #stats-panel[data-tab="skills"] { width: 600px; }
  #tree-paths .tp-tier { grid-template-columns: 84px 34px; column-gap: 12px; }
}
/* chips clear the names; heads sit over the icon column, not the icon+rune pair */
#stats-tree .tp-node .tmn-name { margin-top: 9px; }
#tree-paths .tp-head { transform: translateX(-19px); }
@media (max-width: 700px) { #tree-paths .tp-head { transform: translateX(-15px); } #tree-paths .tp-tier { padding: 16px 0; } }
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) { #tree-paths .tp-head { transform: translateX(-23px); } }
/* the rune's rank sits on its lower-right corner, off the spine's gate chip */
#stats-tree .tp-pass .tp-rank { left: auto; right: -9px; bottom: -6px; transform: none; }
/* MOBILE, MEASURED (v3.1.23). On a 375px phone the panel was 388px wide:
   its width rule never counted its own padding (Alan: "how is this mobile
   responsive? i cant even see the whole window"). border-box everywhere,
   the hotbar row shrinks to fit, and a landscape phone (short, wide) gets
   a panel that starts at the top and uses the height it actually has. */
#stats-panel { box-sizing: border-box; }
#stats-panel[data-tab="skills"] { width: 468px; }
@media (max-width: 700px) {
  #stats-panel { width: min(320px, 92vw); left: 4vw; }
  #stats-panel[data-tab="skills"] { width: 92vw; left: 4vw; max-width: none; }
  #lo-slots { flex-wrap: nowrap; gap: 4px; }
  #lo-slots .lo-slot { width: 40px; height: 40px; }
  #lo-slots .lo-slot .lo-art { width: 30px; height: 30px; }
  #tree-paths .tp-tier { grid-template-columns: 56px 26px; }
}
@media (max-height: 520px) {
  #stats-panel { top: 6px; max-height: calc(100dvh - 12px); }
  #stats-panel[data-tab="skills"] { width: min(92vw, 520px); }
}
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) {
  #stats-panel { width: 568px; }
  #stats-panel[data-tab="skills"] { width: 628px; }
}
@media (max-width: 700px) {
  #tree-paths .tp-tier { grid-template-columns: 50px 24px; column-gap: 4px; }
  #stats-tree .tp-node { width: 50px; } #stats-tree .tp-node .tp-ico { width: 40px; height: 40px; } #stats-tree .tp-node .tp-ico .tmn-art { width: 36px; height: 36px; }
  #tree-paths .tp-head { transform: translateX(-14px); padding: 3px 4px; }
  #tree-paths .tp-head b { font-size: 10px; }
}
/* a short (landscape) phone: the panel takes the height there is, whatever
   the panel-open rule says about keyboards */
@media (max-height: 520px) {
  body.panel-open:not(.kb) #stats-panel, body #stats-panel { top: 6px; max-height: calc(100dvh - 12px); }
  #stats-panel[data-tab="skills"] { width: min(92vw, 520px); }
}
@media (max-width: 700px) {
  #tree-paths .tp-tier { grid-template-columns: 40px 20px; column-gap: 4px; padding: 14px 0; }
  #stats-tree .tp-node { width: 40px; }
  #stats-tree .tp-node .tp-ico { width: 36px; height: 36px; } #stats-tree .tp-node .tp-ico .tmn-art { width: 32px; height: 32px; }
  #stats-tree .tp-node .tmn-name { width: 92px; max-width: none; margin: 8px 0 0 -26px; font-size: 8px; line-height: 1.1; }
  #stats-tree .tp-node .tp-chip { font-size: 7.5px; padding: 1px 4px; }
  #stats-tree .tp-pass { width: 20px; height: 20px; } #stats-tree .tp-pass .tp-rune { font-size: 9px; }
  #stats-tree .tp-pass .tp-rank { font-size: 7px; right: -8px; }
  #tree-paths .tp-head { transform: translateX(-12px); padding: 2px 3px; max-width: 100%; }
  #tree-paths .tp-head b { font-size: 9px; letter-spacing: 0; } #tree-paths .tp-head em { font-size: 8px; }
  #tree-paths .tp-gate { font-size: 7px; }
}
@media (max-width: 700px) { #tree-paths .tp-col:last-child .tp-pass .tp-rank { right: auto; left: -8px; } }

/* ===== THE SKILL TREE, v3.1.24 (redone: nothing absolute) ===== */
#tree-paths { position: static; display: block; overflow: visible; margin: 8px 0 4px; }
#tree-paths.wide { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 14px; }
.tt-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 0 0 10px; }
.tt-tab { font: inherit; color: #a99ed4; background: #17102e; border: 1px solid #3a2f5e; border-radius: 10px; padding: 6px 4px; cursor: pointer; text-align: center; }
.tt-tab b { display: block; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: #cabffa; }
.tt-tab small { display: block; font-size: 10px; color: #8f83bd; }
.tt-tab.active { border-color: #ffd166; background: #2a1f4a; } .tt-tab.active b { color: #ffd166; }
.tt-path { display: flex; flex-direction: column; align-items: center; }
.tt-head { text-align: center; margin: 0 0 6px; }
.tt-head b { display: block; font-size: 15px; color: #ffd166; letter-spacing: .05em; text-transform: uppercase; }
.tt-head small { display: block; font-size: 11px; color: #a99ed4; }
.tt-head em { display: block; font-style: normal; font-size: 10.5px; color: #8f83bd; margin-top: 2px; }
#tree-paths.wide .tt-head b { font-size: 13px; } #tree-paths.wide .tt-head small { font-size: 9.5px; }
.tt-tier { display: flex; align-items: center; justify-content: center; gap: 0; width: 100%; }
.tt-skill { font: inherit; background: none; border: none; padding: 0; margin: 0; cursor: pointer; width: 112px; display: flex; flex-direction: column; align-items: center; gap: 4px; color: inherit; }
.tt-ico { display: block; width: 64px; height: 64px; border-radius: 14px; border: 2px solid #3a2f5e; overflow: hidden;
  background: linear-gradient(170deg, #221741 0%, #120b28 55%, #0d0720 100%); box-shadow: 0 4px 10px rgba(0,0,0,.45); }
.tt-art { display: block; width: 100%; height: 100%; object-fit: cover; }
.tt-name { font-size: 11px; line-height: 1.15; color: #cabffa; text-align: center; max-width: 112px; }
.tt-chip { font-size: 10px; line-height: 1; padding: 3px 8px; border-radius: 8px; color: #8f83bd; background: #17102e; border: 1px solid #3a2f5e; }
.tt-chip.can { color: #0d2a18; background: #7fd0a0; border-color: #a5e8c2; font-weight: bold; }
.tt-chip.got { color: #ffd166; border-color: #6a5533; }
.tt-skill.s-learnable .tt-ico { border-color: #7fd0a0; animation: node-breathe 2.2s ease-in-out infinite; }
.tt-skill.s-known .tt-ico, .tt-skill.s-learned .tt-ico { border-color: #cfa54e; box-shadow: 0 0 12px rgba(207,165,78,.35); }
.tt-skill.s-known .tt-name, .tt-skill.s-learned .tt-name { color: #ffd166; }
.tt-skill.s-level .tt-ico, .tt-skill.s-parent .tt-ico { filter: grayscale(.85) brightness(.55); }
.tt-skill.s-level .tt-name, .tt-skill.s-parent .tt-name { color: #6f639c; }
.tt-skill.selected .tt-ico { border-color: #fff; box-shadow: 0 0 12px rgba(255,255,255,.55); }
.tt-skill.on-bar .tt-name::after { content: ' ●'; color: #7fd0a0; font-size: 8px; vertical-align: 2px; }
.tt-empty { cursor: default; opacity: .35; } .tt-empty .tt-ico { border-style: dashed; }
.tt-side { display: flex; align-items: center; gap: 0; }
.tt-hline { display: block; width: 18px; height: 2px; background: #453a6e; }
.tt-side.lit .tt-hline { background: #d9b25e; }
.tt-rune { font: inherit; cursor: pointer; width: 44px; height: 44px; border-radius: 50%; border: 2px solid #3a2f5e; background: #120b28; color: #a99ed4;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0; padding: 0; margin: 0; line-height: 1; }
.tt-rune .tt-glyph { font-size: 14px; }
.tt-rune .tt-rank { font-size: 9px; color: #8f83bd; margin-top: 2px; }
.tt-rune.s-learnable { border-color: #7fd0a0; }
.tt-rune.s-learned { border-color: #cfa54e; color: #ffd166; } .tt-rune.s-learned .tt-rank { color: #ffd166; }
.tt-rune.s-parent { opacity: .45; }
.tt-rune.selected { border-color: #fff; }
/* the spine: a line, the gate chip riding on it, a line, an arrowhead - all in flow */
.tt-link { display: flex; flex-direction: column; align-items: center; width: 100%; }
.tt-link .tt-line { display: block; width: 3px; height: 12px; background: #453a6e; border-radius: 2px; }
.tt-link .tt-arrow { display: block; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid #453a6e; }
.tt-link.lit .tt-line { background: #d9b25e; } .tt-link.lit .tt-arrow { border-top-color: #ffd166; }
.tt-gate { font-size: 10px; line-height: 1; padding: 3px 8px; border-radius: 8px; color: #6f639c; background: #17102e; border: 1px solid #453a6e; margin: 2px 0; white-space: nowrap; }
.tt-gate.open { color: #ffd166; border-color: #8a6a20; }
/* the tier row is centred on the SKILL: a rune-wide spacer balances the side */
.tt-tier::before { content: ''; display: block; width: 62px; flex: none; }
.tt-tier .tt-side { width: 62px; flex: none; }
.tt-tier:not(:has(.tt-side))::after { content: ''; display: block; width: 62px; flex: none; }
#tree-paths.wide .tt-skill { width: 92px; } #tree-paths.wide .tt-ico { width: 52px; height: 52px; }
#tree-paths.wide .tt-name { font-size: 10px; max-width: 92px; } #tree-paths.wide .tt-rune { width: 36px; height: 36px; }
#tree-paths.wide .tt-rune .tt-glyph { font-size: 12px; } #tree-paths.wide .tt-rune .tt-rank { font-size: 8px; }
#tree-paths.wide .tt-hline { width: 12px; } #tree-paths.wide .tt-tier::before, #tree-paths.wide .tt-tier .tt-side, #tree-paths.wide .tt-tier:not(:has(.tt-side))::after { width: 50px; }
/* the old map's rules must not leak in */
#stats-tree .tp-node, #stats-tree .tp-pass, #tree-arrows { display: none !important; }

/* v3.1.25: learned = the frame, not a chip; painted runes; the card is a sheet */
.tt-ico { position: relative; }
.tt-skill.s-known .tt-ico, .tt-skill.s-learned .tt-ico { border-color: #e0b95a; box-shadow: 0 0 0 1px #6a5533, 0 0 14px rgba(224,185,90,.35); }
.tt-skill.s-known .tt-ico::after, .tt-skill.s-learned .tt-ico::after { content: ''; position: absolute; right: -6px; top: -6px; width: 14px; height: 14px; border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #fff3c4, #e0b95a 55%, #8a6a20); box-shadow: 0 0 0 2px #120b28, 0 0 6px rgba(255,209,102,.8); }
.tt-skill.s-known .tt-ico::before, .tt-skill.s-learned .tt-ico::before { content: ''; position: absolute; right: -2px; top: -3px; width: 4px; height: 7px; border: solid #3a2a00; border-width: 0 2px 2px 0; transform: rotate(45deg); z-index: 1; }
.tt-ico { overflow: visible; } .tt-art { border-radius: 12px; }
.tt-rune { overflow: hidden; padding: 0; }
.tt-rune .tt-runeart { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.tt-rune .tt-rank { position: absolute; }   /* on top of the art */
.tt-rune { position: relative; }
.tt-rune .tt-rank { left: 50%; bottom: 3px; transform: translateX(-50%); font-size: 9px; padding: 1px 5px; border-radius: 6px; color: #fff; background: rgba(10,6,24,.85); }
.tt-rune.s-parent .tt-runeart { filter: grayscale(.9) brightness(.55); }
.tt-card { position: sticky; bottom: 0; z-index: 4; margin-top: 10px; background: linear-gradient(160deg, rgba(30,18,60,.98), rgba(14,8,32,.98)); border-color: #6a5a9a;
  box-shadow: 0 -8px 24px rgba(0,0,0,.55); }
.tt-close { position: absolute; top: 6px; right: 8px; width: 26px; height: 26px; border-radius: 50%; border: 1px solid #4b3d7a; background: #17102e; cursor: pointer; }
.tt-close::before, .tt-close::after { content: ''; position: absolute; left: 7px; top: 12px; width: 11px; height: 1.5px; background: #cabffa; transform: rotate(45deg); }
.tt-close::after { transform: rotate(-45deg); }
.tree-card { position: relative; }
/* v3.1.25: the sticky skill card never stuck. `overflow-x: hidden` on
   #stats-tree made IT the card's scroll container (any overflow but visible
   does), and #stats-tree never scrolls - the panel does. `clip` still cuts
   horizontal overflow but is not a scroll container, so the card sticks to
   the panel's foot as intended. */
#stats-tree { overflow-x: clip; }
/* and `.tree-card { position: relative }` (for the close button) came later in
   the cascade than `.tt-card { position: sticky }` and quietly won */
.tree-card.tt-card { position: sticky; bottom: 0; }
/* phone landscape: the panel is short, so the sheet is compact */
@media (max-height: 520px) {
  .tree-card.tt-card { padding: 6px 10px 8px; font-size: 12px; margin-top: 6px; }
  .tree-card.tt-card .tc-head { margin-bottom: 2px; }
  .tree-card.tt-card p, .tree-card.tt-card .tc-desc { margin: 2px 0; }
}

/* v3.1.29: the stats panel on a desktop-width window.
   The character's shadow was an absolute ellipse pinned to the panel's LEFT
   edge, so widening the panel slid the sprite right and left the shadow
   behind it (Alan: "the characters shadow is not aligned"). It belongs under
   the sprite, so it hangs off the sprite's own box now. And a stat row is
   label-left / value-right: at 540px all that extra width opened up between
   the two words instead of around them, so the rows are inset instead. */
#stats-body::before { display: none; }
#stats-sprite-wrap { position: relative; display: inline-flex; flex: 0 0 auto; }
#stats-sprite-wrap::after { content: ''; position: absolute; left: 50%; bottom: -4px;
  transform: translateX(-50%); width: 84%; height: 24px; border-radius: 50%;
  background: radial-gradient(ellipse, rgba(138, 108, 255, .35), transparent 70%);
  filter: blur(1px); pointer-events: none; }
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) {
  #stats-panel .stat-row { padding-left: 22px; padding-right: 22px; }
  #stats-panel #stats-grid { padding-left: 4px; padding-right: 4px; }
  #stats-panel #stats-body { gap: 34px; justify-content: center; }
}

/* the graphics row says what the device is actually running */
#set-gfx { flex-wrap: wrap; }
#set-gfx-note { flex: 1 0 100%; margin-top: 2px; font-size: 11px; color: #8f83bd; letter-spacing: .02em; }

/* ONE BUTTON SIZE EVERYWHERE, and on a phone the row only moves UP. The phone
   used to get smaller buttons, which is backwards - a thumb needs a bigger
   target than a mouse does - and it was part of why the cluster looked
   mismatched. This lives at the end of the file because the base declaration
   is further down than the old media block, so an earlier override lost. */
@media (max-width: 540px) { body { --map-size: 92px; } }

/* the tracker's own dismiss: small, quiet, and out of the way of the tap that
   opens the journal. NO position rule here - the tracker is already absolutely
   positioned with its own top/left/right, and restating `position: relative`
   threw all of that away and let it span the whole width of the screen. */
#qt-hide { position: absolute; top: 2px; right: 4px; z-index: 2;
  width: 22px; height: 22px; padding: 0; line-height: 1;
  background: none; border: 0; border-radius: 50%; cursor: pointer;
  color: #8f83bd; font-size: 15px; font-family: Georgia, serif;
  transition: color .12s ease, background .12s ease; }
#qt-hide:hover { color: #ffd166; background: rgba(255, 209, 102, .12); }
#qt-hide:active { transform: scale(.9); }

/* the way back, in the journal's own head */
#ql-track { margin-left: auto; margin-right: 8px; padding: 2px 9px;
  background: rgba(255, 209, 102, .14); border: 1px solid #cfa54e; border-radius: 999px;
  color: #ffd98a; font-family: Georgia, serif; font-size: 11px; cursor: pointer;
  transition: opacity .12s ease, border-color .12s ease; }
#ql-track.off { opacity: .5; border-color: #4b3d7a; color: #8f83bd;
  background: rgba(138, 108, 255, .08); }
#ql-track::after { content: ' on'; }
#ql-track.off::after { content: ' off'; }
#ql-head { display: flex; align-items: center; }

/* THE HIDDEN ATTRIBUTE NEVER WORKED ON THIS ONE. #quest-tracker sets
   `display: flex`, and any author display rule beats the hidden attribute's UA
   style - the same trap #death-flash and #dialogue are already guarded against
   a thousand lines above. So the dismiss button set .hidden and nothing
   happened (Alan: "why doesnt that quest window close when i click the x?").
   It also means the strip stayed on screen with no quest at all. */
#quest-tracker[hidden] { display: none !important; }

/* BOTH CLOSES WERE 19-22px, well under a thumb. The glyph stays small and
   quiet; the HIT AREA grows to 34px, which matters more on the tracker because
   the rest of that strip opens the journal - a near miss did the opposite of
   what you pressed. */
#qt-hide { width: 34px; height: 34px; top: 0; right: 0; font-size: 16px; }
/* flex-shrink had been eating it back down to 24px inside the flex head */
#ql-close { flex: 0 0 34px; width: 34px; height: 34px; padding: 0;
  display: flex; align-items: center; justify-content: center; font-size: 15px; }

/* a live quest row is a control: it puts the tracker back on screen */
.ql-row.ql-pick { cursor: pointer; border-radius: 8px;
  transition: background .12s ease; }
.ql-row.ql-pick:hover { background: rgba(255, 209, 102, .10); }
.ql-row.ql-pick:active { background: rgba(255, 209, 102, .18); }

/* PANELS SIT ABOVE THE CHAT, ALWAYS. The chat box is z-index 7 and its toggle
   is 8, while the panels were 5-6, so chat text drew straight through an open
   journal (Alan: "chat still over"). Stacking is the actual bug, and it is not
   a phone-only one - the earlier fix was gated on `pointer: coarse` and so did
   nothing in a narrow desktop window, which is where this was seen. */
#stats-panel, #quest-log, #inv-panel, #bank-panel, #shop-panel, #map-panel,
#settings-panel, #help-panel, #dialogue { z-index: 12; }

/* and while one is open, the chat cluster steps aside on any NARROW screen,
   touch or not: there is no room to show both and none of it is usable while
   you are reading a panel. Wide screens keep the chat, where it does not
   overlap anything. */
@media (max-width: 900px) {
  body.panel-open:not(.kb) #log,
  body.panel-open:not(.kb) #chat,
  body.panel-open:not(.kb) #chat-toggle,
  body.panel-open:not(.kb) #chat-hist { display: none !important; }
}

/* the bag slot, one number for the whole grid */
body { --slot: 46px; }
@media (max-width: 400px) { body { --slot: 40px; } }

/* DURABILITY (craftData.js). One bar, three moods: quiet while it is full
   enough to ignore, amber under twenty, red and named when it is broken. */
.dur-bar { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px;
  width: 100%; max-width: 150px; height: 6px; position: relative; }
.dur-bar i { display: block; height: 6px; border-radius: 3px; background: #7ce8a4; flex: none;
  max-width: calc(100% - 44px); }
.dur-bar em { font-style: normal; font-size: 8.5px; color: #8f83bd; letter-spacing: .5px; white-space: nowrap; }
.dur-bar.low i { background: #ffb066; }
.dur-bar.low em { color: #ffb066; }
.dur-bar.broken i { background: #ff5d73; width: 3% !important; }
.dur-bar.broken em { color: #ff5d73; font-weight: bold; letter-spacing: 1.2px; }
.gear-slot.broken .gs-art { filter: grayscale(1) brightness(.55); }
.gear-slot.broken { border-color: #7a2a3a; }
#forge-repairs { margin: 6px 0 10px; }
#forge-repairs[hidden] { display: none !important; }
.fg-sub { font-size: 10px; letter-spacing: 2px; text-transform: uppercase; color: #b9a48d; margin: 4px 0 6px; }
.fg-rep { display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center;
  padding: 7px 6px; border-bottom: 1px solid #3a2016; }
.fg-rep .shop-name { line-height: 1.35; }
.fg-rep .fg-title { font-size: 13px; }
.fg-rep.broken .shop-ico { filter: grayscale(1) brightness(.55); }
#forge-repairs .fg-btn.ascend { margin-top: 8px; width: 100%; }

/* THE PACK COVERS THE LIMIT METER. On a desktop the bag opens down the right
   edge and the limit bar lives on the same edge at mid-height, so the two
   overlapped (Alan: "when u open bag on desktop it overlays with the limit
   break"). The touch layout already stood the meter down under an open pack;
   now every layout does. Last in the file so it beats each variant above. */
body:has(#inv-panel:not(.closed)) #limit-btn { opacity: 0 !important; pointer-events: none !important; }

/* the map's own close, matching the other panels' ✕ */
#map-close { position: absolute; top: 8px; right: 10px; z-index: 2; width: 28px; height: 28px;
  padding: 0; line-height: 1; background: none; border: 0; border-radius: 50%; cursor: pointer;
  color: #8f83bd; font-size: 15px; font-family: Georgia, serif; }
#map-close:hover { color: #fff; background: rgba(138, 108, 255, .18); }
#map-title { padding-right: 30px; }

/* the painted astra mark beside every sum of currency */
.astra-ico { width: 15px; height: 15px; vertical-align: -3px; margin-right: 3px;
  filter: drop-shadow(0 0 3px rgba(255, 209, 102, .45)); }
.shop-buy .astra-ico, .fg-btn .astra-ico { width: 13px; height: 13px; vertical-align: -2px; }

/* INVITE BANNERS ON A PHONE. Party, duel and trade invites are one flex row
   of text + two buttons, sized to their content - a long name pushed the row
   past both edges of a 375px screen (Alan: "the box is not mobile
   responsive"). They cap at the viewport and wrap the buttons under the
   text when there is no room beside it. */
#party-invite, #duel-invite, #trade-invite { max-width: min(92vw, 560px); box-sizing: border-box;
  flex-wrap: wrap; justify-content: center; row-gap: 8px; }
#pi-text, #di-text, #ti-text { min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 540px) {
  #pi-text, #di-text, #ti-text { flex: 1 1 100%; text-align: center; font-size: 12.5px; }
  #party-invite button, #duel-invite button, #trade-invite button { flex: 1 1 40%; }
}
.fg-rep .fg-worn { font-size: 9.5px; letter-spacing: 1.2px; text-transform: uppercase; color: #b9a48d; margin-left: 7px; }

/* THE SLOT AND ITS TRACK ARE ONE NUMBER. The wide-desktop block used to set
   .inv-slot to 58px directly while #inventory's columns stayed at
   var(--slot) = 46px, so on any desktop over 1100px with a mouse the slots
   outgrew their columns and piled onto each other - the bag overlap Alan
   reported ten times, which never reproduced in a window under 1100px. The
   variable is the only thing allowed to size a slot, and it is set HERE, after
   the base `body { --slot: 46px }`, or the base wins the cascade. */
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) { body { --slot: 58px; } }

/* FISHING (fishData.js): the line's state, in words, above the hotbar. The
   bite pulses so a thumb sees it before it reads it. */
#fish-prompt { position: fixed; bottom: 118px; left: 50%; transform: translateX(-50%);
  z-index: 6; padding: 7px 16px; border-radius: 12px; font-family: Georgia, serif; font-size: 13px;
  color: #cfe8ff; background: rgba(10, 16, 34, .82); border: 1px solid #3c6a9a; pointer-events: none;
  text-shadow: 0 1px 2px #000; }
#fish-prompt[hidden] { display: none !important; }
#fish-prompt.bite { color: #fff; border-color: #ffd166; background: rgba(60, 30, 8, .9);
  animation: fish-bite .35s ease-in-out infinite alternate; }
@keyframes fish-bite { from { transform: translateX(-50%) scale(1); } to { transform: translateX(-50%) scale(1.12); } }
@media (max-width: 540px) { #fish-prompt { bottom: 150px; font-size: 14px; } }

/* the gathering channel fills the same pill from the left */
#fish-prompt { background-image: linear-gradient(90deg, rgba(140, 240, 160, .28) var(--fill, 0%), transparent var(--fill, 0%)); }

/* THE COOK'S FIRE (cookData.js). The forge's kit in warmer light: a row per
   dish, what it leaves on you above what it takes, and the two ingredients
   green when you have them and red when you do not. */
#cook-panel { position: absolute; top: 96px; left: 50%; transform: translateX(-50%);
  width: min(430px, 92vw); max-height: min(72vh, 640px); overflow-y: auto; z-index: 9;
  background: linear-gradient(160deg, rgba(34, 18, 10, .96), rgba(16, 8, 4, .97));
  border: 1px solid #b8622e; border-radius: 14px; padding: 12px 14px 14px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .6); font-family: Georgia, serif;
  transform-origin: top center; transition: transform .18s ease, opacity .18s ease; }
#cook-panel.closed { transform: translateX(-50%) scale(.7); opacity: 0; pointer-events: none; }
#cook-title { font-size: 12px; letter-spacing: 2px; color: #ffd7b0; text-transform: uppercase;
  margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
#cook-gold { margin-left: auto; color: #ffd166; font-size: 12px; letter-spacing: 0; text-transform: none; }
#cook-close { background: none; border: 0; color: #b9a48d; font-size: 15px; cursor: pointer;
  font-family: Georgia, serif; padding: 0 2px; }
#cook-note { font-size: 11.5px; color: #b9a48d; line-height: 1.45; margin-bottom: 10px; }
.ck-row { display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center;
  padding: 8px 6px; border-bottom: 1px solid #3a2016; }
.ck-row .shop-name { line-height: 1.4; min-width: 0; }
.ck-row .fg-title { font-size: 13.5px; }
.ck-row small { display: block; margin-top: 3px; font-size: 10.5px; }
.ck-buff { color: #c9bfff; }
.ck-need { color: #b9a48d; }
.ck-need b { font-weight: normal; }
.ck-need b.ok { color: #7ce8a4; }
.ck-need b.no { color: #ff8f8f; }
.ck-row.cant .shop-ico { filter: grayscale(.8) brightness(.7); }

/* the meal you are running, top-left under the portrait's HP bar */
#meal-chip { position: fixed; top: 176px; left: 22px; z-index: 6; display: flex; align-items: center;
  gap: 6px; padding: 4px 10px 4px 5px; border-radius: 999px; pointer-events: none;
  background: rgba(34, 18, 10, .86); border: 1px solid #b8622e; font-family: Georgia, serif;
  font-size: 11px; color: #ffd7b0; text-shadow: 0 1px 2px #000; }
#meal-chip[hidden] { display: none !important; }
#meal-ico { width: 20px; height: 20px; }
#meal-chip.fading { border-color: #8a4a2a; color: #c9a68d; animation: meal-fade 1.6s ease-in-out infinite; }
@keyframes meal-fade { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@media (max-width: 540px) { #meal-chip { top: 168px; left: 14px; font-size: 10.5px; } }

/* HOW MANY ARE IN THIS SLOT. Bottom-right so it never fights .inv-delta, which
   owns the top-right corner on gear; only fungible items ever carry one, and
   they never show a delta. Matches the vault's .bank-qty gold. */
.inv-qty {
  position: absolute; right: 3px; bottom: 2px;
  font-size: 10.5px; font-weight: 700; line-height: 1;
  color: #ffd166; text-shadow: 0 1px 2px #000, 0 0 4px rgba(0, 0, 0, .9);
  pointer-events: none;
}

/* --- the drift screen ------------------------------------------------------
   What an idle drop looks like now. It used to be an 11px pill at the top of
   the screen and Alan came back from being away and did not see it - which is
   fair, because that is the furthest corner from where anyone is looking and
   it is the same shape as the banner that means "connecting…" and nothing
   else. The world has actually let go by this point, nothing reconnects on
   its own, and everything still drawn underneath is a lie: same weight as the
   handover screen, for the same reason. */
#drifted { position: fixed; inset: 0; z-index: 60; display: flex;
  align-items: center; justify-content: center; padding: 20px; cursor: pointer;
  background: rgba(6, 4, 16, .84); backdrop-filter: blur(3px);
  animation: df-in .28s ease-out both; }
@keyframes df-in { from { opacity: 0; } to { opacity: 1; } }
#drifted .df-card { width: min(380px, 92vw); text-align: center;
  background: linear-gradient(160deg, rgba(20, 12, 44, .98), rgba(9, 6, 24, .98));
  border: 1px solid #6a5acd; border-radius: 16px; padding: 22px 22px 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .65); }
/* the one moving thing on a frozen screen, so the eye lands on it */
#drifted .df-mark { font-size: 26px; color: #ffd166; line-height: 1;
  margin-bottom: 8px; animation: df-pulse 2.4s ease-in-out infinite; }
@keyframes df-pulse {
  0%, 100% { opacity: .55; transform: scale(1); }
  50%      { opacity: 1;   transform: scale(1.14); }
}
#drifted h2 { margin: 0 0 10px; font-size: 17px; color: #ffd166; letter-spacing: 1px; }
#drifted p { margin: 0 0 14px; font-size: 13px; color: #cfc7ea; line-height: 1.5; }
#drifted button { display: block; width: 100%; margin-top: 8px;
  padding: 12px 12px; font: inherit; font-size: 14.5px; cursor: pointer;
  background: #3a2d6b; color: #ffd166; border: 1px solid #8a6f2e;
  border-radius: 10px; }
#drifted button:hover { background: #4a3a86; }
#drifted button:disabled { opacity: .6; cursor: default; }
#drifted .df-quiet { background: #241c44; color: #b7aede; border-color: #4b3d7a;
  font-size: 12.5px; padding: 8px 12px; }
#drifted .df-note { margin: 10px 0 0; font-size: 12px; color: #ff9aa5; }
#drifted .df-note[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  #drifted, #drifted .df-mark { animation: none; }
}

/* --- the shop's two tabs ---------------------------------------------------
   The stock and your pack used to be one column, so selling meant scrolling
   past every ware in the shop first. Same data-tab mechanism as the character
   sheet: one attribute on the panel, CSS hides the other half. */
#shop-tabs { display: flex; gap: 6px; margin: 0 0 8px; }
.shtab { flex: 1; padding: 6px 0; border-radius: 9px; font-family: Georgia, serif;
  font-size: 12px; letter-spacing: 1px; cursor: pointer; color: #8f83bd;
  background: #17102e; border: 1px solid #2c2250; }
.shtab.active { color: #ffd166; border-color: #cfa54e; background: #221741;
  box-shadow: 0 0 8px rgba(212, 169, 79, .25); }
#shop-sell-n { color: #ffd166; font-size: 11px; }
#shop-sell-n:empty { display: none; }
#shop-panel[data-tab="buy"]  #shop-sell-title,
#shop-panel[data-tab="buy"]  #shop-sell  { display: none; }
#shop-panel[data-tab="sell"] #shop-items { display: none; }
/* the heading is the tab now; keeping it would say the same word twice */
#shop-panel[data-tab="sell"] #shop-sell-title { border-top: 0; margin-top: 0; }

/* --- the pinned food -------------------------------------------------------
   Which food the quick-eat orb will use. Top-LEFT, because .inv-delta owns the
   top-right on gear and .inv-qty the bottom-right on a stack, and this has to
   be readable on a slot that may already carry a count. */
.inv-pin {
  position: absolute; left: 3px; top: 2px;
  font-size: 9px; line-height: 1; color: #ffd166;
  text-shadow: 0 1px 2px #000, 0 0 5px rgba(212, 169, 79, .9);
  pointer-events: none;
}
#im-pin.on { color: #ffd166; border-color: #cfa54e; }

/* --- gear you cannot wear yet ----------------------------------------------
   It used to draw the same green/red arrow as anything else, so "this is worse
   than what you have" and "you are twenty levels off this" were the same
   picture. The lock replaces the arrow, the number on it is the level it
   wants, and the slot itself goes cold so a full pack reads at a glance. */
.inv-slot.locked { border-color: #5a4a3a; filter: saturate(.45) brightness(.72); }
.inv-slot.locked .inv-art { opacity: .8; }
.inv-lock {
  position: absolute; top: 1px; right: 2px;
  min-width: 13px; padding: 0 3px; border-radius: 5px;
  font-size: 9.5px; font-weight: 700; line-height: 14px; text-align: center;
  color: #ffdca8; background: rgba(52, 28, 12, .92); border: 1px solid #8a6f2e;
  pointer-events: none;
}
/* THE BLOCKER GETS ITS OWN LINE, under the name and above the stats, so the
   card reads: what it is, what it asks, what it gives. Only ever drawn when
   you cannot wear the thing. */
#im-need { margin: 2px 0 5px; font-size: 12px; color: #ffca7a; }
#im-need[hidden] { display: none; }
/* ...and a requirement you HAVE met is not news: a quiet chip in the detail
   line, which still teaches the tier ladder without nagging about it. */
.lvl-chip { display: inline-block; padding: 0 6px; border-radius: 999px;
  font-size: 11px; line-height: 16px; vertical-align: 1px;
  color: #9d8fd6; background: #1d1738; border: 1px solid #3b2f66; }
#im-act:disabled { opacity: .55; cursor: default; background: #241c3e;
  color: #9a8fc4; border-color: #3a3060; }
