/* =====================================================================
   game.css — Haki-Arena recreation : 960x700 selection + battle
   Matches the haki-arena.site in-client look.
   ===================================================================== */
:root {
  --screen-w: 960px; --screen-h: 700px;
  --screen-w-num: 960; --screen-h-num: 700;   /* unitless, for scale() ratios */
  --face: 50px; --skill: 50px;
  --red: #e0241c; --red-d: #9e1812; --green: #4fa02e;
  --tan: #efe6cc; --tan-d: #d8c79c; --ink: #1a1a1a;
  /* the one shared face for the selection screen's bottom-row buttons.
     MEASURED: cream gradient, translucent red-brown hairline, 7px radius. */
  --gm-btn-face: linear-gradient(#f8ead1, #f4e1bf 52%, #eed8ae);
  --gm-btn-line: #000;                 /* v301: black, was rgba(154, 30, 9, .55) */
  /* Franklin for both game screens. "Libre Franklin" leads because it is the
     one name that is actually LOADED (the webfont link in pages/select.html
     and pages/battle.html) — every player then sees the same face instead of
     Windows users getting Franklin Gothic Medium and everyone else Arial. The
     locally-installed names stay behind it as an offline fallback, and Arial
     last so the layout never drops to a serif. */
  --gm-font: "Libre Franklin", "Franklin Gothic Medium", "Franklin Gothic",
             "ITC Franklin Gothic", Arial, sans-serif;
}

.stage { background: #10639e; min-height: 100vh; padding: 16px 0 40px;
  font-family: "Trebuchet MS", Verdana, sans-serif; }

/* =====================================================================
   THE IN-GAME PAGE KIT  (v186)  -- .gm-page
   ---------------------------------------------------------------------
   Every panel that opens ON TOP of the selection screen is one of these:
   Pirate Quests, Styles, Filter, the Team Manager, the searching popup,
   and the global chat. They were each skinned separately as they were
   built, so they drifted -- one drew a cream gradient, one drew a linear
   gradient at 145deg, one used the scroll rod and the Rectangle art and
   the others did not. Side by side they read as four different games.

   The Team Manager was the one that already looked right, so this kit is
   that treatment, named:

     THE BACKGROUND IS `Rectangle.png` -- the same placeholder every other
       in-game surface uses, via --asset-rectangle, stretched to the panel.
       Paste a resized Rectangle into ASSET_LINKS and every one of these
       changes at once. That is the whole point of it being one kit.
     THE FRAME IS THE SCROLL. `ScrollSmall.png` down the left edge, at its
       own 46:239 ratio -- the rod is (46/239) * height wide, and sits half
       its own width outside the panel so it rests ON the edge. Any panel
       that sets a different height MUST recompute both, which is why they
       are derived from --gm-page-h here rather than typed per panel.
     THE TYPE IS --gm-font and the ink is #591307, with #9a1e09 for the
       accents and #c01818 for a heading. Those are the selection screen's
       own colours, not new ones.

   FOR EVERY NEW IN-GAME PAGE: put .gm-page on the panel, give it
   --gm-page-h, and add the rod element. Do not restyle it locally. The
   measured GEOMETRY of a panel is still its own -- parity is per panel --
   but the SKIN comes from here.
   ===================================================================== */
/* v271: SQUARE. Every corner in this kit is 0 — the panel, and everything
   drawn inside it: quest cards, pictures, the search box, filter chips,
   count badges, progress bars, the close button, the footer buttons, the
   Styles cards and pills, the Filter popup's name box, energy buttons and
   ONLY track, the Team Manager's rows and buttons, the searching popup's
   input and buttons, the scrollbar thumbs.
   Asked 2026-09-24: these panels are the new ones, and they must look like
   the rest of the game. The rest of the game is square — the old
   naruto-arena.com has no rounded corner at all (0 elements, measured
   2026-09-24 off its Wayback copy; docs/specs/2026-09-24-classic-theme-v271.md).
   Their radii below this line were naruto-arena.site's newer panels,
   measured 2026-09-14; the numbers in the old comments are kept as the
   record of what theirs are, and ours now deliberately differ from them.
   What stays: the geometry (every size and offset is still the measured
   one), the font and the ink, which were already the selection screen's
   own. test_v271_classic.js holds this at zero. */
/* v272: AND FLAT. Asked 2026-09-24: on these panels a close is a bare × —
   no fill, no edge, no shadow, the same as .gm-x — and no button carries a
   gradient: each gradient became its own middle colour. A hover darkens a
   control a shade instead of lifting it on a shadow. */
/* the two surfaces a panel draws INSIDE itself — a sheet, a well, a row —
   as tokens, so an inner surface is the same colour in all six panels */
:root { --gm-inner: linear-gradient(145deg, #f7e5c5, #f7efe1 65%, #ebc98f);
        --gm-edge: rgba(0, 0, 0, .55); }   /* v301: black, was rgba(154, 30, 9, .55) */
.gm-page {
  --gm-page-h: 360px;
  /* v222: `--gm-rod-w` is GONE. It derived the rod's width from the height
     at the reference's 46:239, which forced every scroll to that ratio
     whatever the art was drawn at — the squash this release removes. The
     rod is height-only now and `.rod img` (height:100%, width:auto) lets
     the picture's own proportions set the width. A declaration nothing
     reads is the same defect as one that reads the wrong thing, so it is
     deleted rather than left lying around. */
  box-sizing: border-box;
  font-family: var(--gm-font);
  color: #000;
  background: var(--asset-rectangle) no-repeat;
  background-size: 100% 100%;
  background-color: #f7e5c5;            /* until the art loads */
}
/* the drawn fallback, only while no Rectangle art is linked: without it a
   panel is a bare colour block on the day the link 404s */
html:not(.has-art-rectangle) .gm-page {
  background-image: linear-gradient(#f6e7c8, #efdcb4 60%, #e6cf9f);
  border: 1px solid rgba(0, 0, 0, .45);
  box-shadow: 0 6px 22px #0009;
}
/* v222: EACH SURFACE DRAWS ITS OWN PICTURE, and falls back to the shared
   one until that picture exists — so shipping this changes nothing on
   screen, and pasting one link into ASSET_LINKS changes exactly one panel.
   Sizes are in assetlinks.js beside the keys; draw each at its own size and
   `100% 100%` becomes a 1:1 draw instead of six different stretches. */
.s-tm     { background-image: var(--asset-bg-teammanager, var(--asset-rectangle)); }
.s-quests { background-image: var(--asset-bg-missions,    var(--asset-rectangle)); }
.s-styles { background-image: var(--asset-bg-styles,
  var(--asset-bg-missions, var(--asset-rectangle))); }
.s-filter { background-image: var(--asset-bg-filter,      var(--asset-rectangle)); }
.s-detail .paper { background-image: var(--asset-bg-description, var(--asset-rectangle)); }

/* v222: THE ROD IS 20px TALLER THAN ITS PANEL AND OVERLAPS IT BY 10.
   Asked for 2026-09-19. Two rules, and both are written so the ART'S OWN
   PROPORTIONS decide the width:

     height  = the panel's height + 20, hanging 10px above and 10px below,
               which is the overhang the reference's own rod has.
     width   = never set. `.rod img` is height:100% / width:auto, so a
               scroll drawn at any proportion scales evenly — resize one
               axis and the other follows. Setting a width here is what
               squashed it before.
     overlap = the rod's RIGHT edge sits 10px inside the panel's left edge.
               `right: calc(100% - 10px)` says exactly that and needs no
               width to say it, so it keeps holding when the art changes.

   `--gm-rod-w` is gone from the box for the same reason: it pinned the
   width to the reference's 46:239 and fought the image's own ratio. */
/* v225: +10 AND CENTRED, not +20. Asked 2026-09-19: every scroll stands
   10px taller than the panel it hangs on and is centred on it — 5px proud
   above, 5px below. v222-v224 had +20 / 10-and-10. `width` is still never
   declared: the art's own proportions carry it, on both axes. */
.gm-page .rod { position: absolute; top: -5px;
  height: calc(var(--gm-page-h) + 10px); width: auto;
  right: calc(100% - 10px); left: auto;
  z-index: 2; filter: drop-shadow(2px 3px 4px #0006); pointer-events: none; }
.gm-page .rod img { height: 100%; width: auto; display: block; }
/* the shared furniture: a heading, a close, a button */
.gm-page .gm-head { margin: 0; color: #000; font-size: 11px;
  letter-spacing: .5px; text-align: center; }
.gm-page .gm-x { position: absolute; top: 8px; right: 10px; width: 18px; height: 18px;
  display: flex; align-items: center; justify-content: center; z-index: 3;
  font-size: 14px; line-height: 1; font-weight: bold; color: #9a3a1a;
  cursor: pointer; user-select: none; }
.gm-page .gm-x:hover { opacity: .6; }
.gm-page .gm-btn { background: #cf1d1d; color: #fff;
  border: 1px solid #8a1410; font-weight: bold; font-size: 8px; letter-spacing: .3px;
  padding: 4px 8px; cursor: pointer; border-radius: 0; white-space: nowrap;
  font-family: inherit; }
.gm-page .gm-btn:hover { filter: brightness(1.08); }
.gm-page .gm-btn.off { opacity: .45; cursor: default; }
.gm-page .gm-btn.off:hover { filter: none; }

/* =====================================================================
   PIRATE QUESTS IN GAME  (v182)
   ---------------------------------------------------------------------
   The reference's missions are an OVERLAY on the running selection screen,
   not a page you navigate to. Ours navigated away. Every number here is the
   measurement recorded in ADMIN.md's v179 section (stage 770x560):

     panel          558x504  at (106,28)
     title strip    478x36   at (136,65)   -> in the panel, (30,37)
     groups button  180x28   at (454,54)   -> in the panel, (348,26)
     card grid      508x331  at (136,143)  -> in the panel, (30,115)
     card           238x200, cream #f7e5c5c2, 1px #9a1e0947, radius 12
     card name      218x20, 11px #591307, left
     card picture   198x112
     card reqs      198x39

   508 holds two 238 cards, so the column gutter is exactly 32. The grid is
   331 tall and a card is 200, so a second row is always partly below the
   fold — it SCROLLS, which is the only way a roster that "grows forever"
   (the standing preference) can keep the panel at its measured height.

   The panel is a child of #select.screen, which is `position: relative`, so
   every offset below is stage-relative without any extra containing block.
   ===================================================================== */
.s-quests-wrap { position: absolute; inset: 0; z-index: 40;
  background: #0000004d;                 /* the reference's 25%-ish dim */
  display: flex; }
/* v186: geometry stays (it is measured); the SKIN now comes from .gm-page,
   so this panel, the Team Manager, Filter and the searching popup are one
   look rather than four. A quests_panel.png link still wins over the shared
   Rectangle for this one panel — see the rule under it. */
.s-quests { position: absolute; left: 106px; top: 28px; width: 558px; height: 504px;
  --gm-page-h: 504px; border-radius: 0; }
.s-quests[style*="--asset-quests-panel"],
.has-art-quests_panel .s-quests { background-image: var(--asset-quests-panel); }
/* art pasted into quests_panel.png replaces the drawn panel, and then our
   own border and gradient must get out of its way — same rule as every
   other .has-art-* surface on the site. */
.has-art-quests_panel .s-quests { border-color: transparent; box-shadow: none; }

/* The BOX is the measured 478x36 at (30,37) and the rule under it runs the
   full width of it. The TYPE is 14px, and that is ours rather than theirs:
   the groups button is measured at (348,26) and therefore sits ON TOP of
   this strip's right-hand third, so the title has to be small enough to
   clear it. At 19px "AVAILABLE MISSIONS" ran to x=430 and the button sat
   across the last four letters — caught in a render, not reasoned about.
   MEASURED on the reference 2026-09-14 (docs/specs/2026-09-14-ingame-overlays.md),
   which settles what v182 had to guess twice. Their title is **20px, 700,
   #591307, LEFT-ALIGNED**, in a 478x36 box with a
   `border-bottom: 1px solid rgba(154,30,9,.22)`.

   v182 set 14px and centred it, because at 20px centred the words ran under
   the groups button. The reference does not centre them: "AVAILABLE MISSIONS"
   occupies holder x 30-199 and the button starts at 348 — 149px of air. Left
   alignment was the missing piece; the font size never needed touching. */
.s-quests .q-title { position: absolute; left: 30px; top: 37px; width: 478px; height: 36px;
  box-sizing: border-box;
  display: flex; align-items: center; justify-content: flex-start;
  font-size: 20px; font-weight: 700; letter-spacing: .3px; color: #000;
  text-transform: uppercase; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis;
  border-bottom: 1px solid rgba(0, 0, 0, .22); }
/* v187: it was an 18px glyph in #591307 sitting on a cream panel at the very
   top-right — the same brown as the body text, no edge of its own, and close
   enough to the title strip's own top that it read as part of it. It is a
   real button now: a filled red disc with a white cross, and it is moved
   down and in so nothing it sits over belongs to anything else. */
/* v225: moved for the 6px stroke clearance rule — see ADMIN.md v225. */
.s-quests .q-close { position: absolute; right: 24px; top: 19px; width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center; z-index: 6;
  font-size: 16px; font-weight: 700; line-height: 1; color: #000;
  background: none; border: none;
  border-radius: 0; box-shadow: none; cursor: pointer; user-select: none; }
.s-quests .q-close:hover { opacity: .6; }

/* THE FILTER BUTTON (v183). The reference's groups button is 180x28 at
   (348,26); ours is 110x22 at (398,29) — smaller, asked for, and it opens a
   filter SHEET rather than cycling one category per click. Right edge stays
   on the reference's 528 so the strip still ends where theirs does. */
.s-quests .q-filterbtn { position: absolute; left: 398px; top: 29px; width: 110px; height: 22px;
  box-sizing: border-box; display: flex; align-items: center; justify-content: center; gap: 5px;
  background: #f4e1bf; border: 1px solid var(--gm-btn-line); border-radius: 0;
  font-size: 9px; font-weight: 700; letter-spacing: .5px; color: #000;
  text-transform: uppercase; cursor: pointer; user-select: none; white-space: nowrap; }
.s-quests .q-filterbtn:hover { box-shadow: none; filter: brightness(.96); }
.s-quests .q-filterbtn.on { background: #000; color: #fff4e1;
  border-color: #000; }
/* the count badge: a filter left on must never be invisible */
.s-quests .q-fcount { font-style: normal; min-width: 13px; height: 13px; line-height: 13px;
  padding: 0 3px; border-radius: 0; background: #000; color: #fff4e1;
  font-size: 8px; text-align: center; box-sizing: border-box; }
.s-quests .q-filterbtn.on .q-fcount { background: #fff4e1; color: #000; }

/* the search box — the reference's own ._mastermode-search, 478x23 at (30,84) */
.s-quests .q-search { position: absolute; left: 30px; top: 84px; width: 478px; height: 23px;
  box-sizing: border-box; padding: 0 9px; font-family: inherit; font-size: 9px;
  color: #000; background: rgba(247, 229, 197, .8);
  border: 1px solid rgba(0, 0, 0, .35); border-radius: 0; outline: none; }
.s-quests .q-search::placeholder { color: rgba(0, 0, 0, .5); }
.s-quests .q-search:focus { border-color: rgba(0, 0, 0, .7); }

/* THE FILTER SHEET. Dressed as the reference's own FILTER popup — the cream
   gradient, the 5px chips, the CLEAR button (measured 2026-09-14, 222x91 at
   stage 64,415) — but sized for this holder and carrying the six groups that
   were asked for. It covers the card grid rather than floating beside it:
   at 478 wide there is nowhere beside it to float. */
.s-quests .q-filters { position: absolute; left: 30px; top: 84px; width: 478px;
  max-height: 362px; overflow-y: auto; box-sizing: border-box; padding: 9px 10px 10px;
  background: var(--gm-inner); border: 1px solid var(--gm-edge); border-radius: 0; z-index: 4;
  scrollbar-width: thin; scrollbar-color: #b9a878 transparent; }
.s-quests .q-filters::-webkit-scrollbar { width: 5px; }
.s-quests .q-filters::-webkit-scrollbar-thumb { background: #b9a878; border-radius: 0; }
.s-quests .q-frow { margin-bottom: 7px; }
.s-quests .q-frow:last-child { margin-bottom: 0; }
.s-quests .q-fhead { font-size: 8px; font-weight: 700; color: #000;
  letter-spacing: .5px; margin-bottom: 3px; }
.s-quests .q-fchips { display: flex; flex-wrap: wrap; gap: 4px; }
.s-quests .q-chip { height: 20px; line-height: 18px; padding: 0 8px; box-sizing: border-box;
  font-size: 8px; font-weight: 700; letter-spacing: .3px; color: #000;
  background: rgba(255, 249, 239, .72); border: 1px solid rgba(0, 0, 0, .36);
  border-radius: 0; cursor: pointer; user-select: none; white-space: nowrap; }
.s-quests .q-chip:hover { box-shadow: none; filter: brightness(.96); }
.s-quests .q-chip.on { background: #000; border-color: #000; color: #fff4e1; }
.s-quests .q-frow.foot { display: flex; gap: 6px; justify-content: flex-end;
  margin-top: 9px; padding-top: 8px; border-top: 1px solid rgba(0, 0, 0, .22); }
.s-quests .q-chip.clear { background: rgba(0, 0, 0, .08); border-color: rgba(0, 0, 0, .3);
  color: #000; }
.s-quests .q-chip.done { background: #e8d9ae; border-color: #7a6128; }

/* THE GRID. 508x331, two 238 cards a row, 32 between them. `overflow-y`
   rather than a taller panel: the panel height is measured and the quest
   list is not fixed. */
/* v272: THE SECOND COLUMN WAS CUT. The grid is 508 = 238+32+238, and its
   vertical scrollbar is drawn INSIDE those 508 — measured 2026-09-24 on the
   live panel (Chrome 152, Windows): offsetWidth 508, clientWidth 498, a 10px
   scrollbar, so the right-hand card lost its last 10px. The grid keeps its
   measured box (x 30-538, clear of the panel art's frame); what changed is
   what goes in it. Two columns of AT MOST 238, pushed to the two edges, with
   at least 16 between them: at a 10px scrollbar that is exactly 238 + 22 +
   238, with none it is 238 + 32 + 238 as measured, and a 17px classic
   scrollbar gives two 237.5 columns. A card is its column's width, so both
   columns are whole on every scrollbar. */
.s-quests .q-grid { position: absolute; left: 30px; top: 115px; width: 508px; height: 331px;
  display: grid; grid-template-columns: repeat(2, minmax(0, 238px)); gap: 26px 16px;
  justify-content: space-between;
  align-content: start; overflow-y: auto; overflow-x: hidden;
  padding-right: 0; scrollbar-width: thin; scrollbar-color: #b9a878 transparent; }
.s-quests .q-grid::-webkit-scrollbar { width: 5px; }
.s-quests .q-grid::-webkit-scrollbar-thumb { background: #b9a878; border-radius: 0; }

.s-quests .q-note { grid-column: 1 / -1; font-size: 12px; color: #000; padding: 8px 2px; }
.s-quests .q-note.bad { color: #9e1812; }

.s-quests .q-card { width: 100%; max-width: 238px; height: 200px; box-sizing: border-box;
  background: #f7e5c5c2; border: 1px solid #00000047; border-radius: 0;
  padding: 5px 10px 6px; display: flex; flex-direction: column; gap: 4px; }
/* v272: THE COLOURS OF THE WEBSITE'S PIRATE QUESTS PAGES. Asked 2026-09-24,
   measured the same day on haki-arena.site's /missions section page with
   getComputedStyle: quest title #0c3552 bold (.pq-h), "Rank:" in the ink
   #2b2318 (.pq-label), a rank met #0e970e (.pq-met), not met #c02020
   (.pq-unmet), "Quest complete!" #00bf00 bold (.pq-done), "View this quest"
   #c8791a bold (.pq-h6), a locked line muted #8a7c63 italic (.pq-locked).
   The panel had its own reds and browns for each of these. */
.s-quests .q-card.done { border-color: #00bf0088; }

.s-quests .q-name { width: 100%; height: 20px; flex: none; font-size: 11px; font-weight: 700;
  color: #0c3552; text-align: left; line-height: 20px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.s-quests .q-pic { width: 198px; height: 112px; flex: none; align-self: center;
  overflow: hidden; border-radius: 0; background: #e7dcc0; }
.s-quests .q-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* v287: a 1px black frame on every quest picture in the selection screen
   (asked 2026-09-27), drawn INSIDE the measured box so the card keeps its
   198x112 / 260x120 geometry — the same edge the Pirate Quests pages got in
   v286 (style.css, .pq-col-r > img). */
.s-quests .q-pic, .s-quests .q-dpic { border: 1px solid #000; box-sizing: border-box; }
.s-quests .q-pic-blank { width: 100%; height: 100%;
  background: repeating-linear-gradient(45deg, #e7dcc0 0 8px, #dfd2b0 8px 16px); }

/* THE REQUIREMENTS BOX — 198x39 at card +20,+148, measured. The live card
   holds exactly two lines: a rank line and a view line. v182 put a progress
   bar in here on the strength of ADMIN.md's v179 table; the live panel does
   not have one, and the bar moved to the detail level where the reference
   really does have one (410x7, one per goal). */
.s-quests .q-req { width: 198px; height: 39px; flex: none; align-self: center;
  display: flex; flex-direction: column; justify-content: space-between;
  font-size: 9px; line-height: 1.1; color: #2a2010; }
.s-quests .q-rank { height: 10px; font-size: 9px; font-weight: 700; color: #2b2318;
  text-transform: uppercase; letter-spacing: .2px; }
.s-quests .q-label { font-weight: 700; }
.s-quests .q-met { color: #0e970e; font-weight: 700; }
/* v301: the one red kept — a rank not reached is the website's status colour
   (.pq-unmet, v272), like the done green, not the panel's old red chrome */
.s-quests .q-unmet { color: #c02020; font-weight: 700; }
.s-quests .q-view { height: 9px; font-size: 8px; font-weight: 700; color: #c8791a;
  text-transform: uppercase; letter-spacing: .3px; }
.s-quests .q-view.locked { color: #8a7c63; font-style: italic; }
.s-quests .q-view.done { color: #00bf00; }
.s-quests .q-card { cursor: pointer; }
.s-quests .q-card:hover { box-shadow: none; filter: brightness(.96); }

/* ---- the footer buttons: 106x33, radius 8, 1px #701506, 11px/700 white.
   One centred at (217,452) on the list; two at (153,452) and (281,452) on
   the detail. All measured 2026-09-14. ---- */
.s-quests .q-foot { position: absolute; top: 452px; width: 106px; height: 33px;
  box-sizing: border-box; display: flex; align-items: center; justify-content: center;
  border: 1px solid #000; border-radius: 0;
  background: #000; color: #fff;
  font-size: 11px; font-weight: 700; letter-spacing: .5px;
  cursor: pointer; user-select: none; }
.s-quests .q-foot:hover { filter: brightness(1.1); }
.s-quests .q-foot.mid   { left: 217px; }
.s-quests .q-foot.left  { left: 153px; }
.s-quests .q-foot.right { left: 281px; }

/* =====================================================================
   THE QUEST DETAIL, IN THE SAME HOLDER (v183)
   Measured off the reference's view-mission panel, 2026-09-14:
     container   508x362 at (30,84)
     picture     260x146 at (146,87), radius 11
     goals panel 446x142 at (53,242), radius 10, rgba(247,229,197,.74)
     caption     15px/600 #280092, centred
     goal row    414x27 at (69,276), pitch 35
     goal bar    410x7, radius 999, rgba(154,30,9,.1)
     reward line 15px/600 #280092, centred, at (223,406)
   ONE DEPARTURE: the picture is 120 tall, not 146. The 26px it gives up
   carries the quest's own flavour text, which their quests do not have in
   this view and ours do.
   ===================================================================== */
.s-quests .q-detail { position: absolute; left: 30px; top: 84px; width: 508px; height: 362px; }
.s-quests .q-dpic { position: absolute; left: 116px; top: 3px; width: 260px; height: 120px;
  border-radius: 0; overflow: hidden; background: #e7dcc0; }
.s-quests .q-dpic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.s-quests .q-dflavour { position: absolute; left: 23px; top: 129px; width: 462px; height: 24px;
  font-size: 9px; line-height: 12px; color: rgba(72, 23, 15, .78); text-align: center;
  overflow: hidden; }
/* padding 16 each side, not 13: their goals LIST is 414 inside a 446 panel
   (measured at (69,276) against a panel at (53,242)), and the bar inside it
   is 410 with 2px either side. At 13 the list came out 420 and the bar with
   it — close, and wrong, and a render check said so. */
.s-quests .q-dgoals { position: absolute; left: 23px; top: 158px; width: 446px; height: 142px;
  box-sizing: border-box; padding: 11px 16px;
  background: rgba(247, 229, 197, .74); border-radius: 0; }
.s-quests .q-dcap { height: 18px; font-size: 15px; font-weight: 600; color: #0c3552;
  text-align: center; letter-spacing: .3px; }
.s-quests .q-dlist { margin-top: 5px; max-height: 97px; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: #b9a878 transparent; }
.s-quests .q-dlist::-webkit-scrollbar { width: 5px; }
.s-quests .q-dlist::-webkit-scrollbar-thumb { background: #b9a878; border-radius: 0; }
.s-quests .q-drow { height: 27px; margin-bottom: 8px; }
.s-quests .q-dtext { display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; height: 16px; font-size: 13px; color: #000; text-transform: uppercase;
  letter-spacing: .1px; }
.s-quests .q-dtext span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-quests .q-dtext strong { flex: none; font-size: 8px; font-weight: 700; color: #000; }
.s-quests .q-dbar { position: relative; height: 7px; margin: 4px 2px 0; border-radius: 0;
  background: rgba(0, 0, 0, .1); overflow: hidden; }
.s-quests .q-dbar-fill { position: absolute; inset: 0 auto 0 0; border-radius: 0;
  background: #3f7d3f; }
/* v272: flat, the website quest bar's green (.mission-bar .fill); a finished
   goal in the "Quest complete!" green */
.s-quests .q-dbar.full .q-dbar-fill { background: #00bf00; }
.s-quests .q-dreward { position: absolute; left: 0; top: 322px; width: 508px;
  text-align: center; font-size: 13px; color: #000; }
.s-quests .q-dreward i { font-size: 15px; font-weight: 600; color: #0c3552; font-style: italic; }
.s-quests .q-dreward b { font-weight: 700; color: #2b2318; }
.s-quests .q-dgate { position: absolute; left: 0; top: 344px; width: 508px;
  text-align: center; font-size: 8px; font-weight: 700; letter-spacing: .4px; color: #000; }
.s-quests .q-dgate.ok { color: #0e970e; }
.s-quests .q-dgate.done { color: #00bf00; }

.s-quests .q-locked { font-size: 9px; color: #7d6f4c; }

/* =====================================================================
   FILTER  (v183)  — the character-grid filter popup
   ---------------------------------------------------------------------
   MEASURED on the reference 2026-09-14 (docs/specs/2026-09-14-ingame-overlays.md):
     ._selection-filter-popup   222x91 at stage (64,415), radius 9,
                                1px rgba(154,30,9,.55),
                                linear-gradient(145deg,#f7e5c5,#f7efe1 65%,#ebc98f)
     header 204x14 (9,8) 8px/700 #591307   close x 16x16 (197,7) 14px #9a1e09
     name box 204x22 (9,28) 9px, rgba(255,249,239,.72), 1px rgba(154,30,9,.35), r6
     four energy buttons 27x20 from (9,60), pitch 31, r5, 1px rgba(89,19,7,.36), 7px/700
     ONLY 35x20 (137,60): track 22x12 (137,64) r999, knob 6x6 (140,67), label 6px/700 #713122
     CLEAR 33x20 (180,60) r5, rgba(154,30,9,.08), 1px rgba(154,30,9,.3), 7px/700 #9a1e09
   Theirs is 91 tall because it holds nothing else. Ours grows by one 12px
   line when a filter is on, to print how many characters are left — an empty
   grid with no explanation is the one failure this control can have.
   ===================================================================== */
/* v186: same geometry, kit skin.
   A DELIBERATE DEPARTURE FROM THE REFERENCE, said out loud. Their filter
   popup's own background is `linear-gradient(145deg,#f7e5c5,#f7efe1 65%,
   #ebc98f)`, measured 2026-09-14 and still written in the block above this
   one. We are not drawing that any more: the ask was that every panel
   inside the selection screen share one material, and this popup was the
   odd one out. The MEASURED GEOMETRY is untouched — 222x91 at (64,415),
   radius 9 — and the gradient survives as --gm-inner, which is what every
   panel now uses for a surface drawn inside itself. */
.s-filter { position: absolute; left: 64px; top: 415px; width: 222px; height: 91px;
  --gm-page-h: 91px; z-index: 9; border-radius: 0; }
/* v225: moved for the 6px stroke clearance rule — see ADMIN.md v225. */
.s-filter .sf-head { position: absolute; left: 14px; top: 15px; width: 194px; height: 14px;
  font-size: 8px; font-weight: 700; letter-spacing: .4px; line-height: 14px; }
.s-filter .sf-x { position: absolute; right: 0; top: 0; width: 16px; height: 16px;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; line-height: 1; color: #000; cursor: pointer; user-select: none; }
.s-filter .sf-name { position: absolute; left: 14px; top: 30px; width: 194px; height: 22px;
  box-sizing: border-box; padding: 0 7px; font-family: inherit; font-size: 9px; color: #000;
  background: rgba(255, 249, 239, .72); border: 1px solid rgba(0, 0, 0, .35);
  border-radius: 0; outline: none; }
.s-filter .sf-name::placeholder { color: rgba(0, 0, 0, .45); }
.s-filter .sf-row { position: absolute; left: 14px; top: 55px; width: 194px; height: 20px; }
.s-filter .sf-chak { position: absolute; left: 0; top: 0; width: 120px; height: 20px; }
.s-filter .sf-e { position: absolute; top: 0; width: 27px; height: 20px; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  font-size: 7px; font-weight: 700; letter-spacing: .2px;
  border: 1px solid rgba(0, 0, 0, .36); border-radius: 0;
  cursor: pointer; user-select: none; opacity: .42; filter: saturate(.5); }
.s-filter .sf-e.on { opacity: 1; filter: none; box-shadow: 0 0 0 1px rgba(0, 0, 0, .5) inset; }
.s-filter .sf-e:hover { opacity: 1; }
/* OUR four energies and OUR colours — Strength / Weapon / Devil Fruit /
   Concentration, straight off CHAKRA_META, not their Tai/Bld/Gen/Nin. The
   pitch is theirs (31) because that is a measurement; the palette is not. */
.s-filter .sf-e.e-t { left: 0;   background: #7f047f; color: #fff; }
.s-filter .sf-e.e-n { left: 31px; background: #0cff00; color: #16300f; }
.s-filter .sf-e.e-g { left: 62px; background: #f12529; color: #fff; }
.s-filter .sf-e.e-b { left: 93px; background: #0bb6ff; color: #04263a; }   /* v339: Concentration is #0bb6ff (data.js CHAKRA_META) */
/* row-relative, and the row itself starts at panel x=9 — so the measured
   panel (137,60) is row 128, and the ONLY label's measured (162,67) lands
   at 128+25. Writing 137 here put the label on top of CLEAR. */
.s-filter .sf-only { position: absolute; left: 128px; top: 0; width: 35px; height: 20px;
  cursor: pointer; user-select: none; }
.s-filter .sf-track { position: absolute; left: 0; top: 4px; width: 22px; height: 12px;
  box-sizing: border-box; border-radius: 0;
  background: rgba(0, 0, 0, .12); border: 1px solid rgba(0, 0, 0, .32); }
.s-filter .sf-track i { position: absolute; left: 2px; top: 2px; width: 6px; height: 6px;
  border-radius: 0; background: #000; transition: left .12s ease; }
/* v187: dead until an energy is picked — ONLY narrows an energy filter, so
   on its own there is nothing for it to narrow. It stays visible and stays
   clickable (the click answers with the hint below) rather than vanishing,
   because a control that disappears is as confusing as one that does
   nothing. */
.s-filter .sf-only.dead { opacity: .42; }
.s-filter .sf-only.dead .sf-track { background: rgba(0, 0, 0, .08); }
.s-filter .sf-hint { position: absolute; left: 9px; right: 9px; top: 84px;
  font-size: 7px; font-weight: 700; line-height: 9px; color: #000; }
.s-filter .sf-only.on .sf-track { background: rgba(0, 0, 0, .3); }
.s-filter .sf-only.on .sf-track i { left: 12px; }
.s-filter .sf-onlylbl { position: absolute; left: 25px; top: 7px;
  font-size: 6px; font-weight: 700; color: #000; letter-spacing: .2px; }
.s-filter .sf-clear { position: absolute; left: 161px; top: 0; width: 33px; height: 20px;
  box-sizing: border-box; display: flex; align-items: center; justify-content: center;
  font-size: 7px; font-weight: 700; color: #000;
  background: rgba(0, 0, 0, .08); border: 1px solid rgba(0, 0, 0, .3);
  border-radius: 0; cursor: pointer; user-select: none; }
.s-filter .sf-clear:hover { box-shadow: none; filter: brightness(.96); }
/* the extra line needs the extra height, or it is drawn outside the panel */
.s-filter.has-count { height: 104px; }
.s-filter .sf-count { position: absolute; left: 9px; top: 86px; width: 204px; height: 10px;
  font-size: 7px; font-weight: 700; color: rgba(0, 0, 0, .7); letter-spacing: .3px; }

/* =====================================================================
   STYLES  (v183)  — the cosmetics panel, in the shared holder
   ---------------------------------------------------------------------
   The reference draws STYLES in the SAME ._mastermode-holder as MISSIONS
   (558x504 at stage 106,28), which is why .s-styles is a modifier on
   .s-quests rather than a panel of its own. Measured 2026-09-14:
     current block   478x105 at (30,89), r13, rgba(247,229,197,.72)
     avatar box      76x76 at (48,104) with a 60x60 image at (56,112)
     copy            270x45 at (138,119): kicker 7px/700 #9a1e09,
                     heading 15px/700 #591307, blurb 7px rgba(89,19,7,.67)
     ACTIVE pill     36x12 at (456,103), r999, #477a38, 6px/700 #fff8e9
     card grid       478x174 at (30,206); cards 153x174, pitch 163, r12
     card visual     82x82 at card+35, y220;  name 11px/700 y312;
                     sub 6px rgba(89,19,7,.62) y328;  arrow 13px y355
   Theirs has three cards; ours has the two that were asked for, so the row
   is centred on the holder rather than starting at x=30 — 2 cards at a 163
   pitch is 316 wide, and (478-316)/2 = 81, so the first card sits at 111.
   ===================================================================== */
.s-styles .st-current { position: absolute; left: 30px; top: 89px; width: 478px; height: 105px;
  box-sizing: border-box; background: rgba(247, 229, 197, .72);
  border: 1px solid rgba(0, 0, 0, .28); border-radius: 0; }
.s-styles .st-current .st-av { position: absolute; left: 18px; top: 15px; }
.s-styles .st-copy { position: absolute; left: 108px; top: 30px; width: 270px; }
.s-styles .st-kicker { display: block; font-size: 7px; font-weight: 700; color: #000;
  letter-spacing: .5px; }
.s-styles .st-copy strong { display: block; margin-top: 4px; font-size: 15px; font-weight: 700;
  color: #000; letter-spacing: .2px; }
.s-styles .st-copy p { margin: 6px 0 0; font-size: 7px; line-height: 10px;
  color: rgba(0, 0, 0, .67); }
.s-styles .st-active { position: absolute; left: 426px; top: 14px; width: 36px; height: 12px;
  display: flex; align-items: center; justify-content: center; border-radius: 0;
  background: #477a38; color: #fff8e9; font-size: 6px; font-weight: 700; letter-spacing: .4px; }

.s-styles .st-grid { position: absolute; left: 111px; top: 206px; width: 316px; height: 174px;
  display: flex; gap: 10px; }
.s-styles .st-card { position: relative; width: 153px; height: 174px; box-sizing: border-box;
  background: rgba(247, 229, 197, .72); border: 1px solid rgba(0, 0, 0, .3);
  border-radius: 0; cursor: pointer; user-select: none; }
.s-styles .st-card:hover { box-shadow: none; filter: brightness(.96); }
.s-styles .st-visual { position: absolute; left: 35px; top: 14px; width: 82px; height: 82px;
  display: flex; align-items: center; justify-content: center; }
.s-styles .st-card strong { position: absolute; left: 0; top: 106px; width: 100%;
  text-align: center; font-size: 11px; font-weight: 700; color: #000; letter-spacing: .3px; }
.s-styles .st-sub { position: absolute; left: 0; top: 122px; width: 100%; text-align: center;
  font-size: 6px; color: rgba(0, 0, 0, .62); letter-spacing: .3px; }
.s-styles .st-arrow { position: absolute; right: 10px; bottom: 6px;
  font-size: 13px; font-weight: 400; color: #000; }
.s-styles .st-titlechip { padding: 4px 8px; border-radius: 0; background: #f7e5c5;
  border: 1px solid rgba(0, 0, 0, .35); font-size: 8px; font-weight: 700; color: #000;
  max-width: 78px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: center; }

/* ---- a picker: the same grid box the cards sat in, as a scrolling list ---- */
.s-styles .st-list { position: absolute; left: 30px; top: 84px; width: 478px; height: 340px;
  overflow-y: auto; display: flex; flex-direction: column; gap: 5px;
  scrollbar-width: thin; scrollbar-color: #b9a878 transparent; }
.s-styles .st-list::-webkit-scrollbar { width: 5px; }
.s-styles .st-list::-webkit-scrollbar-thumb { background: #b9a878; border-radius: 0; }
.s-styles .st-row { display: flex; align-items: center; gap: 10px; height: 62px; flex: none;
  padding: 0 12px; box-sizing: border-box; background: rgba(247, 229, 197, .72);
  border: 1px solid rgba(0, 0, 0, .3); border-radius: 0; cursor: pointer; }
.s-styles .st-row.wide { height: 30px; }
.s-styles .st-row:hover { box-shadow: none; filter: brightness(.96); }
.s-styles .st-row.on { border-color: #477a38; background: rgba(71, 122, 56, .16); }
/* A LOCKED ROW IS STILL SHOWN. The same rule as a locked character: you may
   look at what you have not earned. It is simply not clickable. */
.s-styles .st-row.shut { opacity: .45; cursor: default; }
.s-styles .st-row.shut:hover { box-shadow: none; }
.s-styles .st-name { flex: 1; font-size: 11px; font-weight: 700; color: #000;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-styles .st-gate { flex: none; font-size: 7px; font-weight: 700; color: #000;
  letter-spacing: .4px; }
.s-styles .st-row.on .st-gate { color: #0a7a2e; }
/* v376: a locked style that is for sale — the row stays dimmed, its BUY
   button does not, and it opens the Patreon page in a new tab */
.s-styles .st-row.shut.forsale { opacity: 1; }
.s-styles .st-row.shut.forsale > :not(.st-buy) { opacity: .45; }
.s-styles .st-buy, .tst-lock.st-buy { flex: none; display: inline-block; font: normal bold 8px Arial, sans-serif;
  letter-spacing: .6px; color: #fff; background: #f96854; border: 1px solid #000; padding: 3px 9px;
  text-decoration: none; cursor: pointer; box-shadow: 1px 1px 0 #000; }
.s-styles .st-buy:hover, .tst-fx.buy:hover .st-buy { background: #ff7d6a; }
.s-styles .st-note { position: absolute; left: 30px; top: 428px; width: 478px;
  text-align: center; font-size: 8px; font-weight: 700; color: #000; letter-spacing: .3px; }

/* ---- the avatar-in-a-border widget, used by every row above ---- */
.st-av { position: relative; flex: none; display: flex; align-items: center;
  justify-content: center; }
.st-av .st-avimg { border-radius: 0; object-fit: cover; display: block;
  border: 1px solid rgba(89, 19, 7, .5); }
/* the DRAWN fallback: a ring in the catalog's own colour, so a border that
   has no art pasted into it still reads as that border rather than as
   nothing at all */
.st-av:not(.bare)::after { content: ""; position: absolute; inset: 0; border-radius: 0;
  border: 3px solid var(--ring, transparent); pointer-events: none; }
.st-av .st-avring { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; pointer-events: none; }
/* art wins: with a real picture on top, drop the drawn ring under it */
.st-av:has(.st-avring)::after { display: none; }
/* v280: an animated border draws itself (NA.borderFrame) — no drawn ring */
.st-av.anim::after { display: none; }

/* =====================================================================
   IN-GAME CURSOR  (v182)
   ---------------------------------------------------------------------
   Asked for: a sword pointer, and a glowing version of it while the button
   is down — IN GAME ONLY. "In game" is the selection screen and the battle
   screen, which are the two pages that carry <body class="ingame">; every
   site page, account page and the boards keep the system cursor.

   HOTSPOT 11 3 / 11 2 — the blade tip, read off the alpha channel of each
   file (see the note in assetlinks.js, which carries the measurement).
   The glow art's tip is one pixel higher than the plain one's, so the two
   hotspots differ by one and the point stays put when the button goes down.

   The art comes through ASSET_LINKS like everything else: --asset-cursor /
   --asset-cursor-click are published by applyAssetLinkOverrides(), and the
   url() in the fallback slot is the bundled copy, so an empty or blocked
   link still gives a sword rather than the system arrow.

   `body.ingame *` and not just `body.ingame`: the screens are full of
   elements that set their own `cursor: pointer` / `default` / `not-allowed`
   / `grab`, and a rule on the ancestor alone loses to every one of them.
   One cursor in game is the point, so this overrides them all — except
   typed fields, which keep the I-beam below.

   `*:active` covers the pressed element AND its ancestors, which is what
   makes the whole board show the glow for as long as the button is held.
   ===================================================================== */
/* !important, deliberately, and MEASURED into being. The first two goes at
   this were `body.ingame *` and then `html body.ingame *` — (0,1,0) and
   (0,1,2) — and a render check read `cursor: default` off a character cell
   and a skill tile both times. The reason is that the rules being overridden
   are not single classes: `.s-cell.picked`, `.s-cell.empty` and
   `.b-lane.busy .b-skill` are (0,2,0) and (0,3,0), and there is no honest
   specificity number that beats "every cursor rule anyone might add to a
   game screen from now on". A blanket override IS what was asked for — one
   cursor in game — so it is written as one instead of as an arms race.
   The typed-field exception below carries !important too, or it would lose
   to this the moment it stopped being a specificity contest. */
/* v193 (SUPERSEDED BY v288 — read the last sentences): THESE TWO
   PLACEHOLDER FILES MUST STAY ON DISK.
   Every other replaced placeholder was deleted once its imgur link was
   verified live — a filled link never touches the local file, because
   assetURL() returns the link and stops. These two are the exception: they
   are named here as a CSS FALLBACK, and `--asset-cursor` is only published
   by applyAssetLinkOverrides on DOMContentLoaded. Between first paint and
   that call the fallback is what resolves, so deleting the file would mean a
   404 and no cursor for those few frames. (v288: the fallback is the SAME
   link ASSET_LINKS carries — the bundled copy went with the placeholder
   folder; test_v288 holds the two equal.) So nothing on disk is load-
   bearing here any more: the fallback below IS the link, and test_v193
   holds each fallback equal to its ASSET_LINKS entry. */
html body.ingame,
html body.ingame * {
  cursor: var(--asset-cursor, url("/assets/imgur/asvN2Od.png")) 11 3, auto !important;
}
html body.ingame *:active,
html body.ingame *:active * {
  cursor: var(--asset-cursor-click, url("/assets/imgur/tARCXp1.png")) 11 2, auto !important;
}
/* v218: the CALCULATING pointer. MEASURED 2026-09-19 — their `cursorLoading`
   hides the pointer sprite and shows a second, spinning one, for exactly as
   long as `battleStatus` is "calculating"; `cursorUp` puts it back. Ours is
   a CSS cursor, so the swap is a class on <body> and the art is a still
   image (the departure is written up in assetlinks.js beside the key).

   THE FALLBACK IS THE RESTING SWORD ON PURPOSE. Until cursor_wait.png has
   art, --asset-cursor-wait is unset and this rule resolves to exactly what
   is on screen today, so shipping it changes nothing visible until the
   picture exists. It is listed BELOW :active so a click still wins.

   Specificity: (0,3,2) against the :active rule's (0,2,2), so it would beat
   a held button — which is why the :active pair is repeated under .calc
   below rather than left to lose. */
/* v339: THE CALCULATING POINTER SPINS. The still-image swap above was the
   v218 departure ("a CSS cursor cannot animate"); asked 2026-10-06 for the
   movement itself, it is built as theirs is (REF_V339): the real pointer is
   hidden and #na-spin — a fixed, pointer-events:none picture that app.js's
   NA_CURSOR keeps on the pointer — turns once every .6 s, linear, about its
   own centre. Its picture is cursor_wait.png when the panel has one, else
   the resting sword. `na-pos`: only once a pointer position is known, so
   the pointer is never hidden with nowhere to draw the spinner. */
html body.ingame.calc.na-pos,
html body.ingame.calc.na-pos * {
  cursor: none !important;
}
#na-spin {
  position: fixed; left: 0; top: 0; width: 55px; height: 55px;
  pointer-events: none; z-index: 2147483000; opacity: 0; visibility: hidden;
  background: var(--asset-cursor-wait, var(--asset-cursor, url("/assets/imgur/asvN2Od.png"))) 0 0 / 55px 55px no-repeat;
  transform-origin: 50% 50%;
  transform: translate3d(var(--na-cx, -100px), var(--na-cy, -100px), 0);
  will-change: transform;
}
html body.ingame.calc.na-pos #na-spin {
  opacity: 1; visibility: visible;
  animation: na-cursor-spin .6s linear infinite;
}
@keyframes na-cursor-spin {
  from { transform: translate3d(var(--na-cx, -100px), var(--na-cy, -100px), 0) rotate(0deg); }
  to   { transform: translate3d(var(--na-cx, -100px), var(--na-cy, -100px), 0) rotate(1turn); }
}
/* a reader who asked for less motion still sees that it is working: the
   picture stands in for the pointer, it just does not turn */
@media (prefers-reduced-motion: reduce) {
  html body.ingame.calc.na-pos #na-spin { animation: none; }
}
/* a touch screen has no pointer to follow: app.js never sets na-pos for a
   finger, so on a phone neither of the two rules above ever applies */
/* A sword over a field you are typing into hides the caret and the text
   under it. Chat, the private-game name box and the energy inputs keep the
   I-beam. The `:active` variants are listed because `*:active` above is
   (0,2,2) and a bare `input` is only (0,1,3) — it would lose for as long as
   the button was held, which is exactly while you are clicking into a field. */
html body.ingame input,
html body.ingame textarea,
html body.ingame input:active,
html body.ingame textarea:active { cursor: text !important; }

/* v273: NO TEXT-SMOOTHING RULES. v182 re-declared antialiased / grayscale /
   geometricPrecision on #select, #battle and .stage; naruto-arena sets none
   of the three (measured 2026-09-24, see style.css), so neither do we. */

/* =====================================================================
   MOTION — page transitions and micro-interactions
   ---------------------------------------------------------------------
   Selection -> battle was a bare location.href: the window blanked white,
   then repainted. Now the outgoing page fades down (NA.goTo adds .leaving
   and waits before navigating) and the incoming one fades up. body carries
   the same blue as .stage so the fade has nothing to flash against —
   without that, fading .stage just reveals white paper behind it.
   Only opacity is animated on .screen: applyFitScale() owns its transform
   in the popup window, and an animation on that property would fight it.
   ===================================================================== */
body { background: #10639e; }
/* Page-change timings, halved. The old chain settled at ~1.85s before the
   selection screen stopped moving: 250ms fade-out + .34s page-in + .5s
   screen-in + 1.1s panel unroll + a .55s info rise delayed .75s. That reads
   as a slow game, not a smooth one. Every step below is roughly half its old
   value; the whole sequence now settles in ~0.85s and still overlaps the same
   way, so nothing snaps. */
.stage { animation: gm-page-in .2s ease both; }
.stage.leaving { animation: gm-page-out .13s ease both; }   /* == NA.goTo's delay */
@keyframes gm-page-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes gm-page-out { from { opacity: 1; } to { opacity: 0; } }
.screen { animation: gm-screen-in .7s ease-in-out both; }   /* .selectanime */
@keyframes gm-screen-in { from { opacity: 0; } to { opacity: 1; } }

/* MEASURED on naruto-arena.site (2026-09-09): the reference has NO coloured
   hover outline anywhere. Its stylesheets contain exactly one hover treatment
   for tiles — `.skillimg0:hover { box-shadow: rgba(0,0,0,.2) 0 5px 15px 0 }`,
   a soft lift — plus opacity dims (0.5-0.8) on buttons and avatars. Character
   grid cells get nothing at all: hovering one is visually identical to its
   neighbours. Our red #e0241c and yellow #ffd400 outlines were invented here.
   --hover-lift is that measured shadow, used for every hoverable tile.
   --ink-sel is the parchment's ink, for the SELECTED state, which still has to
   be unmistakable — the reference marks a chosen skill by sliding it to the
   front of the row instead, which we do not do. */
:root { --hover-lift: 0 5px 15px 0 rgba(0,0,0,.2); --ink-sel: #2c1e08; }

/* MEASURED off naruto-arena.site's own stylesheets (2026-09-09). These are its
   real numbers, not an impression of them:
     #_char_select._animation_enter   1.3s cubic-bezier(.08,.82,.17,1)
     #_char_select._animation_leave   0.9s cubic-bezier(.9,.03,.69,.22)
     #_scroll_charselect              the SAME two, so the rod and the paper
                                      move as one piece
     #_top_menu._animation_enter      0.6s ease  (and its content counter-slides)
     .selectanime                     0.7s ease-in-out opacity
     .opacity-ease                    0.15s
     .skillimgselected*               0.4-0.5s cubic-bezier(.17,.84,.44,1)
   Note the reference's animations are LONGER than ours were, not shorter. What
   made this game feel slow was never the animation length — it was ~1.3s of
   blocking API calls before the first paint. That is fixed elsewhere; these
   timings exist to make the motion FEEL like the reference. */
:root {
  --ref-in:    cubic-bezier(.08, .82, .17, 1);    /* screens arriving */
  --ref-out:   cubic-bezier(.9, .03, .69, .22);   /* screens leaving */
  --ref-slide: cubic-bezier(.17, .84, .44, 1);    /* small pieces moving */
  --t-fast:    .15s;                              /* .opacity-ease */
}

/* Hover and state changes used to snap. These are short enough to feel
   instant while taking the hard edge off. Kept to properties that never
   affect layout, so nothing here can shift a measured position. */
.s-cell, .s-team .slot, .s-mode, .s-foot .lbl, .btn-tm, .s-page, .s-detail .rod,
.b-skill, .bb-btn, .b-exchange, .tm-btn, .s-tm .close, .s-detail .close {
  transition: filter var(--t-fast) ease, border-color var(--t-fast) ease,
              outline-color var(--t-fast) ease, color var(--t-fast) ease,
              opacity var(--t-fast) ease, background-color var(--t-fast) ease,
              box-shadow var(--t-fast) ease; }
/* the paging arrows lift very slightly under the cursor */
.s-page { transition: filter .12s ease, transform .12s ease; }
.s-page.up:hover   { transform: scaleX(-1) translateY(-1px); }
.s-page.down:hover { transform: translateY(-1px); }

@media (prefers-reduced-motion: reduce) {
  .stage, .stage.leaving, .screen { animation: none; }
  .s-cell, .s-team .slot, .s-mode, .s-foot .lbl, .btn-tm, .s-page, .s-detail .rod,
  .b-skill, .bb-btn, .b-exchange, .tm-btn, .s-tm .close, .s-detail .close { transition: none; }
  .s-page.up:hover { transform: scaleX(-1); }
  .s-page.down:hover { transform: none; }
}
.stage .home-link { display: block; width: var(--screen-w); margin: 0 auto 8px; color: #3a2400;
  font-weight: bold; text-decoration: none; font-size: 12px; }
.screen {
  width: var(--screen-w); height: var(--screen-h); position: relative; overflow: hidden;
  margin: 0 auto; border: 2px solid #1e1e1e; box-shadow: 0 4px 18px #0007;
  background-size: cover; background-position: center; color: #111; font-size: 12px;
  user-select: none;
}

/* ---- standalone game window (opened from "Start Playing") ----
   The game screen fits-to-window: it scales DOWN uniformly so the whole
   design (960x700 select / 770x560 battle) is always visible even in a
   smaller window, and never scales UP past 1:1 native on its own. Both
   screens use this identical rule, so they are ALWAYS the same on-screen
   size — no exceptions.

   NA.applyFitScale() (js/app.js) reads the viewport and writes --fit-scale;
   it's read-only (never resizes the window itself, so there's no feedback
   loop). It's re-run on the window's "resize" event. Browser zoom (Ctrl +/-,
   Ctrl+scroll, pinch) also fires "resize", so applyFitScale specifically
   measures the viewport in CSS pixels against the CSS-pixel design size, and
   divides out any change in devicePixelRatio since load so that zoom (which
   moves innerWidth and devicePixelRatio in lockstep) cancels out and only a
   genuine window resize moves the scale. This is what lets the
   person zoom in or out as much as they want without the game fighting them
   back to its old size. */
body.game-window { background: #1a1a1a; margin: 0; overflow: hidden; }
/* The stage paints the SITE's background (the sea blue) so the embedded view
   sits on the page's colour. In the popup there is no page around it, so any
   viewport taller or wider than 770x560 showed that blue as bands above and
   below the board. Letterbox against the window's own dark instead: the
   leftover then reads as a border rather than a stray strip of the website.
   The bands still shrink to nothing whenever the window is an exact 770x560. */
body.game-window .stage { height: 100vh; width: 100vw; min-height: 0; padding: 0;
  background: transparent;
  display: flex; align-items: center; justify-content: center; overflow: hidden; }
body.game-window .stage .home-link { display: none; }
body.game-window .screen {
  position: relative; left: auto; top: auto; margin: 0; flex: none;
  border: none; box-shadow: none;
  /* scale is set by NA.applyFitScale() which reads the viewport and writes
     --fit-scale (read-only: it never resizes the window, so no feedback loop
     like the old bug). Defaults to 1 until JS runs. */
  transform: scale(var(--fit-scale, 1));
  transform-origin: center center;
  /* scale() paints smaller but leaves the full-size layout box behind, which
     would overflow the window and raise scrollbars around a stage that
     visually fits. These negative margins (written by applyFitScale alongside
     --fit-scale) shrink the layout box to the painted size. */
  margin: var(--fit-my, 0px) var(--fit-mx, 0px);
}
body.game-window .log { display: none; }

/* chakra squares */
.ck-row { display: flex !important; justify-content: space-between; align-items: center;
  width: 193px; margin: 4px auto 0; box-sizing: border-box; }
.ck-item { display: inline-flex; align-items: center; gap: 3px; flex: none; }
.ck-sq { display: inline-block; width: 10px; height: 10px; border: 1px solid #000; flex: none; }
/* Strength / Weapon / Devil Fruit / Concentration / Random */
.ck-sq.t { background: #7f047f; } .ck-sq.n { background: #0cff00; }
.ck-sq.g { background: #f12529; } .ck-sq.b { background: #0bb6ff; }
/* RANDOM chakra is plain black on the reference, not a grey. Ours rendered it
   #888 everywhere it appeared — the counter row, skill costs, the exchange
   dialog and the info panel all draw this same square. */
/* Random energy. The black stays as the base (and as the fallback when no
   icon is configured); the icon rides on top and scales to whatever size the
   square is drawn at — 10px in the counter row, 11-12px in the dialogs. */
.ck-sq.r { background: #000 center / contain no-repeat;
  background-image: var(--asset-energy-random, none); }
/* v340: THE TOTAL'S "T" IS THEIR PIXEL GLYPH. Asked 2026-10-06 ("make the T
   bolder, same style naruto-arena has it"). MEASURED that day: theirs is not
   text at all — it is drawn into their energy bar picture (wehuKoC.png,
   174x10), at x 164-173: a 10x10 solid-black T, the bar 3px thick across the
   top, the stem 4px wide (x 167-170) down the remaining 7 rows, no
   anti-aliasing — the same 10px height as the four squares beside it
   (REF_V340). Ours draws exactly that with two gradients; the letter stays in
   the DOM for readers, at size 0. */
.ck-t { display: inline-block; flex: none; width: 10px; height: 10px; font-size: 0; line-height: 0;
  color: transparent; overflow: hidden;
  background: linear-gradient(#000, #000) 0 0 / 10px 3px no-repeat,
              linear-gradient(#000, #000) 3px 3px / 4px 7px no-repeat; }
/* reference: "x N" labels are 14px REGULAR black on a 41px pitch */
.ck-n { font-size: 13px; color: #000; font-weight: 400 !important; }

/* =====================================================================
   SELECTION / LOBBY  (960x700 client — matches reference screenshots)
   ===================================================================== */
/* Both screens now share one native size, so the game window can be a
   fixed 770x560. Geometry below is measured from the reference client at
   the same stage size, so positions transfer 1:1. */
#select.screen { width: 770px; height: 560px; font-family: var(--gm-font); }

/* ---- top bar : language (left) + volume (right) ---- */
/* reference: toggle 37x18 at (106,6) with two 12x7 flags at x=111 and x=149 */
.s-lang { position: absolute; top: 8px; left: 10px; display: flex; align-items: center;
  justify-content: space-between; box-sizing: border-box;
  width: 52px; gap: 0; height: 24px; padding: 0 6px; border-radius: 12px;
  background: linear-gradient(#243a5c, #16243c); border: 1px solid #4a6a9a;
  box-shadow: 0 1px 3px #0006; }
.s-lang .flag { width: 17px; height: 11px; border-radius: 2px; cursor: pointer;
  box-shadow: 0 0 0 1px #0007; position: relative; overflow: hidden; opacity: .55; }
.s-lang .flag.active { opacity: 1; box-shadow: 0 0 0 1px #ffe07a; }
.flag.us { background: repeating-linear-gradient(#b22234 0 2px, #fff 2px 4px); }
.flag.us::before { content: ""; position: absolute; left: 0; top: 0; width: 9px; height: 9px;
  background: #3c3b6e; }
.flag.br { background: #169b46; }
.flag.br::before { content: ""; position: absolute; left: 50%; top: 50%; width: 13px; height: 13px;
  background: #ffd000; transform: translate(-50%, -50%) rotate(45deg); }
.flag.br::after { content: ""; position: absolute; left: 50%; top: 50%; width: 6px; height: 6px;
  background: #1d3f8a; border-radius: 50%; transform: translate(-50%, -50%); }


/* reference volume cluster: 11x11 minus @685, 36x11 slider @697,
   11x11 plus @737, speaker 13x13 @749 — all on y=8 */
.s-vol { position: absolute; top: 8px; left: 685px; right: auto; display: flex;
  align-items: center; justify-content: space-between; box-sizing: border-box;
  width: 77px; gap: 0; height: 13px; color: #f3ead2; user-select: none; }
/* ---- volume controls (selection top bar + battle bottom strip) ----------
   One design, two sizes. Shared shape: two carved brass studs either side of a
   segmented wedge that fills with gold, and a speaker that gets a red slash
   when muted. The wedge keeps the reference's triangular clip and the cluster
   keeps its MEASURED 77px footprint on the selection screen (11px studs, 46px
   wedge, 13px speaker on y=8) — only the painting changed, not the layout, so
   nothing here can move a measured element.
   --vol-* lets the battle strip reuse every rule at a larger size. */
.s-vol, .bb-vol { --vol-stud: 11px; --vol-face: 10px; --vol-h: 14px; }
/* The battle strip used its own LARGER size (16px studs, 52px wedge). It is
   dropped: the cluster now uses the same small size as the selection screen's,
   which is what "make them smaller" asks for and removes the overlap below. */

/* Plain black, on BOTH screens. These were carved brass studs with a radial
   highlight, an inset gloss and a drop shadow; the ask was "just black", so
   the paint is gone and only the shape is left. The +/- glyphs stay white so
   the control is still readable at 11px. */
/* MEASURED on the reference's selection screen, 2026-09-12. Its control is
   five PICTURES in a row, not glyphs in studs, which is why its minus and
   plus look like a matched pair — they are two halves of one drawing:

     minus  11x11  at 0     i.imgur.com/PxVkIqf.png
     bar    36x11  at 12    i.imgur.com/RSwKJXh.png  (the fill is the same
                            picture, clipped by width)
     plus   11x11  at 52    i.imgur.com/tiG7dfM.png
     mute    8x11  at 67    i.imgur.com/h1KsVwQ.png

   75px across, 11 tall. Ours are those boxes exactly, with an ASSET_LINKS
   key each so real art drops in without touching CSS; until then the CSS
   below draws them, and the minus and plus are the SAME drawing so they
   cannot drift apart again. */
.s-vol .vb, .bb-vol .vb {
  width: 11px; height: 11px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--vol-face); font-weight: 700; line-height: 1; text-align: center;
  color: #fff; text-shadow: none;
  background: #000;
  border: 1px solid #000; box-sizing: border-box;
  box-shadow: none;
  cursor: pointer; user-select: none; flex: none; }
/* THE GLYPHS ARE DRAWN, NOT TYPED (v177). They were the characters "\u2212"
   and "+", which are different widths, different stroke weights and sit on
   different baselines in the same font — at 10px inside an 11px stud the
   minus read as a thin dash and the plus as a fat cross, which is the
   "the - is different from the +" that was asked about. Both are two 7x1
   bars now, from the same rule; the plus simply keeps the second one. */
.s-vol .vb, .bb-vol .vb { font-size: 0; position: relative; }
.s-vol .vb::before, .bb-vol .vb::before,
.s-vol .vb::after,  .bb-vol .vb::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 7px; height: 1px; background: #fff;
  transform: translate(-50%, -50%); }
.s-vol .vb::after, .bb-vol .vb::after { transform: translate(-50%, -50%) rotate(90deg); }
/* the minus keeps only the bar */
.s-vol .vb.minus::after, .bb-vol .vb.minus::after { content: none; }
.s-vol .vb:hover, .bb-vol .vb:hover { filter: brightness(1.12); }
.s-vol .vb:active, .bb-vol .vb:active {
  transform: translateY(1px); box-shadow: inset 0 2px 3px #0007; }

.s-vol .wedge, .bb-vol .wedge {
  position: relative; height: var(--vol-h); cursor: pointer; flex: none;
  clip-path: polygon(0 100%, 100% 0, 100% 100%);
  background: #000; }
.s-vol .wedge { width: 46px; }
.bb-vol .wedge { width: 44px; }
/* the filled part stays light so the LEVEL is still readable against the
   black track — a black fill on a black track would show nothing at all */
.s-vol .wedge .vf, .bb-vol .wedge .vf {
  position: absolute; left: 0; top: 0; bottom: 0;
  background: #fff;
  box-shadow: none; }
/* the notches: vertical cuts back to the track colour, so the wedge reads as
   a row of rising bars rather than one solid triangle */
.s-vol .wedge::after, .bb-vol .wedge::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(90deg,
    transparent 0 5px, rgba(0,0,0,.62) 5px 6.5px); }

.s-vol .spk, .bb-vol .spk { position: relative; cursor: pointer; flex: none;
  line-height: 1; transition: opacity .14s ease, filter .14s ease; }
.s-vol .spk { font-size: 14px; text-shadow: none; color: #000; }
.bb-vol .spk { color: #000; }
.bb-vol .spk { font-size: 15px; }
.s-vol .spk:hover, .bb-vol .spk:hover { filter: brightness(1.15); }
.s-vol .spk.muted, .bb-vol .spk.muted { opacity: .45; }
/* a slash through the speaker, so mute reads instantly and not just as "dim" */
.s-vol .spk.muted::after, .bb-vol .spk.muted::after {
  content: ""; position: absolute; left: 0; top: 50%; width: 100%; height: 2px;
  background: #d0392b; border-radius: 1px; box-shadow: 0 0 3px #000a;
  transform: translateY(-50%) rotate(-22deg); }
@media (prefers-reduced-motion: reduce) {
  .s-vol .vb:active, .bb-vol .vb:active { transform: none; }
}

/* ---- game-mode buttons ---- */
/* Reference row: y=255, four buttons at x=23/152/358/563. Anchored left at
   23 with a fixed gap so the row spans 23..755 and never exceeds 770. */
/* Reference row: LOGOUT 118x57 @x23, then 192x57 at x=152/358/563, y=255 */
.s-modes { position: absolute; top: 255px; left: 0; right: 0; height: 57px; }
.s-mode { position: absolute; top: 0; display: inline-flex; align-items: center;
  height: 57px; width: 192px; justify-content: center;
  padding: 0 26px 0 10px; cursor: pointer; text-decoration: none; font-weight: bold;
  font-size: 11px; letter-spacing: .2px; color: #000; overflow: hidden;
  background: linear-gradient(#f7efdc, #d9c38a); border: 1px solid #7a6128;
  border-radius: 21px; box-shadow: 0 2px 6px #0006, inset 0 1px 0 #fff8; white-space: nowrap; }
.s-mode:hover { filter: brightness(1.05); color: #000; }
.s-mode:nth-of-type(1) { left: 23px;  width: 118px; top: 1px; }  /* 1px down: the art sits high in its box */
.s-mode:nth-of-type(2) { left: 152px; }
.s-mode:nth-of-type(3) { left: 358px; }
.s-mode:nth-of-type(4) { left: 563px; }
.s-mode[disabled] { opacity: .5; cursor: not-allowed; filter: grayscale(.3); }
.s-mode .fan { position: absolute; right: 3px; top: 50%; transform: translateY(-50%);
  width: 26px; height: 26px; border-radius: 50%; background: #15233f;
  border: 2px solid #f4ecd6; box-shadow: 0 1px 4px #0007;
  display: flex; align-items: center; justify-content: center; }
.s-mode .fan img { width: 16px; height: 16px; }

/* Hover on the four mode buttons: a small lift, a touch brighter, and a
   shadow that grows underneath — enough to feel alive without moving anything
   around it (transform and filter never affect layout, so the row stays put).
   The press pushes it back down so a click has some weight to it. Disabled
   buttons are excluded: nothing should invite a click it will not accept. */
.s-mode { will-change: transform; }
.s-mode:not([disabled]) {
  transition: transform .16s cubic-bezier(.2,.8,.3,1), filter .16s ease, box-shadow .16s ease; }
.s-mode:not([disabled]):hover {
  transform: translateY(-2px) scale(1.015);
  filter: brightness(1.07) drop-shadow(0 4px 7px rgba(0,0,0,.42)); }
.s-mode:not([disabled]):active {
  transform: translateY(0) scale(.995);
  filter: brightness(1.02) drop-shadow(0 1px 3px rgba(0,0,0,.35));
  transition-duration: .06s; }
@media (prefers-reduced-motion: reduce) {
  .s-mode:not([disabled]), .s-mode:not([disabled]):hover, .s-mode:not([disabled]):active {
    transition: none; transform: none; } }

/* An element whose data-skin file has a link in ASSET_LINKS is drawn as pure
   artwork: the supplied image already contains the frame and the label, so our
   gradient, border, shadow, text and (on the mode buttons) the corner badge all
   step aside. Set by applyButtonSkins(); with no link the class is never added
   and the CSS button above is what renders. */
.s-mode.skinned, .bb-btn.skinned {
  background-color: transparent; background-repeat: no-repeat;
  background-size: 100% 100%; background-position: center;
  border-color: transparent; box-shadow: none;
  color: transparent; text-shadow: none; }
.s-mode.skinned .fan, .bb-btn.skinned::before { display: none; }
/* the volume strip skins like the other two, but it must NOT take
   `color: transparent` from the rule above — its minus, wedge, plus and
   speaker are live children that would vanish with it. */
.bb-vol.skinned { background-color: transparent; background-repeat: no-repeat;
  background-size: 100% 100%; background-position: center;
  border-color: transparent; box-shadow: none; }
.bb-vol.skinned::before { display: none; }

/* NOTE on the var(--asset-…) backgrounds below: they carry no url() fallback
   any more. Their placeholder files were deleted once the art moved to a host,
   and a fallback pointing at a deleted file is a guaranteed 404 on first paint
   (the custom property is only set once applyAssetLinkOverrides runs). If one
   of these ASSET_LINKS entries is ever cleared, that surface goes blank rather
   than falling back — put a file back in assets/placeholder AND restore the
   fallback here if you need that. */

/* ---- character detail panel (shown when a character is clicked) ---- */
/* Character detail = an unrolling scroll. The rod (ScrollSmall.png, kept at
   its 46:239 ratio) starts in the middle; on open it slides LEFT while the
   parchment (Rectangle.png) unrolls out from it. Clicking the rod rolls it
   back up and closes the box. No CSS background — the art is everything. */
/* v222: THE SIZE THAT USED TO BE HERE WAS DEAD. This rule declared
   `left:105 top:30 width:750 height:268`, and an identical-specificity
   `.s-detail` further down the file (the v203 rebuild) sets
   `left:141 top:24 width:545 height:222`. Later wins, so the live box has
   been 545x222 for versions while this said 750x268.

   Found by a test that asserted the documented picture size against the
   stylesheet and got `750x268` back for a box that measures 545x222 live.
   Deleted rather than corrected, because two rules setting the same four
   properties is the disagreement, not the numbers. `display` and `z-index`
   stay: nothing else sets them. */
.s-detail { display: none; z-index: 8; }
.s-detail.show { display: block; }
/* MEASURED: the scroll UNROLLS LEFT -> RIGHT. Closed, #_top_scroll sits at
   stage 141,15 (the panel's own left edge) and the parchment is fully rolled
   up behind it; open, the rod has travelled +539px to 680,15 and the paper is
   fully revealed. The rod is 46x239 and overhangs the panel top and bottom. */
.s-detail .paper { position: absolute; inset: 0;
  /* v229: background-IMAGE from the key, not the `background` shorthand.
     The shorthand here sits AFTER the rule 900 lines above that points this
     panel at `--asset-bg-description`, and a shorthand resets
     background-image — so the description box has drawn the shared
     Rectangle since v222 whatever its own key said. Third time this exact
     trap has been found (`.b-info .pic` and `.b-portrait` were the others),
     and the first time it hid a picture rather than a placeholder. */
  background-image: var(--asset-bg-description, var(--asset-rectangle));
  background-repeat: no-repeat; background-size: 100% 100%;
  clip-path: inset(0 100% 0 0); transition: clip-path .6s ease; }   /* #_top_menu: 0.6s ease */
.s-detail.open .paper { clip-path: inset(0 0 0 0); }
/* same rule as .s-scroll: fixed height, natural width */
/* Same centre-line treatment. MEASURED on the reference: closed, its rod sits
   at stage 141..187 — the panel's own left edge (panel is 141..686), centre 164,
   i.e. 23 panel-relative. Open, it has travelled to 680..726, centre 703 = 562
   panel-relative, so its left edge rests 6px inside the panel's right edge and
   it covers only the red frame. */
/* v222: the travelling rod, under the same two rules, and still
   width-independent because it is placed by an EDGE at each end rather
   than by a centre line:
     closed  right edge 10px inside the paper's LEFT edge   (translateX(-100%))
     open    left  edge 10px inside the paper's RIGHT edge  (no translate)
   Both `left` and `transform` transition, so the travel is unchanged. */
/* v226: 46x239, like the character list's. Panel is 222 tall, so -8 centres
   a 239 rod on it (the half-pixel goes to the top, as it did). */
.s-detail .rod { position: absolute; top: -8px; height: 239px; width: auto;
  /* v233: THEIR TRAVEL, THEIR CURVE, ONE PROPERTY.
     MEASURED off their live stylesheet 2026-09-20 — the detail panel is three
     elements and every one of them runs 0.6s ease, forwards:

       #_top_menu          (the window, overflow:hidden, 545x222 at 141,24)
         enter  translate3d(-545px,0,0) -> 0
         leave  0 -> translate3d(-545px,0,0)
       #_top_menu_content  (the paper inside it)
         enter  translate3d( 545px,0,0) -> 0
         leave  0 -> translate3d( 545px,0,0)
       #_top_scroll        (the rod, at 141,15)
         enter  translateX(0) -> translateX(539px)
         leave  translateX(539px) -> translateX(0)

     The rod covers 539 and the window 545 — six pixels apart, on one shared
     curve, which is why theirs never separates. Ours drove the rod with
     `left` AND `transform` between two different anchors, so its distance
     depended on the art's own width and could not match the paper's reveal
     whatever the easing.
     Now the rod moves on ONE property for exactly their 539, and `left` sets
     only where it rests: right edge 60px inside the paper's left (20, + 15,
     + 10, + 10, + 5 — five asks on 2026-09-20; the TRAVEL below has never
     moved through any of them, because that one is measured and the rest is
     taste). */
  left: 60px; transform: translateX(-100%);
  transition: transform .6s ease; cursor: pointer; z-index: 3;
  filter: drop-shadow(2px 3px 4px #0006); }
.s-detail.open .rod { transform: translateX(calc(-100% + 539px)); }
.s-detail .rod img { height: 100%; width: auto; display: block; }
.s-detail .rod:hover { filter: drop-shadow(2px 3px 4px #0006) brightness(1.08); }
.s-detail.instant .paper, .s-detail.instant .rod { transition: none; }
/* The reference opens AND closes #_top_menu in 0.6s ease, so open and close
   are the same length now; .closing is kept as the hook (and as the place to
   change it if you want them to differ again). closeDetail() tears the panel
   out on this number, so CLOSE_MS in select.js must track it. */
.s-detail.closing .paper { transition: clip-path .6s ease; }
/* v233: TRANSFORM, not `left` — and this line was the original fault.
   The open state moved the rod with `left` AND `transform`; this one
   re-declared the transition as `left` alone, so on the way out the
   transform half SNAPPED to its end value in the first frame while the
   paper's clip eased over 600ms. That is exactly "the scroll gets to the end
   faster than the parchment". The rod travels on one property now, so there
   is one thing to transition and it is this one. */
.s-detail.closing .rod { transition: transform .6s ease; }
.s-detail .d-inner { position: absolute; left: 34px; right: 72px; top: 24px; bottom: 20px;
  opacity: 0; transition: opacity .45s ease .9s; overflow: hidden; }
.s-detail.open .d-inner { opacity: 1; }
.s-detail .close { position: absolute; top: 0; right: 0; cursor: pointer; color: #000;
  font-weight: bold; font-size: 15px; line-height: 1; z-index: 2; }
.s-detail .close:hover { opacity: .7; }
/* Top row: the character's face then its skill icons, every tile the same
   size and every tile with the same 1px black border, so they line up
   exactly along both their top and bottom edges. */
.s-detail .d-row { display: flex; gap: 14px; align-items: flex-end; }
/* one rule, one value: the reference is 15px/400 #b91921 with no tracking.
   This was declared here at .4px and again below at .2px, in two reds. */
.s-detail h4 { margin: 0 0 6px; color: #b91921; font-size: 14px; letter-spacing: normal; }
/* The face and the skill tiles are the SAME size, and that size is 77px.

   86px was measured off the reference's own sheet, but it was measured
   against .d-inner's 644px without accounting for the scroll rod, which
   sits at left:552px once the scroll is open and covers the right end of
   the row. The real room is about 518px, and a face plus four 86px tiles
   with 14px gaps is 486px before the rod — so the last tile went under it
   and the next one wrapped below the parchment.

   77 + 4x77 + 5x14 = 455px, which clears the rod with room for the paging
   arrow. .d-port is the large picture in the lower half of the sheet and
   keeps its own 86px. */
.s-detail .d-face, .s-detail .d-skill {
  width: 77px; height: 77px; border: 1px solid #000; background: #cfc4a4;
  flex: none; box-sizing: border-box; overflow: hidden; }
.s-detail .d-port {
  width: 86px; height: 86px; border: 1px solid #000; background: #cfc4a4;
  flex: none; box-sizing: border-box; overflow: hidden; }
/* The pager for a kit with more than four skills. MEASURED on the
   reference, 2026-09-12: two 23x23 round buttons in rgb(185,25,33), sitting
   in the strip ABOVE the tiles (their tops at y=31 against the row's y=56)
   and aligned to the row's own ends — ._top_pass_btt is absolute top:7
   right:19 and ._top_rewind_btt absolute top:7 left:163, which is the first
   tile's left edge. Ours was one 20x77 arrow inline at the end of the row.

   The glyph is the fast-forward double triangle, drawn as two CSS triangles
   rather than pulled from the icon font the reference uses. */
.s-detail .d-page { width: 18px; height: 18px;
  border-radius: 50%; background: #b91921; cursor: pointer; user-select: none;
  display: flex; align-items: center; justify-content: center; gap: 1px; }
.s-detail .d-page i { display: block; width: 0; height: 0;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent; }
.s-detail .d-page-next i { border-left: 5px solid #f3e7c6; }
.s-detail .d-page-prev i { border-right: 5px solid #f3e7c6; }
/* kept so a stale page cannot render a bare, unstyled node */
.s-detail .d-more { display: none; }
/* NOT a colour change on hover. The project's own rule, pinned by
   test_hover_polish, is that no :hover recolours text — the reference
   never does, and a recolour is what makes a classic layout read as a
   modern one. The arrow gets the measured lift instead, which is what
   every other hoverable thing on the site gets. */
.s-detail .d-page:hover { box-shadow: var(--hover-lift); }
.s-detail .d-face img, .s-detail .d-skill img, .s-detail .d-port img,
.s-detail .d-face .sprite, .s-detail .d-port .sprite,
.s-detail .d-skill .sprite { width: 100%; height: 100%; display: block; }
.s-detail .d-skills { display: flex; gap: 14px; align-items: flex-end; }
.s-detail .d-skill, .s-detail .d-face { cursor: pointer; }
.s-detail .d-skill:hover, .s-detail .d-face:hover { box-shadow: var(--hover-lift); }
/* The selected skill/face used to take a 2px near-black outline. It is an
   outer GLOW now, transcribed from the Photoshop layer style asked for:

     colour #fffc0e   opacity 65%   size 6px   spread 13%
     technique Softer   quality range 50   blend mode Screen

   How each maps to CSS:
     size 6px      -> the box-shadow blur radius
     spread 13%    -> 13% of the size as the solid core, 6 * 0.13 = 0.78px
     opacity 65%   -> the colour's alpha
     Softer        -> box-shadow's blur is already a gaussian falloff, which
                      is what Softer means (Precise is the hard-edged one)
     Screen        -> cannot apply to a box-shadow directly, so the glow is
                      painted by an ::after overlay that carries
                      mix-blend-mode: screen. Screen never darkens, so on the
                      dark parchment this reads as light being added rather
                      than a ring being drawn on top — which is the whole
                      point of asking for it.
   The element itself keeps its lift shadow and simply loses the outline. */
.s-detail .d-skill.sel, .s-detail .d-face.sel {
  outline: none;
  box-shadow: var(--hover-lift);
  position: relative;   /* the overlay below is positioned against this */
}
.s-detail .d-skill.sel::after, .s-detail .d-face.sel::after {
  content: ""; position: absolute; inset: 0;
  pointer-events: none;              /* never eat a click meant for the tile */
  box-shadow: 0 0 6px 0.78px rgba(255, 252, 14, .65);
  mix-blend-mode: screen;
}

/* Lower half: title on the left with the energy cost pinned to the right of
   the same line, then the description, then CLASSES left / COOLDOWN right. */
.s-detail .d-bottom { display: flex; gap: 18px; align-items: flex-start; margin-top: 10px; }
.s-detail .d-bio { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.s-detail .d-titlerow { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.s-detail .d-titlerow h4 { margin: 0; }
.s-detail .d-bio p { margin: 4px 0 0; color: #000; font-weight: bold; font-size: 11px;
  line-height: 1.45; text-transform: uppercase; }
.s-detail .d-meta { margin-top: auto; padding-top: 8px; display: flex;
  justify-content: space-between; align-items: baseline; gap: 16px;
  font-size: 9px; font-weight: bold; color: #000; text-transform: uppercase; }
.s-detail .d-cost { display: inline-flex; align-items: center; gap: 4px;
  font-size: 9px; font-weight: bold; color: #000; text-transform: uppercase;
  white-space: nowrap; }
.s-detail .d-cost .ck-sq { width: 10px; height: 10px; border: 1px solid #000; }
.s-detail .d-cd { white-space: nowrap; }

/* ---- selection entrance animation ----
   The character panel unrolls open the same way the description scroll
   does, and the profile block fades up just behind it. Plays once, on the
   first draw of the selection screen only. Uses the same easing and a
   comparable duration to the detail scroll so the two feel related. */
/* THE REFERENCE'S charSelectionEnter, as measured (ours until v329 — see below):
     0%   clip: rect(0, 697px, 222px, 697px)
     100% clip: rect(0, 697px, 222px, 0)
   The LEFT edge travels 697 -> 0, so the panel is revealed RIGHT to LEFT, and
   the rod (scrollCharSelectEnter: translateX(697px) -> 0) sweeps left across
   it at the same time. The two together read as a scroll being unrolled
   leftwards. `clip` is deprecated; inset() with the same left edge is the
   modern spelling of the identical motion. */
@keyframes s-unroll-out {
  from { clip-path: inset(0 0 0 0); }
  to   { clip-path: inset(0 0 0 100%); }
}
@keyframes s-riseIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* v329 — ASKED 2026-10-04 20:23: "make sure the animation on the selection
   screen of the character background works as intended (because scroll may
   be appearing before the parchment ends its animation). Make the scroll
   appear first and from left to right animate the parchment (just like the
   description box but flipped)."
   What it did until now (traced frame by frame in Chromium, 2026-10-04):
   the rod swept in from 697px out on the RIGHT while the panel was revealed
   right-to-left (the reference's charSelectionEnter / scrollCharSelectEnter),
   so for most of the 1.3s the rod was drawn over parchment that was not
   there yet. And its keyframes still carried the translateX(-50%) that v226
   took out of the rod's resting transform, so with `both` holding the last
   keyframe the rod sat half its own width LEFT of its measured spot until
   the next redraw dropped the class and it jumped back.
   Now, as asked — a departure from the reference's right-to-left, by the
   owner's choice: the rod is in place first (a .25s fade where it rests, with a .3s head start; no
   transform in the keyframes, so nothing can jump), then the parchment
   unrolls out of it LEFT to RIGHT on the reference's own 1.3s curve — the
   description scroll's reveal, mirrored onto the rod's side. */
@keyframes s-unroll-ltr {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}
@keyframes s-rod-show { from { opacity: 0; } to { opacity: 1; } }
.s-scroll.entering { animation: s-rod-show .25s ease both; }
.s-panel.entering  { animation: s-unroll-ltr 1.3s var(--ref-in) .3s both; }
/* the info column is ours, not the reference's; it rides in behind the sweep */
.s-info.entering   { animation: s-riseIn .45s ease .85s both; }
/* respect a reduced-motion preference rather than forcing the animation */
@media (prefers-reduced-motion: reduce) {
  .s-panel.entering, .s-scroll.entering, .s-info.entering { animation: none; }
}

/* ---- bottom panel : scroll + grid + footer + profile ---- */
/* MEASURED on naruto-arena.site (2026-09-04): the character box is TWO
   elements, not one, so each can be re-skinned on its own —
     #_char_select       stage (52,315)  697x222  the parchment + red frame
     #_scroll_charselect              46x239  the scroll rod, drawn ON TOP
                                             of the panel's LEFT edge
   and the rod reuses the very same file as the detail scroll (#_top_scroll),
   so it is one asset used twice.
   The rod is on the LEFT. My capture of that one element read x=720 (the
   right edge) and I built it there; corrected after checking the reference
   again. Two things in this very file already said left and should have
   caught it: the "previous" arrow sits at stage x=50 with the note that it
   "rides ON the scroll rod", which only makes sense with the rod on the left,
   and the old single-image bg_charlist.png had its rod baked into its left
   ~55px. That baked-in rod is what fixes the x: scaled to where the panel
   then sat, its left edge fell on stage 17, which is where the rod is now
   (17..63). A straight mirror of my wrong number would have put it at 35,
   and there the arrow sat almost entirely on top of the rod; at 17 only the
   arrow's tail rests on it, which is the look on the reference.
   Ours had the rod baked into bg_charlist.png and stretched that single image
   to 732x242 — 5% wider and taller than its own art — which is why the frame
   never quite lined up. bg_charlist.png is already 697x222 and ScrollSmall.png
   already 46x239, i.e. both match the reference natively: the art was fine,
   the CSS was scaling it. Drawn 1:1 now, and every panel-relative child moved
   35px left to keep its stage position, since the panel's left edge moved
   17 -> 52 while its right edge stayed at 749. */
.s-panel { position: absolute; left: 52px; top: 315px; width: 697px; height: 222px;
  /* v224: fallback added. With the key emptied in the panel the variable is
     unset, `var()` is invalid at computed-value time, and `background` took
     its INITIAL value — the list went transparent, not back to Rectangle.
     Same defect that would have shipped the four popups blank in v223. The
     two pictures are the same file today, so nothing changes on screen. */
  background: var(--asset-bg-charlist, var(--asset-rectangle)) no-repeat;
  background-size: 100% 100%; padding: 0; overflow: visible; }
/* the rod: a sibling of the panel at stage (17,306) so the panel's unroll
   animation cannot clip it, and above the panel so it overlaps the frame.
   scaleX(-1) because the same art also serves the detail scroll on the right;
   flipping it here keeps the rod's highlight on the outward side. It stays
   BELOW .s-page (z-index 8), so the "previous" arrow rides on top of it. */
/* HEIGHT is fixed, WIDTH follows the image's own proportions. It used to be a
   46x239 box with the art stretched to fill it — fine while the art was
   exactly 46x239, but the moment a differently-proportioned scroll was dropped
   in, the width forced it narrow and the rod looked squashed. An <img> lets
   the browser derive the width, so w and h stay linked whatever the art is. */
/* MEASURED on the reference (2026-09-08): its rod is 46 wide and spans stage
   23..69 against a panel that starts at 52 — so 17px of rod sits on the panel
   and 29px on the background, centred on x=46, six pixels outside the frame.
   Positioning by CENTRE rather than by left edge means that holds for whatever
   width the art turns out to be: left is the centre line, translateX(-50%)
   does the rest. With art at the reference's 46:239 proportions this lands
   pixel-exact on 23..69. */
/* v222: the same two rules as `.gm-page .rod`, in stage coordinates,
   because this one is a SIBLING of .s-panel rather than a child of it.
   The panel is 697x222 at (52,315): so the scroll is 242 tall starting at
   305 (10 above), and its right edge lands at 62 — 10px inside the panel.

   `100% - 62px`, NOT `770px - 62px`. Measured: the literal put the right
   edge at 58 and the overlap at 6. `.screen` is `position: relative` with a
   2px border, so an absolutely-positioned child resolves against its
   PADDING box — 766 wide, not 770 — and every `left:` in this file is
   already written in that box. `100%` IS that box, whatever the border does
   next. Same trap as v192's two boxes with the same `bottom`, and the same
   one that made nine selection-screen rows read +2/+2 in the v218 audit. */
/* v226: BACK TO THE REFERENCE'S OWN BOX. `REF_SELECT.json` records their
   selection scroll as 46x239 at stage (23,306), measured off their page —
   panel 697x222 at (52,315), so it overlaps the panel's left edge by 17 and
   stands 9 proud above and 8 below. The +10-and-centred rule the owner set
   on 2026-09-19 was for the NEW panels (team manager, filter); this one and
   the description rod go back to what they always were.
   THE HEIGHT IS THEIRS, THE WIDTH IS THE ART'S. 239 tall is the reference's
   number; `width: auto` lets the drawing keep its own proportions, so it is
   never squashed on either axis (v226 set a literal 46 and squeezed a
   793x1983 scroll to 0.19). Placed by its RIGHT EDGE at stage 69 — the
   reference's 23 + 46 — so the 17px overlap of the panel's left edge holds
   whatever width the art turns out to be. */
.s-scroll { position: absolute; right: calc(100% - 92px); left: auto;
  top: 306px; width: auto; height: 239px;
  /* scaleX(-1) only. The translateX(-50%) that used to be here was half of
     the old centre-line positioning; with `right` doing the placing it
     would drag the scroll off by half its own width, which is exactly the
     width-dependence this change removes. scaleX flips in place. */
  z-index: 6; pointer-events: none; transform: scaleX(-1); }
.s-scroll img { height: 100%; width: auto; display: block; }

/* Reference arrows are 41x44; the "previous" arrow rides the scroll rod at
   x=50, LEFT of the panel (the rod spans 17..63, so the arrow's tail
   overlaps it by 13px).
   STAGE coordinates, and appended to the screen rather than to .s-panel.
   The comment here always claimed they were positioned against the screen
   "which would clip them" — but they were panel children with panel-relative
   offsets, and .s-panel.entering animates clip-path, which creates a stacking
   context. That trapped the arrows' z-index:8 INSIDE the panel, so once the
   rod moved to the left the "previous" arrow was drawn under it and all but a
   sliver disappeared. As siblings their z-index competes with the rod's 6
   directly, which is what "rides ON the rod" needs. */
.s-page { position: absolute; width: 41px; height: 44px; cursor: pointer; z-index: 8;
  background: var(--asset-arrow, url(../assets/placeholder/Arrow.png)) center/contain no-repeat; }
/* prev arrow rides ON the scroll rod (left of the grid, level with the
   top row), barely touching the first face; next arrow hangs off the
   bottom-right face's outer edge, pointing out of the grid */
/* panel-relative: the panel origin is (54,317), so these land at stage
   (50,332) and (505,468) — the "prev" arrow rides the rod just outside the
   panel's left edge, which is why the panel must not clip. */
/* MEASURED on the reference: prev at stage (50,332), next at (492,457), both
   41x44. Ours put prev at (48,330) — 2px out — while next already matched. */
.s-page.up   { left: 50px;  top: 332px; transform: scaleX(-1); }
.s-page.down { left: 492px; top: 457px; }
.s-page:hover { filter: drop-shadow(0 0 3px #fff) brightness(1.05); }

/* left box: the character list. It fills the space left of the player
   details and centers the grid + footer inside it (both axes). */
.s-mid { position: static; }
/* 7 x 3 of 50x50. Reference column pitch is 61 (50 + 11 gap) and row
   pitch 60 (50 + 10 gap). */
.s-grid { position: absolute; left: 35px; top: 25px;
  display: grid; grid-template-columns: repeat(7, 50px); grid-auto-rows: 50px;
  column-gap: 11px; row-gap: 10px; }
/* Every tile carries a loading placeholder behind its art and fades the
   real image in over 0.35s, so a slow or failed CDN fetch reads as
   "loading" rather than a blank hole. Art is moving to imgur, so this
   matters. */
.s-cell { width: 50px; height: 50px; border: 1px solid #000; background: #d8cba6;

  cursor: pointer; position: relative; overflow: hidden; }
.s-cell img, .s-cell .sprite { width: 100%; height: 100%; display: block; }
.screen:not(.art-warm) .s-cell img,
.screen:not(.art-warm) .s-cell .sprite { animation: s-fadein .35s ease both; }
@keyframes s-fadein { from { opacity: 0; } to { opacity: 1; } }
.s-cell:hover { box-shadow: var(--hover-lift); z-index: 2; }
/* MEASURED: a character you do not own is drawn at plain opacity 0.30 —
   no grayscale, no brightness change. */
/* v183: a locked cell is CLICKABLE — it opens the character's sheet so you
   can read the bio and every skill of something you have not earned yet. It
   just cannot join the team. `cursor` is left to the in-game sword rule
   above (which is !important and wins anyway); `not-allowed` here was saying
   "this does nothing", which is no longer true. Opacity lifted .3 -> .45 so
   the face is actually legible once you are meant to look at it, and the
   hover lift is back, because it IS hoverable now. */
.s-cell.locked { opacity: .45; }
.s-cell.locked:hover { opacity: .7; box-shadow: var(--hover-lift); }
/* MEASURED: a character already on your team is removed from the grid —
   ._select_char_item goes visibility:hidden and only the beige backing
   (._select_char_background) is left, so the cell reads as an empty slot. */
.s-cell.picked { cursor: default; background: #e7dcc0; background-image: none; }
.s-cell.picked img, .s-cell.picked .sprite { visibility: hidden; }
.s-cell.picked:hover { box-shadow: none; }
.s-cell.empty { background: #e7dcc0; cursor: default; }
.s-cell.empty:hover { box-shadow: none; }

/* reference labels: 10px weight-700 in #591307; MISSIONS carries wider
   tracking (1.1px) than FILTER/STYLES (0.6px) */
/* MEASURED (2026-09-02): the three footer buttons sit on ONE row at stage
   y=513 — panel-relative 198, not 219 — and they are 14px tall, not 17. */
/* v225: moved for the 6px stroke clearance rule — see ADMIN.md v225. */
/* v226: 3px down and a size smaller, asked 2026-09-19. 196 + 11 = 207,
   which lands exactly on the stroke-clearance line rather than through it —
   the reason the height came down with the move. */
.s-foot { position: absolute; left: 34px; top: 199px; width: 620px; height: 11px;
  display: flex; align-items: center; gap: 0; font-size: 10px; color: #000;
  font-weight: 700; letter-spacing: .6px; }
.s-foot .lbl.mid { letter-spacing: 1.1px; }
/* MEASURED on naruto-arena.site's selection screen: the four buttons along the
   bottom (FILTER, the middle one, STYLES, TEAM MANAGER) all share ONE surface —
   a cream gradient, a translucent red-brown hairline and a 7px radius — with
   10px/700 #591307 labels. This block used to declare font-size, font-weight,
   colour and padding TWICE each, with !important on the first set purely to
   beat the duplicate a few lines below it. The stale half is gone; the values
   that survive are the measured ones, so nothing needs !important any more. */
.s-foot .lbl { position: absolute; top: 0; height: 11px; line-height: 9px; box-sizing: border-box;
  overflow: hidden; text-align: center; cursor: pointer;
  /* v218: 10px / #000, NOT 9px / #000. The comment three lines above has
     said "10px/700 #000 labels" since it was measured, and the code under
     it shipped 9px black — a measurement written down and then not applied,
     which is this project's oldest failure mode wearing a new hat.
     RE-MEASURED 2026-09-19 on their live selection screen, dpr 1, no
     transform: `._mastermode-select-label` computes font-size 10px,
     font-weight 700, color rgb(89,19,7), letter-spacing 0.6px (1.1px on the
     middle one) — which is exactly what the comment already claimed.
     Two readings, sixteen versions apart, agreeing.
     Checked before changing: at 10px the longest label, PIRATE QUESTS, is
     97.3px of text in a 115px box (111px of content after the 2px padding),
     so nothing clips and the ellipsis rule below stays unused. */
  font-size: 9px; font-weight: 700; color: #000; letter-spacing: .6px;
  /* reference padding is 0 2px — 8px side padding was clipping "STYLES" */
  padding: 0 2px;
  background: var(--gm-btn-face); border: 1px solid var(--gm-btn-line); border-radius: 0; }
/* reference: FILTER 55px @x34, MISSIONS 115px @x186, STYLES 55px @x397
   (panel-relative), all 14px tall on one row */
.s-foot .lbl:nth-of-type(1) { left: 0;    width: 50px; }
.s-foot .lbl.mid            { left: 155px; width: 105px; }
.s-foot .lbl:nth-of-type(3) { left: 366px; width: 50px; }
/* one line, always — a wrapped label spills out of the panel */
.s-foot .lbl { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.s-foot .lbl:hover { opacity: .7; }


/* right box: the player details. A fixed-width column, centered
   vertically in the parchment and with every section centered in it. */
.s-info { position: absolute; left: 480px; top: 24px; width: 210px; height: 100px;
  overflow: visible; display: flex; flex-direction: column;
  justify-content: center; align-items: center; text-align: center; }
/* align-self + a fixed margin instead of riding .s-info's align-items:center.
   Centred, this row's x depended on how WIDE it was, and its width is the
   avatar plus whatever the longest profile line happens to be — so the avatar
   drifted whenever the text changed (shrinking the gap from 12 to 3 moved it
   4px, and a long username would have moved it too). .s-info's left edge is
   stage 532 and the avatar belongs at 539, hence the 7px. */
.s-id { display: flex; gap: 5px; text-align: left;
  align-self: flex-start; margin-left: 7px; }
/* 75x75 including its 1px black border (box-sizing is border-box globally) */
.s-id .av { width: 77px; height: 77px; position: relative; flex: none;
  background: #cfc4a4; border: 1px solid #000; }
.s-id .av img { width: 100%; height: 100%; display: block; object-fit: cover; }
/* reference: 12px REGULAR (not bold), black, 16px line pitch */
/* MEASURED: the reference's profile lines start at x=628 on a strict 16px
   pitch. Ours began at 621, and the column was narrow enough that the level
   line wrapped — which pushed everything under it down, so the gap between
   LEVEL and LADDERRANK was 32px where every other gap was 16.
   The nudge is applied HERE rather than on .s-info, because .s-info also
   parents the avatar, both team slots, the ready line and the Team Manager
   button: moving that container fixed the text and dragged those five off
   the marks they already matched. */
/* left:0 — the gap between the avatar and this column is the flex gap alone
   (3px), not a flex gap plus a relative nudge. top:-3px lifts the first row so
   its INK, not its line box, starts on the avatar's first pixel: a 12px font
   in a 16px line box carries 3px of leading + ascent gap above the caps, so
   at top:0 the text reads 3px low. */
/* v225: CAPPED. `white-space: nowrap` in a 204px box that starts at panel-rel
   569 let a long RATIO or LADDERRANK line run to 773 — 93px past the stroke
   clearance line and 76px past the panel itself, where .screen clipped it.
   111px is what is left between x 569 and the safe edge at 680. The box is
   capped rather than .s-info moved, because .s-info also parents the avatar,
   both team slots, the ready line and the Team Manager button, all of which
   already sit on measured marks. */
.s-id .lines { font-size: 11px; line-height: 16px; color: #000; font-weight: 400;
  white-space: nowrap; position: relative; left: 0; top: -3px;
  max-width: 111px; overflow: hidden; text-overflow: ellipsis; }
.s-id .lines .nm { font-size: 11px; letter-spacing: normal; font-weight: 700; }
.s-id .lines .rankline { display: flex; align-items: center; }

/* reference team row: panel-relative (447,128), slots 56x56 on a 66px pitch */
/* selection team slots: 50x50 on a 61px pitch, panel-rel (487,128)
   -> info-relative (0,102). The 56x56 figure belonged to the battle panel. */
.s-team { position: absolute; left: 7px; top: 104px; width: 210px;
  display: flex; flex-direction: column; align-items: flex-start; margin: 0; }
.s-team .slots { display: flex; gap: 11px; justify-content: flex-start; }
.s-team .slot { width: 50px; height: 50px; border: 1px solid #000; background: #d8cba6;

  cursor: pointer; overflow: hidden; }
.s-team .slot img, .s-team .slot .sprite { width: 100%; height: 100%; display: block; pointer-events: none; }
.s-team .slot.empty { background: #e7dcc0; cursor: default; display: flex;
  align-items: center; justify-content: center; color: #a99d7d;
  font-weight: bold; font-size: 25px; }        /* empty spot keeps its number */
.s-team .slots.drop-ok { outline: 2px dashed #2e8f2e; outline-offset: 3px; border-radius: 0; }
.s-cell img { pointer-events: none; }
.s-grid.drop-ok { outline: 2px dashed #c9302c; outline-offset: 2px; border-radius: 0; }
.s-ready { position: absolute; left: -20px; top: 56px; width: 210px !important; height: 13px; line-height: 13px; overflow: hidden; text-align: center;
  font-weight: bold; color: #000; font-size: 9px; margin: 0; letter-spacing: normal;
  width: 100%; box-sizing: border-box; text-align: center; white-space: nowrap; }
.s-ready.no { color: #000; }
.s-ready .drag { color: #e0241c; }
/* MEASURED: 84x14 at stage 584,512 — 8px/700 #591307, line-height 8px, no
   letter-spacing, uppercase, padding 0 7px, on the SAME cream button face as
   the three footer buttons (7px radius), not a button of its own.
   Ours had drifted into a red gradient pill with a 3px radius, and like the
   rule above declared font-size, colour, padding and weight twice with
   !important fighting its own duplicates — so the label was rendering
   #591307 on a dark red gradient, which is also why it kept surfacing in the
   contrast sweep. */
/* 110 wide, not 84: "TEAM MANAGER" at 8px did not fit and wrapped onto a
   second line inside a 14px-tall button, so the second word was clipped.
   Kept on the same centre it had (stage 626), and nowrap so it can never
   silently wrap again — if the label ever outgrows the button it will
   overflow visibly instead. */
/* v225: moved for the 6px stroke clearance rule — see ADMIN.md v225. */
.btn-tm { position: absolute; left: 44px; top: 71px; width: 100px; height: 11px;
  margin: 0; padding: 0 7px; box-sizing: border-box; overflow: hidden; white-space: nowrap;
  font-size: 7px; font-weight: 700; line-height: 7px; letter-spacing: normal;
  color: #000; text-transform: uppercase; text-align: center; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: var(--gm-btn-face); border: 1px solid var(--gm-btn-line); border-radius: 0; }
.btn-tm:hover { filter: brightness(1.04); opacity: .85; }
/* =====================================================================
   BATTLE  (770x560 native client)
   ===================================================================== */
#battle.screen { width: 770px; height: 560px; font-family: var(--gm-font); }

/* ---- top bar : players + ready/timer bar + chakra + exchange ---- */
/* z-index 9, above the character columns (8): the top row's face starts at
   y=63 and a hat now reaches 26px over it, which would otherwise paint across
   the player's name and rank. The band has no background of its own, so
   raising it only protects its own text. pointer-events is handed back to the
   children so the band cannot swallow clicks on the 3px of row-0 portrait it
   overlaps. */
.b-top { position: absolute; top: 0; left: 0; right: 0; height: 66px; z-index: 9;
  pointer-events: none; }
.b-top > * { pointer-events: auto; }
/* the ready/timer bar is dead-center; both player boxes are anchored to the
   center line at the same distance, so they mirror each other exactly */
/* Reference top bar: avatars 50x50 at x=223 (mine) and x=506 (enemy), both
   at y=9; name baseline y=22 at 20px, rank y=43 at 16px. Names sit on the
   OUTSIDE, avatars toward the centre. */
.b-player { position: absolute; top: 9px; display: flex; gap: 6px; align-items: flex-start; cursor: pointer; }
/* avatars sit toward the CENTER, 20px from the timer bar (bar spans
   289.5..480.5); names/ranks on the OUTSIDE. p1: [name][avatar->269.5];
   p2: [500.5<-avatar][name]. */
/* MEASURED against the reference, stage-relative:
     p1  text 0,22 215 wide (RIGHT-aligned)   avatar 223,9 50x50
     p2  avatar 506,9 50x50                   text 564,22 215 wide (left)
   Both blocks are therefore 273 wide: 215 text + 8 gap + 50 avatar. Ours were
   263/262 starting at x=8, which put p1's avatar at 221 and its text edge at
   213 — the whole left header sat 2px inboard. Sizing the text block to a
   fixed 215 also means a long username lines up the same way the reference's
   does, instead of shrink-wrapping to whatever the name happens to be. */
.b-player.p1 { left: 0;     width: 273px; justify-content: flex-end; overflow: hidden; }
.b-player.p2 { left: 506px; width: 273px; overflow: hidden; }
/* object-fit:cover so an uploaded avatar of ANY size fills its box instead of
   stretching — the same picture is shown at 50 here, 77 in selection and 75 in
   the info panel, and a non-square upload distorted at every one of them. */
.b-player img.av { width: 50px; height: 50px; border: 1px solid #000; background: #cfc4a4;
  box-shadow: 0 1px 3px #0007; flex: none; object-fit: cover; }
/* v281: a worn border IS the edge — the 1px black one comes off under it
   (asked 2026-09-24: "remove it when a frame is on") */
.b-player .av-wrap.framed img.av { border: 0; }
/* padding-top places the NAME's first PAINTED row 3px under the avatar's top
   edge — avatar top is y=9, so the caps land on y=12. Zero, not 3, because a
   20px face in a line-height:1 box already carries ~3px of ascent gap above
   its caps; the box top and the ink top are not the same line, which is what
   made the arithmetic here wrong twice. Verified by scanning pixels. */
.b-player .txt { padding-top: 7px; display: inline-flex; flex-direction: column; flex: none;
  width: 215px; }
/* The reference's name and rank are each a FULL-WIDTH 215px box that aligns
   its text, rather than a shrink-wrapped box pushed to one side. The painted
   result is the same, but full-width boxes keep the two lines locked to the
   same edge whatever the username's length — with align-items they each
   shrink to their own text and can drift apart. */
.b-player .txt { align-items: stretch; }
.b-player.p1 .nm, .b-player.p1 .rk { text-align: right; }
.b-player.p2 .nm, .b-player.p2 .rk { text-align: left; }
/* MEASURED across the reference's battle screen: EVERY text run is weight 400
   with letter-spacing normal and no text-shadow at all — it gets its bold
   weights from a separate bold font FAMILY rather than from font-weight, so
   asking for 700 here made our text heavier than the reference everywhere it
   appeared. Sizes and colours below are the measured ones. */
.b-player .nm { color: #bd262c; font-weight: 700; font-size: 18px; letter-spacing: normal; line-height: 1; }
/* MEASURED: the rank sits at y=43, one pixel under the name's 22..42 box.
   Ours used a 7px gap, dropping it to 49 — six pixels low on both sides. */
.b-player .rk { color: #000; font-weight: 700; font-size: 14px; letter-spacing: normal; line-height: 1;
  margin-top: 5px; }
/* v273: THE TITLE ROW, between name and rank — black, italic, the rank's
   weight (asked 2026-09-24). Tuned the same day off the first render, at
   the owner's word: with a title the NAME is 1px smaller (17), the TITLE is
   1px smaller than the rank (13), and the two gaps are 2px tighter than the
   rank's usual 5 (3 each), so the three rows are evenly spaced. The block is
   17 + 3 + 13 + 3 + 14 = 50 tall — exactly the avatar's 50 — and starts at
   the avatar's top edge (y = 9), so name, title and rank span the picture.
   Without a title nothing changes: 18px name, 5px gap, rank. */
/* v274: THE TAILS OF g, j, p, q, y WERE CLIPPED (the owner's eye,
   2026-09-24). line-height 1 makes the row exactly 13px, and overflow:hidden
   — there for the ellipsis on a long title — cut whatever the letters drew
   outside it: the descenders at the bottom, and the italic's overhang at the
   right edge. The row now carries 4px of room below and 3px at each side
   as PADDING, handed straight back as negative MARGIN, so the clip box
   grows but nothing moves: the row sums (below) hold, measured on the line
   box. */
.b-player .ti { color: #000; font-weight: 700; font-size: 13px; font-style: italic; letter-spacing: normal;
  line-height: 1; margin: 2px -3px -4px; padding: 0 3px 4px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.b-player.p1 .ti { text-align: right; }
.b-player.p2 .ti { text-align: left; }
/* v274: the two gaps are 2px (3 less 1, asked 2026-09-24 09:40), so the
   rows are 17 + 2 + 13 + 2 + 14 = 48 — 2px short of the 50px avatar. The
   1px on top centres them on it: 1px in at the top, 1px in at the bottom. */
.b-player .txt.has-title { padding-top: 1px; }
.b-player .txt.has-title .nm { font-size: 17px; }
.b-player .txt.has-title .rk { margin-top: 2px; }
.b-player:hover { opacity: .82; }

.b-center { position: absolute; left: 290px; top: 10px; transform: none; width: 200px;
  text-align: center; }
.b-ready { display: block; cursor: pointer; text-decoration: none; }
.b-ready .label { font-weight: 400; font-size: 16px; letter-spacing: normal; color: #000;
  text-align: center; line-height: 18px; }
/* VERIFIED against the live reference: .bar_ready is Franklin 16px/400 on
   BOTH turns — "PRESS WHEN READY" is not bold there. We had deliberately
   bolded it, which is the one place our battle text was heavier than the
   reference after the weight sweep above. */
.b-ready .bar { width: 193px; height: 12px; margin: 0 auto; background: #ffffff; border: 1px solid #000000;
  overflow: hidden; position: relative; }
.b-ready .bar .fill { position: absolute; left: 0; top: 0; bottom: 0; width: 100%;
  background: #ba1921; }
/* the clock drains in 1s steps; a matching linear transition makes it read as
   continuous motion on both turns instead of ticking */
.b-ready .bar .fill { transition: width 1s linear; }
.b-ready .bar.lowtime .fill { background: #e02a1e; }
.b-ready[disabled] { cursor: default; }
.b-chakra { margin-top: 4px; }
.b-chakra .ck-n { font-weight: 400; }
.b-exchange { display: inline-block; margin-top: 4px; font-size: 12px; font-weight: 400;
  color: #000; letter-spacing: normal; cursor: pointer; }
.b-exchange:hover { opacity: .7; text-decoration: underline; }
.b-exchange.off { visibility: hidden; }   /* gone on the enemy turn, not just dimmed */
/* The chakra poll is YOUR pool to spend, so it has no meaning while the
   opponent is acting — the reference hides it there and so do we now. */
.b-chakra.off { visibility: hidden; }

/* ---- team columns ---- */
/* Row rhythm taken from the reference: skill-bar tops at 87/208/329
   (121px pitch). Face sits 24px above its bar, health bar 55px below it,
   status icons 19px above it. */
.b-col { position: absolute; top: 63px; bottom: 0; width: 140px; }
.b-col.ally { left: 0; }
.b-col.foe  { right: 0; }

.b-unit { position: absolute; width: 134px; height: 152px; z-index: 8; }  /* ABOVE the bar: faces must stay visible and clickable as targets */
.b-unit.r0 { top: 0; } .b-unit.r1 { top: 121px; } .b-unit.r2 { top: 242px; }
/* Rank "hat" — THE AUTHORING CANVAS for hat_<rank>.png. Draw at exactly
   110x105 with transparency and it lands 1:1, no scaling:

     canvas 110 x 105, top-left at (unit + 2, portrait top - 26)
     face picture sits at x 30..106, y 26..102   (77x77 incl. its 1px border)
       -> 26px of headroom above the face
       -> 30px outward of it (left as drawn; the foe column mirrors this)
       ->  3px inward of it
       ->  2px below it, where the health bar starts

   26px is the real ceiling: the health bar of the character ABOVE ends
   exactly 26px over this face (row pitch 121 - hp bottom at portrait+95).
   The box used to be 110x140 starting 5px above the face, which had it
   backwards — 5px of headroom and 58px of dead canvas hanging over the unit's
   own health bar — so any hat drawn to fill it got shrunk by object-fit and
   sat too low. object-fit: contain stays as the safety net for art that is
   not exactly 110x105 (it scales to fit, top-centred, never crops). */
.b-unit .deco { position: absolute; top: -26px; width: 110px; height: 105px;
  pointer-events: none; z-index: 7; object-fit: contain; object-position: top; }
/* Anchored so the FACE lands at the same spot inside the canvas on both
   sides. The two columns are not mirror images of each other — the ally
   portrait is inset 32px from its unit's left, the foe portrait 28px from its
   unit's right — so a plain "2px in from the outer margin" on each side put
   the face 4px further out in the enemy canvas than in the friendly one, and
   one drawing could not be right for both. -2px on the foe side cancels that
   4px, giving 30px outward / 3px inward of the face in both. */
.b-col.ally .deco { left: 2px; }
.b-col.foe .deco { right: -2px; transform: scaleX(-1); }

/* Face picture: 75x75 + 1px border = 77x77 */
.b-portrait { position: absolute; top: 0; width: 77px; height: 77px; border: 1px solid #101010;
  background-color: #cfc4a4; box-shadow: 0 2px 5px #0007; overflow: hidden;
 }
/* MEASURED: ally portraits at x=32, foe portraits at x=659 in the 770-wide
   stage — the reference is NOT symmetric (32 from the left, 34 from the
   right). Ours used right:24, putting the foe column at 663.
   Worth noting how this hid: measured in the site-embedded view the foe side
   read 659 and looked correct, because that view is 4px narrower and the
   column is right-anchored. Only a true game-window measurement shows it. */
.b-col.ally .b-portrait { left: 32px; } .b-col.foe .b-portrait { right: 28px; }
.b-portrait img, .b-portrait .sprite { width: 100%; height: 100%; display: block; }
/* the enemy team faces back across the board, as in the reference */
.b-col.foe .b-portrait img, .b-col.foe .b-portrait .sprite { transform: scaleX(-1); }
.b-unit.dead .b-portrait { filter: none; }
.b-portrait .death-x { width: 100%; height: 100%; display: block; object-fit: cover;
  image-rendering: auto; }
/* v230: NO DIM. Theirs does not darken the characters you cannot target —
   `._mc_choice, ._mc_notarget` is one rule with `background-color:
   rgb(255,255,51)`, and valid targets get `visibility: visible` while the
   rest get `visibility: hidden`. Nothing else changes. Ours added a sepia
   wash on every non-target, which with the yellow hidden UNDER the face
   (see the z-index note on `.b-unit.targetable .b-portrait::after`) made the
   whole effect read backwards: the unavailable ones changed and the
   available ones did not. The class is kept as a hook and paints nothing. */
.b-unit.dimmed { }
@keyframes na-blink { 0%, 100% { opacity: .5; } 50% { opacity: .22; } }
.b-unit.targetable { cursor: pointer; }
/* MEASURED on naruto-arena.site (2026-09-02).
   Arming a skill inserts ._mc_choice over EVERY character portrait:
     left:1 top:1 75x75 ; background-color: rgb(255,255,51) ;
     no border, no box-shadow, NO outer glow.
   Valid targets get visibility:visible, the rest visibility:hidden, and the
   overlay's OPACITY pulses (sampled 0.40 -> 0.59). That is the whole effect:
   a flat yellow wash blinking on the face. The halo we used to draw here was
   inferred, not measured, and is gone. */
/* v228: THEIR OWN NUMBERS, read out of their live stylesheet in a running
   match 2026-09-20:
     ._mc_choice, ._mc_notarget { background-color: rgb(255,255,51);
       position: absolute; left: 1px; top: 1px; width: 75px; height: 75px; }
     @keyframes choice-animation { 0% {opacity:.6} 50% {opacity:.3} 100% {opacity:.6} }
     animation: choice-animation 1.33s linear infinite
   Ours was .8s ease-in-out with a .28 trough — a faster, deeper blink than
   theirs, invented when the effect was inferred rather than measured. The
   colour was already right. Note their rule names BOTH the target wash and
   the caster marker, so the two share one box and one colour. */
@keyframes na-choice { 0% { opacity: .6; } 50% { opacity: .3; } 100% { opacity: .6; } }
/* v230: z-index 2. THE WASH WAS PAINTING UNDER THE FACE. `.b-portrait img`
   is `position: relative; z-index: 1`, which puts the picture on a raised
   stacking level; the `::after` was `z-index: auto`, so the yellow went on
   the portrait's background and the face covered every pixel of it. Measured
   in our own client: `elementFromPoint` at the centre of a targetable face
   returned the portrait, the wash computed 75x75 rgb(255,255,51) at
   1.33s linear — correct in every property and invisible on screen.
   That is "the available targets stay the same". */
.b-unit.targetable .b-portrait::after { content: ""; position: absolute; inset: 0;
  background: #ffff33; pointer-events: none; z-index: 2;
  animation: na-choice 1.33s linear infinite; }
.b-unit.targetable:hover .b-portrait::after { animation: none; opacity: .6; }
/* v226: THE LOADING MARK IS A BACKGROUND, NOT A ::before. v225 put it on
   `.b-portrait::before` and that BROKE THE TARGETING LIGHT: an element has
   exactly one ::before, `.b-unit.casting .b-portrait::before` is the yellow
   wash, and the two rules have the same specificity. The later one won
   per-PROPERTY, so the wash kept its colour but inherited `width: 32px` and
   `transform: translate(-50%,-50%)` from the loader — a 32px yellow chip in
   the middle of the face instead of a full-bleed wash.
   A background sits under the element's own content by definition, so the
   mark shows only where no picture has arrived and a loaded picture covers
   it completely. No pseudo, nothing to collide with. */
.b-portrait img, .b-portrait .sprite { position: relative; z-index: 1; }
/* v234: `.b-unit.casting` IS GONE, AND SO IS THE RULE IT CARRIED.
   `._mc_notarget` was written up here, and in REF_LIVE_2026-09-02.md, as
   "the casting character wears a static 0.5 yellow". READ 2026-09-20 in
   their ingame chunk, it is `state.targets.includes(side+idx)` — the SPLASH
   SET of the armed skill, which contains the caster only when the skill
   reaches the caster. The 2026-09-02 note came from one armed skill that
   happened to reach its own caster, and n=1 generalised is how this project
   has been wrong before. battle.js carries the decoded `setTargetProps`.
   A skill that cannot touch its own caster now leaves it unmarked, which is
   what was asked for on 2026-09-20; a self or area skill still lights its
   caster, through `.targetable`, which is the blinking `._mc_choice`. */
@media (prefers-reduced-motion: reduce) {
  .b-unit.targetable .b-portrait::after { animation: none; opacity: .5; }
}
.b-portrait:hover { box-shadow: var(--hover-lift); cursor: pointer; }

/* Health bar: 75x14 + 1px border, 2px gap below the face picture (border to
   border). Face box is 77 tall at top 23 -> bottom at 100; +2 gap => top 102. */
/* v201: the ground behind the fill is what MISSING health looks like, and it
   was near-black — so a hurt character read as a hole in the bar rather than
   as a bar that is part empty. White, asked for. The number over it is
   already black (.txt below), so it stays readable either way. */
.b-hp { position: absolute; top: 79px; width: 77px; height: 16px; background: #fff;
  border: 1px solid #101010; box-shadow: 0 1px 3px #0006; }
/* the health bar tracks its portrait, so it carries the same 28px inset */
.b-col.ally .b-hp { left: 32px; } .b-col.foe .b-hp { right: 28px; }
/* MEASURED: the reference fill is a FLAT colour, not a gradient —
   full health rgb(59,223,63), 55% rgb(255,204,0), low red. */
.b-hp .fill { position: absolute; left: 0; top: 0; bottom: 0; background: #3bdf3f; }
.b-hp .fill.mid { background: #ffcc00; }
/* v203: their own three, read out of setHealths 2026-09-19 */
.b-hp .fill.low { background: #ff0000; }
/* v203: their `._mc_health_text` is 12px, centred on a 74px box, #151311.
   v295: 11px — asked 2026-09-27 22:16 ("decrease the size of hp text by 1px") */
.b-hp .txt { position: relative; z-index: 2; font-size: 11px; font-weight: 400; color: #151311;
  text-align: center; line-height: 14px; }
.b-hp .def { position: absolute; top: -7px; right: -3px; background: #3f7fcf; color: #faf4e1;
  font-size: 9px; font-weight: bold; padding: 0 3px; border-radius: 2px; z-index: 3; }

/* Effect / status icons: 20x20 (no border), sitting NEXT TO the face picture
   with a 5px gap, and 2px above the skill bar. Skill bar top is 131 (see
   below), so the icon row bottom = 129, top = 109. */
.b-minis { position: absolute; top: 0; display: flex; gap: 3px; z-index: 6;
  pointer-events: none; }
.b-minis .mi { pointer-events: auto; }   /* keep the hover tooltips */
/* MEASURED: ally `.effects` starts at x=114 level with the portrait top
   (y=63/184/305); enemy `.effects1` is right-anchored with its right edge at
   x=653 and grows leftward. Icons are 22x22 on a 25px pitch on both sides. */
.b-col.ally .b-minis { left: 114px; }
/* v201: right 117 -> 114, asked for — the enemy row sat further from its
   portrait than the ally row does from its own (left: 114). Now both are 114
   from their own side, so the two sides are mirror images. */
.b-col.foe .b-minis { right: 114px; flex-direction: row-reverse; }
.b-minis .mi { width: 22px; height: 22px; background: #cfc4a4; overflow: hidden;
  border: 1px solid #000; box-sizing: border-box; box-shadow: 0 1px 2px #0007; }
.b-minis .mi img, .b-minis .mi .sprite { width: 100%; height: 100%; display: block; }
.b-minis .mi.fx { display: flex; align-items: center; justify-content: center; color: #faf4e1;
  font-size: 7px; font-weight: bold; }
.mi.fx.stun { background: #b02018; } .mi.fx.invuln { background: #2f6fb0; } .mi.fx.reduce { background: #2f7d4e; }
.mi.fx.dot { background: #7a2f9a; } .mi.fx.hot { background: #2f8a85; } .mi.fx.counter { background: #9a7320; }
.mi.fx.boost { background: #b0531e; } .mi.fx.weaken { background: #6b6048; }
/* A reaction normally renders as its source skill's icon, so this indigo is
   the FALLBACK branch — a rule whose skill has no icon. It was unstyled,
   which painted the cream .mi.fx text on the bare #cfc4a4 chip ground; the
   hue is deliberately away from dot's violet and invuln's blue. */
.mi.fx.rule { background: #4b3fa0; }
/* v287: THE STACK COUNT ON THE ICON. Their `.stack`, read off their
   stylesheet 2026-09-27: absolute, 22x22, centred, top 1px, colour #fff,
   and a 1px black edge made of four 1px text-shadows. The icon is its
   positioning parent. */
.b-minis .mi { position: relative; }
.b-minis .mi .stack { position: absolute; left: 0; top: 1px; width: 100%; height: 100%;
  /* v295: 13px, asked 2026-09-27 22:16 ("increase the size of stacking text by 1px") */
  text-align: center; color: #fff; font: bold 13px/20px Arial, sans-serif; pointer-events: none;
  text-shadow: 1px 1px 1px #000, -1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000; }

/* ---- red skill scrolls (allies) ----
   324x81 bar. It slides 27px UNDER the face picture on the left (the portrait
   paints on top, see z-index) via the left padding. Its top sits 13px below
   the health bar's bottom (hp bottom = 118 -> skill bar top = 131). */
/* Constant 381x79 bar at x=26 for BOTH turns — the reference does not
   resize it when the leading slot is hidden. */
/* MEASURED on naruto-arena.site (2026-09-02):
   ._mc_mask is a fixed 381x79 window at 103,87 with overflow:hidden.
   #mc_skill_section_i inside it is a fixed 381x79 strip carrying the bar art,
   the "?" slot and the four skills, with `transition: transform .4s`.
   On the enemy turn the strip gets translateX(-77px): the "?" slot leaves
   through the left edge, the skills land at 124/186/248/310 and the bar
   reads as 304px wide. The bar is NEVER resized. */
.b-lane { position: absolute; left: 103px; width: 381px; height: 79px; z-index: 5; }
.b-lane.s0 { top: 87px; } .b-lane.s1 { top: 208px; } .b-lane.s2 { top: 329px; }
/* the left parchment cap (._mc_skill_section_parchment, 25x88 @81,83) sits
   OUTSIDE the mask, so it never moves and it hides the strip's cut edge */
/* The rod at the left end of the bar. 25x88 — the box we drew by hand, and
   the exact size of the supplied art, so the picture drops straight in. With
   a link set the gradient, border and radius all come off (see
   .has-art-skillbar_scroll below); with none, the CSS rod stands as before. */
/* v201: left -22 -> -21, asked for — the rod sat one pixel too far left of
   the bar it caps. */
.b-lane::before { content: ""; position: absolute; left: -21px; top: -4px;
  width: 25px; height: 88px; border-radius: 4px;
  background: linear-gradient(#8d1206, #5c0a03); border: 1px solid #3c0501;
  box-sizing: border-box; }
.has-art-skillbar_scroll .b-lane::before {
  background: var(--asset-skillbar-scroll) center/25px 88px no-repeat;
  border: 0; border-radius: 0;
}
/* v236: THE MASK IS FIXED AGAIN, AND THE PICTURE DID NOT CHANGE.
   v230 gave the mask a width and animated it from 381 to 304 alongside the
   strip's translate, to answer "the bar gets shorter and longer". Its own
   comment recorded that nothing on the reference's lane changes size —
   `.b-mask` stays 381 and only `.b-scroll` translates (REF_LIVE_2026-09-02.md
   §3.1) — and then added the width anyway.

   MEASURED 2026-09-20, both ways, frame by frame off a CDP screencast at the
   compositor's rate, with a flat data-URI standing in for the bar art so the
   painted edge is unambiguous:

       with the width animation : 404 -> 327, 22 distinct widths
       transform only           : 404 -> 327, the SAME 22, same x on every frame

   Pixel-identical. `.b-mask` paints nothing — no background, no border — so
   the shortening a player sees is the STRIP'S OWN RIGHT EDGE sliding left
   behind a fixed clip, which is how theirs does it and how ours already did.
   The width was invisible.

   And it was not free. `width` is a LAYOUT property: every one of those ~24
   frames relaid out the lane and repainted everything in it, the fifteen
   image tiles included, where `transform` alone is a compositor move. That
   is the only remaining candidate for the "millisecond blink" reported
   2026-09-20 after v235 — and it is the one this container cannot show,
   because a data: URI repaints for nothing and imgur is unreachable here.
   Said plainly: the redundancy is measured, the blink is not. */
.b-mask { position: absolute; left: 0; top: 0; bottom: 0; width: 381px;
  overflow: hidden; }
.b-scroll { position: absolute; left: 0; top: 0; width: 381px; height: 79px;
  z-index: 5; box-sizing: border-box;
  display: flex; gap: 6px; align-items: center;
  transform: translateX(0); transition: transform .4s;
  /* paste a link for skillbar.png (381x79) and the strip becomes that image;
     with no link it falls back to the CSS bar it has always drawn. */
  background: var(--asset-skillbar, linear-gradient(#e6382a, #b31408)) center/100% 100% no-repeat;
  border: 2px solid #6e0c05; border-radius: 5px;
  box-shadow: inset 0 0 0 2px #f0a08c55, 0 2px 6px #0007; }
.b-lane.busy .b-scroll { transform: translateX(-77px); }
@media (prefers-reduced-motion: reduce) { .b-scroll { transition: none; } }
/* my turn: 400px wide, the "?" slot at 44px from the bar's left edge, then a
   20px gap to the first skill. Enemy turn: 324px wide (left edge FIXED so the
   bar shortens from the RIGHT), no "?", first skill at 43px. */
/* "?" slot starts 22px into the bar (x=48); the first skill starts at
   x=124, i.e. 20px past the slot. On the enemy turn the slot is hidden and
   the skills keep their positions, so the row never shifts. */
/* The bar is a different LENGTH on each turn. On your turn it carries the
   leading "?" slot (56px + a 20px gap) and runs 103..484. On the enemy turn
   that slot is gone, so the bar keeps its LEFT edge and loses those 76px
   from the right, running 103..408 — and the skills slide left into the
   space the slot vacated, starting at 125 instead of 201. */
/* MEASURED in a live battle (2026-09-03), stage-relative: the reference puts
   the armed-skill slot at 125,98 and the four buttons at 201/263/325/387,98.
   The lane and mask around them (103,87 381x79) already matched exactly, so
   the whole row was landing 2px right and 1px low INSIDE the lane: 22px of
   left padding here plus the lane's own 2px border made 24, and align-items
   centred the 56px tiles 1px low in the 79px lane. */
/* Centred, not nudged. This used to be `align-items: flex-start` plus a
   `margin-top: 9px`, which lands the 56px tile 11px below the bar's top edge
   ONLY while the bar still has its 2px CSS border — 9 + 2. The moment real
   bar art is set that border goes to 0 (.has-art-skillbar below) and the
   same rule put every skill 2px high inside its own bar. Centring is
   independent of the border: (79-56)/2 = 11.5 with the border gone, and
   (75-56)/2 + 2 = 11.5 with it on. The reference measures 11 (bar 87, skills
   98), which 11.5 rounds into. */
.b-scroll { padding: 0 8px 0 20px; align-items: center; }
/* Q10 anchoring: bar top = face top + 23. Face tops (screen) = 80/202/324
   (Q8: first row 80px from top margin, 45px face-bottom to next face-top,
   pitch = 77+45 = 122). */
/* the strip's own right-hand cap travels with it and is clipped by the mask,
   which is what makes the bar look like it shortens from the right */
.b-scroll::after { content: ""; position: absolute; top: -6px; bottom: -6px; right: 0;
  width: 10px; background: linear-gradient(#8d1206, #5c0a03); border: 1px solid #3c0501; border-radius: 4px; }
/* skillbar.png is the WHOLE strip — frame, both end caps and the knob are
   painted into the art. var(--asset-skillbar) only replaced the fill, so our
   2px border, the left cap, the right cap and the knob all stayed on top of
   it and the bar read as two bars stacked. assetlinks.js sets .has-art-skillbar
   on <html> when the link is filled; everything we drew ourselves goes then,
   and the 381x79 art gets the full 381x79 box (border-box would have shrunk
   the picture to 377x75 and blurred it). */
/* border AND the inset highlight + drop shadow: the box-shadow was left
   behind here, so a 2px pink inner line and an outer shadow still sat on top
   of the artwork. */
/* border-COLOR, not border:0 — see the v227 block at the end of this file. */
.has-art-skillbar .b-scroll { border-color: transparent; border-radius: 0; box-shadow: none; }
/* The left rod is NO LONGER hidden here. It used to be, because skillbar.png
   was described as carrying both end caps — but the rod now has art of its
   own (skillbar_scroll.png, 25x88), and this rule sits later in the file at
   equal specificity, so it would have won and hidden that art outright.
   The right cap and the knob stay hidden: those really are painted into
   skillbar.png. */
.has-art-skillbar .b-scroll::after,
.has-art-skillbar .b-scroll .knob { display: none; }
/* …and with no rod art of its own, the CSS rod is still hidden when the bar
   art is on, exactly as before. */
/* v228: PUT BACK, and the v227 line that removed it was wrong.
   MEASURED in a live match on their side 2026-09-20:
   `._mc_skill_section_parchment` is [81,83,25,88] against a skill bar
   `._mc_skill_section` at [103,87,381,79]. The rod is a SEPARATE 25x88
   picture beside the bar, not part of it — v227 reasoned that a supplied
   381x79 strip "is the whole bar" and hid the rod unconditionally, which
   deleted furniture the reference has. The conditional it replaced was
   right: drop only OUR DRAWN gradient once real scroll art exists.
   Their x is 81, ours was -21 against a lane at 103 — one pixel out. */
html:not(.has-art-skillbar_scroll).has-art-skillbar .b-lane::before { display: none; }
/* the knob travels with the strip now that the bar is masked; at -16px it
   used to pop out into the middle of the board on the enemy turn */
.b-scroll .knob { position: absolute; right: 3px; top: 50%; transform: translateY(-50%);
  width: 9px; height: 14px; background: linear-gradient(#a8968a, #6d5c50);
  border: 1px solid #3a2c22; border-radius: 2px; }
/* reference: the bar itself is untouched on the enemy turn — every skill
   image simply carries `.disabled` (opacity .3) */
/* the enemy turn is the same 50% black overlay as an unusable skill (see the
   ::after rule below) — no opacity or filter of its own any more, so the two
   states are identical rather than two different washes. `animation: none`
   still has to cancel s-fadein, which parks these at opacity 1. */
.b-lane.busy .b-skill img, .b-lane.busy .b-skill .sprite { animation: none; }
.b-lane.busy .b-skill { cursor: default; }
.b-scroll.dimmed { filter: sepia(.65) brightness(.85); }

/* 1px black border and no shadow. The state rings used to be box-shadows
   painted outside the tile; the colour of the border alone carries the state
   now, so a selected or queued skill still reads without any glow. */
.b-skill { width: 56px; height: 56px; border: 1px solid #000; background: #e7dab8;

  position: relative; cursor: pointer; flex: none; box-sizing: border-box; }
.b-skill img, .b-skill .sprite { width: 100%; height: 100%; display: block; }
/* v228: the supplied "?" picture replaces our drawn box. `.has-art-queue_slot`
   hides the text glyph too, so the art is not drawn under a 26px character. */
.has-art-queue_slot .b-skill.q {
  background: var(--asset-queue-slot) center / 100% 100% no-repeat;
  color: transparent; }
.b-skill.q { background: #4a4a4a; margin-right: 14px; /* 6 flex gap + 14 = 20 */ display: flex; align-items: center; justify-content: center;
  color: #e6e6e6; font-weight: bold; font-size: 26px; cursor: default; }
/* A skill you cannot pay for reads as dead BEFORE you click it: heavily
   desaturated and dimmed, so the energy pool's constraint is legible at a
   glance across all twelve tiles. */
/* A skill you cannot use is covered by a flat 50% black overlay — nothing
   else. This was a mix of opacity, grayscale and brightness that made the art
   read as washed-out rather than switched off, and the enemy-turn state used
   different numbers again, so the two never matched. One overlay, one rule,
   identical in both cases. */
/* v203: A DISABLED SKILL IS THE ICON AT 30% OPACITY, and nothing else.
   MEASURED 2026-09-19, their whole rule for it:  .disabled { opacity: .3 }
   — applied to `._mc_skillbutton_image`, the icon, not to the tile. Ours
   laid a 50% black sheet over the whole tile, which reads as switched off
   rather than faded and makes the cooldown number sit on a dark card their
   game does not have. */
.b-skill.unaffordable .sprite, .b-skill.cooldown .sprite,
.b-lane.busy .b-skill .sprite,
.b-skill.unaffordable img, .b-skill.cooldown img, .b-lane.busy .b-skill img {
  opacity: .3; }
.b-skill.unaffordable img, .b-skill.cooldown img,
.b-skill.unaffordable .sprite, .b-skill.cooldown .sprite { animation: none; }
.b-skill.unaffordable, .b-skill.cooldown { cursor: pointer; }   /* still clickable for info */
/* the armed skill keeps pulsing yellow — slowly — until a target is picked
   or it is clicked again */
/* The armed skill LIGHTS UP rather than just gaining an outline: a glow
   overlay sits on the tile and pulses, which reads instantly across the
   board the way the reference client's does. */
.b-skill.selected { border-color: #ffd400; }
/* No glow on the armed tile: the reference puts all the emphasis on the
   TARGETS instead, so a competing glow here would fight it. A plain border
   is enough to show which skill is held. */
@keyframes na-armed { 0%, 100% { box-shadow: 0 0 0 3px #ffd400ee, 0 0 14px 2px #ffd40088; }
  50% { box-shadow: 0 0 0 3px #ffd40055, 0 0 8px 1px #ffd40033; } }

/* the "?" slot once a target is chosen: shows the queued skill; click it to
   cancel the action and pick another skill */
/* v233: no green ring and no glow. The committed tile lands in the "?" slot
   and that is the whole signal — theirs draws the slot's own picture with
   the skill icon in it and nothing around it. Asked for directly on
   2026-09-20: no extra effects on a skill you have picked. */
.b-skill.q.filled { background: #e7dab8; cursor: pointer; font-size: 0; }
.b-skill.q.filled:hover { box-shadow: var(--hover-lift); }
/* the gap a skill leaves behind once it moves up to the leading slot:
   keeps the row's spacing but reads as clearly empty */
/* MEASURED: the reference does not draw anything in the hole. The committed
   tile is simply translated onto the "?" slot and its old place shows bare
   bar. Keep the box for spacing, paint nothing. */
/* v202: a COMMITTED skill still sits in the bar (its tile is what you click
   to get back to it). It reads as spoken-for — dimmed, with the same yellow
   edge the armed one wears — rather than as a hole in the bar. */
.b-skill.queued { opacity: .55; border-color: #e8d84a; }
.b-skill.vacated { background: none; border: 1px solid transparent; box-shadow: none;
  cursor: default; pointer-events: none; }
.b-skill.denied { animation: na-deny .35s; }
@keyframes na-deny { 50% { box-shadow: 0 0 0 3px #e0241c; border-color: #e0241c; } }
/* v233: the green edge is gone. It was an undocumented second
   `.b-skill.queued` rule sitting four lines under the documented one and
   quietly overriding it with `#37e037` — no measurement, no comment, and it
   contradicted the note above that says a committed tile wears "the same
   yellow edge the armed one wears". Asked for directly on 2026-09-20: no
   extra effect on a skill you have picked. The dimming and the yellow edge
   from the rule above stand; nothing else is painted. */
/* the reference's own skill hover, exactly: a soft lift and nothing else. */
.b-skill:hover { box-shadow: var(--hover-lift); }
/* Cooldown is a LARGE centred numeral over a dimmed tile — legible at a
   glance from across the board, rather than a small corner badge. */
/* v203: THEIR COOLDOWN NUMBER. `.mc_currentcooldown { font-family: Pedra;
   font-size: 40px; color: #000; left: 1px; top: 9px; width: 54px;
   text-align: center }` over an icon already at .3 opacity — no dark card,
   no white ink, no shadow. Pedra is their display face and is a font file,
   so ours stays in --gm-font; the SIZE, the COLOUR and the absence of a
   backdrop are theirs. */
.b-skill .cd { position: absolute; left: 1px; top: 9px; width: 54px; height: 54px;
  font-size: 40px; font-weight: bold; line-height: 1; color: #000;
  text-align: center; letter-spacing: -1px; z-index: 3; }

/* "Click on a skill" block inside the bottom info panel (character view),
   reused on the skill view to swap skills / go back to the character */
.b-info .inspect { flex: none; align-self: center; width: 200px; }
.b-info .inspect .cap { background: #101010; color: #faf4e1; font-weight: bold; font-size: 11px;
  text-align: center; padding: 3px 0; letter-spacing: .3px; text-transform: uppercase; }
.b-info .inspect .cap.link { cursor: pointer; }
.b-info .inspect .cap.link:hover { opacity: .7; }
.b-info .inspect .row { display: flex; gap: 4px; margin-top: 4px; justify-content: center; }
.b-info .inspect .row .b-skill { width: 42px; height: 42px; }
.b-info .inspect .row .b-skill.sel { border-color: var(--ink-sel); border-width: 2px; }

/* ---- bottom area ---- */
.b-bottom { position: absolute; left: 0; right: 0; bottom: 0; height: 162px; z-index: 10; }
/* MEASURED off the reference battle capture: the standing figure occupies
   x 124-271, y 416-560 — 147x144, flush to the stage bottom. A 150x150 box
   with object-fit:contain lands square art on that footprint. */
.b-announcer { position: absolute; left: 125px; bottom: 0; width: 150px; height: 150px;
  object-fit: contain; object-position: bottom; pointer-events: none; z-index: 12; }

/* Q11 staircase, all measured from the left margin, stacked bottom-to-top
   with 2px gaps: volume strip (bottom) left 7 / bottom 0; OPEN CHAT left 30 /
   bottom 29 (27 vol + 2); SURRENDER left 54 / bottom 77 (29 + 46 + 2). */
.bb-btns { position: absolute; inset: 0; z-index: 11; pointer-events: none; }
.bb-btns > * { pointer-events: auto; }
/* MEASURED: the reference's bottom buttons are 13px/400 #151311, letter-spacing
   normal. This block declared font-size, font-weight and color TWICE — the
   later pair won, but letter-spacing was only set in the stale half, so a
   0.4px track survived the earlier cleanup. Single values now. */
/* 182x46, not 200x62: 62 was the reference's DOM rect (its three rects
   overlap, 426+62 > 468, so it was never the drawn plaque) and the art is
   182x46. Sizing the box to the art keeps it pixel-for-pixel sharp. The
   staircase tops stay on the measured 426/468/509. */
.bb-btn { position: absolute; display: block; width: 182px; height: 46px; line-height: 44px;
  padding: 0 10px 0 24px; background: linear-gradient(#f6efdc, #e2d3a8);
  border: 1px solid #6e5a2a; cursor: pointer; white-space: nowrap; box-sizing: border-box;
  box-shadow: 0 2px 4px #0007, inset 0 1px 0 #fff8;
  font-size: 13px; font-weight: 400; letter-spacing: normal; color: #000; }
/* reference staircase: SURRENDER (53,426), OPEN CHAT (30,468),
   volume (7,509) — each 200x62, stepping left and down */
.bb-btn.w1 { left: 53px; top: 32px; bottom: auto; }
.bb-btn.w2 { left: 30px; top: 74px; bottom: auto; }
.bb-btn::before { content: ""; position: absolute; left: -10px; top: 5px; width: 20px; height: 36px;
  background: linear-gradient(#4a9a2e, #2f6a1c); border: 2px solid #1e4a10; border-radius: 8px; }
/* no colour change on hover: the label is baked into btn_surrender.png /
   btn_openchat.png, so recolouring the (transparent) text did nothing but
   turn the CSS fallback red. */
.bb-btn:hover { filter: brightness(1.06); }
/* THE THIRD STEP (restored v181). v179 stripped the plate off the volume on
   the strength of the SELECTION screen's row, which genuinely floats on
   nothing. The battle bar is a different element and the reference's own
   markup says so, read 2026-09-13:

     <div class="_mc_item2"><div class="_battle-volume-controller"> … </div></div>

   ._mc_item2 carries the SAME 182x46 plate as ._mc_item0 / ._mc_item1, and
   the three of them step left and down together:

     ._mc_item0  SURRENDER   left 53  top 11     <-> our .bb-btn.w1  top 32
     ._mc_item1  OPEN CHAT   left 30  top 53     <-> our .bb-btn.w2  top 74
     ._mc_item2  volume      left  7  top 94     <-> this            top 115

   (their container starts 21px above ours, which is the constant between the
   two columns.) The controls sit inside the plate at 29,18 — that is
   ._battle-volume-controller's own rule. So the overlap v179 was fixing was
   real, but the fix was to move the plate down to 115, not to delete it. */
.bb-vol { position: absolute; left: 7px; top: 115px; bottom: auto;
  width: 182px; height: 46px; padding: 0; border: 0; box-shadow: none;
  background: none; background-repeat: no-repeat; background-size: 182px 46px; }
.bb-vol .volrow { position: absolute; left: 29px; top: 18px; }
/* the .volrow painting lives with .s-vol at the end of the file — one
   design, two screens, one --vol-ink between them. */

/* info panel: skill details / character bio / player profile */
/* MEASURED: the reference's info panel is at 247,426 (523x134). .b-bottom
   starts at y=394, so top:32 puts ours on the same line. */
/* Anchored to the BOTTOM, not the top: the reference's panel is at 247,426
   and is 134 tall, so its bottom edge is exactly the 560px stage bottom.
   Pinning the top instead made it land at 426 online but 430 offline, because
   .b-bottom starts at 394 in one path and 398 in the other. */
.b-info { position: absolute; left: 247px; top: auto; bottom: 0; width: 523px; height: 134px;
  background: linear-gradient(#f6eed6, #ecdfbc); border: 3px solid #8f1408; border-radius: 4px;
  box-shadow: 0 2px 8px #0007; padding: 8px 12px 8px 16px; overflow: hidden; }
.b-info::before { content: ""; position: absolute; left: -12px; top: -8px; bottom: -8px; width: 18px;
  background: linear-gradient(90deg, #2f6a1c, #4a9a2e 55%, #2f6a1c); border: 2px solid #1e4a10;
  border-radius: 8px; }
/* The supplied 523x134 panel. The green rod is PAINTED INTO the art's left
   edge, so our own ::before rod comes off with it.

   The 3px border is kept but made transparent rather than removed. .b-info is
   border-box, so deleting the border would move its 517x128 content box out
   by 3px and drag the portrait and the text with it; keeping the box and
   dropping only the paint means the artwork replaces the frame without
   moving a single thing inside. The default background-clip is border-box,
   so the picture fills all 523x134 including under that transparent edge. */
.has-art-battle_info .b-info {
  background: var(--asset-battle-info) center/523px 134px no-repeat;
  border-color: transparent; border-radius: 0; box-shadow: none;
}
.has-art-battle_info .b-info::before { display: none; }
/* .pic is absolutely positioned against .b-info's PADDING box (x=248), while
   .wrap's padding runs from .b-info's CONTENT box (x=264). Those two origins
   are 16px apart, so a 31px padding here put the text at x=295 — 61px INSIDE
   the 279..356 portrait, and the description ran straight under it.
   The portrait ends at 356; 95px puts the text at 359, a 3px gap. */
/* MEASURED in a live battle, panel-relative (panel at 247,426 523x134):
     avatar    31,28  77x77      name  117,25
     desc     117,40  400x59     classes 117,95      cooldown 418,95
   So the text column starts 117px into the panel and the description is 400
   wide — the full remaining width, not a narrow column beside an inspect
   panel. */
.b-info .wrap { display: flex; align-items: flex-start; gap: 0; height: 100%;
  /* 98 renders the text column at border-box 117, which is where their
     `info_name` [364,451] and `info_desc` [364,466] sit against
     `_mc_infomenu` [247,426]. 104 was tried first and rendered 123. */
  padding: 14px 0 0 98px; box-sizing: border-box; }
/* The PLAYER profile view only — the skill view keeps the measured 98/14 above.
   left 92 puts the text 5px past the 75px avatar (which ends at 353); top 11
   drops the name's first PAINTED row onto the avatar's top edge (450) — the
   15px title carries ~6px of leading and ascent above its caps, so the box
   sits that much higher than the ink. Verified by scanning pixels. */
.b-info.has-team .wrap { padding-left: 92px; padding-top: 11px; }
/* the flex gap used to sit between .av-media and .body, so the player view
   started 10px further right than the skill view; the spacing now lives on
   .inspect, which is the only child that actually needs it */
.b-info .inspect { margin-left: 10px; }
/* v228: MEASURED live, `info_avatar` [278,454] against `_mc_infomenu`
   [247,426] = border-box 31,28. Declared 28,25 because an absolutely
   positioned child of `.b-info` resolves against its PADDING box and the
   panel has a 3px border. The original numbers were right; a first pass at
   this moved them 3px and the render caught it. */
.b-info .pic { position: absolute; left: 28px; top: 25px; width: 77px; height: 77px;

  border: 1px solid #101010; background-color: #cfc4a4; flex: none; box-sizing: border-box; }
/* the player avatar in the description box: 75x75 with the same solid 1px
   black frame the other two avatars carry, so all three match */
.b-info .pic.av-pic { width: 75px; height: 75px; border: 1px solid #000; }
.b-info .pic.av-pic img { width: 100%; height: 100%; display: block; object-fit: cover; }
.b-info .av-media { display: flex; gap: 6px; flex: none; }
.b-info .pic img, .b-info .pic .sprite { width: 100%; height: 100%; display: block; }
.b-info .body { flex: 1; min-width: 0; position: relative; height: 100%; }
/* title at a FIXED position (top of the body), never moving with description
   length; red like everywhere else */
/* v225: moved for the 6px stroke clearance rule — see ADMIN.md v225. */
.b-info .ttl { color: #cb0400; font-weight: 700; font-size: 13px; letter-spacing: normal;
  /* v228: 2px up from v226's 4, landing on their measured 25 — which is
     exactly the "move skill names up 2px" that was asked for. */
  text-transform: uppercase; position: absolute; top: 0; left: 0; right: 0; }
/* description: black body, starts below the fixed title, scrolls if too long */
/* Full width from just past the portrait to the panel's right edge, and NO
   scrollbar — the text must be readable in one go. */
/* Stops 13px short of the bottom so a long description can never run into
   the CLASSES line (they used to overlap at 5+ lines), and scrolls rather
   than being silently clipped — the reference's ._top_item_desc does the same. */
/* v192: top 15 -> 17. Two more pixels of air under the skill/character name,
   asked for — at 10px body copy (v191) the description's first line sat
   almost against the 13px red title and the two read as one block.
   v294 NOTE: the rule reads 15 again, and has since before v234; the version
   that moved it back is not written down anywhere (searched ADMIN.md and the
   changelog 2026-09-27). Left at 15 because it is what has been live and
   nobody has asked for 17 back — but it is recorded here as unexplained
   rather than as a decision. Same for `.classes`'s `+ 3px` below. */
.b-info .desc { position: absolute; top: 15px; left: 0; right: 0; bottom: 13px;
  /* MEASURED: the reference's description is 12px, not 10px.
     v191: ours is 10px, two below theirs, ASKED FOR and a deliberate
     departure — v189 gave every description generated mechanical clauses on
     top of its prose, and ours run longer than theirs as a result. At 12px
     the longest were four lines with nothing to spare; at 10px the same text
     is three and a bit. A departure from a measured number, said out loud
     rather than quietly re-measured. */
  font-size: 10px; font-weight: 400; line-height: 1.35; color: #000; text-transform: uppercase;
  overflow: auto; padding-right: 0; scrollbar-width: thin; }
.b-info .desc::-webkit-scrollbar { width: 5px; }
.b-info .desc::-webkit-scrollbar-thumb { background: #b9a878; border-radius: 3px; }
/* MEASURED: the reference tints description phrases by MEANING, with five
   colours inside the 12px body copy. Ours had only two (an orange and a
   blue), so damage, healing, cost and skill references all collapsed into
   the same highlight. */
.b-info .desc .hl { color: #c73001; }              /* damage */
.b-info .desc .hl.heal { color: #00b14a; }         /* healing / gain */
.b-info .desc .hl.shield { color: #089f78; }       /* defence, absorption */
.b-info .desc .hl.cost { color: #7b0089; }         /* energy cost */
.b-info .desc .hl.ref { color: #db004f; }          /* another skill by name */
.b-info .desc .hl.blue { color: #089f78; }         /* legacy alias */
/* MEASURED: the reference sets the CLASSES and COOLDOWN lines at 10px in the
   same warm grey (#7d6f4c). Ours had them at 9px in two different colours —
   #6b5a3a for classes and near-black for the cooldown — so they read as
   unrelated bits of text rather than one row of metadata. */
/* v192: bottom 14 -> 13, the same baseline as .cdline below, so CLASSES and
   COOLDOWN sit on ONE line across the panel instead of one pixel apart —
   which at 10px reads as a misalignment rather than as a row. Size 10 -> 8,
   asked for, because the class list is the longest string in the panel and
   was competing with the description above it. */
/* THE TWO ARE NOT IN THE SAME BOX, which is why they never lined up.
   `.classes` lives inside `.body` -> `.wrap`, and `.wrap` is `height:100%` of
   the panel's CONTENT box; `.cdline` is a direct child of `.b-info` and
   resolves against its PADDING box. The panel's own `padding-bottom: 8px`
   sits between the two, so an identical `bottom: 13px` put them 8px apart.
   Written as the subtraction rather than as "5px", because 8px is the
   panel's padding and this has to follow it if that ever changes. */
.b-info .classes { position: absolute; bottom: calc(13px - 8px + 3px); left: 0; right: 8px;
  font-size: 8px; line-height: 12px; color: #000; font-weight: 400;
  text-transform: uppercase; letter-spacing: normal; }
/* ENERGY cost, top-right corner (like the reference skill card) */
/* v192: 10 -> 8 with the classes line, and the cost squares 11 -> 9 so they
   still sit level with their own label rather than towering over it. */
.b-info .corner { position: absolute; top: 22px; left: 415px; display: flex; gap: 4px;
  align-items: center; font-size: 8px; font-weight: 400; color: #000; }
.b-info .corner .ck-sq { width: 9px; height: 9px; border: 1px solid #000; }
/* v182: up 7px, asked for — bottom 6 -> 13. The description box under it is
   full-width (.desc has right:0) and ends at bottom:13, so at 6 the COOLDOWN
   line sat BELOW the text box and the last wrapped line of a long description
   ran straight through it. 13 puts it on the description's own bottom edge. */
/* v192: an explicit line box, matching .classes above. Both are pinned at
   bottom:13, but two different font sizes give two different line heights and
   therefore two different BASELINES — the text sat visibly stepped even
   though the boxes agreed. One line-height on both is what puts the two
   strings on one line. */
 /* v201: 10px -> 8px, the same size as .classes, asked for. The line-height
   stays 12px on both, which is what keeps the two baselines together — see
   the v192 note above; changing the size alone does not move the baseline,
   and that was MEASURED after the change rather than assumed. */
.b-info .cdline { position: absolute; bottom: 16px; left: 415px; right: auto; font-size: 8px;
  line-height: 12px; font-weight: 400; color: #000; text-transform: uppercase; }
/* player-profile lines (level/clan/rank etc.): 8px, black 151311, 6px row gap */
/* .ttl is absolutely positioned at the top of .body, so the profile lines
   have to start below it or the name prints on top of the rank line. */
.b-info .lines { margin-top: 21px; font-size: 11px; font-weight: bold; line-height: 1; color: #000; }
.b-info .lines > * { margin-bottom: 2px; }
.b-info .lines > *:last-child { margin-bottom: 0; }
/* the rank row is the same size as every other profile row — it used to be
   held 2px smaller, which read as a mistake next to CREW / LEVEL / RATIO. */
/* team view: 21px from the right, 10px gap between faces, vertically centered */
/* team faces: 56x56 with a 66px pitch, matching the reference row */
.b-info .team { position: absolute; left: 314px; top: 36px; display: flex; gap: 10px; }
/* the description must not run under the team faces */
.b-info.has-team .body { padding-right: 220px; box-sizing: border-box; }
.b-info .team img, .b-info .team .sprite { width: 56px !important; height: 56px !important; border: 1px solid #101010;
  background-color: #cfc4a4; cursor: pointer; }
.b-info .team img:hover, .b-info .team .sprite:hover { box-shadow: var(--hover-lift); }

/* ---- modals : exchange chakra / chat / surrender (scroll dialogs) ---- */
.b-dim { position: absolute; inset: 0; z-index: 20; background: #0003; display: none; }
.b-dim.show { display: block; }
/* MEASURED in a live match (2026-09-03): the reference's end-turn dialog is
   391x290 at stage 203,101 — ours was 470x312 at 148,130, noticeably wider and
   lower. Dialog-relative, its parts sit at:
     queue bar   77,162  280x57
     queue tile  85,172  36x36   (ONE tile per action)
     OK         106,220  90x27
     CANCEL     236,220  90x27  */
/* v226: CENTRED ON THE STAGE, not on their 203,101. `.screen` is 770x560
   with a 2px border, so an absolutely-positioned child resolves against the
   766x556 PADDING box — the calc says "centre me" without a literal, so it
   keeps holding for the 560-wide chat box too (which overrides width below
   and gets its own centring with it). */
.b-dialog { position: absolute; left: calc((100% - 391px) / 2);
  top: calc((100% - 290px) / 2); z-index: 21;
  width: 391px; min-height: 290px; background: linear-gradient(#f9f1dc, #f1e5c4);
  border: 3px solid #8f1408; border-radius: 3px; box-shadow: 0 6px 24px #000a;
  padding: 22px 24px 18px 80px; display: none; box-sizing: border-box; }
.b-dialog.show { display: block; }

/* v223: EACH POPUP DRAWS ITS OWN PICTURE. Their panel is one imgur
   background — measured, and written down in the v203 block below — and
   ours is the gradient above standing in for it. Four keys, all empty, so
   this changes nothing until a picture exists; sizes are in assetlinks.js
   beside them. `.b-dialog` is border-box, so each is the WHOLE 391x290 (or
   560x290) panel, its 3px frame included.

   And when art IS pasted in, the drawn border, radius and shadow get out of
   its way — the same rule every other .has-art-* surface follows, because
   a supplied panel already contains its own frame and two frames is worse
   than none. */
/* EVERY ONE CARRIES THE GRADIENT AS ITS FALLBACK, and that is not tidiness.
   `background-image: var(--asset-bg-endturn)` with the variable unset is
   INVALID AT COMPUTED-VALUE TIME, so the property takes its INITIAL value
   — `none` — rather than the gradient the base rule cascaded. Measured on
   the live page: base `linear-gradient(...)`, with the bare var `none`,
   with the fallback `linear-gradient(...)`. All four popups would have
   shipped as transparent boxes with a border round them.
   Caught by test_v193.js's "an unlinked asset still has something to draw"
   ceiling going 49 -> 53, which is exactly the defect it was written for. */
.b-dialog.endturn  { background-image: var(--asset-bg-endturn,   linear-gradient(#f9f1dc, #f1e5c4)); }
.b-dialog.exchange { background-image: var(--asset-bg-exchange,  linear-gradient(#f9f1dc, #f1e5c4)); }
.b-dialog.chat     { background-image: var(--asset-bg-chat,      linear-gradient(#f9f1dc, #f1e5c4)); }
.b-dialog.sur      { background-image: var(--asset-bg-surrender, linear-gradient(#f9f1dc, #f1e5c4)); }
.has-art-bg_endturn  .b-dialog.endturn,
.has-art-bg_exchange .b-dialog.exchange,
.has-art-bg_chat     .b-dialog.chat,
.has-art-bg_surrender .b-dialog.sur {
  background-size: 100% 100%; background-repeat: no-repeat;
  border-color: transparent; box-shadow: none; }

/* v223: the rod, under the rule set on 2026-09-19 for every scroll in the
   game — 20px taller than the surface it sits against, its RIGHT edge 10px
   inside that surface's left edge, and NO width, so the picture's own
   proportions survive.

   It was `width: 46px; height: 239px` with `img { width:100%; height:100% }`,
   which STRETCHED the art to the reference's 46:239 whatever it was drawn
   at: measured 2026-09-19, the rendered aspect was 0.1925 against the
   file's own 0.3999. That is the same squash v222 removed from the
   selection screen, still here.

   WORTH KNOWING BEFORE YOU DRAW ONE: the reference's dialog has NO rod. The
   v203 measurement lists every part of their 391x290 panel and there is no
   scroll among them — ours is our own decoration. If it should go, this
   block and the two `el("div", {class:"rod"})` lines in battle.js are the
   whole of it.

   THE 3px CORRECTION. An absolutely positioned child of `.b-dialog`
   resolves against its PADDING box, inside the 3px border — the same
   correction every number in the v203 block carries ("theirs minus 3").
   So 10px against the VISIBLE panel is 13 and 7 here, and writing the
   subtraction out is what stops the next edit losing it:
     top   -(10 + 3) = -13   -> 10px of rod above the border box
     right 100% - (10 - 3)   -> the right edge 10px inside the left border
   Measured before this correction: overlap 13, overhang 7. Measured
   after: 10 and 10. */
/* v226: moved 10px RIGHT (asked 2026-09-19), so the overlap is 20 rather
   than 10. Still written as "minus the 3px border", because an absolute
   child of .b-dialog resolves against its padding box.
   v229: the further 10px asked for on 2026-09-20 is the SURRENDER box only —
   it was briefly applied to this shared rule, which carried the roundup, the
   exchange and the chat along with it. Scoped below instead. */
.b-dialog.sur .rod { right: calc(100% - (30px - 3px)); }
.b-dialog .rod { position: absolute; top: calc(-5px - 3px); height: calc(290px + 10px);
  width: auto; right: calc(100% - (20px - 3px)); left: auto; transform: none;
  filter: drop-shadow(2px 3px 4px #0006); pointer-events: none; }
.b-dialog .rod img { height: 100%; width: auto; display: block; }
.b-dialog h3 { margin: 0 0 14px; font-size: 14px; letter-spacing: .4px; color: #101010;
  text-align: center; text-transform: uppercase; }
.b-dialog h3 .num { color: #c01818; }
.b-dialog h3.sub { margin: 4px 0 10px; font-size: 12px; color: #3a2a10; }

/* End-turn skill queue, in casting order.
   MEASURED: the reference draws ONE 36x36 tile per queued action. Ours drew a
   PAIR of 44x44 cells — the caster's face next to the skill icon — which is
   what made the swap bar look like every icon appeared twice, and made the bar
   twice as wide as the reference's. One tile, the skill icon, 36x36. */
/* The reference anchors the queue bar LOW in the dialog — 162px down, sitting
   directly above the buttons — with the chakra picker filling the space above
   it. Ours floated it near the top under a title, so the two dialogs read
   completely differently even at the same outer size. Both rows are pinned to
   the measured coordinates; the dialog is a fixed 391x290 so this is exact. */
/* minus the dialog's own 3px border: absolutely positioned children are
   placed against the padding box, not the border box. */
/* v191: THE QUEUE BAR STOPS EATING CLICKS.
   MEASURED 2026-09-14 in the SKILL QUEUE dialog: the Devil Fruit row's "+"
   sits at y 154-172 and this box is 162-219 over the top of it, so
   elementFromPoint on that button answered `q-row`. The fourth energy — and
   only the fourth — could not be clicked, which is exactly what was
   reported.

   Two changes, because one alone would be luck:
     * the BOX is transparent to the pointer and only the TILES take it back.
       It is a 280x57 area that is mostly empty, and empty space should not
       be a click target. Drag-to-reorder is on the tiles, so it is untouched.
     * the picker above it is tightened (below) so the two no longer overlap
       at all. The first fix would have hidden the second one's absence. */
/* v204: THE TILES SIT ON A 44px PITCH FROM x=8, not on a centred flex row.
   MEASURED 2026-09-19: their `_mc_queue_item` is absolutely positioned at
   `left: 44*n + 8` with `margin-top: 10` inside a 280x57 `container_queue`
   that scrolls horizontally, and carries `transition: transform .3s ease`
   (their `.queue_transition`) so the other tiles slide out of the way while
   one is dragged. Ours was `display:flex; gap:10px; justify-content:center`,
   which is a 46px pitch that also re-centres every time the queue changes
   length — so no two queue lengths put a tile in the same place. */
.b-dialog .q-row { position: absolute; left: 74px; top: 159px; width: 280px; height: 57px;
  margin: 0; overflow-x: auto; overflow-y: hidden; pointer-events: none;
  touch-action: none; }
.b-dialog .q-pair { position: absolute; top: 10px; width: 36px; height: 36px;
  cursor: grab; pointer-events: auto; transition: transform .3s ease; }
/* theirs takes the ORIGIN tile to opacity 0 while a ghost follows the cursor
   (`selectedItem.originIndex === n ? "0" : ...`) — not to .4 in place. */
.b-dialog .q-pair.dragging { opacity: 0; transition: none; }
/* v344: a CARRIED entry — last turn's damage or healing over time, paid at
   its place in the queue — at theirs' 0.7 (`t.new ? "1" : "0.7"`, read off
   their ingame chunk 2026-10-09). After .dragging so a lifted one still goes to 0. */
.b-dialog .q-pair.carried { opacity: .7; }
.b-dialog .q-pair.carried.dragging { opacity: 0; }
/* their ghost is `#selected_queue_item`: 42x42, cursor-following, above all */
.q-ghost { position: fixed; left: 0; top: 0; width: 42px; height: 42px;
  border: 1px solid #101010; background: #cfc4a4; box-sizing: border-box;
  overflow: hidden; pointer-events: none; z-index: 2000; }
.b-dialog .q-cell { width: 36px; height: 36px; border: 1px solid #101010; background: #cfc4a4;
  overflow: hidden; box-sizing: border-box; }
.b-dialog .ex-col.wide { width: 240px; margin: 0 auto; }
/* the queue dialog's picker has the queue bar pinned under it at y=159, so
   its four rows have to finish above that. 25px of pitch put the fourth at
   154-172; 22px puts it at 145-163... still short, so the sub-heading gives
   up its margin too and the four land at 66/88/110/132, clear by 11px. */
.b-dialog h3.sub { margin: 2px 0 4px; }
.b-dialog .ex-col.wide .ex-row { margin-bottom: 4px; }
/* v203: 88x25 at (106,220) and (236,220) — theirs, minus our 3px border.
   A single button sits at (172,220), their `_holder_buttom_center`. */
/* v211: 220 wide, so with 90px buttons the right one lands on their 236.
   It was 221 with 88px buttons, which put it at 239. */
/* v226: the surrender box centres its pair on the panel; the roundup and
   exchange keep the reference's 103 (their OK/CANCEL are at 106/236). */
.b-dialog.sur .btnrow { left: calc((100% - 220px) / 2); }
.b-dialog .btnrow { position: absolute; left: 103px; top: 217px; width: 220px;
  display: flex; justify-content: space-between; margin: 0; }
.b-dialog .btnrow.one { left: 169px; width: 90px; justify-content: center; }
/* MEASURED: 90x27, flat cream #fbe9b8 with a 1px #1e1e1e edge, label 15px/400
   in #141414 — no tracking, no bold, no gradient. */
.b-btn { display: inline-flex; align-items: center; justify-content: center;
  /* v211: 90x27, which is what the comment above always said and the rule
     did not -- re-measured off their live panel 2026-09-19. */
  width: 90px; height: 27px; box-sizing: border-box; text-align: center; padding: 0;
  background: #fbe9b8; border: 1px solid #1e1e1e; font-weight: 400;
  font-size: 15px; letter-spacing: normal; cursor: pointer; text-transform: uppercase; color: #141414; }
.b-btn:hover { filter: brightness(1.05); opacity: .85; }
.b-btn.off { opacity: .45; cursor: default; }
.b-btn.off:hover { color: #101010; filter: none; }

/* =====================================================================
   THE TWO IN-MATCH PANELS, rebuilt to their own layout. v203.
   MEASURED 2026-09-19 out of their game stylesheet. Both panels are the
   SAME widget — a two-column energy board — and the exchange is that board
   with a type picker bolted on top. Their numbers, verbatim:

     ._display            770x560, #000 at .25
     ._holder             391x290 at (203,101)
     ._holder_buttom_left 88x25 at (106,220); _right (236,220); _center (172,220)
       span               font Pedra, 15px, top 3px
     .txtroundup          abs (130,44)      .txtexchange abs (130,78)
     .chp1                abs w312 (-2,-1)  .random abs (47,-1) #bd262c w20 centred
     .chp2                abs (65,-1)       .enp1 (-51,25) 14px   .enp2 (119,25) 14px
     .energyleft2         abs (-90,41)      .energyright2 abs (80,41)
     .textleft            abs (-77,43) 13px .textright abs (93,43) 13px
     .energyleft          abs (0,42) 14px   .randomenergy abs (171,42) 14px
       (both move to top 44 inside .txtroundup)
     .min                 abs (70,50), li 20x12 at (-45,-5), margin-bottom 6
     .plus                abs (95,46), li 20x14 at (-45,-2), margin-bottom 4
     .chooseenergy        margin (-33,-1), li 20x20 inline margin-right 10
     .container_queue     abs (77,162) 280x57

   EVERY X AND Y BELOW IS THEIRS MINUS 3, because an absolutely positioned
   child of `.b-dialog` resolves against its padding box, inside our 3px
   border — the same correction the queue row has carried since v191.

   The ART is ours: their panel is one imgur background and two sprite
   strips for the +/- columns, and art is the one thing that never crosses.
   ===================================================================== */
.b-dialog .na-board { position: absolute; color: #000; font-family: inherit; }
/* Their measured pair, unchanged (v211, in a running match). A +1 was tried
   here on 2026-09-20 to land `chp1` on their 98 and it moved every row below
   it too, making our internal gap 26 where theirs is 25. The missing pixel
   was the TITLE ROW's own -1, not the board's. */
.b-dialog .na-board.roundup  { left: 127px; top: 41px; }
.b-dialog .na-board.exchange { left: 127px; top: 75px; }
/* v226: FOUR LABELS WERE ALL 312px WIDE and they overlapped each other and
   the +/- column. MEASURED in our own running panel 2026-09-19:
     .chp1 [128,43,312]  .random [177,43,20]  .chp2 [195,43,312]
     .enp1 [ 79,69,312]  .enp2   [245,69,312]
   `.chp1` ran 128..440 and `.chp2` 195..507 — two boxes 67px apart, each
   three times wider than its text, stacked on top of the buttons and off the
   391px panel entirely. Only the INK looked right, which is why it survived
   every earlier measurement: the boxes were never read.
   Each is now the width of the phrase it holds, taken from the rendered ink:
   CHOOSE ends at .random's left edge (49), RANDOM ENERGY runs from 195 to the
   panel's inner edge, ENERGY LEFT: is 99, RANDOM ENERGY: is 128. */
.b-dialog .na-board .chp1 { position: absolute; top: 0; left: -2px; width: 49px;
  text-align: right; white-space: nowrap; }
.b-dialog .na-board .random { position: absolute; top: 0; left: 47px; width: 20px;
  text-align: center; color: #bd262c; }
.b-dialog .na-board .chp2 { position: absolute; top: 0; left: 65px; width: 193px;
  text-align: left; white-space: nowrap; }
.b-dialog .na-board .enp1 { position: absolute; top: 25px; left: -51px; width: 99px;
  text-align: left; white-space: nowrap; font-size: 14px; }
/* v225: 115, not their 119. "RANDOM ENERGY:" is 128px of ink starting at
   panel-rel 249 and ending at 377; the stroke clearance line on our 391x290
   art is 374. Theirs has no stroke, so their 119 was free to sit there. Four
   pixels of deliberate divergence, and the label is a fixed string so this
   cannot drift with content. */
.b-dialog .na-board .enp2 { position: absolute; top: 25px; left: 115px; width: 128px;
  text-align: left; white-space: nowrap; font-size: 14px; }
.b-dialog .na-board ul { list-style: none; margin: 0; padding: 0; position: absolute; }
/* v229: every read-out column on this board is `pointer-events: none`.
   Their boxes deliberately overlap each other and the -/+ pair — the INK does
   not, but an OVERLAPPING BOX still eats the click, which is what stopped the
   + working. Only the two button columns take a pointer here. */
.b-dialog .na-board .energyleft2,
.b-dialog .na-board .energyright2,
.b-dialog .na-board .textleft,
.b-dialog .na-board .textright,
.b-dialog .na-board .energyleft,
.b-dialog .na-board .randomenergy,
.b-dialog .na-board .chp1,
.b-dialog .na-board .chp2,
.b-dialog .na-board .random,
.b-dialog .na-board .enp1,
.b-dialog .na-board .enp2 { pointer-events: none; }
.b-dialog .na-board .energyleft2  { top: 41px; left: -90px; width: 50px; }
.b-dialog .na-board .energyright2 { top: 41px; left: 80px;  width: 50px; }
/* v211 EVERY COLUMN ON THIS BOARD IS FLUSH RIGHT INSIDE ITS BOX.
   MEASURED 2026-09-19 off their live panel, all four columns:
     .energyleft2 box x40 w50  -> icon  at x80  (40+40, hard against the end)
     .textleft    box x53 w94  -> label at x93  (53+40), ending exactly 147
     .energyleft  box x130 w47 -> count at x170 (130+40), ending exactly 177
   Ours had the boxes at their x and the CONTENTS flush LEFT in boxes of
   w14 / w85 / w14, so the label ran 53..138 while the count started at 134
   -- a four-pixel overlap, and the reason the panel read as "all wrong and
   overlapping". Right-aligning inside their measured box widths puts every
   element back on their exact x AND is what makes room for our longer
   words: CONCENTRATION grows leftward into the box instead of rightward
   into the numbers. */
/* ONE PITCH FOR ALL FOUR COLUMNS. Theirs come out to 14.5 / 18.5 / 17.4px
   of pitch from three different font sizes and three different margins,
   which lines up in their metrics and does not in ours — our labels are
   longer words at a smaller size. The x and y of every column is still
   theirs; the row pitch is harmonised at 17px so the icon, the name, the
   +/- and the count of one energy are on one line. Said out loud because it
   is a departure, and it is in the row rhythm, not in the layout. */
.b-dialog .na-board ul li { height: 17px; line-height: 17px; margin: 0; }
/* v211 THE ONE DEPARTURE ON THIS BOARD, AND IT IS FORCED BY THE REBRAND.
   Theirs puts the energy ICON flush right in its 50px box, at x80..90, with
   the label starting at 93. That leaves 80px between the icon and the count
   glyph -- and CONCENTRATION renders 85.4px at our 10px. It does not fit
   between them at any alignment, so something has to move.

   The smallest possible move is this one: the icon goes flush LEFT in the
   SAME 50px box (40..54 / 210..224) and every other x on the board stays
   exactly theirs. That buys the label column 93px of clear run for an 85.4px
   word, with 7.6px to spare, and it costs one column 40px.

   Measured rendered widths at 10px/-0.2px, right-aligned to their 147:
     STRENGTH      50.4  -> 96.6..147
     CONCENTRATION 85.4  -> 61.6..147   (the one that decides it)
     WEAPON        44.8  -> 102.2..147
     DEVIL FRUIT   57.8  ->  89.2..147
   Against their GENJUTSU, which is nine characters and fits their 54. */
/* v228: FLUSH RIGHT, which is what their own measurement says and what stops
   the collision. Their spec (v211, measured in a running match):
     .energyleft2  box (40,85)  50x74 -> icon flush right at x80..90
     .energyright2 box (210,85) 50x74 -> icon flush right at x250..260
   These boxes deliberately overlap their neighbours; the INK does not,
   because every column in this board is right-aligned. Ours were the one
   column left-aligned, so both icon stacks sat at the LEFT of their boxes —
   the right-hand one at x210..224, straight under the -/+ hotspots at
   x195..240. Ten of the twelve overlaps measured in our own panel were that
   one declaration. */
/* v228 MEASURED in their running match 2026-09-20, which corrects both the
   alignment AND the icon size this board has carried since v211:
     energyleft2  box[40,139,50,74]   its li box[80,139,10,18]  img 10x10
     energyright2 box[210,139,50,74]  its li box[250,139,10,18] img 10x10
     textleft     box[53,141,94,74]   its li box[93,141,54,15]  13px, START
     textright    box[223,141,94,74]  its li box[263,141,54,15] 13px, START
   So the icon is a 10px square flush RIGHT in its 50px box, and the label is
   LEFT-aligned in a 54px box three pixels after it. Ours drew a 14px icon and
   right-aligned the labels — the note above claiming "flush right, so every
   label ENDS where theirs ends" was an inference, and their own boxes say the
   opposite. */
/* v229: THE ROW IS 10 WIDE, NOT 50 — and this is what "I can't click the +
   at all" was. Their own li is `box[250,139,10,18]`: shrink-wrapped to the
   icon. Ours kept the ul's full 50px and merely pushed the icon to its right
   edge, so the row still SPANNED 210..260 and sat on top of the +/- column at
   220..240, swallowing every click. Playwright names it exactly:
     "<li> from <ul class="energyright2"> intercepts pointer events".
   `margin-left: auto` keeps the icon flush right without the box reaching
   back across the buttons. `pointer-events: none` as well, because these rows
   are a read-out and nothing on them is ever clicked. */
.b-dialog .na-board .energyleft2 li,
.b-dialog .na-board .energyright2 li {
  margin-bottom: 0; width: 10px; margin-left: auto; pointer-events: none; }
.b-dialog .na-board .energyleft2 .ck-sq, .b-dialog .na-board .energyright2 .ck-sq {
  display: inline-block; vertical-align: middle; width: 10px; height: 10px; }
/* 10px with a hair of negative tracking, and the two number columns pushed
   4px right of their measured 0 and 171. THE REASON IS THE REBRAND: their
   longest label is GENJUTSU (9 characters), ours is CONCENTRATION (13), and
   at their 13px it runs straight through the count beside it. Everything
   else on this board is their number; this is four pixels and a font size,
   and it is written down rather than quietly re-measured. */
.b-dialog .na-board .textleft  { top: 43px; left: -37px; width: 72px;
  font-size: 8.5px; letter-spacing: -.2px; }
.b-dialog .na-board .textright { top: 43px; left: 133px; width: 72px;
  font-size: 8.5px; letter-spacing: -.2px; }
/* THEIR alignment (start) and THEIR x (93 / 263). The width is the one
   number that cannot be theirs: their longest label is BLOODLINE at 54px and
   ours is CONCENTRATION, which is 84px at their 13px. The room actually
   available is from the label's x to the count column's ink — 93..165 and
   263..335, i.e. 72px — so the column is 72 wide and the size drops to the
   largest that fits it. Said out loud rather than quietly re-measured. */
.b-dialog .na-board .textleft li, .b-dialog .na-board .textright li {
  white-space: nowrap; text-align: left; width: 72px; }
/* their 13px on four words of theirs; ours are longer ("DEVIL FRUIT" against
   "GENJUTSU") and 13px runs into the numbers, so the label column alone is
   11px. Said out loud rather than quietly re-measured. */
.b-dialog .na-board .energyleft   { top: 44px; left: 0px;   width: 47px; font-size: 14px; }
.b-dialog .na-board .randomenergy { top: 44px; left: 171px; width: 47px; font-size: 14px; }
/* v211: back to their measured 0 and 171. The old +4 was a dodge around the
   label collision fixed above, and a dodge that moves a number off its
   measurement is worse than the collision -- it hides it. */
.b-dialog .na-board .energyleft li, .b-dialog .na-board .randomenergy li {
  text-align: right; width: 47px; }
/* v211 MEASURED: their -/+ hotspots sit at x195 and x220 (their .min box is
   x200 w60 and its 20px cells overhang it 5px to the left; same for .plus at
   x225). Ours were at 159 and 184 -- 36px left of theirs, jammed against the
   count column. */
.b-dialog .na-board .min  { top: 47px; left: 65px; }
.b-dialog .na-board .plus { top: 47px; left: 90px; }
/* v229: ONE SIZE, and a bigger one. Theirs are 20x12 and 20x14 on a 17px
   pitch — two different boxes for two buttons that do the same job, which is
   what "different sizes and I can't click them at all" is describing. Both
   are 20x15 now with a 1px margin, so the pitch is still 17 and the two
   columns line up with each other and with the energy rows beside them.
   The extra height is three pixels of hit area on a control that had twelve. */
.b-dialog .na-board .min li,
.b-dialog .na-board .plus li { width: 20px; height: 15px; margin: 1px 0; line-height: 13px; }
.b-dialog .na-board .min li, .b-dialog .na-board .plus li {
  border: 1px solid #101010; background: #f6efdc; cursor: pointer; font-size: 10px;
  display: block; text-align: center; color: #101010; user-select: none; box-sizing: border-box; }
.b-dialog .na-board .min li.off, .b-dialog .na-board .plus li.off { opacity: .35; cursor: default; }
.b-dialog .na-board .min li:not(.off):hover, .b-dialog .na-board .plus li:not(.off):hover {
  filter: brightness(.94); }
/* the body of the board. In the exchange copy their eleven elements each
   carry `transform: translateY(20px)` to make room for the picker above; one
   wrapper is the same twenty pixels in one place. */
.b-dialog .na-board .na-body { position: absolute; left: 0; top: 0; }
/* v228: the picker is NOT carried by this shift. MEASURED on their live
   exchange 2026-09-20: `chooseenergy` is at panel 129,45 while `chp1` is at
   128,98 — the picker sits ABOVE the board's title row and does not move
   with the body. Ours rendered it at 129,65, twenty low, because it was
   inside the shifted element. */
.b-dialog .na-board.exchange .na-body { transform: translateY(20px); }
.b-dialog .na-board.exchange .picktitle,
.b-dialog .na-board.exchange .chooseenergy { transform: translateY(-19px); }
/* the exchange's own row: pick the energy you want first.
   MEASURED: their title is `.chp1` at left 7 with margin-top -32, and
   `.chooseenergy` sits at margin-top -33 with its images pulled up 23 more —
   i.e. both live ABOVE the board's own origin. */
.b-dialog .na-board .pickrow { position: absolute; left: 0; top: 0; }
.b-dialog .na-board .picktitle { position: absolute; left: 7px; top: -32px; width: 312px; }
/* an explicit width, because every box in this chain is absolutely
   positioned with none and the shrink-to-fit collapsed to one square wide,
   which wrapped the row into a column. 4 x (20 + 10). */
.b-dialog .na-board .chooseenergy { position: absolute; left: -1px; top: -14px;
  width: 130px; white-space: nowrap; }
.b-dialog .na-board .chooseenergy li { display: inline-block; width: 20px; height: 20px;
  margin-right: 10px; margin-top: 5px; }
.b-dialog .na-board .chooseenergy .ck-sq { display: block; width: 20px; height: 20px;
  cursor: pointer; opacity: .4; }
.b-dialog .na-board .chooseenergy li.on .ck-sq { opacity: 1; }

/* exchange dialog columns */
.ex-cols { display: flex; gap: 34px; }
.ex-col { flex: 1; }
.ex-col h4 { margin: 0 0 8px; font-size: 11px; letter-spacing: .4px; color: #101010; text-transform: uppercase; }
.ex-row { display: flex; align-items: center; gap: 6px; margin-bottom: 7px; font-size: 11px;
  font-weight: bold; color: #101010; text-transform: uppercase; }
.ex-row .ck-sq { width: 12px; height: 12px; flex: none; }
.ex-row .nmm { flex: 1; }
.ex-row .cnt { width: 16px; text-align: center; }
.ex-row .pm { width: 16px; height: 16px; line-height: 14px; text-align: center; border: 1px solid #101010;
  background: #f6efdc; cursor: pointer; font-weight: bold; user-select: none; }
.ex-row .pm:hover { opacity: .6; }
.ex-row .pm.off { opacity: .35; cursor: default; }
.ex-row .pm.off:hover { color: #101010; }
/* v201 exchange: pick the type you want, then click five energies to give up */
.ex-row.pick { cursor: pointer; padding: 2px 4px; border: 1px solid transparent; margin-bottom: 4px; }
.ex-row.pick:hover { background: #00000010; }
.ex-row.pick.on { border-color: #101010; background: #00000018; }
.ex-bank { display: flex; flex-wrap: wrap; gap: 5px; margin: 2px 0 4px; max-width: 320px; }
.ex-bank .ck-sq { width: 16px; height: 16px; flex: none; cursor: pointer; }
/* a ring, not an `outline`: the reference never uses outline on hover and
   test_hover_polish.js holds us to that (MEASURED 2026-09-03) */
.ex-bank .ck-sq:hover { box-shadow: 0 0 0 1px #101010; }
/* a sacrificed square stays in place and stays clickable — clicking it takes
   it back, which is how theirs undoes a sacrifice */
.ex-bank .ck-sq.gone { opacity: .25; }
.ex-skill { width: 44px; height: 44px; border: 2px solid #101010; margin-top: 6px; }
.ex-skill img { width: 100%; height: 100%; }

/* chat dialog */
.b-dialog.chat { width: 560px; left: calc((100% - 560px) / 2); padding-bottom: 16px; }
.chat-close { position: absolute; top: -40px; right: -6px; background: linear-gradient(#e6382a, #b31408);
  color: #faf4e1; border: 1px solid #6e0c05; font-weight: bold; font-size: 12px; letter-spacing: .5px;
  padding: 6px 14px; cursor: pointer; box-shadow: 0 2px 5px #0008; }
.chat-close:hover { filter: brightness(1.1); }
.chat-msgs { height: 190px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px;
  font-size: 12px; color: #2a2010; padding: 4px 2px; }
.chat-msgs .empty { margin: auto; color: #c01818; font-weight: bold; font-size: 14px;
  letter-spacing: .4px; text-transform: uppercase; }
.chat-msgs .m b { color: #c01818; }
.chat-form { display: flex; gap: 6px; margin-top: 10px; }
.chat-form input { flex: 1; border: 2px solid #8f1408; background: #fdf8ea; padding: 7px 9px;
  font-size: 12px; font-family: inherit; color: #2a2010; }
.chat-form .cbtn { background: linear-gradient(#e6382a, #b31408); color: #faf4e1; border: 1px solid #6e0c05;
  font-weight: bold; font-size: 12px; padding: 0 13px; cursor: pointer; }
.chat-form .cbtn:hover { filter: brightness(1.1); }
.chat-form .cbtn.muted { filter: grayscale(.6); }
/* v299: emotes in the battle chat — the picture a "(code)" becomes, the
   picker under the form (the list gives up 84px so the scroll keeps its
   290px), and the "NEW: n" count on OPEN CHAT */
.chat-msgs .c-emote { width: 20px; height: 20px; vertical-align: -5px; }
.chat-msgs .m[data-who="theirs"] b { color: #1a4f8f; }
.chat-form .cbtn.emo { padding: 0 8px; display: flex; align-items: center; }
.chat-form .cbtn.emo img { width: 18px; height: 18px; display: block; }
.chat-form .cbtn.emo.on { filter: brightness(1.25); box-shadow: inset 0 0 0 2px #faf4e1; }
.b-dialog.chat.emo-open .chat-msgs { height: 106px; }
.chat-emo { display: flex; flex-wrap: wrap; gap: 2px; margin-top: 6px; height: 78px; overflow-y: auto;
  border: 1px solid #8f1408; background: #fdf8ea; padding: 3px; box-sizing: border-box; }
.chat-emo-i { width: 30px; height: 30px; padding: 2px; border: 1px solid transparent; background: none;
  cursor: pointer; display: flex; align-items: center; justify-content: center; }
.chat-emo-i:hover { border-color: #8f1408; background: #f1e5c4; }
.chat-emo-i img { width: 22px; height: 22px; display: block; }
.bb-btn .bb-new { display: none; position: absolute; right: 8px; top: -7px; line-height: 14px;
  font-size: 10px; font-weight: bold; letter-spacing: .3px; color: #fff; background: #c01818;
  border: 1px solid #6e0c05; padding: 0 5px; pointer-events: none; }
.bb-btn .bb-new.on { display: block; }

/* surrender dialog
   MEASURED 2026-09-13 on naruto-arena.site, mid-match, off the live nodes
   (docs/specs/2026-09-13-endgame-and-surrender.md). Holder-relative:
     title    60,50   320x18   16px #000 centred
     picture 120,80   192x125  NO border
     OK      106,220  88x25
     CANCEL  236,220  88x25
   Ours drew a 220x140 picture inside a 2px black frame and let the title flow
   from the padding box, so neither sat where theirs does. Every coordinate
   below is the measured one minus the 3px border our dialog has and theirs
   does not — absolutely positioned children are placed against the padding
   box, the same compensation .q-row and .btnrow already use. */
.b-dialog.sur { text-align: center; }
/* v226: 15px higher, and it must never wrap — at 16px the sentence was
   taking two rows in a box sized for one, which pushed its second line onto
   the illustration. `nowrap` plus the full content width instead of 320. */
.b-dialog.sur h3 { position: absolute; left: 0; right: 0; top: 32px; height: 18px;
  margin: 0; font-size: 14px; font-weight: 400; letter-spacing: normal;
  white-space: nowrap; color: #000; text-align: center;
  /* v234: 16 -> 14, asked 2026-09-20. left:0/right:0 already resolve against
     the padding box, which is symmetric, so `text-align:center` centres the
     ink on the paper; the rod is a separate element OUTSIDE this box (see
     `.b-dialog.sur .rod`) and does not pull the centre. Measured after the
     change: the ink's centre and the box's centre agree to under a pixel. */
  display: block; }
/* v224b: CENTRED, not carried across at 117.
   MEASURED off their own background art 2026-09-19 (i.imgur.com/pSy9UUx.png,
   natural 391x290): their holder picture is rod AND paper in ONE file. The
   green rod occupies x 0..60 and the cream paper runs x 61..377 — 317 wide,
   centre 219.5. Their `.surrenderimg{left:120px}` puts a 192-wide picture at
   centre 216, so theirs IS centred on its paper, 3.5px shy of exact.
   OURS draws the rod as a separate element (v222), so our 391x290 is paper
   edge to edge and their 120 lands 20px right of our centre. Centred in the
   content box instead: (391 - 6 - 192) / 2 = 96.5.
   Vertical is unchanged and was already right: their gaps are 12px under the
   title and 15px above the buttons, and 65 + 12 = 77. */
.b-dialog.sur .pic { position: absolute; left: 96px; top: 77px;
  width: 192px; height: 125px; border: 0; margin: 0; background: #cfc4a4; }
.b-dialog.sur .pic img { width: 100%; height: 100%; display: block; }

/* ---- team manager : saved teams panel ---- */
.s-dim { position: absolute; inset: 0; background: #0005; z-index: 14; }
/* Centred on the stage: 600x360 leaves an even 85px left/right and 100px
   top/bottom. The previous 660x380 at x=150 overflowed the stage by 40px. */
/* v187: 20px shorter and re-centred. 600x340 on a 770x560 stage leaves 85
   left and right and 110 top and bottom — centred by arithmetic, not by eye,
   so it stays centred if either number changes. --gm-page-h follows the
   height because the rod's width is derived from it. */
.s-tm { position: absolute; left: 85px; top: 110px; width: 600px; height: 340px;
  --gm-page-h: 340px; z-index: 15; }
/* 46:239 — ScrollSmall's own ratio, the same one the detail rod and the
   character-panel rod use. This was 43x360, which stretched the art to less
   than two thirds of its proper width; at 360 tall the rod is 69 wide, and
   left moves from -21 to -34 so it stays centred on the panel edge where it
   was. Keep the two linked if this is ever resized again. */
/* v187: the rod override is GONE; the kit owns it, and the Filter popup
   gets the same treatment for free because it is the same rule.
   v222 CORRECTS WHAT THIS PARAGRAPH USED TO SAY. It described the rod as
   centred on the panel's edge, `left: calc(var(--gm-rod-w) / -2)`, half of
   it outside. That is no longer true and the variable no longer exists: the
   rod is 20px taller than its panel, hangs 10px above and below, and its
   RIGHT edge sits 10px inside the panel's left edge. Asked for
   2026-09-19. Left corrected rather than deleted, because a comment that
   describes the old behaviour is how this project has been misled before. */
/* v186: the background and the rod now come from .gm-page — this panel was
   the one the kit was taken FROM, so what is left here is only its padding. */
/* v225: moved for the 6px stroke clearance rule — see ADMIN.md v225. */
.s-tm .tm-inner { position: absolute; inset: 0;
  padding: 18px 30px 18px 38px; display: flex; flex-direction: column; box-sizing: border-box; }
.s-tm h4 { margin: 0 28px 6px 0; color: #000; font-size: 11px; letter-spacing: .5px; text-align: center; }
.s-tm .close { position: absolute; top: 15px; right: 22px; cursor: pointer; color: #9a3a1a;
  font-weight: bold; font-size: 16px; line-height: 1; z-index: 3; padding: 4px; }
.s-tm .close:hover { opacity: .6; }
.s-tm .tm-saverow { display: flex; gap: 8px; margin-bottom: 8px; flex: none; }
.s-tm .tm-saverow input { flex: 1; border: 2px solid #8f1408; background: #fdf8ea;
  padding: 4px 7px; font-size: 9px; font-family: inherit; color: #2a2010; }
.tm-btn { background: #cf1d1d; color: #fff; border: 1px solid #8a1410;
  font-weight: bold; font-size: 8px; letter-spacing: .3px; padding: 4px 8px; cursor: pointer;
  border-radius: 0; white-space: nowrap; align-self: center; }
.tm-btn:hover { filter: brightness(1.08); }
.tm-btn.off { opacity: .45; cursor: default; }
.tm-btn.off:hover { filter: none; }
.tm-btn.del { background: #565656; border-color: #3a3a3a; }
.s-tm .tm-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
.s-tm .tm-empty { margin: auto; color: #9a3a1a; font-weight: bold; font-size: 9px; letter-spacing: .4px; }
/* one row per saved team, and it must not wrap: the name box, the three faces
   and both buttons all have to sit on a single line, so the type and the gaps
   are sized to that rather than to comfort. */
.s-tm .tm-row { display: flex; gap: 6px; align-items: center; background: #f2e9cdcc;
  border: 1px solid #c9b98c; border-radius: 0; padding: 4px 8px; flex-wrap: nowrap; }
.s-tm .tm-row .tm-name { width: 132px; flex: none; border: 1px solid #c9b98c; background: #fdf8ea;
  padding: 3px 6px; font-size: 9px; font-weight: bold; color: #2a2010; font-family: inherit; }
.s-tm .tm-row .tm-faces { display: flex; gap: 4px; flex: 1; min-width: 0; }
.s-tm .tm-row .tm-faces .sprite { border: 1px solid #101010; }
.s-tm .tm-row .tm-faces .gone { width: 38px; height: 38px; border: 1px solid #101010;
  background: #d8cba6; display: flex; align-items: center; justify-content: center;
  font-weight: bold; color: #9a3a1a; }

/* sprite-sheet cells (see js/data.js SPRITE SHEETS) */
/* the caller's size is the default; any container rule below that sets a
   width/height now wins, because these are no longer inline styles */
.sprite { width: var(--sprite-w, auto); height: var(--sprite-h, auto);
  background-repeat: no-repeat; flex: none; display: block;
  image-rendering: auto; pointer-events: none; }

/* tooltip + overlay (shared) */
/* MEASURED (.tooltiptext on naruto-arena.site, 2026-09-02): a 241px-wide
   parchment card, background rgb(243,231,203), 1px solid black, padding
   0 5px 3px. Name 12px/16 in rgb(189,38,44); body 10px/13 in rgb(21,19,17);
   the duration line is 10px/13 in rgb(185,25,33). It is NOT a dark tooltip. */
.tip { position: fixed; z-index: 50; width: 241px; max-width: 241px;
  background: #f3e7cb; color: #000; border: 1px solid #000;
  padding: 0 5px 3px; font-size: 10px; line-height: 13px;
  pointer-events: none; display: none; border-radius: 0; }
.tip h5 { margin: 0; font-size: 12px; line-height: 16px; font-weight: 400; color: #bd262c; }
.tip .meta { color: #877956; margin-bottom: 2px; }
/* separator between stacked effects — ._mc_tooltip-effect-separator */
.tip .sep { height: 1px; background: rgba(89,19,7,.18);
  margin: 1px 4px 3px; }
.tip .dur { color: #b91921; }
/* effect-icon typewriter tooltip: a blinking cursor trails the typed text.
   v188: the BOX was already right — measured on 2026-09-14 their tooltip is
   bg rgb(243,231,203) (#f3e7cb), 1px solid black, padding 0 5px 3px, the
   name 12px rgb(189,38,44) (#bd262c) and the body 10px, all of which .tip
   above already had. What was missing was the width and the duration line. */
.tip.efftip { width: 241px; box-sizing: border-box; }
.tip.efftip .typed { white-space: pre-wrap; }
/* the duration, on its own line in their red — <font color="#B91921"> in
   their markup */
.tip.efftip .efftip-dur { color: #b91921; margin-top: 1px; }
/* v289: their `nothings` — grey, no "- " (setHTMLEffects: color #776F67) */
.tip.efftip .efftip-note { color: #776f67; }
/* v287: the gap between two applications of one skill in one tooltip */
.tip.efftip .efftip-gap { height: 8px; }
/* v191: the blinking caret went with the typewriter. The whole tooltip is
   painted at once now, so a caret would be pointing at nothing. */
/* absolute, not fixed: the overlay is a child of the 770x560 screen so it
   centres on the BOARD and is clipped to it, however large the window is. */
.overlay { position: absolute; inset: 0; background: #000a; display: none;
  align-items: center; justify-content: center; z-index: 100; overflow: hidden; }
.overlay .card { background: var(--tan); border: 3px solid var(--red); padding: 22px 36px; text-align: center; border-radius: 4px; }
/* ---- win / lose end screen ----------------------------------------------
   MEASURED 2026-09-13 off the live LOSER screen of a finished quick match on
   naruto-arena.site (docs/specs/2026-09-13-endgame-and-surrender.md), and the
   BOX corrected 2026-09-19 by measuring their background art itself.

   THEIR 642x322 IS ROD PLUS PAPER IN ONE FILE. Sampled off
   i.imgur.com/ogIxFPX.png: the green rod runs x 0..67 and the cream paper is
   560x255 at (68,34). Every panel-relative number in the 09-13 spec is written
   in the 642 box, so it carries a 68,34 offset that belongs to their rod —
   and ours draws the rod as a SEPARATE element (v222 rule, 20px taller,
   10px overlap, both axes scaled). Carried across unchanged, every element sat
   68px right and 34px low on a panel that is paper edge to edge.

   So the panel IS their paper: 560x255, and the numbers below are theirs minus
   (68,34), which invents nothing:
     picture   13,13   236x226 + 1px #000
     title    347,0    44px  #bd262c
     body     273,49   280x160  13px #000
     CONTINUE 349,216  120x30  25px #bd262c
   Stage position follows: horizontally centred, (770-560)/2 = 105, and their
   paper's own top edge, 112 + 34 = 146.

   Three things ours had wrong and this fixes: the panel was 645x320 and
   floated by the flexbox; the win title was green (theirs is the SAME red in
   both outcomes — the positioned .endtitle rule re-declares the colour and
   wins the cascade, and the live LOSER title computed to rgb(189,38,44)); and
   the notes were a separate grey 10.5px list (theirs are the same 13px black
   text inside the one body block). */
/* v226: BACK TO 642x322 at 64,112, the reference's own box. v224b shrank
   this to their PAPER (560x255) because their background file bakes the rod
   in and ours draws it separately — correct reasoning, and the owner then
   supplied art at 642x322 and asked to keep it. The supplied file is ALL
   PAPER with its own stroke, so the panel is the picture's size and the rod
   hangs off it like every other panel here. Every coordinate below is
   therefore theirs, unshifted. */
.overlay .card.result-card { position: absolute; left: 64px; top: 112px;
  width: 642px; max-width: 642px; height: 322px; box-sizing: border-box;
  padding: 0; display: block; border: 0; border-radius: 0; text-align: left;
  background: var(--asset-result-panel, url(../assets/placeholder/result_panel.png)) no-repeat 0 0 / 642px 322px; }
/* v224: ONE MERGED PICTURE PER OUTCOME.
   `.result-card` already carries `win` or `lose` (a draw carries neither and
   keeps `result_panel.png`), so each outcome can be a single 642x322 image
   with its illustration already in it. Both fall back to the shared panel,
   so an empty key leaves that outcome exactly as it is today. */
.overlay .card.result-card.win  { background-image: var(--asset-bg-winner,
  var(--asset-result-panel, url(../assets/placeholder/result_panel.png))); }
.overlay .card.result-card.lose { background-image: var(--asset-bg-loser,
  var(--asset-result-panel, url(../assets/placeholder/result_panel.png))); }
/* ...AND THEN THE SEPARATE <img> HAS TO GO, or the illustration is drawn
   twice — once inside the merged background and once on top of it. Same
   shape as `.has-art-*` dropping a drawn border when supplied art already
   contains one. */
.has-art-bg_winner .overlay .card.result-card.win  .result-pic,
.has-art-bg_loser  .overlay .card.result-card.lose .result-pic,
.has-art-bg_surrender .b-dialog.sur .pic { display: none; }

/* v224: the result panel gets a rod, under the same rule as every other
   scroll: 255 + 20 = 275 tall, hanging 10px above and below, its RIGHT edge
   10px inside the panel's left edge, and NO width so the art's own
   proportions survive.
   NO 3px correction here, unlike `.b-dialog`: this panel declares
   `border: 0`, so its padding box and its border box are the same box. */
/* v228: 10px bigger and 20px further right (asked 2026-09-20). Bigger on
   BOTH axes comes free from `width: auto` — a taller box makes a
   proportionally wider one — so the height carries the whole change:
   322 + 20 = 342, and `top: -10` keeps it centred on the panel. */
.overlay .card.result-card .rod { position: absolute; top: -10px;
  height: calc(322px + 20px); width: auto;
  right: calc(100% - 30px); left: auto; z-index: 2;
  filter: drop-shadow(2px 3px 4px #0006); pointer-events: none; }
.overlay .card.result-card .rod img { height: 100%; width: auto; display: block; }

/* content-box on purpose: the reference's <img> is 236x226 of picture WITH a
   1px black border outside it, so the painted box is 238x228 and its rect
   starts 13,13 inside their paper (81,47 in their rod-inclusive box). Under our global border-box the same declaration
   drew 236x228 total and ate two pixels of the art. */
.overlay .card.result-card .result-pic { position: absolute; left: 81px; top: 47px;
  box-sizing: content-box; width: 236px; height: 226px;
  border: 1px solid #000; object-fit: cover; }
.overlay .card.result-card h2 { position: absolute; left: 415px; top: 34px;
  margin: 0; font-size: 42px; font-weight: 400; line-height: 50px; color: #bd262c; }
.overlay .card.result-card .endtext { position: absolute; left: 341px; top: 83px;
  width: 280px; height: 160px; margin: 0; overflow: hidden;
  font-size: 11px; font-weight: 400; line-height: 1.35; color: #000; text-align: left; }
/* v344: the quests this battle finished go under the text; a long list
   scrolls inside the same 280x160 box rather than spilling over the button */
.overlay .card.result-card .endtext:has(.quests-done) { overflow-y: auto; }
.overlay .card.result-card .endtext .quests-done b { font-weight: 700; }
.overlay .card.result-card .btn { position: absolute; left: 417px; top: 250px;
  width: 120px; height: 30px; padding: 0; margin: 0; background: none; border: 0;
  border-radius: 0; font-size: 23px; font-weight: 400; line-height: 30px;
  color: #bd262c; text-align: center; text-decoration: none; }
.overlay h2 { margin: 0 0 6px; font-size: 26px; }
.overlay .btn { display: inline-block; background: var(--red); color: #faf4e1; border: 1px solid var(--red-d);
  padding: 5px 14px; font-weight: bold; border-radius: 3px; text-decoration: none; margin: 0 4px; }

.log { width: var(--screen-w); margin: 8px auto 0; height: 92px; overflow-y: auto; background: #faf4e1;
  border: 1px solid #bbb; padding: 5px 9px; font-size: 11px; color: #222; }
.log .turnsep { color: var(--red-d); font-weight: bold; margin-top: 3px; }

/* rank badge shown beside rank text (select / battle headers) */
/* v258: 20x20, not 14x14. The rank badges are exactly 20x20 (all twelve
   measured 2026-09-21) and the reference draws them at natural size — six
   rank icons on naruto-arena.site's homepage that day, every one
   naturalWidth 20 and getBoundingClientRect().width 20. The reference's
   PROFILE page could not be measured: it is behind a login. */
.rank-ico { display: inline-block; width: 20px; height: 20px; margin-right: 4px; vertical-align: middle; }
.rankline { display: inline; }

/* =====================================================================
   OG-CLIENT JUICE: turn playback animations, sounds feedback, buttons
   ===================================================================== */

/* hp bars slide between values during playback */
/* v203: NO CSS TRANSITION ON THE BAR. The bar is walked a point at a time
   by tickHealth, in step with the number printed on it — their own
   setInterval(...,40). A transition on top of that would slide the fill
   while the reading beside it stepped, which is the mismatch this removes. */

/* v203: the per-skill announcement banner is GONE, and so is the element it
   drew. MEASURED 2026-09-19: their battle screen has no such thing — no rule
   for one in 371 KB of their game stylesheet, nothing in the battle chunk. A
   turn resolves as ONE event there: the touched avatars pulse once and the
   health bars walk down together. */

/* no hit/heal flash or shake: the original has no extra visual effects */

/* floating damage / heal numbers over the face picture */
/* v203: the floating "-20" is GONE. MEASURED 2026-09-19: their game has no
   such element — not one rule for it in 371 KB of their stylesheet, not one
   string for it in their 886 KB battle chunk. The only number that moves is
   the one printed on the health bar, counting down a point at a time.

   THE AVATAR PULSE replaces it, and it is theirs:
     @keyframes avatarPulse { 0%{transform:scale(1);opacity:1}
                             to{transform:scale(2.107);opacity:0} }
     ._animation_avatar_pulse { animation: avatarPulse .75s linear forwards }
   One pulse per RESOLVED TURN, on every avatar the turn touched — not one
   per skill. */
.b-unit.pulsed .b-portrait::after { content: ""; position: absolute; inset: 0;
  background: inherit; animation: na-avatar-pulse .75s linear forwards;
  pointer-events: none; }
@keyframes na-avatar-pulse {
  0%   { transform: scale(1);     opacity: 1; }
  100% { transform: scale(2.107); opacity: 0; }
}
@keyframes na-float { 0% { opacity: 0; transform: translate(-50%, 8px); }
  15% { opacity: 1; } 100% { opacity: 0; transform: translate(-50%, -28px); } }

/* timer bar stays a plain solid color the whole time — no pulse or gradient */

/* buttons physically press down like the OG client */
.bb-btn:active, .b-btn:not(.off):active, .cbtn:active, .chat-close:active,
.b-ready:not([disabled]):active .label, .vb:active, .spk:active {
  transform: translateY(1px); filter: brightness(.93); }
.b-skill:not(.q):active, .b-skill.q.filled:active { transform: scale(.96); }
.b-exchange:not(.off):active { filter: brightness(.85); }

/* result card pops in */
.overlay .card { animation: na-card .35s ease-out; }
@keyframes na-card { from { transform: scale(.72); opacity: 0; }
  to { transform: scale(1); opacity: 1; } }

/* ---- OPPONENT FOUND interstitial ----
   Sits over the selection screen between "searching" and the battle board
   loading, so the transition reads as one continuous sequence. */
/* v183: NOT flex-centred any more. The holder's measured home is stage
   (203,101) and 391x290 centred on a 770x560 stage would be (189.5,135) —
   so centring put every dialog 14px left and 34px low. The dim is the
   reference's #000 at ~25%. */
.s-found-wrap { position: absolute; inset: 0; z-index: 40; background: #00000040; }
/* MEASURED on the reference, 2026-09-12, off its START PRIVATE BATTLE popup
   (the one that opens without queueing anyone): ._holder is 391x290 at stage
   (203,101) on a 770x560 stage, title .var_battle_popup_text 317x18 at
   panel-relative (60,47) in 16px #f00 centred, body text 317x17 at (60,123)
   in 15px black, and two 90x27 buttons at (106,220) and (236,220). Ours was
   320 wide with whatever height its padding made. */
.s-found { position: absolute; left: 203px; top: 101px;
  width: 391px; height: 290px; padding: 0; text-align: center; box-sizing: border-box;
  background: linear-gradient(#f6efdc, #e6d7ae); border: 3px solid #b31408;
  box-shadow: 0 6px 24px #000a, inset 0 0 0 2px #f0a08c55;
  animation: s-found-in .35s ease both; }
/* THE BACKGROUND IS A PLACEHOLDER, asked for: paste 391x290 art into
   `popup_holder.png` and it becomes the holder. data-skin does the swap and
   adds .skinned, which strips our gradient, our border and our inner glow —
   the frame is part of the artwork, exactly as it is on the reference (its
   own dialog picture has no border of its own for the same reason). */
.s-found.skinned { background-size: 100% 100%; background-repeat: no-repeat;
  border-color: transparent; box-shadow: none; }
.s-found h3 { position: absolute; left: 37px; top: 47px; width: 317px; height: 18px;
  margin: 0; color: #f00; font-size: 16px; line-height: 18px; letter-spacing: 1px; }
.s-found-sub { position: absolute; left: 37px; top: 123px; width: 317px; height: 17px;
  margin: 0; font-size: 15px; line-height: 17px; font-weight: bold; color: #000;
  letter-spacing: .6px; animation: s-found-pulse 1s ease-in-out infinite; }
/* =====================================================================
   THE SEARCHING POPUP  (v186 — rebuilt, centred, nested)
   ---------------------------------------------------------------------
   v183 positioned every part of this absolutely from numbers recorded in
   ADMIN.md v180 — title at left 60 width 320, picture at left 120, buttons
   at left 106 and 236. Put a ruler on them and they all agree with each
   other and all disagree with the panel:

     panel          391 wide  -> its centre is 195.5
     picture slot   120 + 192/2                 = 216
     title          60 + 320/2                  = 220
     the button pair (106 .. 236+90) midpoint   = 216

   So the whole content group sits ~20px right of the holder it is drawn
   in, which is exactly what "the wheel and the text are not centred" looks
   like. Those numbers came from a popup whose own width was never recorded
   next to them, so they cannot be trusted as panel-relative; the only
   number that is certainly right is that the content is centred in its
   holder, which is what the reference shows.

   So this is now a CENTRED COLUMN rather than six absolute offsets. The
   panel centres its children; nothing can drift from the frame again
   because nothing is positioned against an edge any more.

   TWO IMAGES INSIDE EACH OTHER, asked for, and the way the reference does
   it: an OUTER holder picture (`popup_holder.png`) and an INNER picture box
   (`popup_inner.png`) with the wheel turning inside that. Both are
   placeholders until art is pasted into ASSET_LINKS; both drop their
   placeholder border the moment real art arrives, because on real art the
   frame is part of the picture.
   ===================================================================== */
.s-found.searching {
  display: flex; flex-direction: column; align-items: center;
  justify-content: flex-start; gap: 9px; padding: 16px 14px 14px;
}
.s-found.searching h3 { position: static; left: auto; top: auto;
  width: 100%; max-width: 340px; height: auto; min-height: 18px;
  font-size: 13px; line-height: 18px; letter-spacing: .4px; color: #b31408;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the inner picture — its own image, nested in the holder's */
.s-found .s-found-pic { position: static; left: auto; top: auto;
  width: 192px; height: 125px; flex: none;
  display: flex; align-items: center; justify-content: center; color: #8a3c14;
  background: #00000018; border: 2px solid #b3140840; box-sizing: border-box; }
.s-found .s-found-pic.skinned { background-size: 100% 100%; background-repeat: no-repeat;
  background-color: transparent; border-color: transparent; }
.s-found .s-wheel { width: 116px; height: 116px; display: block; }
.s-found .s-wheel-art { width: 100%; height: 100%; object-fit: contain; display: block; }
/* A CONSTANT rate, deliberately. An eased or bouncing spinner reads as
   progress and there is no progress to report — only waiting. */
.s-found .s-found-pic.spin .s-wheel,
.s-found .s-found-pic.spin .s-wheel-art { animation: s-wheel-turn 5.5s linear infinite; }
@keyframes s-wheel-turn { to { transform: rotate(360deg); } }
.s-found .s-found-line { position: static; left: auto; top: auto;
  width: 100%; max-width: 340px; height: auto; min-height: 14px;
  font-size: 10px; font-weight: bold; line-height: 14px; letter-spacing: .4px; color: #000;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-found .s-found-input { position: static; left: auto; top: auto;
  width: 190px; height: 15px; flex: none;
  box-sizing: border-box; padding: 0 7px; font-family: inherit; font-size: 10px; color: #2a2010;
  background: #fff9ef; border: 1px solid #7a6128; border-radius: 0; outline: none;
  text-align: center; }
/* the buttons sit in a row of their own so one button is centred and two
   are centred as a pair — the old .solo/.left/.right offsets were three
   more numbers that could disagree with the frame */
.s-found .s-found-row { display: flex; gap: 40px; justify-content: center;
  margin-top: auto; }
.s-found.searching .s-found-btn { position: static; left: auto; top: auto; }

/* =====================================================================
   THE MATCH-FOUND SCREEN  (v186)
   ---------------------------------------------------------------------
   Asked for: its own picture, shown when a match actually lands, rather
   than the searching panel swapping its text. Same holder, a different
   inner image (`popup_found.png`), and the wheel stops — the stop IS the
   news, so it must not keep turning underneath.
   ===================================================================== */
.s-found.found .s-found-pic { border-color: #1d7a2f60; background: #1d7a2f14; }
.s-found.found h3 { color: #1d7a2f; }
.s-found.found .s-found-line { animation: s-found-pulse 1s ease-in-out infinite; }
.s-found .s-found-btn.solo { }

/* the two buttons, when a popup has them */
.s-found .s-found-btn { position: absolute; top: 220px; width: 90px; height: 27px;
  line-height: 25px; font-size: 15px; color: #141414; cursor: pointer;
  background: #e8d9ae; border: 1px solid #7a6128;
  border-radius: 0; box-sizing: border-box; }
.s-found .s-found-btn.left  { left: 106px; }
.s-found .s-found-btn.right { left: 236px; }
.s-found .s-found-btn:hover { filter: brightness(1.06); }
/* =====================================================================
   THE SEARCHING SCROLL  (v329) — select.js buildSearchPop has the ask.
   The box takes the picture's own size and is centred on the 770x560
   stage from JS (a replacement picture is centred too). No border, no
   gradient: the frame is the picture.
   The text started from the reference's, MEASURED 2026-10-04 on their
   /ingame (`.var_battle_popup_text`): Franklin 16px/400, #f00, an 18px
   line, centred — here centred on the gif; then asked 20:18 for bold, 1px
   smaller and 2px higher (15px/700, 7px clear of the border).
   ===================================================================== */
/* a fade, not a scale: a scale would move the gif off the pixel it is placed on */
.s-search { position: absolute; background-repeat: no-repeat; background-size: 100% 100%;
  animation: s-search-in .25s ease both; }
@keyframes s-search-in { from { opacity: 0; } to { opacity: 1; } }
.s-search-col { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 5px; }
/* asked 2026-10-04 20:18: bold and 1px smaller than the reference's 16px —
   15px/700 (the 2px lift is the rule at the end of this block) */
.s-search-title { height: 18px; line-height: 18px; font-family: var(--gm-font); font-size: 15px;
  font-weight: 700; color: #f00; letter-spacing: normal; text-align: center; white-space: nowrap; }
/* the 1px black border sits OUTSIDE the gif: the gif keeps its own size */
.s-search-gif { border: 1px solid #000; line-height: 0; flex: none; }
.s-search-gif .na-gif { display: block; }
/* no gif linked yet: the turning wheel stands in, in a 192x125 box */
.s-search-still { width: 192px; height: 125px; display: flex; align-items: center; justify-content: center; line-height: normal; }
.s-search-still .s-wheel, .s-search-still .s-wheel-art { width: 116px; height: 116px; animation: s-wheel-turn 5.5s linear infinite; }
.s-search-col .s-found-row { margin-top: 5px; }
/* the CANCEL button: the searching holder's own 90x27 button, here in the column under the gif */
.s-search-col .s-found-btn { position: static; width: 90px; height: 27px; line-height: 25px; font-size: 15px;
  color: #141414; cursor: pointer; background: #e8d9ae; border: 1px solid #7a6128; border-radius: 0;
  box-sizing: border-box; text-align: center; }
.s-search-col .s-found-btn:hover { filter: brightness(1.06); }
.s-search-wrap.found .s-search-cancel { visibility: hidden; }
/* asked 2026-10-04 20:18: "Move the cancel button and text up by 2px and
   the searching for an opponent one up by 2px" — moved, not re-laid-out: the
   gif keeps its pixel, so the title now clears the border by 7px */
.s-search-title, .s-search-col .s-found-row { position: relative; top: -2px; }
/* v330: THE PRIVATE GAME'S NAME BOX on the same scroll (select.js
   buildPrivatePop). The name bar is dead centre on the scroll — the
   190x16 name bar — and SEARCH, the holder's 90x27
   button, sits 8px under it. Asked 2026-10-05 06:24. */
.s-private-wrap .s-private-bar { position: absolute; left: 50%; top: 50%; width: 190px; height: 16px;
  margin: -8px 0 0 -95px; box-sizing: border-box; padding: 0 7px; font-family: inherit; font-size: 10px;
  color: #2a2010; background: #fff9ef; border: 1px solid #7a6128; border-radius: 0; outline: none; text-align: center; }
.s-private-wrap .s-private-search { position: absolute; left: 50%; top: 50%; margin: 16px 0 0 -45px;
  width: 90px; height: 27px; line-height: 25px; font-size: 15px; color: #141414; cursor: pointer;
  background: #e8d9ae; border: 1px solid #7a6128; border-radius: 0; box-sizing: border-box; text-align: center; }
.s-private-wrap .s-private-search:hover { filter: brightness(1.06); }
@keyframes s-found-in { from { opacity: 0; transform: scale(.94); }
  to { opacity: 1; transform: scale(1); } }
@keyframes s-found-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
  .s-found, .s-found-sub { animation: none; }
}


/* ---- remote-art loading treatment (all screens) ----
   Battle art comes from the same CDN as the selection grid, so every image
   slot carries the placeholder behind it and fades the real picture in.
   Without this a slow fetch leaves blank tiles that read as broken. */
/* ---- v225: the loading mark turns --------------------------------------
   A background-image cannot be rotated on its own, so the mark moves onto a
   ::before that carries it and spins; the real picture sits above it at
   z-index 1, so the moment it paints the spinner is hidden exactly as the
   old background was. Nothing about WHEN it shows changed.
   1s linear, transform-origin center, 0 -> 1turn — measured off their
   `._sharingan_running` 2026-09-19 (see assetlinks.js beside the key).
   `prefers-reduced-motion` stops it and leaves the mark standing still. */
/* v225: THE LOADER IS NOT ANIMATED BY US, AND IT NEVER WAS BY THEM.

   MEASURED 2026-09-19 off their own stylesheet: `img_loading.png` appears in
   exactly ONE rule in the whole sheet —
   `._mc_skillbutton_image{...background-image:url(.../img_loading.png);
   background-size:54px}` — with no animation of any kind. The only full-turn
   rotation in their client is `#shuri`, which is their BUSY CURSOR: fixed,
   35x35, tracking `--cursor-x/--cursor-y`, `.6s linear infinite`. Not this.

   The movement lives INSIDE the picture (confirmed by the owner the same
   day), so a CSS rotation would spin an already-spinning drawing. The
   `na-loadspin` keyframes and both `animation:` declarations are gone; what
   is left paints the file and nothing else.

   `cursor_wait.png` stays empty on purpose: a CSS `cursor:` cannot be
   animated, so their spinning-cursor effect has no reproduction here and is
   deliberately not attempted. */
/* v226: the mark is declared in each slot's OWN base rule, not in a combined
   one further down. A combined `.s-cell, .b-skill { … }` lost to
   `.s-cell.picked` and `.b-skill.vacated`, which are more specific — and
   those two SHOULD clear it, because a picked cell and a vacated slot are
   not waiting for a picture. Declaring it at the base keeps that override
   working and stops the mark being reinstated after them. */
/* the tiles need a containing block for it, and the art needs to be above it */
.s-team .slot, .b-skill { position: relative; }
.s-cell img, .s-cell .sprite, .s-team .slot img, .s-team .slot .sprite,
.b-skill img, .b-skill .sprite, .b-info .pic img, .b-info .pic .sprite {
  position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) {
  .s-cell::before, .s-team .slot::before, .b-skill::before,
  .b-info .pic::before, .b-portrait::before { animation: none; }
}
/* The fade-in is a FIRST-PAINT affordance only. It used to run on every
   element of every render(), so any full rebuild — closing a dialog, clicking
   around in chat — replayed a 0.35s fade across the whole board and read as a
   page refresh. Once the screen has painted once it gets .art-warm and the
   art simply appears. */
.screen:not(.art-warm) .b-skill img, .screen:not(.art-warm) .b-skill .sprite,
.screen:not(.art-warm) .b-portrait img, .screen:not(.art-warm) .b-portrait .sprite,
.screen:not(.art-warm) .b-info .pic img, .screen:not(.art-warm) .b-info .pic .sprite,
.screen:not(.art-warm) .s-team .slot img, .screen:not(.art-warm) .s-team .slot .sprite {
  animation: s-fadein .35s ease both; }
/* the "?" slot and the vacated gap are UI, not remote art — no placeholder */
.b-skill.q, .b-skill.vacated { background-image: none; }


/* ---- character detail panel (measured from the reference) ----
   545x222 at stage (141,24). Every child is positioned absolutely against a
   single known origin: nesting flow-wrappers made offsets compound and put
   elements outside the panel where overflow:hidden ate them. */
/* Re-measured live 2026-09-02 (DOM, not screenshots): #_top_menu is
   545x222 at stage (141,24) — the old 660x232@60,24 was inferred and wrong.
   Overflow must stay VISIBLE here because the scroll rod ends at x=726,
   40px past the panel's right edge; the content is clipped by .d-inner. */
.s-detail { position: absolute; left: 141px; top: 24px; width: 545px; height: 222px;
  box-sizing: border-box; overflow: visible; }
.s-detail .d-inner { position: absolute; inset: 0; padding: 0; margin: 0; overflow: hidden; }
.s-detail .d-row, .s-detail .d-bottom { position: static; }

/* two titles, both 15px regular in the reference red: one at the very top
   and one again above the description. The repeat is intentional. */
.s-detail h4 { position: absolute; margin: 0; font-size: 14px; font-weight: 700;
  color: #b91921; white-space: nowrap; letter-spacing: normal; }
/* ._top_name -> stage 169,37 = panel-relative 28,13 */
/* v225: moved for the 6px stroke clearance rule — see ADMIN.md v225. */
.s-detail .d-row h4 { left: 28px; top: 14px; }

/* ._top_avatar 168,56 -> 27,32 ; ._top_skills 305/400/495/590 @56 -> 164 on a
   95px pitch (77 + 18 gap), all 77x77 */
.s-detail .d-face { position: absolute; left: 27px; top: 32px; }
.s-detail .d-skills { position: absolute; left: 164px; top: 32px; display: flex; gap: 18px; }
.s-detail .d-face, .s-detail .d-skill, .s-detail .d-port {
  width: 77px; height: 77px; box-sizing: border-box; border: 1px solid #000;
  background: #cfc4a4; overflow: hidden; }

/* lower half, all measured: ._top_item_name 170,145 -> 29,121 ;
   ._top_item_desc 170,160 415x59 -> 29,136 ; ._top_item 590,147 -> 449,123 */
.s-detail .d-bio { position: absolute; left: 29px; top: 121px; width: 415px; height: 96px; }
.s-detail .d-bio h4 { position: static; display: block; margin: 0; line-height: 15px; height: 15px; }
/* uppercase, like every other description in the game. This rule overrides
   the .d-bio p block further up, so setting it there had no effect — the
   explicit text-transform:none here was what kept the bio in mixed case. */
.s-detail .d-bio p { margin: 0; width: 415px; height: 59px; overflow: auto;
  /* v191: 12 -> 10, and the line box with it (14 -> 12), so the 59px stays
     whole lines: 59/12 is four lines and 11px of slack where 59/14 was four
     lines and 3px. Same reason as the battle panel above. */
  font-size: 10px; font-weight: 400; color: #000; line-height: 12px;
  text-transform: uppercase; }
.s-detail .d-port { position: absolute; left: 449px; top: 123px; }

/* ENERGY sits top-right level with the skill title; its swatches are 10x10
   and there may be several side by side for a multi-type cost. */
/* ._top_energy 471,147 -> panel 330,123 -> 301,2 inside .d-bio */
.s-detail .d-titlerow { position: static; display: block; }
.s-detail .d-cost { position: absolute; left: 301px; top: 2px; width: 110px;
  display: flex; align-items: center; gap: 5px;
  font-size: 10px; font-weight: 400; color: #877956; }
.s-detail .d-cost .ck-sq { width: 10px; height: 10px; border: 1px solid #000; }

/* CLASSES bottom-left and COOLDOWN bottom-right, both pinned to the panel
   so they hold position however many lines the description runs to. */
/* ._top_classes 171,217 -> panel 30,193 -> 1,72 inside .d-bio ;
   ._top_cooldown 471,217 -> panel 330,193 -> 301,72 inside .d-bio.
   Both are pinned, not flexed, so a long CLASSES line cannot push COOLDOWN. */
.s-detail .d-meta { position: absolute; left: 0; top: 72px; width: 415px; height: 11px;
  display: block; font-size: 10px; font-weight: 400; color: #877956; }
.s-detail .d-classes { position: absolute; left: 1px; top: 0; width: 300px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.s-detail .d-cd { position: absolute; left: 301px; top: 0; width: 110px; white-space: nowrap; }

/* The skill-row pager. `.d-pager` used to live here — one button at
   left:503, top:7 — and NOTHING ever rendered it: the 2026-09-02 measurement
   put the rule in and select.js went on drawing an inline `.d-more` arrow at
   the end of the row instead. Re-measured live 2026-09-12, there are TWO,
   and they are one-way:

     ._top_pass_btt    fast-forward circle, 23x23, absolute top:7 right:19
                       -> panel-relative left 503 — shown only when a NEXT
                       page exists
     ._top_rewind_btt  rewind circle, 23x23, absolute top:7 left:163
                       -> the first tile's own left edge — shown only when a
                       PREVIOUS page exists

   Checked on three characters: a 4-skill kit shows neither;
   two 5-skill kits show the fast-forward on page 1 and the
   rewind, alone, on page 2. Both absolute against the panel, like every
   other child of .s-detail, rather than against the tile row — the row
   shrink-wraps to one tile on the last page and `right:0` would follow it. */
/* ONE position for both directions, and it is not the reference's. Theirs
   are at 163 and 503 with their tops at 7; ours is a single button that
   turns round on the last page, moved down and in from the right-hand one so
   it is clear of the scroll rod (which starts at left:552 and takes the
   click for itself). Asked for; a departure, recorded as one. */
/* Nudged right three times, asked for each time: 450 -> 460 (v182) -> 470
   (v183) -> 490 (v330, "moved to the right by 20px so it's closer to the
   margin", 2026-10-05 06:52). Still clear of the scroll rod at left:552 —
   490 + the 18px button = 508, so 44px of clearance. */
.s-detail .d-page, .s-detail .d-page-prev, .s-detail .d-page-next {
  position: absolute; left: 490px; top: 12px; z-index: 3; }
/* the press must not move it — it is small and it is being aimed at */
.s-detail .d-page:active { transform: none; }

/* ---- page arrows: yellow glow while held ---- */
.s-page { transition: filter .12s ease; }
.s-page:active,
.s-page.flash { filter: drop-shadow(0 0 6px #ffd400) drop-shadow(0 0 12px #ffd40088) brightness(1.15); }

/* ---- fatal connect error ----
   Shown when the battle page can't reach the server or has no session. A
   silent failure here left the player looking at a bare background. */
/* v188: the generated transformation note. Set apart from the authored
   description, because it is the game explaining itself rather than the
   character's own prose. */
.b-info .desc .tnote { color: #7a1607; font-style: italic; }

.b-fatal { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 420px; padding: 22px 26px; text-align: center; z-index: 60;
  background: linear-gradient(#f6efdc, #e6d7ae); border: 3px solid #b31408;
  box-shadow: 0 6px 24px #000a; box-sizing: border-box; }
.b-fatal h3 { margin: 0 0 8px; color: #bd262c; font-size: 16px; }
.b-fatal p { margin: 0 0 8px; font-size: 12px; color: #000; line-height: 1.45; }
.b-fatal p.hint { color: #5a4a2a; }
.b-fatal .btn { display: inline-block; margin-top: 6px; padding: 5px 14px; font-size: 12px; }

/* ---- online connect status ----
   Shown on the board while the match is being set up, and replaced by the
   board itself once the first state arrives. */
.b-connecting { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  padding: 12px 22px; z-index: 55; font-size: 13px; font-weight: bold; color: #f6efdc;
  background: #0009; border: 1px solid #ffffff33; border-radius: 4px; }


/* =====================================================================
   ROUND-START SWEEP
   MEASURED on the reference: an outgoing panel (338x488) leaves to the right
   over 0.55s on cubic-bezier(.55,.08,.68,.53); an incoming panel (214x526)
   follows from the right after a 0.35s delay over 0.65s on
   cubic-bezier(.17,.84,.44,1). The geometry and easing are matched; the
   panels themselves are ours.
   ===================================================================== */
.b-sweep { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 60; }
.b-sweep .sw { position: absolute; will-change: transform; }
.b-sweep .sw.out {
  left: 0; top: 72px; width: 338px; height: 488px;
  background: linear-gradient(105deg, #10639e 0%, #0b4b78 60%, #08456e 100%);
  box-shadow: 0 0 24px #0009; opacity: .92;
  animation: sw-out .55s cubic-bezier(.55, .08, .68, .53) both;
}
.b-sweep .sw.in {
  left: 482px; top: 34px; width: 214px; height: 526px;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(105deg, #c8352b 0%, #a32b22 60%, #7d1f18 100%);
  box-shadow: 0 0 24px #0009; opacity: .95;
  animation: sw-in .65s cubic-bezier(.17, .84, .44, 1) .35s both;
}
.b-sweep .sw.in span {
  font: 700 22px/1 "Trebuchet MS", Verdana, sans-serif; letter-spacing: 2px;
  color: #fff4e0; text-shadow: 0 2px 6px #0008; transform: rotate(-90deg); white-space: nowrap;
}
@keyframes sw-out { from { transform: translateX(-338px); } to { transform: translateX(770px); } }
/* The incoming panel sweeps in, holds briefly, then leaves the same way, so a
   missed render can never strand it over the board. */
@keyframes sw-in { 0% { transform: translateX(288px); } 45% { transform: translateX(0); }
                   70% { transform: translateX(0); } 100% { transform: translateX(288px); } }
@media (prefers-reduced-motion: reduce) { .b-sweep { display: none; } }


/* The v177 screen-brightness button is gone (v181): it was ours, not the
   reference's, and it stood in a row the reference fills with exactly four
   controls. Its --screen-bright custom property and the brightness() filter
   that used to sit on .screen / .s-detail went with it — that filter also
   made .screen a containing block for every absolutely positioned child,
   which is a side effect nothing should pay for a control nobody used. */


/* =====================================================================
   THE VOLUME CONTROL, DRAWN FROM THE REFERENCE'S OWN ART (v181)
   ---------------------------------------------------------------------
   v179 got the LAYOUT right and the DRAWING wrong. The row is theirs:

     _volume_controller_min              11x11  at left 0
     _volume_controller_bar_background   36x11  at left 12, opacity .4
     _volume_controller_bar              36x11  at left 12, clipped to level
     _volume_controller_plus             11x11  at left 52
     _volume_controller_mute              8x11  at left 67
     _volume_controller_red_bar          13x13  at left 64, only when muted

   and the row sits at 685,8 on the selection screen. What v179 invented was
   everything INSIDE those boxes. Read pixel by pixel off their four PNGs on
   2026-09-13 (drawn to a canvas and sampled, not eyeballed):

     minus  12x12 file: ONE bar, 10 wide x 2 tall, at x1 y5. Pure #fff.
                        No disc, no ring, no border.
     plus   12x12 file: THE SAME bar, plus a 2x10 vertical at x5 y1. #fff.
     mute    9x12 file: a speaker cone silhouette. #fff. Not a font glyph.
     muted  14x14 file: a diagonal slash, pure #f00, over the speaker.
     bar    40x12 file: a RAMP rising left to right, #fff, filled solid.

   Two corrections this makes to v179, both of which the user saw before I
   did:

     1. THE STUD. Ours drew a black disc with a white mark inside it. There
        is no disc. That is what made the minus and the plus read as two
        different controls: on a disc, one bar looks like a dash and two look
        like a crosshair, while bare on the background they are obviously the
        same drawing with one stroke added.
     2. THE BAR IS A WEDGE AFTER ALL. v179 rebaselined test_hover_polish from
        "segmented wedge" to "flat bar" — wrong on the second half. Their
        RSwKJXh.png is a triangular ramp; what was never there is the
        SEGMENTING. Track = the ramp at .4, level = the same ramp clipped by
        width, which is exactly how they do it (one drawing, twice).

   Colour: white on the selection screen, where the row floats over the
   banner art. In BATTLE it sits on a cream plate and reads dark — their ids
   there are literally suffixed `_black`. I could not read the battle row's
   computed colour without playing another match, so that half is taken from
   their id names and from how it reads in the battle capture, and is called
   out as such.

   This block is last in the file on purpose: it overrides the older sizing
   rules above rather than leaving them to be hunted down one at a time.
   Every part takes a data-skin key (see ASSET_LINKS) so real art replaces
   the CSS without touching this.
   ===================================================================== */
/* The row is 75x11 and every part is ABSOLUTELY placed at the reference's own
   left, because that is how theirs is built — four `position:absolute` rules
   at 0 / 12 / 52 / 67. v179 laid ours out with flex and gaps, which reproduces
   the same total width but not the same offsets: the leftovers get shared out
   by `justify-content`, so the bar landed at 14 instead of 12 and the speaker
   at 63 instead of 67. Absolute offsets cannot drift.
   It was 89 wide while we carried a brightness stud on the end; that stud is
   gone (v181), so 75 is now their number exactly. */
.s-vol, .bb-vol .volrow { position: absolute; display: block;
  width: 75px; height: 11px; gap: 0; justify-content: flex-start; }
/* --vol-ink is the whole colour story: one property, set per screen. */
.s-vol { --vol-ink: #fff; }
.bb-vol .volrow { --vol-ink: #151311; }   /* on the cream plate, like their _black ids */

.s-vol .vb, .bb-vol .vb {
  width: 11px; height: 11px; flex: none;
  cursor: pointer; box-sizing: border-box;
  background: none; border: 0; border-radius: 0; }
/* the horizontal stroke BOTH carry — one drawing, so they cannot drift.
   Centred with translate(-50%,-50%) and margin:0, NOT with negative margins:
   the older rule higher up the file already puts that translate on both
   pseudo-elements, so a negative margin stacks on top of it and shoves the
   stroke a further half-width left. That is what turned the plus into "|-". */
.s-vol .vb::before, .bb-vol .vb::before {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 10px; height: 2px; margin: 0; background: var(--vol-ink);
  transform: translate(-50%, -50%); }
/* and the vertical stroke only the plus adds */
.s-vol .vb::after, .bb-vol .vb::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 2px; height: 10px; margin: 0; background: var(--vol-ink);
  transform: translate(-50%, -50%); }
.s-vol .vb.minus::after, .bb-vol .vb.minus::after { content: none; }
.s-vol .vb:hover, .bb-vol .vb:hover { filter: brightness(1.25); }
.s-vol .vb:active, .bb-vol .vb:active { opacity: .6; }

/* the bar: one ramp, drawn twice. The clip-path shapes the track AND the
   fill inside it, so the level is literally the same picture cut short. */
.s-vol .wedge, .bb-vol .wedge {
  width: 36px; height: 11px; flex: none;
  box-sizing: border-box; border: 0; border-radius: 0; cursor: default;
  clip-path: polygon(0 100%, 100% 0, 100% 100%); background: none; }
/* the TRACK is its own layer, not `opacity` on .wedge: opacity applies to the
   whole subtree, so a .4 wedge can never hold a full-strength fill inside it. */
.s-vol .wedge::before, .bb-vol .wedge::before {
  content: ""; position: absolute; inset: 0; background: var(--vol-ink); opacity: .4; }
.s-vol .wedge::after, .bb-vol .wedge::after { content: none; background: none; }
.s-vol .wedge .vf, .bb-vol .wedge .vf {
  position: absolute; left: 0; top: 0; height: 100%; background: var(--vol-ink); }

/* the mute speaker: 8x11, a drawn cone, not a character */
.s-vol .spk, .bb-vol .spk {
  width: 8px; height: 11px; flex: none;
  cursor: pointer; font-size: 0; overflow: visible;
  background: var(--vol-ink);
  clip-path: polygon(0 30%, 45% 30%, 100% 0, 100% 100%, 45% 70%, 0 70%); }
/* muted: their 13x13 red slash, laid over the speaker from row-left 64 —
   3px left of the speaker's own box, which is why it is offset here */
.s-vol .spk.muted, .bb-vol .spk.muted { opacity: .45; }
.s-vol .spk::after, .bb-vol .spk::after { content: none; }
.s-vol .spk.muted::before, .bb-vol .spk.muted::before {
  content: ""; position: absolute; left: -3px; top: -1px; width: 13px; height: 13px;
  background: #f00; clip-path: polygon(78% 0, 100% 15%, 22% 100%, 0 85%); }

/* the placement carries `position` and `top` itself rather than leaning on a
   `.s-vol > *` rule: the per-part rules higher up the file are two classes
   deep and would out-specify a child selector and put `position: relative`
   back, which is exactly what floated the bar 17px down the first time. */
.s-vol .vb.minus,       .bb-vol .vb.minus       { position: absolute; top: 0; left: 0; margin: 0; }
.s-vol .wedge,          .bb-vol .wedge          { position: absolute; top: 0; left: 12px; margin: 0; }
.s-vol .vb:not(.minus), .bb-vol .vb:not(.minus) { position: absolute; top: 0; left: 52px; margin: 0; }
.s-vol .spk,            .bb-vol .spk            { position: absolute; top: 0; left: 67px; margin: 0; }
@media (prefers-reduced-motion: reduce) {
  .s-vol .vb:active, .bb-vol .vb:active { transform: none; }
}


/* =====================================================================
   THE GLOBAL CHAT  (v187) — an actual xat room
   ---------------------------------------------------------------------
   v186 built this from the REFERENCE's chat, which is Discord-shaped, and
   then skinned it. That was wrong in five structural ways, not cosmetic
   ones. `docs/specs/2026-09-14-xat-room-layout.md` has the measurement —
   xat.com/chat read at 1:1 on 2026-09-14 — and the short version is:

     a message is a TWO-LINE block with the avatar at the left, the name on
     its own line and the text in bold underneath;
     the emoji bar is ALWAYS VISIBLE, one horizontal row between the panes
     and the input;
     the user list is divided by solid GROUP BARS;
     VISITORS / FRIENDS sit at the FOOT of the user list;
     the input is a TALL box with a tall send beside it and a button column
     to its right.

   Their room is 728x486. Ours has to live on the 770x560 stage, and the
   OPEN CHAT button's own position is measured from naruto-arena (62x22 at
   (33,3)) — so the room is placed to fit the stage and the proportions are
   xat's. Where a number below is not in the spec it is ours, chosen to
   match what the image shows; their stylesheet is not reachable.
   ===================================================================== */
.s-chat-wrap { position: absolute; inset: 0; z-index: 42; background: #00000055; }

/* =====================================================================
   BORDER-BOX, EVERYWHERE IN THE ROOM.
   ---------------------------------------------------------------------
   Three separate 2px bugs have now come from the same place: a box given
   an exact pixel size AND a border, without box-sizing, so it renders two
   bigger than the number asked for and pushes everything beside it out.
   v186 shipped it on the avatar and the member list; v187 shipped it again
   on the side panel. Each was invisible by eye and each moved a measured
   layout off by exactly 2.

   Policing it per rule clearly does not work, so it is settled once here.
   Every measurement in this room is the WHOLE box, which is what a
   measurement is.
   ===================================================================== */
.s-chat, .s-chat *, .s-chat *::before, .s-chat *::after { box-sizing: border-box; }

/* 728x486 does not fit under a 22px button bar on a 560 stage, so the room
   is 700x500 at (35,30) — the same 1.5:1 proportion, centred. */
.s-chat { position: absolute; left: 35px; top: 30px; width: 700px; height: 500px;
  box-sizing: border-box; display: flex; flex-direction: column;
  font-family: var(--gm-font); color: #10202f;
  background: #b9c6d6; border: 1px solid #10202f;
  /* v191: ROUNDED, like a real xat room. `overflow: hidden` is what makes
     the radius actually cut — without it the square corners of the first and
     last children poke straight back out through it. */
  border-radius: 10px; overflow: hidden;
  box-shadow: inset 1px 1px 0 #ffffffb3, inset -1px -1px 0 #8593a6, 0 8px 26px #000a; }

/* =====================================================================
   THE THREE PANELS  (v191)
   ---------------------------------------------------------------------
   A xat room is a background picture with three translucent white panels
   floating on it: the message log, the typing box, and the member list.
   Ours painted all three OPAQUE, which is why it kept reading as "not a
   xat" however close the furniture got — the room's own background could
   only ever be seen in the 6px gaps between them.

   So: the picture moves to the ROOM, and each of the three panels is a
   white wash at 28% over it. 28% is the middle of the 25-30% asked for and
   is the number where 10px text still passes over a dark picture.

   `--chat-veil` so the whole set can be tuned in one place, and so the
   admin panel's background slider has something to talk to later.
   ===================================================================== */
.s-chat { --chat-veil: .28; }
/* the room's picture, behind everything, drawn once */
.s-chat::before { content: ""; position: absolute; inset: 0; z-index: 0;
  pointer-events: none; background-image: var(--chat-bg, none);
  background-size: cover; background-position: center; }
/* the four structural rows sit above it. NAMED, not `> *`: the room also
   holds the user card, the settings sheet and the note, and every one of
   those is `position: absolute` — a blanket `> * { position: relative }`
   would drop all three back into the flow and scatter the layout. */
.s-chat > .c-panes, .s-chat > .c-foottabs,
.s-chat > .c-emobar, .s-chat > .c-foot { position: relative; z-index: 1; }

/* ---- the two panes ---- */
.s-chat .c-panes { flex: 1; min-height: 0; display: flex; gap: 6px; padding: 5px 5px 0; }
/* THE MESSAGE PANE — a white wash, not a white box. The picture is on the
   room now (above), so this only has to veil it. */
.s-chat .c-main { flex: 1; min-width: 0; position: relative; box-sizing: border-box;
  background: rgba(255, 255, 255, var(--chat-veil));
  border: 1px solid #ffffff8c; border-radius: 8px; overflow: hidden; }
.s-chat .c-log { position: relative; height: 100%; overflow-y: auto; padding: 4px 5px; }
.s-chat .c-log::-webkit-scrollbar { width: 8px; }
.s-chat .c-log::-webkit-scrollbar-thumb { background: #97a5b8; border: 1px solid #78869a; }

/* ---- a message: avatar left, name on its own line, text bold under it ---- */
.s-chat .c-msg { display: flex; align-items: flex-start; gap: 7px; padding: 2px 3px;
  margin-bottom: 1px; }
.s-chat .c-msg:hover { background: #ffffffa8; }
.s-chat .c-av { width: 28px; height: 28px; flex: none; box-sizing: border-box;
  object-fit: cover; border: 1px solid #8593a6; background: #dbe3ec; }
.s-chat .c-block { min-width: 0; flex: 1; }
.s-chat .c-who { display: block; font-size: 10px; line-height: 12px; font-weight: 400;
  color: #2f6ea8; }
.s-chat .c-who.r-moderator { color: #1d7a2f; }
.s-chat .c-who.r-global_moderator { color: #9a5b00; }
.s-chat .c-who.r-administrator { color: #b31408; }
.s-chat .c-who.r-webmaster { color: #4999fd; }   /* v274: the reference's webmaster colour, read 2026-09-24 */
.s-chat .c-role { font-size: 7px; font-weight: 700; letter-spacing: .3px; margin-left: 4px;
  padding: 0 3px; border-radius: 2px; background: #2f6ea8; color: #fff; }
.s-chat .c-who.r-moderator .c-role { background: #1d7a2f; }
.s-chat .c-who.r-global_moderator .c-role { background: #9a5b00; }
.s-chat .c-who.r-administrator .c-role { background: #b31408; }
.s-chat .c-who.r-webmaster .c-role { background: #4999fd; }
.s-chat .c-text { display: block; font-size: 11px; line-height: 14px; font-weight: 700;
  color: #0b1620; word-wrap: break-word; overflow-wrap: anywhere; }
.s-chat .c-emote { width: 18px; height: 18px; vertical-align: -4px; }
/* v376: the time a message was said — the name line's own colour, quieter */
.s-chat .c-time { font-size: 8px; font-weight: 400; margin-left: 5px; opacity: .7; color: inherit;
  letter-spacing: .2px; white-space: nowrap; }
.s-chat .c-msg.k-system .c-time { display: block; margin-left: 0; font-size: 8px; line-height: 10px; color: #52637a; }
.s-chat .c-msg.k-system .c-text { font-weight: 400; font-style: italic; color: #52637a; }
.s-chat .c-msg.k-announce { background: #fff4d2; box-shadow: inset 3px 0 0 #e0a21c; }
.s-chat .c-del { flex: none; align-self: flex-start; color: #b31408; cursor: pointer;
  font-size: 9px; opacity: 0; padding: 0 3px; }
.s-chat .c-msg:hover .c-del { opacity: .85; }

/* ---- the user list: name bar, group bars, Visitors/Friends at the foot ---- */
/* 176 INCLUDING its border. The same 2px overflow bit the avatar and the
   member list in v186 and it is the same cause: a border on an element with
   no box-sizing. A measured size is the WHOLE box. */
/* THE MEMBER LIST — same wash, same radius */
.s-chat .c-side { width: 176px; flex: none; box-sizing: border-box;
  border-radius: 8px; overflow: hidden;
  display: flex; flex-direction: column;
  border: 1px solid #ffffff8c;
  background: rgba(255, 255, 255, var(--chat-veil)); }
.s-chat .c-roomname { flex: none; height: 20px; line-height: 20px; text-align: center;
  font-size: 10px; font-weight: 700; letter-spacing: .4px; color: #3a2a08;
  background: linear-gradient(#f0b232, #d99312); border-bottom: 1px solid #8593a6; }
.s-chat .c-users { flex: 1; min-height: 0; overflow-y: auto; }
.s-chat .c-users::-webkit-scrollbar { width: 8px; }
.s-chat .c-users::-webkit-scrollbar-thumb { background: #97a5b8; border: 1px solid #78869a; }
/* the group strip — xat's Idle / Cellar, ours per role */
.s-chat .c-group { height: 20px; line-height: 20px; text-align: center;
  font-size: 9px; font-weight: 700; letter-spacing: .4px; color: #fff;
  background: #5a6ff0; margin: 3px 0 1px; }
.s-chat .c-group.g-staff { background: #b31408; }
.s-chat .c-group.g-members { background: #2f6ea8; }
.s-chat .c-group.g-muted { background: #7a6a55; }
.s-chat .c-user { display: flex; align-items: center; gap: 5px; padding: 1px 6px;
  font-size: 10px; cursor: pointer; }
.s-chat .c-user:hover { background: #fff; }
.s-chat .c-uav { width: 16px; height: 16px; flex: none; box-sizing: border-box;
  object-fit: cover; border: 1px solid #8593a6; background: #cfd8e4; }
.s-chat .c-uname { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-chat .c-tabs { flex: none; display: flex; border-top: 1px solid #8593a6; }
.s-chat .c-tab { flex: 1; height: 22px; line-height: 22px; text-align: center;
  font-size: 9px; font-weight: 700; cursor: pointer; color: #40506a;
  background: #cfd8e4; border-right: 1px solid #8593a6; }
.s-chat .c-tab:last-child { border-right: 0; }
.s-chat .c-tab.on { background: #f2f6fb; color: #10202f; }
.s-chat .c-tab .badge { display: inline-block; min-width: 11px; height: 11px; padding: 0 2px;
  margin-left: 3px; box-sizing: border-box; border-radius: 6px;
  background: #d62929; color: #fff; font-size: 7px; line-height: 11px; }

/* ---- the room tab under the message pane ---- */
.s-chat .c-foottabs { flex: none; display: flex; gap: 6px; padding: 3px 5px 0; }
.s-chat .c-foottab { height: 22px; line-height: 22px; padding: 0 12px;
  font-size: 9px; font-weight: 700; color: #40506a; background: #cfd8e4;
  border: 1px solid #8593a6; border-bottom: 0; cursor: pointer;
  display: flex; align-items: center; gap: 4px; }
.s-chat .c-foottab.on { background: #f2f6fb; color: #10202f; }
.s-chat .c-foottab .meg { color: #e0a21c; }
.s-chat .c-foottab .badge { min-width: 11px; height: 11px; padding: 0 2px; box-sizing: border-box;
  border-radius: 6px; background: #d62929; color: #fff; font-size: 7px; line-height: 11px;
  text-align: center; }

/* ---- the emoji bar: ALWAYS VISIBLE, one horizontal row ---- */
.s-chat .c-emobar { flex: none; height: 34px; box-sizing: border-box;
  display: flex; align-items: center; gap: 3px; padding: 0 5px;
  background: #cfd8e4; border-top: 1px solid #8593a6; border-bottom: 1px solid #8593a6;
  overflow: hidden; }
.s-chat .c-emobar .strip { flex: 1; min-width: 0; display: flex; gap: 3px;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
.s-chat .c-emobar .strip::-webkit-scrollbar { height: 0; }
.s-chat .c-emo { width: 28px; height: 28px; flex: none; box-sizing: border-box;
  padding: 2px; cursor: pointer; background: none; border: 1px solid transparent; }
.s-chat .c-emo:hover { background: #fff; border-color: #8593a6; }
.s-chat .c-emo img { width: 22px; height: 22px; display: block; }
.s-chat .c-emobar .xtra { flex: none; display: flex; gap: 3px; align-items: center;
  padding-left: 5px; border-left: 1px solid #a8b5c6; }
.s-chat .c-xbtn { height: 24px; padding: 0 7px; font-family: inherit; font-size: 9px;
  font-weight: 700; cursor: pointer; border: 1px solid #78869a; border-radius: 2px;
  background: linear-gradient(#f4f8fc, #cfd8e4); color: #10202f; }
.s-chat .c-xbtn:hover { filter: brightness(1.06); }
.s-chat .c-xbtn.on { background: linear-gradient(#3d6fa5, #1d3d63); color: #fff; }

/* ---- the input row: a TALL box, a tall send, a button column ---- */
.s-chat .c-foot { flex: none; height: 76px; box-sizing: border-box;
  display: flex; gap: 6px; padding: 6px 5px; background: #b9c6d6; }
/* THE TYPING BOX — the third panel. A textarea cannot carry a ::before, so
   the wash is its own background; it is layered over the room's picture by
   `background-blend-mode` doing nothing here, simply by being translucent. */
.s-chat .c-input { flex: 1; min-width: 0; box-sizing: border-box; resize: none;
  padding: 5px 7px; font-family: inherit; font-size: 11px; line-height: 15px;
  color: #10202f; background: rgba(255, 255, 255, calc(var(--chat-veil) + .45));
  border: 1px solid #ffffff8c; border-radius: 8px; outline: none; }
.s-chat .c-input:focus { border-color: #2f6ea8;
  background: rgba(255, 255, 255, calc(var(--chat-veil) + .58)); }
.s-chat .c-send { width: 30px; flex: none; box-sizing: border-box; cursor: pointer; font-size: 15px;
  background: linear-gradient(#f4f8fc, #cfd8e4); border: 1px solid #78869a;
  color: #10202f; border-radius: 2px; }
.s-chat .c-send:hover { filter: brightness(1.06); }
.s-chat .c-btns { width: 176px; flex: none; display: flex; flex-direction: column; gap: 5px; }
.s-chat .c-btn { flex: 1; font-family: inherit; font-size: 10px; font-weight: 700;
  cursor: pointer; color: #fff; border: 1px solid #7a4a08; border-radius: 3px;
  background: linear-gradient(#f0b232, #d08c0c); }
.s-chat .c-btn:hover { filter: brightness(1.06); }
.s-chat .c-btn.quiet { color: #10202f; border-color: #78869a;
  background: linear-gradient(#f4f8fc, #cfd8e4); }
.s-chat .c-note { position: absolute; left: 6px; bottom: 84px; right: 6px;
  font-size: 9px; color: #40506a; pointer-events: none; }
.s-chat .c-note.bad { color: #b31408; font-weight: 700; }

/* ---- the user card: click a name, act on them ---- */
.s-chat .c-card { position: absolute; z-index: 6; width: 186px; padding: 8px;
  background: #f2f6fb; border: 1px solid #10202f; border-radius: 8px;
  box-shadow: 0 6px 18px #0007; font-size: 10px; }
.s-chat .c-card h6 { margin: 0; font-size: 11px; }
/* v191: the card opens with WHO, not with what you may do to them */
.s-chat .c-cardhead { display: flex; gap: 7px; align-items: center;
  padding-bottom: 6px; border-bottom: 1px solid #c3cedd; }
.s-chat .c-cardav { width: 34px; height: 34px; flex: none; box-sizing: border-box;
  object-fit: cover; border: 1px solid #8593a6; background: #dbe3ec; border-radius: 4px; }
.s-chat .c-cardwho { min-width: 0; }
.s-chat .c-cardrole { font-size: 9px; color: #40506a; }
.s-chat .c-cardstats { padding: 5px 0 2px; }
.s-chat .c-cardstats .ln { display: flex; justify-content: space-between; gap: 8px;
  line-height: 15px; }
.s-chat .c-cardstats .ln .k { color: #40506a; }
.s-chat .c-cardstats .ln .v { font-weight: 700; text-align: right; }
.s-chat .c-cardlink { display: block; margin-top: 2px; font-size: 9px; color: #2f6ea8; }
.s-chat .c-card .row { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 5px; }
.s-chat .c-card .c-xbtn { font-size: 8px; padding: 0 5px; height: 20px; }

/* ---- the room settings sheet ---- */
.s-chat .c-settings { position: absolute; inset: 0; z-index: 7; background: #eef3f9f5;
  padding: 12px 14px; overflow-y: auto; font-size: 10px; }
.s-chat .c-settings h5 { margin: 0 0 8px; font-size: 12px; color: #14406e; }
.s-chat .c-settings label { display: block; margin: 8px 0 3px; font-size: 9px;
  font-weight: 700; color: #40506a; }
.s-chat .c-settings input[type=text], .s-chat .c-settings input[type=range] {
  width: 100%; box-sizing: border-box; font-family: inherit; font-size: 10px;
  padding: 3px 6px; border: 1px solid #8593a6; background: #fff; }

/* =====================================================================
   THE OPEN CHAT BUTTON  (v187)
   ---------------------------------------------------------------------
   Beside the language pill, the same size as it, and a placeholder like
   every other button.

   The language pill is 52x24 at (10,8). This sits at (68,8) — a 6px gap —
   so the two cannot touch whatever art either of them is given. Both
   numbers are derived from the pill's own rule six hundred lines up rather
   than typed twice; if the pill moves, move this with it.

   v186 put it at the reference's (33,3) 62x22, which is where THEIR chat
   button lives — but their top bar has no language pill in it, and ours
   does, so the two overlapped.
   ===================================================================== */
.s-chat-btn { position: absolute; left: 10px; top: 36px; width: 52px; height: 24px;
  border-radius: 12px; z-index: 30; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center; gap: 3px;
  cursor: pointer; user-select: none; font-family: var(--gm-font);
  font-size: 9px; font-weight: 700; letter-spacing: .3px;
  color: rgba(255, 255, 255, .92); background: rgba(17, 35, 54, .62);
  border: 1px solid rgba(255, 255, 255, .18); border-radius: 3px; }
.s-chat-btn { background: linear-gradient(#243a5c, #16243c); border: 1px solid #4a6a9a;
  box-shadow: 0 1px 3px #0006; color: #dce8fa; font-size: 8px; }
.s-chat-btn:hover { filter: brightness(1.15); }
/* art pasted into btn_chat.png replaces the drawn pill, frame and all */
.s-chat-btn.skinned { border-color: transparent; box-shadow: none; background: none;
  background-size: 100% 100%; background-repeat: no-repeat; color: transparent; }
.s-chat-dot { position: absolute; top: -5px; right: -5px; min-width: 12px; height: 12px;
  padding: 0 2px; box-sizing: border-box; border-radius: 6px; background: #d62929;
  color: #fff; font-size: 8px; font-weight: 700; line-height: 12px; text-align: center; }
/* v304 — THREE KINDS OF NEW. Red (top right) = chat lines, orange (top left)
   = announcements, violet "@n" (bottom right) = somebody tagged you. Each
   counts to 99, then "99+". The same three colours sit on the room's foot
   tabs, and a line that tags you is tinted in the log. */
.s-chat-dot.ann { right: auto; left: -5px; background: #ef8a17; }
.s-chat-dot.tag { top: auto; bottom: -6px; background: #7a3fe0; box-shadow: 0 0 0 1px #fff8; }
.s-chat .c-foottab .badge.b-ann { background: #ef8a17; }
.s-chat .c-foottab .badge.b-tag { background: #7a3fe0; }
.s-chat .c-tag { color: #3a63b8; font-weight: 700; }
.s-chat .c-tag.me { color: #fff; background: #7a3fe0; padding: 0 3px; }
.s-chat .c-link { color: inherit; text-decoration: underline; }   /* v376: a link to this site */
.s-chat .c-msg.c-mentioned { background: rgba(122, 63, 224, .14); box-shadow: inset 3px 0 0 #7a3fe0; }
.s-chat .c-tagpick { position: absolute; left: 8px; bottom: 76px; z-index: 40;
  display: flex; flex-direction: column; background: #f2f6fb; border: 1px solid #8593a6;
  box-shadow: 0 2px 6px #0005; }
.s-chat .c-tagpick:empty { display: none; }
.s-chat .c-tagopt { padding: 3px 10px; font-size: 10px; font-weight: 700; color: #10202f; cursor: pointer; }
.s-chat .c-tagopt:hover, .s-chat .c-tagopt:first-child { background: #dfe6f0; }
.s-chat .c-tagbtn { margin-top: 6px; }
.s-tagtoast { position: fixed; left: 50%; top: 14px; transform: translateX(-50%); z-index: 9000;
  max-width: 320px; padding: 8px 12px; background: #2a1650; color: #f3ecff;
  border: 1px solid #7a3fe0; box-shadow: 0 4px 14px #0008;
  font-family: var(--gm-font); font-size: 11px; cursor: pointer; }
.s-tagtoast b { color: #c9a8ff; }
.s-tagtoast-body { margin-top: 3px; font-size: 10px; opacity: .85; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }

/* =====================================================================
   v189 — SKILL DESCRIPTION MARKUP  (.dtag)
   ---------------------------------------------------------------------
   The reference does not print a skill description as one colour of text.
   Every mechanical phrase inside it is tinted by WHAT IT IS, a named skill
   carries its own 13px icon inline, and a phrase the skill forbids is
   struck through in red on top of its own colour.

   MEASURED 2026-09-14 — pixel samples taken off the four in-game
   description shots, because the characters-and-skills page strips the
   markup and these colours exist only on the battle/detail sheet:

       Effects       "STUNNED"                   rgb(0,168,72)
       Defense       destructible defence        rgb(24,120,144)
       Defense       struck ("cannot reduce …")  rgb(24,120,144) + rgb(252,0,0) rule
       Improvements  "replaced" / "improved"     rgb(120,0,132)
       SkillName     'Gum-Gum Pistol'            rgb(216,0,72)
       Damage        "deals 30 damage"           rgb(192,48,0)
       Classes       "Physical, Melee"           rgb(0,156,120)
       Invisible     "invisible"                 rgb(180,24,24)
       parchment ground                          rgb(240,228,192)

   `SkillText` is the eighth tag in their vocabulary — it holds a whole
   character's kit as one idea (`<name>'s skills`, 32 uses). ITS COLOUR WAS
   NOT MEASURED: none of the four in-game sheets sampled on 2026-09-14
   contained one, and the characters-and-skills page strips the markup, so
   there was nothing to sample. Rather than invent a plausible tint it is
   left at body colour and said so here. If one turns up in a later sheet,
   sample it and fill this in. Every other tag above is a real pixel
   reading.

   Two boxes wear these: `.b-info .desc` (the battle panel) and
   `.s-detail .d-desc` (the selection detail sheet). They are styled
   together because a phrase that is teal in one and black in the other is
   the bug this file exists to prevent.
   ===================================================================== */
/* v191: EVERY TAG IS UNDERLINED, and every tag answers.
   Asked for: "all elements you coloured should also be underlined, and when
   you click them it should redirect to it (if it's a skill) or get a hover
   description if it's an effect". Colour alone announced that a phrase meant
   something and then refused to say what.
   The underline is `text-decoration-color: currentColor` by default, so it
   takes each tag's own ink and the struck rule below can still be red on top
   of it. `text-underline-offset` keeps it off the descenders at 10px. */
.b-info .desc .dtag,
.s-detail .d-desc .dtag { font-weight: inherit;
  text-decoration: underline; text-underline-offset: 1px;
  text-decoration-thickness: 1px; }
/* A skill name is a LINK. The cursor CANNOT say so on these two screens —
   `html body.ingame *` sets the sword with !important, deliberately, and one
   cursor in game is the rule. The affordance is therefore the underline plus
   the hover lift, and the hint card that appears says "Click to open their
   page" in words. The `cursor` below is kept for the day these spans appear
   on a page that is not a game screen. */
.b-info .desc .dtag.clickable,
.s-detail .d-desc .dtag.clickable { cursor: pointer; }
.b-info .desc .dtag.clickable:hover,
.s-detail .d-desc .dtag.clickable:hover { filter: brightness(1.25); }
/* a mechanic explains itself — the cursor says "ask me", not "click me" */
.b-info .desc .dtag.explained,
.s-detail .d-desc .dtag.explained { cursor: help; }
/* the inline skill icon sits ON the text baseline, not above it, and never
   stretches the line — theirs is the same height as the capital letters */
.b-info .desc .dtag .sprite,
.b-info .desc .dtag img,
.s-detail .d-desc .dtag .sprite,
.s-detail .d-desc .dtag img {
  display: inline-block; vertical-align: -2px; margin-right: 3px;
  image-rendering: pixelated; }

.b-info .desc .t-damage,       .s-detail .d-desc .t-damage       { color: rgb(192,48,0); }
.b-info .desc .t-defense,      .s-detail .d-desc .t-defense      { color: rgb(24,120,144); }
.b-info .desc .t-skillname,    .s-detail .d-desc .t-skillname    { color: rgb(216,0,72); }
.b-info .desc .t-effects,      .s-detail .d-desc .t-effects      { color: rgb(0,168,72); }
.b-info .desc .t-classes,      .s-detail .d-desc .t-classes      { color: rgb(0,156,120); }
.b-info .desc .t-improvements, .s-detail .d-desc .t-improvements { color: rgb(120,0,132); }
.b-info .desc .t-invisible,    .s-detail .d-desc .t-invisible    { color: rgb(180,24,24); }
.b-info .desc .t-skilltext,    .s-detail .d-desc .t-skilltext    { color: inherit; }

/* v376: KEYWORDS OFF (the Settings menu; NA.setDescKeywords). Every tag is
   the description's own ink, no underline, no skill icon — a denied phrase
   keeps its rule, in the same ink, because "cannot" is still part of what
   the sentence says. */
html.na-plain-desc .b-info .desc .dtag,
html.na-plain-desc .s-detail .d-desc .dtag { color: inherit !important; text-decoration: none !important; filter: none !important; }
html.na-plain-desc .b-info .desc .dtag.struck,
html.na-plain-desc .s-detail .d-desc .dtag.struck { text-decoration: line-through !important; text-decoration-color: currentColor !important; }
html.na-plain-desc .b-info .desc .dtag .sprite, html.na-plain-desc .b-info .desc .dtag img,
html.na-plain-desc .s-detail .d-desc .dtag .sprite, html.na-plain-desc .s-detail .d-desc .dtag img { display: none !important; }

/* A FORBIDDEN PHRASE. "…cannot be countered", "…cannot reduce damage" — the
   phrase keeps its own colour and gets a red rule through it, so you can
   still read WHAT is being denied. `text-decoration-color` rather than a
   second colour on the text: setting `color:red` would lose the teal and
   the phrase would stop reading as a defence phrase at a glance. */
/* a denied phrase keeps the underline AND gains the red rule — the two say
   different things (this is a mechanic; this mechanic is being forbidden) */
.b-info .desc .dtag.struck,
.s-detail .d-desc .dtag.struck {
  text-decoration: underline line-through;
  text-decoration-color: rgb(252,0,0);
  -webkit-text-decoration-color: rgb(252,0,0);
  text-decoration-thickness: 1px; }
/* the icon in front of a struck skill name is not itself struck */
.b-info .desc .dtag.struck .sprite,
.s-detail .d-desc .dtag.struck .sprite,
.b-info .desc .dtag.struck img,
.s-detail .d-desc .dtag.struck img { text-decoration: none; }


/* =====================================================================
   v227 — WHEN A SURFACE HAS ITS OWN PICTURE, WE DRAW NOTHING ELSE ON IT
   ---------------------------------------------------------------------
   Asked 2026-09-20: on every supplied background, "if you had any other
   popup/stroke/overlay other than buttons and text, remove them, because
   they are overlapping. It only has to stay background + text + buttons".

   Each of these panels was drawn by us before any art existed — a frame, a
   radius, a drop shadow, and on three of them a whole painted rod or end
   cap. Every one of those is now inside the supplied picture, so ours sits
   on top of it and doubles the stroke.

   `border-color: transparent`, NEVER `border: 0`.
   The border is part of the box. Removing it grows the padding box by twice
   its width and every absolutely-positioned child inside shifts — and on
   `.b-dialog` that is 3px on a panel whose children are all written as
   "theirs minus 3". Making the border transparent keeps the geometry
   identical to the pixel and draws nothing; the background reaches under it
   because `background-clip` is `border-box` by default, so the art fills the
   whole box including the band where the frame used to be.

   Anything hidden here is hidden ONLY while that surface's key is filled, so
   emptying a key in the panel puts our own drawing back rather than leaving
   a bare box. */
.has-art-bg_charlist    .s-panel,
.has-art-bg_description .s-detail .paper,
.has-art-bg_filter      .s-filter,
.has-art-bg_teammanager .s-tm,
.has-art-bg_missions    .s-quests,
.has-art-bg_missions    .s-styles,
.has-art-bg_endturn     .b-dialog.endturn,
.has-art-bg_exchange    .b-dialog.exchange,
.has-art-bg_chat        .b-dialog.chat,
.has-art-bg_surrender   .b-dialog.sur,
.has-art-bg_winner      .overlay .card.result-card.win,
.has-art-bg_loser       .overlay .card.result-card.lose {
  border-color: transparent; border-radius: 0; box-shadow: none;
  background-size: 100% 100%; background-repeat: no-repeat; }


/* =====================================================================
   v228 — THE LOADING MARK ONLY WHILE THE ART IS STILL COMING
   ---------------------------------------------------------------------
   Asked 2026-09-20: "you're using the loading icon wrong. It shouldn't be
   here at all unless a skill image is loading (at the beginning of the
   game)."

   v226 put it on each slot's base rule, so it sat under every tile for the
   whole match — invisible behind a loaded picture, but visible through any
   sprite cell that is transparent, for ever.

   `.art-warm` already exists and already means exactly this: both screens
   add it 400ms after their first draw (battle.js:218, select.js:238), and
   the fade-in rules beneath it are gated the same way. The mark is now
   scoped to `.screen:not(.art-warm)`, so it shows during the warm-up and
   never again. Nothing new had to be invented to know when loading ends. */
.screen:not(.art-warm) .s-cell,
.screen:not(.art-warm) .s-team .slot,
.screen:not(.art-warm) .b-skill,
.screen:not(.art-warm) .b-portrait,
.screen:not(.art-warm) .b-info .pic {
  background-image: var(--asset-loading, url("/assets/imgur/75qY9tm.png"));
  background-position: center; background-repeat: no-repeat;
  /* v259: THE MARK IS THE SIZE OF THE PICTURE IT STANDS IN FOR.
     It was 24x24 centred in the box, and 32x32 for the two 77px slots —
     numbers nobody measured. Theirs, read off their own stylesheet
     2026-09-21: `._mc_skillbutton_image { width:54px; height:54px;
     background-image:url(.../img_loading.png); background-size:54px }`.
     The box is 54 and the mark is 54 — it FILLS the slot, exactly.
     All five of ours are square (50, 50, 56, 77, 77), so 100% 100% is
     their rule written for boxes of different sizes and distorts nothing.
     The 32px override below it is gone: it existed only to make the old
     fixed number look less wrong on the two big slots. */
  background-size: 100% 100%; }

/* v273: a character named in a quest's goal is a link to its page — in the
   goal's own colour, underlined, opening in a new tab */
.s-quests .q-dtext a.task-char { color: inherit; text-decoration: underline; }
.s-quests .q-dtext a.task-char:hover { opacity: .75; }   /* the reference dims on hover, never recolours (test_hover_polish) */

/* v273: the Styles pickers carry three footer buttons — RETURN, SAVE, CLOSE —
   106 wide each, 24 apart, centred on the 558 holder (96, 226, 356). SAVE
   is dimmed until a different row has been picked. */
.s-quests .q-foot.f3a { left: 96px; }
.s-quests .q-foot.f3b { left: 226px; }
.s-quests .q-foot.f3c { left: 356px; }
.s-quests .q-foot.off { opacity: .45; cursor: default; }
.s-quests .q-foot.off:hover { filter: none; }

/* =====================================================================
   THE CHARACTER FILTER PANEL  (v273)
   ---------------------------------------------------------------------
   In the Styles holder (.s-quests.s-styles: 558x504 at stage 106,28, with
   Styles' own background picture). The option groups scroll in the box the
   quests filter sheet uses (30,84 / 478 wide), chips are the quests chips,
   and the count sits above the footer. Square and flat, like every panel
   since v271-v272.
   ===================================================================== */
.s-cfilter .cf-sheet { position: absolute; left: 30px; top: 84px; width: 478px; height: 344px;
  box-sizing: border-box; padding: 2px 6px 8px 0; overflow-y: auto; overflow-x: hidden;
  scrollbar-width: thin; scrollbar-color: #b9a878 transparent; }
.s-cfilter .cf-sheet::-webkit-scrollbar { width: 5px; }
.s-cfilter .cf-sheet::-webkit-scrollbar-thumb { background: #b9a878; border-radius: 0; }
.s-cfilter .cf-row { margin-bottom: 9px; }
.s-cfilter .cf-name { width: 100%; height: 22px; box-sizing: border-box; padding: 0 8px;
  font-family: inherit; font-size: 10px; color: #000; background: rgba(247, 229, 197, .8);
  border: 1px solid rgba(0, 0, 0, .35); border-radius: 0; outline: none; }
.s-cfilter .cf-name:focus { border-color: rgba(0, 0, 0, .7); }
.s-cfilter .q-chip.dead { opacity: .45; }
.s-cfilter .cf-count { position: absolute; left: 30px; top: 432px; width: 478px; height: 14px;
  text-align: center; font-size: 9px; font-weight: 700; letter-spacing: .4px; color: #000; }

/* v274: the Titles picker shares its 340px with the colour/effect chooser —
   the list keeps the top 150 (five rows at 30px, then it scrolls), the
   chooser the 184 under it; the note line at 428 stays where it was. */
.s-styles .st-list.titles { height: 150px; }
.s-styles .st-styler { position: absolute; left: 30px; top: 240px; width: 478px; height: 184px;
  overflow-y: auto; overflow-x: hidden; box-sizing: border-box; padding: 2px 4px 0 0; }
.s-styles .st-styler .tst-label:first-child { margin-top: 0; }

/* =====================================================================
   v288 THE IN-GAME LOGIN — after LOGOUT on the selection screen.
   Their geometry, read out of their stylesheet 2026-09-27 (select.js has
   the table): the 770x560 dim at #000/.25, the 391x290 holder at (203,101),
   the two 140x20 boxes, the red error line, the 88x25 OK at (172,220).
   The holder is .s-found — the same one every popup here already uses.
   ===================================================================== */
.s-login-dim { position: absolute; left: 0; top: 0; width: 770px; height: 560px; background: #000; opacity: .25; }
#select .s-found.s-login { position: absolute; left: 203px; top: 101px; width: 391px; height: 290px; }
.s-login-text { position: absolute; left: 100px; top: 60px; width: 255px; font-size: 14px; text-align: center; line-height: 17px; }
.s-login-lbl { position: absolute; left: 100px; font-size: 15px; }
.s-login-lbl.user { top: 115px; } .s-login-lbl.pass { top: 150px; }
.s-login-box { position: absolute; left: 175px; width: 140px; height: 20px; box-sizing: border-box; border: 1px solid #000;
  font-size: 15px; color: #000; outline: 0; padding: 0 3px; background: #fff; }
.s-login-box.user { top: 114px; } .s-login-box.pass { top: 147px; }
.s-login-err { position: absolute; left: 90px; top: 185px; width: 260px; text-align: center; color: #c00; font-size: 14px; }
#select .s-found .s-found-btn.s-login-ok { position: absolute; left: 172px; top: 220px; width: 88px; height: 25px; }
#select.s-leaving > * { transition: opacity .35s; opacity: 0; }

/* v293: the reconnect banner (battle.js "_closed") — ours, the reference has
   no socket to lose (its turns are HTTP requests) */
.b-reconnect { position: fixed; top: 8px; left: 50%; transform: translateX(-50%); z-index: 9999;
  background: #f4e4bc; color: #bd262c; border: 2px solid #5a3a14; padding: 6px 14px;
  font: 700 12px/1.3 Arial, sans-serif; letter-spacing: .5px; box-shadow: 0 2px 8px rgba(0,0,0,.4); }
.b-reconnect a { color: #5a3a14; }

/* v299: a live online match hides the way out (battle.js matchLive) */
body.match-live .home-link { display: none; }
/* v299: the RECONNECTING scroll on the selection screen — the end-turn
   popup's own box, background and rod (.b-dialog.endturn), over the dim */
#select .s-rejoin { position: absolute; inset: 0; z-index: 60; }
#select .s-rejoin .b-dim { z-index: 0; }
#select .s-rejoin .b-dialog.rejoin { z-index: 1; text-align: center; padding-top: 44px; }
#select .s-rejoin .b-dialog.rejoin h3 { font-size: 18px; color: #8f1408; margin-bottom: 16px; }
#select .s-rejoin .rj-line { margin: 0 0 6px; font-size: 13px; color: #101010; font-weight: bold; }
#select .s-rejoin .rj-count { font-size: 44px; line-height: 52px; font-weight: bold; color: #c01818; margin: 4px 0 8px; }
#select .s-rejoin .rj-note { margin: 0; font-size: 11px; color: #3a2a10; }
/* v299: AVAILABLE ONLY sits left of FILTERS, same button */
.s-quests .q-filterbtn.q-availbtn { left: 283px; }
/* v299 THE WHOLE TASK. Asked 2026-09-28: "the whole task is written there
   and make it smaller" — a long goal ("win 3 games with Luffy and …") was
   cut to one line with an ellipsis. It wraps now, one size down. */
.s-quests .q-drow { height: auto; min-height: 27px; }
.s-quests .q-dtext { height: auto; min-height: 16px; font-size: 11px; line-height: 13px; align-items: flex-start; }
.s-quests .q-dtext span { white-space: normal; overflow: visible; text-overflow: clip; }
.s-quests .q-dtext strong { padding-top: 2px; }
/* v299: the kit in the info box (battle.js infoPanel, mode "char"), to the
   owner's mockup v3 of 2026-09-28: face 60px and 10px in, name and icons
   down, description as it was, no selection border */
.b-info.kit .pic { width: 60px; height: 60px; margin-left: 10px; margin-top: 8px; overflow: hidden; }
.b-info.kit .ttl { top: -3px; }
.b-info.kit .desc { margin-top: 5px; }
.b-info.kit .k-top { position: absolute; right: 14px; top: 20px; display: flex; align-items: center; gap: 10px; }
.b-info.kit .k-top .corner { position: static; }
.b-info.kit .k-strip { display: flex; align-items: center; gap: 3px; }
.b-info.kit .k-sk { width: 22px; height: 22px; border: 1px solid #101010; overflow: hidden; cursor: pointer; flex: none; }
.b-info.kit .k-arrow { flex: none; width: 12px; height: 22px; line-height: 22px; text-align: center; font-size: 10px;
  color: #faf4e1; background: #101010; cursor: pointer; }
/* v300: a skill's sheet in the kit panel — its name 10px and its text 5px
   lower than the character's (asked 2026-09-28 15:31) */
.b-info.kit.k-skill .ttl { top: 7px; }
.b-info.kit.k-skill .desc { margin-top: 10px; }

/* =====================================================================
   v301 BLACK AND SQUARE. Asked 2026-09-28 17:06: "all the rounded corner
   buttons (from selection 'styles' manager etc and others) make them
   squarish instead. Also, there's a slightly red color on those elements
   and those pages. Instead make the stroke and the text black."
   MEASURED 2026-09-28 on haki-arena.site/select (computed styles, every
   visible element): radius 7px on FILTER / PIRATE QUESTS / STYLES and the
   team-manager button, 12px on the language pill, 3px on CHAT; red-brown
   (154,30,9 / #591307 / #701506 / #c01818) on every panel's frame, titles,
   kickers, arrows, chips, search box and footer buttons. Every rule of the
   selection panels (.gm-page kit, quests, styles, filter, team manager,
   character filter, bottom row) had those colours swapped for black and
   its radius set to 0 (the pre-v301 file is the record of the old values).
   The footer buttons were red FILLS with white text; they keep their
   shape and become the cream face with a black stroke and black text.
   ===================================================================== */
.s-quests .q-foot, .s-styles .q-foot, .s-tm .q-foot, .gm-page .q-foot {
  background: #f4e1bf; color: #000; border: 1px solid #000; }
.s-quests .q-foot:hover, .s-styles .q-foot:hover, .gm-page .q-foot:hover { background: #e9d3a8; }
.s-lang, .s-lang .flag, .s-chat-btn { border-radius: 0; }
/* v301: the quest in the holder, as the website's quest page draws it —
   its own classes (style.css .pq-*), laid in two columns inside the
   measured 508x362 detail box. The picture keeps its shape (263x175 art ->
   250x166 here); the cards' pictures stop cropping too: the measured 198x112
   box is kept (test_v182) and the art is contained in it, the box's own
   #e7dcc0 showing either side. */
.s-quests .q-detail.q-web { display: flex; gap: 12px; font-family: "Trebuchet MS", Verdana, Geneva, sans-serif;
  font-size: 11px; line-height: 16px; color: #2b2318; text-transform: none; }
.s-quests .q-web .q-wcol { width: 246px; flex: none; overflow-y: auto; max-height: 362px; scrollbar-width: thin; }
.s-quests .q-web .q-wcol.right { width: 250px; }
.s-quests .q-web .pq-block { width: 100%; padding-bottom: 10px; }
.s-quests .q-web .pq-info { float: none; }
.s-quests .q-web .q-dpic { position: static; width: 250px; height: 166px; margin: 0 0 10px; }
.s-quests .q-web .q-dpic img { object-fit: contain; }
.s-quests .q-web .pq-face { margin: 4px 0; }
.s-quests .q-web .st-av { margin: 4px 0; }
.s-quests .q-web .q-wtitle { display: inline-block; margin: 4px 0; padding: 3px 9px; border: 1px solid #000;
  background: #f4e1bf; font-weight: 700; font-size: 12px; color: #0c3552; }
.s-quests .q-pic img { object-fit: contain; }

/* =====================================================================
   v330 THE QUEST CARDS ARE THE WEBSITE'S CARDS. Asked 2026-10-05 06:52:
   "quests inside selection screen now have borders but they also have
   some margins … remove the margins and only apply the border to the
   image, as it is without margins. Also bring the text (font) in exactly
   the same style as it is on website quests page (all - title, body,
   requirements etc)".
   MEASURED that day on haki-arena.site/select: a card's 263x175 picture
   was contained (v301) in the 198x112 box, so it drew 165x110 with 15px of
   the box's #e7dcc0 either side INSIDE the 1px frame (v287) — the
   "margins". The detail's 250x166 box did the same by a pixel. Now the
   frame is on the picture itself, at the picture's own shape, and the box
   around it is see-through.
   The text is the website card's (missions/east-blue, measured the same
   day): select.js now builds it with the website's own classes
   (style.css .pq-title/.pq-h/.pq-req/.r-in/.pq-label/.pq-met/.pq-done/
   .pq-h6/.pq-locked); these rules only undo the v272 Franklin capitals
   that the old q-* classes still carry on the same nodes.
   ===================================================================== */
.s-quests .q-card { height: auto; padding: 5px 10px 8px; gap: 4px;
  font-family: "Trebuchet MS", Verdana, Geneva, sans-serif; font-size: 11px; line-height: 16px; color: #2b2318; }
.s-quests .q-card .q-name { height: 17px; line-height: 16px; font-size: 11px; color: var(--blue-ink, #0c3552);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-quests .q-card .q-name .pq-h { font: bold 11px/normal "Trebuchet MS", arial, verdana, sans-serif; color: var(--blue-ink, #0c3552); }
.s-quests .q-card .q-req { width: 100%; height: auto; margin: 2px 0 0; display: block; align-self: stretch;
  font-size: 11px; line-height: 16px; color: #2b2318; }
.s-quests .q-card .q-req .pq-title { height: 17px; }
.s-quests .q-card .q-rank, .s-quests .q-card .q-view { height: auto; font-size: 11px; line-height: 16px;
  font-weight: 400; text-transform: none; letter-spacing: normal; color: #2b2318; }
.s-quests .q-card .q-label { font-weight: 400; }
.s-quests .q-card .q-met, .s-quests .q-card .q-unmet { font-weight: 400; }
.s-quests .q-card .q-view.locked { color: var(--muted, #8a7c63); font-style: italic; }
/* the picture: no box of its own, the frame on the art */
.s-quests .q-pic, .s-quests .q-dpic { border: 0; background: none; overflow: visible; }
.s-quests .q-pic { display: flex; align-items: center; justify-content: center; }
.s-quests .q-pic img { width: auto; height: auto; max-width: 100%; max-height: 100%;
  border: 1px solid #000; box-sizing: border-box; object-fit: fill; }
.s-quests .q-web .q-dpic { width: 250px; height: auto; }
.s-quests .q-web .q-dpic img { width: 250px; height: auto; border: 1px solid #000; box-sizing: border-box; object-fit: fill; }
.s-quests .q-pic .q-pic-blank, .s-quests .q-dpic .q-pic-blank { border: 1px solid #000; box-sizing: border-box; }
.s-quests .q-web .q-dpic .q-pic-blank { height: 166px; }
/* v330: the cards are WHITE, all of them, requirements box included —
   asked 2026-10-05 07:03: "make their whole background white instead, no
   more of that color" (was #f7e5c5c2, the parchment tint) */
.s-quests .q-card, .s-quests .q-card .r-box { background: #fff; }

/* =====================================================================
   v331 SIZE, FULLSCREEN, THE PHONE, THE HOME-SCREEN APP (js/screenmode.js)
   ---------------------------------------------------------------------
   Asked 2026-10-05 08:31; the canvas "Haki-Arena Fullscreen & Mobile" was
   agreed at 08:42. The board keeps its 770x560 layout; body.game-window
   (v186, above) scales it with --fit-scale, and these rules add what sits
   AROUND it: the filled sides, the phone's side strips, the rotate screen,
   the resume chip and the install card — all outside the scaled .screen,
   in the page's own pixels, so a 56px button is 56px on the phone.
   ===================================================================== */
/* the button: CHAT's own look and size, 6px to its right (CHAT is 6px right
   of the language pill: 10 + 52 + 6 = 68, 68 + 52 + 6 = 126) */
/* v364 THE TOP-LEFT BUTTONS ARE A COLUMN. Asked 2026-10-14 10:01: "put
   language, chat, size buttons under each other and add 1 more under them
   with ... Wars". Each is the pill's 52x24 at x 10, 4px apart: language 8,
   CHAT 36, SIZE 64, WARS 92 — clear of everything (the game modes start at
   255), measured by test_v364_render's pairwise overlap check. */
.s-size-btn { left: 10px; top: 64px; gap: 3px; }
.s-wars-btn { left: 10px; top: 92px; }
/* v376: SETTINGS takes the language pill's place (8), CHAT stays at 36 and
   WARS moves up into SIZE's (64) — the column is one pill shorter */
.s-set-btn { left: 10px; top: 8px; gap: 2px; font-size: 6.5px; letter-spacing: 0; padding: 0 2px; }
.s-set-btn svg { flex: none; }
#select .s-wars-btn { top: 64px; }
.na-size-menu.na-set-menu { width: 200px; }
.na-size-menu.na-set-menu .na-size-item { height: auto; min-height: 22px; padding-top: 3px; padding-bottom: 3px; white-space: nowrap; }
.na-set-h { height: 14px; padding: 0 8px; display: flex; align-items: center; font-size: 7px; font-weight: 700;
  letter-spacing: .6px; color: #f4e1bf; background: #3a2a14; user-select: none; }
.na-set-menu .na-set-h + .na-size-item { border-top: 0; }
.na-set-lang { display: inline-flex; align-items: center; gap: 6px; }
.na-set-lang .flag { width: 15px; height: 10px; position: relative; overflow: hidden; flex: none; box-shadow: 0 0 0 1px #0007; }
.na-set-note { padding: 4px 8px 6px; font-size: 7px; line-height: 9px; color: #3a2a14; border-top: 1px solid #000; }
.s-size-btn svg { flex: none; }
/* in battle: on top of the SURRENDER / OPEN CHAT / sound staircase, under
   the third character. Measured 2026-10-05 on the local board (padding box,
   where these lefts and tops are written): the third character's health bar
   ends at y 400 and SURRENDER's plate starts at y 426 (its fan reaching left
   to x 43) — so 52x24 at (6,401) sits in the gap and touches nothing. */
.s-size-btn.b-size-btn { left: 6px; top: 401px; }
/* the battle's own type rule (test_battle_type: no bold or tracked text on
   the battle board, as the reference's) — the pill keeps CHAT's look, its
   word is set plain */
#battle .s-size-btn { font-weight: 400; letter-spacing: 0; }
/* the menu: square, 1px black (the standing rule, 2026-10-05 06:24) */
.na-size-menu { position: absolute; z-index: 45; width: 120px; box-sizing: border-box;
  background: #f4e1bf; border: 1px solid #000; border-radius: 0; box-shadow: 0 2px 6px #0006;
  font-family: var(--gm-font, "Libre Franklin", Arial, sans-serif); }
.na-size-item { height: 22px; padding: 0 8px; display: flex; align-items: center; justify-content: space-between;
  font-size: 9px; font-weight: 700; color: #000; cursor: pointer; letter-spacing: .3px; user-select: none; }
.na-size-item + .na-size-item { border-top: 1px solid #000; }
.na-size-item:hover, .na-size-item:focus { background: #e9d3a8; }
.na-size-item.on { background: #000; color: #f4e1bf; }
.na-size-item .na-key { border: 1px solid currentColor; padding: 0 3px; font-size: 8px; line-height: 10px; }

/* the sides of a wide screen: the screen's own picture, blurred and dark */
body.game-window .screen { z-index: 1; }
.na-bands { display: none; }
body.game-window.na-bands-art .na-bands { display: block; position: fixed; inset: -40px; z-index: 0;
  background: #0e2a44 var(--na-band-art) center / cover no-repeat; filter: blur(16px) brightness(.42) saturate(.9);
  pointer-events: none; }
body.game-window.na-fullscreen { background: #232323; }
body.na-fs-pending .screen { transition: none; }

/* the phone held sideways: the side strips are the controls */
.na-rail { position: fixed; top: 0; bottom: 0; z-index: 30; width: var(--na-band-w, 0px); box-sizing: border-box;
  display: flex; flex-direction: column; justify-content: center; gap: 12px;
  padding: max(12px, env(safe-area-inset-top)) 12px max(12px, env(safe-area-inset-bottom)); }
.na-rail.left { left: 0; padding-left: max(12px, env(safe-area-inset-left)); }
.na-rail.right { right: 0; padding-right: max(12px, env(safe-area-inset-right)); }
/* the phone's buttons are CHAT's pill (the game's own button look), at CHAT's
   52:24 with width and height LINKED (asked 2026-10-05 09:05: "if you need
   to resize anything, do it with W and H linked"): 130 wide is 60 tall; a
   narrower strip makes them smaller, never squashed. Art in btn_home /
   btn_chat / btn_size / btn_sound fills the same 52:24 box. */
.na-rail-btn { width: min(130px, 100%); aspect-ratio: 52 / 24; height: auto; min-height: 0; align-self: center; box-sizing: border-box;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; padding: 2px 4px;
  background: linear-gradient(#243a5c, #16243c); border: 1px solid #4a6a9a; border-radius: 0; box-shadow: 0 1px 3px #0006;
  color: #dce8fa; cursor: pointer; font-family: var(--gm-font, "Libre Franklin", Arial, sans-serif); font-size: 12px; font-weight: 700;
  letter-spacing: .4px; -webkit-tap-highlight-color: transparent; }
.na-rail-btn:active { filter: brightness(1.2); }
.na-rail-btn.skinned { border-color: transparent; box-shadow: none; background-color: transparent;
  background-size: 100% 100%; background-repeat: no-repeat; color: transparent; }
.na-rail-btn.skinned svg { visibility: hidden; }
.na-rail-main { display: flex; align-items: center; gap: 6px; }
.na-rail-sub { font-size: 9px; font-weight: 400; color: #9fb6d6; letter-spacing: 0; }

/* the next page of a fullscreen session */
.na-fs-resume { position: fixed; top: 8px; left: 50%; transform: translateX(-50%); z-index: 50;
  display: flex; border: 1px solid #000; background: #f4e1bf; box-shadow: 0 2px 8px #0008;
  animation: na-chip-in .25s ease both; }
.na-fs-resume button { border: 0; border-radius: 0; background: none; cursor: pointer; color: #000;
  font-family: var(--gm-font, "Libre Franklin", Arial, sans-serif); font-weight: 700; font-size: 11px; height: 28px; }
.na-fs-go { display: flex; align-items: center; gap: 6px; padding: 0 12px; }
.na-fs-x { width: 28px; border-left: 1px solid #000 !important; font-size: 15px; }
.na-fs-resume button:hover { background: #e9d3a8; }
@keyframes na-chip-in { from { opacity: 0; transform: translate(-50%, -8px); } to { opacity: 1; transform: translate(-50%, 0); } }

/* held upright */
.na-rotate { position: fixed; inset: 0; z-index: 70; background: #0e2a44; color: #f4e1bf;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px;
  padding: 0 36px; text-align: center; font-family: var(--gm-font, "Libre Franklin", Arial, sans-serif); }
.na-rotate-h { font-size: 22px; font-weight: 700; }
.na-rotate-p { font-size: 15px; line-height: 22px; color: #b9cbe3; }
.na-rotate-home { margin-top: 14px; width: 220px; height: 48px; display: flex; align-items: center; justify-content: center; gap: 8px;
  background: #f4e1bf; color: #000; border: 1px solid #000; border-radius: 0; font: 700 14px var(--gm-font, "Libre Franklin", Arial, sans-serif); cursor: pointer; }

/* add to home screen */
.na-inst { position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, .55);
  display: flex; align-items: center; justify-content: center; padding: 12px; box-sizing: border-box; }
.na-inst-card { width: 440px; max-width: 100%; max-height: 100%; overflow: auto; box-sizing: border-box;
  background: #f4e1bf; border: 1px solid #000; border-radius: 0; padding: 18px 22px; color: #2b2318;
  display: flex; flex-direction: column; gap: 10px; font-family: var(--gm-font, "Libre Franklin", Arial, sans-serif); }
.na-inst-top { display: flex; gap: 14px; align-items: center; }
.na-inst-icon { width: 56px; height: 56px; border: 1px solid #000; flex: none; }
.na-inst-h { font-size: 16px; font-weight: 700; color: #0c3552; }
.na-inst-host { font-size: 12px; color: #5a4a2a; }
.na-inst-p { font-size: 13px; line-height: 19px; }
.na-inst-step { display: flex; align-items: center; gap: 10px; font-size: 13px; line-height: 18px; }
.na-inst-step svg { vertical-align: -3px; color: #0c3552; }
.na-inst-n { width: 26px; height: 26px; flex: none; background: #000; color: #f4e1bf; font-weight: 700;
  display: flex; align-items: center; justify-content: center; }
.na-inst-dont { display: flex; align-items: center; gap: 8px; font-size: 12px; cursor: pointer; }
.na-inst-dont input { width: 16px; height: 16px; margin: 0; accent-color: #000; }
.na-inst-row { display: flex; gap: 10px; }
.na-inst-row button { height: 44px; border: 1px solid #000; border-radius: 0; cursor: pointer;
  font: 700 13px var(--gm-font, "Libre Franklin", Arial, sans-serif); }
/* the selection panels' own footer buttons (v301 .q-foot: cream face, black
   stroke, black text; square) */
.na-inst-yes { flex: 1; background: #f4e1bf; color: #000; }
.na-inst-no { width: 120px; background: #f4e1bf; color: #000; font-weight: 400; }
.na-inst-row button:hover { background: #e9d3a8; }

/* the phone: no page around the board, no text selection while tapping */
body.na-mobile { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; overscroll-behavior: none; }
body.na-mobile input, body.na-mobile textarea { -webkit-user-select: text; user-select: text; }

/* =====================================================================
   v332 A PHONE HELD UPRIGHT (js/screenmode.js)
   ---------------------------------------------------------------------
   2026-10-05 11:14: "on phone i should always have the phone resolution,
   even on browser". v331 covered an upright phone with a "turn your phone
   sideways" screen (z-index 70, over the install card's 60 — so the card
   never showed upright either). Now the board fits the phone's width
   (the same fit as sideways), the strips' buttons sit in one row under
   it, and a line over it says sideways is bigger. Panel -> Screen & App
   can still choose the v331 message.
   ===================================================================== */
.na-under { position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; height: var(--na-band-h, 0px); box-sizing: border-box;
  display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8px;
  padding: 0 max(12px, env(safe-area-inset-right)) env(safe-area-inset-bottom) max(12px, env(safe-area-inset-left)); }
/* four across at most, each still CHAT's 52:24 with W and H linked */
.na-under .na-rail-btn { flex: none; width: min(130px, calc((100vw - 48px) / 4)); align-self: center; }
.na-under-hint { position: fixed; left: 0; right: 0; top: 0; z-index: 30; height: var(--na-band-h, 0px);
  display: flex; align-items: center; justify-content: center; padding: env(safe-area-inset-top) 16px 0; box-sizing: border-box;
  color: #dce8fa; text-align: center; font: 400 13px/18px var(--gm-font, "Libre Franklin", Arial, sans-serif); pointer-events: none; }
.na-inst-dots { font-size: 15px; }

/* =====================================================================
   v333 THE PHONE FILLS ITS SCREEN (js/screenmode.js, body.na-wide)
   ---------------------------------------------------------------------
   Asked 2026-10-05 13:20: "full screen (with no margins). You can zoom in
   the backgrounds, increase the space among objects and do whatever else
   is necessary with only changing both Width and height of an element (so
   it doesnt stretch)".
   The board keeps its 770x560 layout and its one scale. --na-ex / --na-ey
   are what is left of the glass across / down, in the board's own px.
   Every object below is MOVED to its anchor with `translate` (its own
   property: an object's transform animations and hovers are untouched),
   never resized — except the small top-row pills, which grow with W and H
   linked (`scale`) so a thumb can hit them. An object not named here stays
   in the middle (translate 0), which is where every popup belongs.
   Anchor a (0 = left/top, .5 = middle, 1 = right/bottom) moves an object
   by (a - .5) x E, because the board itself is drawn in the middle.
   ===================================================================== */
body.na-wide { --na-hx: calc(var(--na-ex, 0px) / 2); --na-hy: calc(var(--na-ey, 0px) / 2);
  /* G = g - 1: how much the big pieces grow (13:31 "make all the elements a
     bit bigger if you have enough space"). `scale` grows a piece about its
     own centre; the extra translate (C - P) x G — written with 50% of the
     piece's own box — makes a GROUP grow about one point P together. */
  --na-G: calc(var(--na-g, 1) - 1); --na-Gx: calc(var(--na-gx, 1) - 1);
  --na-Gp: calc(var(--na-gp, 1) - 1); --na-Gm: calc(var(--na-gm, 1) - 1); }
body.na-wide .screen { overflow: visible; border: none; box-shadow: none; }
/* the background: the screen's own picture, covering the whole glass
   (background-size: cover on the bigger box — zoomed, never stretched) */
body.na-wide .screen::before { content: ""; position: absolute; z-index: -1; pointer-events: none;
  left: calc(var(--na-hx) * -1); right: calc(var(--na-hx) * -1); top: calc(var(--na-hy) * -1); bottom: calc(var(--na-hy) * -1);
  background: inherit; background-position: var(--na-bgpos, 50% 0%); }
body.na-wide .na-bands { display: none !important; }
/* a layer over the whole board covers the whole glass; what it places by
   px goes back to the middle */
body.na-wide .screen > .na-cover { left: calc(var(--na-hx) * -1) !important; right: calc(var(--na-hx) * -1) !important;
  top: calc(var(--na-hy) * -1) !important; bottom: calc(var(--na-hy) * -1) !important; width: auto !important; height: auto !important; }
body.na-wide .screen > .na-cover > .na-cshift { translate: var(--na-csx, var(--na-hx)) var(--na-csy, var(--na-hy)); }
/* v336: what each child needs is MEASURED (screenmode.js markCovers): a
   child placed by % (.b-dialog's left: calc((100% - 391px) / 2)) is already
   at the glass's middle once its cover grows, and v333's flat half-margin
   pushed it off — the RECONNECTING scroll sat right of and below the
   middle on a phone (read live 2026-10-05 20:5x). A child that stretches
   with the cover (.b-dim, inset 0) is not moved at all. While measuring,
   the cover is put back to the board's own box for one layout. */
body.na-wide .screen > .na-cover.na-cmeasure { left: 0 !important; right: auto !important; top: 0 !important; bottom: auto !important;
  width: 100% !important; height: 100% !important; }

/* ---- the selection screen ---- */
/* top-left ROW on the glass (phone, fullscreen fill): language, CHAT, SIZE,
   WARS, HOME — x1.5, W and H linked, CHAT's 6px gaps (52 x 1.5 = 78; lefts
   10, 94, 178, 262, 346). v364: the desktop board stacks them in a column
   (asked 2026-10-14 10:01); here they stay a row, because upright the
   character's detail opens under the top row and a column would run into it
   (test_v333_render C, test_v364_render E2). */
body.na-wide #select .s-lang      { transform-origin: 0 0; scale: 1.5; translate: calc(var(--na-hx) * -1) calc(var(--na-hy) * -1); }
body.na-wide #select .s-chat-btn  { transform-origin: 0 0; scale: 1.5; top: 8px; left: 94px; translate: calc(var(--na-hx) * -1) calc(var(--na-hy) * -1); }
body.na-wide #select .s-size-btn  { left: 178px; }
body.na-wide #select .s-wars-btn  { left: 262px; }
body.na-wide #select .s-home-btn  { left: 346px; }
/* v376: SETTINGS first, in the language pill's place; WARS and HOME one up */
body.na-wide #select .s-set-btn   { left: 10px; }
body.na-wide #select .s-wars-btn  { left: 178px; top: 8px; }
body.na-wide #select .s-home-btn  { left: 262px; }
body.na-wide #select .s-home-btn svg { flex: none; }
.s-home-btn { gap: 3px; }
/* the volume: top right, x1.5 from its right edge */
body.na-wide #select .s-vol { transform-origin: 100% 0; scale: 1.5; translate: var(--na-hx) calc(var(--na-hy) * -1); }
/* the four game modes: spread edge to edge, the room shared out between them */
/* the modes grow up from their bottom (it stays 3px over the panel) by gm,
   at most x1.15 — then their top meets the bottom of a character's open
   detail scroll (246); the outer two grow inward so the row stays on the glass */
body.na-wide #select .s-modes { translate: 0 calc(var(--na-ey, 0px) * -0.05); }
body.na-wide #select .s-modes > .s-mode { scale: var(--na-gm, 1); }
body.na-wide #select .s-modes > .s-mode:nth-child(1) { translate: calc(var(--na-hx) * -1 + 50% * var(--na-Gm)) calc(-50% * var(--na-Gm)); }
body.na-wide #select .s-modes > .s-mode:nth-child(2) { translate: calc(var(--na-ex, 0px) * -1 / 6) calc(-50% * var(--na-Gm)); }
body.na-wide #select .s-modes > .s-mode:nth-child(3) { translate: calc(var(--na-ex, 0px) / 6) calc(-50% * var(--na-Gm)); }
body.na-wide #select .s-modes > .s-mode:nth-child(4) { translate: calc(var(--na-hx) - 50% * var(--na-Gm)) calc(-50% * var(--na-Gm)); }
/* a character's detail scroll opens over the top, as on the board; upright
   it goes to the top of the glass, just under the top row (which ends at
   y 46 at x1.5) — in the middle it would land on the modes (measured
   2026-10-05: LADDER / QUICK / PRIVATE stopped answering taps) */
body.na-wide #select > .s-detail { translate: 0 calc(var(--na-hy) * -1 + min(22px, var(--na-hy))); }
/* the character panel, its scroll and its page arrows: centred, at the
   bottom, grown together by gp about the panel's TOP centre P = (400.5, 315)
   — down into the 23px under it (panel 52..749 x 315..537; x1.09 at most) */
body.na-wide #select .s-panel, body.na-wide #select .s-page, body.na-wide #select .s-scroll { scale: var(--na-gp, 1); }
body.na-wide #select .s-panel { translate: calc((52px - 400.5px) * var(--na-Gp) + 50% * var(--na-Gp))
  calc(var(--na-hy) + 50% * var(--na-Gp)); }
body.na-wide #select .s-page.up { translate: calc((50px - 400.5px) * var(--na-Gp) + 50% * var(--na-Gp))
  calc(var(--na-hy) + (332px - 315px) * var(--na-Gp) + 50% * var(--na-Gp)); }
body.na-wide #select .s-page.down { translate: calc((492px - 400.5px) * var(--na-Gp) + 50% * var(--na-Gp))
  calc(var(--na-hy) + (457px - 315px) * var(--na-Gp) + 50% * var(--na-Gp)); }
/* the scroll is placed by its RIGHT edge (x 92) and its width is its art's:
   its left is 92px - 100% */
body.na-wide #select .s-scroll { translate: calc((92px - 400.5px) * var(--na-Gp) - 50% * var(--na-Gp))
  calc(var(--na-hy) + (306px - 315px) * var(--na-Gp) + 50% * var(--na-Gp)); }

/* ---- the battle ---- */
/* the top bar: you on the left edge, the opponent on the right, the turn in the middle */
body.na-wide #battle .b-top { translate: 0 calc(var(--na-hy) * -1); }
/* v334 (15:55 "avatars and player detail closer to the timer bar"): the
   two players stay where the desktop has them, 15px either side of the turn,
   and grow by gx OUTWARD from their inner top corner (p1 top-right, p2
   top-left) — v333 had sent them to the glass's edges */
body.na-wide #battle .b-top > .b-player { scale: var(--na-gx, 1); }
body.na-wide #battle .b-top > .b-player.p1 { translate: calc(-50% * var(--na-Gx)) calc(50% * var(--na-Gx)); }
body.na-wide #battle .b-top > .b-player.p2 { translate: calc(50% * var(--na-Gx)) calc(50% * var(--na-Gx)); }
/* the turn in the middle keeps its size: grown, its EXCHANGE ENERGY line
   (bottom 86) would reach the first skill row (top 89) */
/* your team on the left edge, theirs on the right; upright, the three rows
   share the height (15%, 50%, 85%) */
body.na-wide #battle .b-col.ally { translate: calc(var(--na-hx) * -1) calc(var(--na-hy) * -1); }
body.na-wide #battle .b-col.foe  { translate: var(--na-hx) calc(var(--na-hy) * -1); }
body.na-wide #battle .b-col .b-unit.r0 { translate: 0 calc(var(--na-ey, 0px) * 0.15); }
body.na-wide #battle .b-col .b-unit.r1 { translate: 0 calc(var(--na-ey, 0px) * 0.5); }
body.na-wide #battle .b-col .b-unit.r2 { translate: 0 calc(var(--na-ey, 0px) * 0.85); }
/* your skills follow your characters, with room between (anchor .15) */
/* v334 (15:55 "skill bars in the same position (next to the facepictures
   and a bit under them) as desktop"): the rows go with your team — moved
   exactly as your column is (-E/2), so each sits beside and a little under
   its face as on the desktop. Grown by g from their left edge; the first
   keeps its top (the turn ends 3px above it), the last its bottom (the
   announcer starts there), the middle one grows both ways — rows 121
   apart, 79 tall, so x1.2 leaves 18px */
body.na-wide #battle .b-lane { scale: var(--na-g, 1); }
body.na-wide #battle .b-lane.s0 { translate: calc(var(--na-hx) * -1 + 50% * var(--na-G)) calc(var(--na-ey, 0px) * -0.35 + 50% * var(--na-G)); }
body.na-wide #battle .b-lane.s1 { translate: calc(var(--na-hx) * -1 + 50% * var(--na-G)) 0; }
body.na-wide #battle .b-lane.s2 { translate: calc(var(--na-hx) * -1 + 50% * var(--na-G)) calc(var(--na-ey, 0px) * 0.35 - 50% * var(--na-G)); }
body.na-wide #battle .b-size-btn { translate: calc(var(--na-hx) * -1) calc(var(--na-ey, 0px) * 0.35); }
/* the bottom: announcer and SURRENDER / OPEN CHAT / sound on the left, the info panel on the right */
body.na-wide #battle .b-bottom { translate: 0 var(--na-hy); }
body.na-wide #battle .b-bottom > .b-announcer, body.na-wide #battle .b-bottom > .bb-btns { translate: calc(var(--na-hx) * -1) 0; }
/* v334: grown by gi from its bottom-right corner (screenmode.js sets gi so
   its top stays under the last enemy's health bar and clear of your last
   skill row) */
body.na-wide #battle .b-bottom > .b-info { scale: var(--na-gi, 1); translate: calc(var(--na-hx) - 50% * (var(--na-gi, 1) - 1)) calc(-50% * (var(--na-gi, 1) - 1)); }

/* =====================================================================
   v334 THE PHONE, AFTER A FIRST LOOK ON A REAL ONE (2026-10-05 15:55/15:56)
   ===================================================================== */
/* the stage is as tall as the glass the page measured, not 100vh: on a phone
   100vh is the height with the browser's bars hidden, so the board fitted to
   the visible height was centred in a taller box and its bottom row went
   under the bar ("last row of characters is cut"). Its WIDTH stays 100vw:
   a measured width goes stale for a moment when the phone turns, and a
   stage wider than the glass makes a phone's browser widen the page to fit
   it, which it then measures as the new width — stuck (found by
   test_v332_render turning a phone back upright, 2026-10-05) */
body.game-window .stage { height: var(--na-vh, 100vh); }
/* no double-tap zoom while tapping skills */
body.na-mobile { touch-action: manipulation; }
/* "button homepage wider so it fits the whole text inside": the HOME pill
   takes its label's width (HOMEPAGE in the installed app) — its height and
   its x1.5 stay */
body.na-wide #select .s-home-btn { width: auto; min-width: 52px; padding: 0 5px; box-sizing: border-box; white-space: nowrap; }
/* "theres a size button in battle. remove it, its useless" — on a phone the
   battle fills the glass already */
body.na-mobile #battle .b-size-btn { display: none; }
/* "make the end turn/exchange energy popup bigger (so - and + are bigger and
   i can actually hit them)": x1.75 about its centre (391x290 -> 684x508,
   inside the 560-high board; the - / + boxes 20x15 -> 35x26) — surrender too */
body.na-mobile #battle .b-dialog.endturn, body.na-mobile #battle .b-dialog.exchange, body.na-mobile #battle .b-dialog.sur { scale: 1.75; }
/* upright the height is plenty and the width (770) is the limit: x1.9 (743 wide) */
body.na-mobile.na-portrait #battle .b-dialog.endturn, body.na-mobile.na-portrait #battle .b-dialog.exchange, body.na-mobile.na-portrait #battle .b-dialog.sur { scale: 1.9; }


/* =====================================================================
   v335 THE FACES ABOVE THE SKILL BARS, ON A PHONE (2026-10-05 18:25:
   "skillbars are over the facepictures. they should be underneath")
   ---------------------------------------------------------------------
   .b-unit is z-index 8 so a face sits ABOVE the skill bars (.b-lane, 5)
   and stays clickable as a target. On the board that works because
   .b-col has no stacking context of its own. v333 moves .b-col with
   `translate` when a phone fills its screen, and a translated box IS a
   stacking context — at z-index auto, i.e. level 0 — so the 8 inside it
   only counted inside the column, and the whole column, faces and health
   bars included, painted under the bars. The column carries the faces'
   own 8 now; it holds nothing but the three units, so this is the board's
   order exactly. (Below .b-top 9, .b-bottom 10, the dim 20, dialogs 21.)
   ===================================================================== */
body.na-wide #battle .b-col { z-index: 8; }

/* =====================================================================
   v336 — THE PHONE'S KEYBOARD (screenmode.js keyboardLift). While a text
   box has the focus on a phone, the board keeps the size it was fitted to
   and slides up by --na-kb, so the box sits just above the keyboard
   (2026-10-05 21:03: "check if messages can be sent from mobile in chat").
   ===================================================================== */
body.na-kb.game-window .stage { translate: 0 calc(var(--na-kb, 0px) * -1); }

/* v364 — A WAR GAME on the selection screen. Its details are a popup in
   the game's own holder (.s-found, the private game's), so nothing is laid
   over the screen itself (asked 2026-10-14 09:43); the picture slot holds
   the war's lines instead of the wheel. The barred cells are the grid's own. */
.s-found .s-war-info { flex-direction: column; gap: 3px; padding: 6px; color: #2a2010;
  font: bold 10px/13px Verdana, Arial, sans-serif; letter-spacing: .3px; text-align: center; }
.s-found .s-war-info > div { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-found .s-war-info .s-war-vs { font-size: 12px; color: #b31408; }
.s-found .s-war-info .s-war-score { font-size: 14px; line-height: 17px; }
.s-cell.war-blocked { opacity: .3; filter: grayscale(1); position: relative; }
.s-cell.war-blocked::after { content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(45deg, transparent 46%, #c00 46%, #c00 54%, transparent 54%),
              linear-gradient(-45deg, transparent 46%, #c00 46%, #c00 54%, transparent 54%); }

/* v364: the WARS list popup — the game's holder, a list where the picture
   slot is, a row per set waiting on the player */
.s-found .s-wars-list { width: 330px; height: 150px; flex: none; overflow-y: auto; box-sizing: border-box;
  background: #00000012; border: 2px solid #b3140840; padding: 4px; text-align: left; }
.s-found .s-wars-row { display: flex; align-items: center; gap: 6px; padding: 3px 4px; cursor: pointer;
  font: bold 10px/14px Verdana, Arial, sans-serif; color: #2a2010; border-bottom: 1px solid #b3140820; }
.s-found .s-wars-row:hover { background: #fff6; }
.s-found .s-wars-row .w { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-found .s-wars-row .sc { flex: none; color: #b31408; }
.s-found .s-wars-empty { padding: 30px 8px; text-align: center; font: bold 10px/14px Verdana, Arial, sans-serif; color: #2a2010; }
.s-wars-btn .s-chat-dot { background: #b31408; }

/* =====================================================================
   v365 THE CREW WARS WINDOW (js/wars-game.js). Asked 2026-10-14 15:58:
   "dont redirect me to the crew wars page from the website, instead create
   one there (just like pirate quests one work like)" and "crew wars text
   ... doesnt fit into its button size. make it smaller, in 1 row, and the
   button longer". It is the quests' holder (.s-quests, 558x504), and every
   label in it is ONE ROW: nowrap, a small size, and a button as long as
   its word (test_v365_render measures each one against its own box).
   Measured on haki-arena.site 2026-10-14 13:03Z: the v364 popup's
   "CREW WARS" was 15px in a 90x27 button, its text 50px tall in a 25px
   line (two rows).
   ===================================================================== */
.s-wars-win .wg-tabs { position: absolute; left: 30px; top: 80px; width: 498px; height: 22px; display: flex; gap: 4px; }
.s-wars-win .wg-tab { flex: 1 1 auto; height: 22px; box-sizing: border-box; display: flex; align-items: center; justify-content: center;
  padding: 0 6px; white-space: nowrap; font-size: 10px; font-weight: 700; letter-spacing: .4px; cursor: pointer; user-select: none;
  background: #f4e1bf; border: 1px solid var(--gm-btn-line); color: #000; }
.s-wars-win .wg-tab:hover { filter: brightness(.96); }
.s-wars-win .wg-tab.on { background: #000; color: #fff4e1; }
.s-wars-win .wg-body { position: absolute; left: 30px; top: 108px; width: 498px; height: 312px; overflow-y: auto; overflow-x: hidden;
  box-sizing: border-box; padding-right: 4px; font-size: 11px; line-height: 15px; }
.s-wars-win .wg-body::-webkit-scrollbar { width: 5px; }
.s-wars-win .wg-body::-webkit-scrollbar-thumb { background: #b9a878; border-radius: 0; }
.s-wars-win .wg-h { margin: 6px 0 3px; padding-bottom: 2px; font-size: 11px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
  border-bottom: 1px solid rgba(0, 0, 0, .22); }
.s-wars-win .wg-h:first-child { margin-top: 0; }
.s-wars-win .wg-row { display: flex; align-items: center; gap: 6px; min-height: 24px; padding: 2px 3px; border-bottom: 1px solid rgba(0, 0, 0, .12); }
.s-wars-win .wg-w { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.s-wars-win .wg-sc { flex: none; font-weight: 700; color: #9a1e09; white-space: nowrap; }
.s-wars-win .wg-st { color: #000; font-weight: 400; font-size: 10px; }
.s-wars-win .wg-acts { flex: none; display: flex; gap: 4px; align-items: center; }
.s-wars-win .wg-mini { font-size: 10px; color: rgba(0, 0, 0, .65); white-space: nowrap; }
.s-wars-win .wg-off { color: #9a1e09; }
.s-wars-win .wg-empty { padding: 6px 2px; font-size: 11px; color: rgba(0, 0, 0, .7); }
.s-wars-win .wg-line { margin: 2px 0 4px; overflow-wrap: anywhere; }
.s-wars-win .wg-btn { display: inline-flex; align-items: center; justify-content: center; height: 20px; box-sizing: border-box;
  padding: 0 9px; min-width: 52px; white-space: nowrap; font-size: 10px; font-weight: 700; letter-spacing: .4px;
  background: #f4e1bf; border: 1px solid var(--gm-btn-line); color: #000; cursor: pointer; user-select: none; }
.s-wars-win .wg-btn:hover { filter: brightness(.95); }
.s-wars-win .wg-btn.big { height: 24px; padding: 0 14px; background: #000; color: #fff4e1; }
.s-wars-win .wg-btn.off { opacity: .45; cursor: default; filter: none; }
.s-wars-win .wg-actrow { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; margin: 8px 0 4px; }
.s-wars-win .wg-actrow.top { margin: 0 0 6px; }
.s-wars-win .wg-note { position: absolute; left: 30px; top: 426px; width: 498px; height: 16px; font-size: 11px; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.s-wars-win .wg-note.ok { color: #1d7a2f; }
.s-wars-win .wg-note.bad { color: #9e1812; }
.s-wars-win .wg-entries { display: flex; flex-wrap: wrap; gap: 4px; }
.s-wars-win .wg-entry { padding: 1px 6px; border: 1px solid rgba(0, 0, 0, .3); background: #fff8; white-space: nowrap; }
/* the website's forms, inside the window: compact */
.s-wars-win .wg-form .field { display: flex; align-items: center; gap: 6px; margin: 0 0 4px; }
.s-wars-win .wg-form .field > label { flex: 0 0 118px; font-size: 10px; font-weight: 700; white-space: nowrap; }
.s-wars-win .wg-form .ap-input { max-width: none; flex: 1 1 auto; height: 20px; font: 11px var(--gm-font); background: #fff9ef;
  border: 1px solid #7a6128; border-radius: 0; box-sizing: border-box; padding: 0 5px; }
.s-wars-win .wg-form textarea.ap-input { height: 40px; padding: 3px 5px; resize: none; }
.s-wars-win .wg-form .war-checks { margin: 2px 0 6px 124px; font-size: 10px; }
.s-wars-win .wg-form .war-field-h { font-size: 10px; font-weight: 700; margin: 6px 0 2px; }
.s-wars-win .wg-form .war-picker { flex: 1 1 auto; min-width: 0; }
.s-wars-win .wg-form .war-picklist { height: 96px; }
.s-wars-win .wg-form .rq-grid { max-height: 92px; overflow-y: auto; }
.s-wars-win .wg-form .ap-btn, .s-wars-win .wg-form .btn { font: 700 10px var(--gm-font); height: 20px; padding: 0 9px; white-space: nowrap;
  background: #f4e1bf; border: 1px solid #000; border-radius: 0; color: #000; cursor: pointer; }
.s-wars-win .wg-form .btnrow { display: flex; justify-content: flex-end; margin-top: 6px; }
.s-wars-win .t-bracket { max-width: 100%; }
/* the set's popup: the opponent is not online */
.s-found .s-war-info .s-war-off { color: #9e1812; }
