* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

@font-face {
  font-family: "PiratesBay";
  src: url("/fonts/PiratesBay.ttf") format("truetype");
  font-display: swap;
}

html,
body {
  width: 100%;
  height: 100%;
  background: #0b1f2a;
  color: #f4ead0;
  font-family: "Segoe UI", system-ui, sans-serif;
  overscroll-behavior: none;
  -webkit-user-select: none;
  user-select: none;
}

.hidden {
  display: none !important;
}

/* The React mount point itself should never participate in layout --
   body's flex rules (controller) and the individual view elements' own
   sizing (screen) are both written assuming they're direct children of
   body, exactly as they were before React owned rendering. */
#root {
  display: contents;
}

/* Screen page */
body.screen {
  overflow: hidden;
  /* Replaces the plain body background (see `body` above) for this page
     only -- shows through everywhere the shared .ship-gl-surface canvas is
     transparent (glSurface.ts clears to alpha 0), same as the plain color
     did. `cover` fills the viewport with no letterboxing while keeping the
     image's own aspect ratio (crops instead of stretching); `center`
     anchors that crop in the middle. */
  background-image: url("/img/background/background-wall.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.lobby-view {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2vh;
  padding: 4vh 4vw;
  text-align: center;
}

.lobby-title {
  font-family: "PiratesBay", Georgia, serif;
  font-size: clamp(28px, 5vw, 56px);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #d4af37;
}

.lobby-hint {
  font-size: clamp(16px, 1.6vw, 22px);
  color: rgba(244, 234, 208, 0.6);
}

.lobby-roster {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 1vh;
  min-width: 280px;
}

.lobby-roster-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 18px;
  background: rgba(244, 234, 208, 0.06);
  border: 1px solid rgba(244, 234, 208, 0.15);
  border-radius: 10px;
  font-size: clamp(16px, 1.8vw, 22px);
}

.ready-badge {
  font-size: 0.7em;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: rgba(244, 234, 208, 0.5);
}

.ready-badge.ready {
  color: #4cd964;
}

.ship-windows {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-wrap: wrap;
  align-content: center;
  justify-content: center;
  align-items: center;
  gap: 16px;
  padding: 16px;
  overflow: hidden;
  /* See .game-view's identical comment (style.css) -- same #root:
     display:contents situation on the screen side. */
  position: relative;
  z-index: 1;
}

/* Ornate frame (frame.png) around every ship viewport on the screen -- both
   the player's own .ship-window and an enemy's .combat-window reuse this
   same class (see CombatWindow.tsx's className), and both wrap their
   content in <ShipWindowFrame> (ShipWindowFrame.tsx) to get it.
   A 5x5 CSS grid (.ship-window-frame-grid, an absolute inset:0 overlay
   spanning .ship-window's full box) -- not 3x3, because frame.png's edges
   each carry their own single centered medallion (not a repeating motif),
   so each axis needs 5 tracks -- corner, gap, medallion, gap, corner --
   not 3.

   Two DIFFERENT measurements of frame.png are in play here, and they are
   NOT the same number -- conflating them (using the smaller one for both)
   was a real bug this session hit and had to walk back:
   - --frame-corner-w/h: the grid's own corner/edge TRACK size, a literal
     1/3 of frame.png's own 1197x1196 canvas (the "cut it into 9 equal
     cells" the frame art itself was designed against). This is what the 4
     corner cells and the 8 trim cells actually sit inside -- get this
     wrong and everything in that row/column (trim included) gets forced
     into a track shorter than the art expects, which for the trim tiles
     specifically (background-size: auto 100%, so their own *width* scales
     down proportionally with whatever height they're squeezed into) meant
     rendering at a fraction of a pixel wide -- reads as "way too thin,"
     not as an obviously-wrong crop.
   - var(--frame-col-l/r), var(--frame-row-t/b) (defined further down, near
     .ship-window-viewport): a frame-mask.png pixel-scan measurement, much
     smaller than a third of the image (~97-109px native vs ~399px) --
     this is ONLY where .ship-window-viewport itself gets positioned, i.e.
     guidance for "where does the boat content go," not a slice boundary
     for the frame art. The two happen to be visually compatible without
     any extra alignment work because frame.png's own opaque pixels
     already end at roughly the mask's boundary -- the *rest* of each
     399px corner track, beyond that, is simply transparent in the source
     image, so it doesn't paint anything extra, it just gives the trim
     tiles the correctly-sized track to render into.
   --frame-medallion-col/row is a third, independent measurement (~150px
   native) -- read the same way, off a close-cropped grid-overlay
   screenshot -- for the medallion's own track size along the OTHER axis
   from the corner tracks. */
:root {
  --frame-scale: 0.4;
  --frame-corner-w: calc(1197px / 3 * var(--frame-scale));
  --frame-corner-h: calc(1196px / 3 * var(--frame-scale));
  --frame-medallion-col: calc(150px * var(--frame-scale));
  --frame-medallion-row: calc(150px * var(--frame-scale));
}

.ship-window {
  position: relative;
  flex: 0 0 auto;
  /* Transparent, not black -- the actual ship art now renders on the one
     shared .ship-gl-surface canvas sitting behind the whole page (see
     glSurface.ts), not a canvas child of this element, so an opaque
     background here would hide it. */
  background: transparent;
}

.ship-window-frame-grid {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: var(--frame-corner-w) 1fr var(--frame-medallion-col) 1fr var(--frame-corner-w);
  grid-template-rows: var(--frame-corner-h) 1fr var(--frame-medallion-row) 1fr var(--frame-corner-h);
  pointer-events: none;
}

.ship-window-frame-cell {
  background-image: url("/img/background/frame.png");
  background-repeat: no-repeat;
  background-size: calc(1197px * var(--frame-scale)) calc(1196px * var(--frame-scale));
}
.ship-window-frame-cell-tl { grid-column: 1 / 2; grid-row: 1 / 2; background-position: left top; }
.ship-window-frame-cell-t { grid-column: 3 / 4; grid-row: 1 / 2; background-position: center top; }
.ship-window-frame-cell-tr { grid-column: 5 / 6; grid-row: 1 / 2; background-position: right top; }
.ship-window-frame-cell-l { grid-column: 1 / 2; grid-row: 3 / 4; background-position: left center; }
.ship-window-frame-cell-r { grid-column: 5 / 6; grid-row: 3 / 4; background-position: right center; }
.ship-window-frame-cell-bl { grid-column: 1 / 2; grid-row: 5 / 6; background-position: left bottom; }
.ship-window-frame-cell-b { grid-column: 3 / 4; grid-row: 5 / 6; background-position: center bottom; }
.ship-window-frame-cell-br { grid-column: 5 / 6; grid-row: 5 / 6; background-position: right bottom; }

/* Fills the 8 gap cells either side of each medallion (columns/rows 2 and 4
   -- see .ship-window-frame-grid's own doc comment) with the user's own
   trim tile assets, repeating to fill whatever length that particular gap
   works out to. Each source image is a thin cross-section strip (e.g.
   frame-trim-top.png is 7x399 native -- narrow in the repeat direction,
   tall in the frame-thickness direction) meant to repeat losslessly at
   that fine a grain, not a "single visible tile" the eye would need to see
   whole -- so background-size deliberately scales it to the *cell's own*
   track size on the thickness axis (100%, matching the same row/column
   track var(--frame-row-t) etc. the corners/medallions already use) and
   lets the repeat axis size itself proportionally (`auto`), rather than
   hard-coding these images' own native pixel dimensions (which have no
   relationship to frame.png's own scale -- the user authored them
   separately). The 8 gap cells not listed here (the ones the mask-inset
   .ship-window-viewport already covers entirely) are left with no element,
   same as before. */
.ship-window-frame-trim {
  pointer-events: none;
}
.ship-window-frame-trim-t {
  background-image: url("/img/background/frame-trim-top.png");
  background-repeat: repeat-x;
  background-size: auto 100%;
  grid-row: 1 / 2;
}
.ship-window-frame-trim-b {
  background-image: url("/img/background/frame-trim-bottom.png");
  background-repeat: repeat-x;
  background-size: auto 100%;
  grid-row: 5 / 6;
}
.ship-window-frame-trim-l {
  background-image: url("/img/background/frame-trim-left.png");
  background-repeat: repeat-y;
  background-size: 100% auto;
  grid-column: 1 / 2;
}
.ship-window-frame-trim-r {
  background-image: url("/img/background/frame-trim-right.png");
  background-repeat: repeat-y;
  background-size: 100% auto;
  grid-column: 5 / 6;
}
.ship-window-frame-trim-gap-2-col { grid-column: 2 / 3; }
.ship-window-frame-trim-gap-4-col { grid-column: 4 / 5; }
.ship-window-frame-trim-gap-2-row { grid-row: 2 / 3; }
.ship-window-frame-trim-gap-4-row { grid-row: 4 / 5; }

/* frame-mask.png pixel-scan measurement (see .ship-window-frame-grid's own
   doc comment for why this is deliberately NOT the same as
   --frame-corner-w/h above) -- purely where the boat content itself gets
   positioned, mirrored in shipWindowFrameLayout.ts as FRAME_COL_L/R and
   FRAME_ROW_T/B so ShipWindow.tsx/CombatWindow.tsx's own internal sizing
   and ShipWindows.tsx's grid-layout aspect-ratio math can agree with this
   without duplicating the pixel-scan result a third time. */
:root {
  --frame-col-l: calc(97px * var(--frame-scale));
  --frame-col-r: calc(98px * var(--frame-scale));
  --frame-row-t: calc(109px * var(--frame-scale));
  --frame-row-b: calc(105px * var(--frame-scale));
}

.ship-window-viewport {
  position: absolute;
  left: var(--frame-col-l);
  right: var(--frame-col-r);
  top: var(--frame-row-t);
  bottom: var(--frame-row-b);
  overflow: hidden;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
}

.ship-window canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Wraps ShipCanvas.tsx's two stacked layers (a WebGL region for the hull/
   ocean/walls/doors/weapons/room-highlights/crew/effects, a transparent
   Canvas2D canvas on top for system-label text only -- see its own doc
   comment) -- the wrapper itself is layout-transparent (fills whatever box
   className already sizes the canvases to), it only exists so the second
   canvas can sit exactly over the first via position:absolute. */
.ship-canvas-stack {
  position: relative;
  width: 100%;
  height: 100%;
}

.ship-canvas-stack canvas {
  position: absolute;
  inset: 0;
  /* A <canvas>'s own width/height content attributes (set in JS, to the
     ship's native pixel size or a devicePixelRatio-scaled buffer size --
     see sizeCanvasToShip/ShipViewport.tsx) give it an intrinsic size that
     inset:0 alone does not override, since canvas is a CSS replaced element
     -- see .ship-gl-surface's own doc comment for the identical bug this
     already caused once. Explicit here rather than relying on this always
     being nested inside a .ship-window (whose own "canvas" rule happens to
     set the same width/height) -- this wrapper is meant to be self-contained
     wherever it's used. */
  width: 100%;
  height: 100%;
}

/* One canvas, one WebGL context, shared by every ship view on the page
   (see glSurface.ts's own doc comment for why), and never intercepts
   pointer events, so it's purely a visual layer each ship view's own
   (otherwise-transparent) box shows through to. Hidden except while at
   least one ship view is actually mounted (see registerView).
   z-index: 0 here, paired with an explicit z-index: 1 on the real content
   that needs to paint above it (.game-view, .ship-windows) -- a negative
   z-index here did NOT reliably stay behind in-flow content in practice
   (`#root: display: contents` puts this canvas and that content in the
   same stacking context as direct siblings under <body>, and relying on
   plain DOM-order/paint-order rules to sort a negative-z-index canvas
   behind unpositioned siblings there didn't hold up -- verified visually
   before landing this). Two explicit, opposite-signed z-index values on
   both sides is unambiguous regardless of why the implicit rule didn't hold. */
.ship-gl-surface {
  position: fixed;
  inset: 0;
  /* A <canvas> is a replaced element with its own intrinsic size (from its
     width/height *content attributes*, set in glSurface.ts's resizeCanvas()
     to the devicePixelRatio-scaled buffer size, e.g. 638x1274 at dpr 1.5 on
     a 425x849 viewport) -- inset:0 alone does NOT override that intrinsic
     size the way it would for a non-replaced element, so without an
     explicit width/height here the canvas rendered at its buffer size
     treated 1:1 as CSS px, ~dpr times too large, throwing off every
     ship-view region's on-screen alignment by exactly that factor (this is
     what a captain on an actual dpr != 1 device saw as "the tappable area
     is smaller than what's drawn"). Forcing the CSS box to the viewport
     size here is what actually makes the canvas *display* at 1 CSS px per
     CSS px regardless of its higher-resolution backing buffer, same as any
     other DPR-aware canvas.
  */
  width: 100vw;
  height: 100vh;
  z-index: 0;
  pointer-events: none;
  display: none;
}

.ship-window-systems-ui {
  position: absolute;
  bottom: 0;
  left: 0;
  pointer-events: none;
}

.ship-window-hull-gauge {
  position: absolute;
  bottom: 10px;
  right: 10px;
  pointer-events: none;
}


.ship-window-combat {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 12px;
  font-size: clamp(12px, 1.2vw, 16px);
  font-weight: 700;
  color: #f4ead0;
  background: rgba(160, 39, 31, 0.85);
  border-radius: 6px;
  pointer-events: none;
  white-space: nowrap;
}

/* Enemy frame, shown alongside a captain's own ship-window while they're in
   combat -- same chrome as .ship-window (border/shadow/rounding), just its
   own content: a tinted placeholder ship image and a hull gauge. */
.combat-window-ship-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Placeholder recolor so it doesn't look identical to the player's own
     ship -- drop once real enemy art exists. Applied via ShipCanvas's
     enemyTint prop (a GL shader, see glCore.ts) now, not a CSS filter --
     see this session's own performance investigation for why a compound
     filter like this got expensive once several of these render at once. */
}

.combat-window-label {
  position: absolute;
  bottom: 10px;
  left: 10px;
  max-width: calc(100% - 20px);
  box-sizing: border-box;
  padding: 4px 12px;
  font-family: "PiratesBay", Georgia, serif;
  font-size: clamp(15px, 1.6vw, 20px);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: #ff9a8a;
  background: rgba(11, 31, 42, 0.72);
  border-radius: 6px;
  pointer-events: none;
}

.combat-window-hull-gauge {
  position: absolute;
  bottom: 10px;
  right: 10px;
  pointer-events: none;
}

.dialog-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4vh 4vw;
  background: rgba(0, 0, 0, 0.72);
}

.dialog-box {
  max-width: 640px;
  padding: 32px 36px;
  background: #e8d4a8;
  color: #2b1b0e;
  border: 3px solid #8b5a2b;
  border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
  font-family: "PiratesBay", Georgia, "Times New Roman", serif;
}

.dialog-text {
  font-size: clamp(16px, 2vw, 22px);
  line-height: 1.55;
}

/* A hard stop, not just another dialog -- covers the whole viewport above
   everything else on the page (including .dropdown-menu's z-index 60), and
   is the only thing rendered once phase flips to "gameover" (see
   controller/App.tsx and screen/App.tsx), so nothing underneath is even
   still mounted to peek out from behind it. */
.game-over-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4vh 4vw;
  background: #0b0704;
}

.game-over-box {
  max-width: 640px;
  text-align: center;
  font-family: "PiratesBay", Georgia, "Times New Roman", serif;
  color: #e8d4a8;
}

.game-over-title {
  font-size: clamp(28px, 5vw, 48px);
  margin-bottom: 16px;
  letter-spacing: 0.03em;
}

.game-over-text {
  font-size: clamp(16px, 2vw, 22px);
  line-height: 1.55;
  color: #c9b08a;
}

.dialog-hint {
  margin-top: 18px;
  font-family: "Segoe UI", system-ui, sans-serif;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: right;
  opacity: 0.6;
}

.event-title {
  font-size: clamp(18px, 2.4vw, 26px);
  font-weight: 700;
  margin-bottom: 10px;
  color: #6b3f1d;
}

.event-choices {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 18px;
}

.event-choice-btn {
  padding: 14px 18px;
  font: 600 clamp(14px, 3.8vw, 17px)/1.3 "Segoe UI", sans-serif;
  text-align: left;
  color: #2b1b0e;
  background: rgba(139, 90, 43, 0.15);
  border: 2px solid #8b5a2b;
  border-radius: 10px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.event-choice-btn:active {
  background: rgba(139, 90, 43, 0.3);
}

.event-choice-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.event-outcome {
  margin-top: 18px;
}

.event-combat-note {
  margin-top: 12px;
  font-family: "Segoe UI", system-ui, sans-serif;
  font-weight: 700;
  color: #a0271f;
}

/* Bottom-toolbar combat panel -- docks in SailPanel's flex slot (see
   App.tsx) instead of a blocking dialog, so it has to stay compact: thin
   hull bars plus a row of small fire buttons, not the old full-size
   modal layout. */
.combat-bar {
  flex: 0 0 auto;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 16px;
  background: rgba(244, 234, 208, 0.06);
  border-top: 1px solid rgba(244, 234, 208, 0.15);
  font-family: "Segoe UI", system-ui, sans-serif;
}

.combat-bar-resolution {
  margin: 0;
  text-align: center;
  font-weight: 700;
  font-size: 14px;
}

.combat-bar-hulls {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.combat-bar-hull-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.combat-bar-hull-label {
  flex: 0 0 auto;
  width: 130px;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.combat-hull-bar {
  flex: 1 1 auto;
  height: 10px;
  background: rgba(43, 27, 14, 0.15);
  border: 1px solid rgba(43, 27, 14, 0.4);
  border-radius: 6px;
  overflow: hidden;
}

.combat-hull-fill {
  height: 100%;
  width: 0%;
  transition: width 0.2s linear;
}

.combat-hull-fill.enemy {
  background: #a0271f;
}

.combat-hull-fill.own {
  background: #2f7d3a;
}

.combat-bar-weapons {
  display: flex;
  gap: 6px;
}

.combat-bar-weapon-btn {
  flex: 1 1 0;
  /* Flex items default to min-width: auto, which floors shrinking at the
     content's own min-content width -- without this, a long weapon name
     stops the row from fitting all 4 buttons and pushes the last one out. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 6px 4px;
  background: #3a4250;
  color: #f4ead0;
  border: 1px solid rgba(244, 234, 208, 0.25);
  border-radius: 8px;
  cursor: default;
  -webkit-tap-highlight-color: transparent;
}

.combat-bar-weapon-btn.active {
  background: #d4af37;
  color: #0b1f2a;
  cursor: pointer;
}

.combat-bar-weapon-btn.unmanned {
  opacity: 0.5;
}

.combat-bar-weapon-name {
  width: 100%;
  min-width: 0;
  font-size: 10px;
  font-weight: 700;
  text-align: center;
  line-height: 1.15;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.combat-bar-weapon-charge {
  display: block;
  width: 100%;
  height: 6px;
  background: rgba(0, 0, 0, 0.3);
  border-radius: 3px;
  overflow: hidden;
}

.combat-bar-weapon-charge-fill {
  display: block;
  height: 100%;
  width: 0%;
  background: #d4af37;
  transition: width 0.2s linear;
}

.combat-bar-weapon-btn.active .combat-bar-weapon-charge-fill {
  background: #0b1f2a;
}

.combat-bar-weapon-status {
  font-size: 10px;
  font-weight: 700;
  min-height: 12px;
}

/* Controller page */
body.controller {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

body.controller .lobby-view {
  flex: 1 1 auto;
  min-height: 0;
}

.status {
  font-size: clamp(16px, 4vw, 22px);
  color: rgba(244, 234, 208, 0.75);
}

.name-label {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: rgba(244, 234, 208, 0.5);
}

.name-input {
  font: 600 20px/1.2 "Segoe UI", sans-serif;
  text-align: center;
  padding: 12px 20px;
  width: min(280px, 70vw);
  border-radius: 12px;
  border: 2px solid rgba(244, 234, 208, 0.25);
  background: rgba(244, 234, 208, 0.06);
  color: inherit;
}

.name-input:focus {
  outline: none;
  border-color: #d4af37;
}

.ready-btn {
  margin-top: 8px;
  padding: 18px 48px;
  font-size: 22px;
  font-weight: 700;
  color: #0b1f2a;
  background: #d4af37;
  border: none;
  border-radius: 12px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.ready-btn.active {
  background: #4cd964;
}

.ready-btn:disabled {
  background: #3a4250;
  color: #aab2bf;
  cursor: default;
}

.game-view {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  /* Explicit stacking context, above .ship-gl-surface's -1 (see its own
     comment) -- #root is `display: contents` (so body's flex layout can see
     straight through to elements like this one), which means this sits as
     a *sibling* of that fixed canvas under <body>, not a descendant of some
     already-positioned ancestor. Relying on the plain "in-flow content
     paints above a negative z-index sibling" rule to sort that out wasn't
     reliable in practice, so this makes the ordering explicit instead. */
  position: relative;
  z-index: 1;
}

.crew-bar {
  flex: 0 0 auto;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 16px;
  background: rgba(244, 234, 208, 0.06);
  border-bottom: 1px solid rgba(244, 234, 208, 0.15);
}

.crew-nav {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 12px;
}

.crew-name {
  flex: 1 1 auto;
  text-align: center;
  font-size: clamp(18px, 5vw, 24px);
  font-weight: 700;
  color: #d4af37;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Consolidated burger menu (see controller/components/DropdownMenu.tsx) --
   replaces the old standalone RefreshButton/FullscreenButton and the
   top-bar's own map/systems toggle buttons with one list. Fixed to the
   viewport corner, not the layout flow, so it sits in the same spot
   regardless of whatever's currently in the header (or lobby, which has no
   .top-bar at all). z-index above .dialog-overlay so it -- Refresh
   especially, the escape hatch for a captain stuck in a bad state -- still
   works with a dialog/event up. */
.dropdown-menu {
  position: fixed;
  /* 30px .hull-pill-bar-top height + the original 10px margin. */
  top: 40px;
  left: 10px;
  z-index: 60;
}

.dropdown-menu-toggle {
  position: relative;
  display: block;
  width: 48px;
  height: 48px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* Shared by the toggle and every expanded item -- menuIcon (the circular
   medallion) sits full-bleed behind whichever glyph (burger, map, ship...)
   is drawn centered on top of it at a slight inset. */
.dropdown-menu-icon-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.dropdown-menu-icon-glyph {
  position: absolute;
  inset: 0;
  width: 62%;
  height: 62%;
  margin: auto;
}

.dropdown-menu-list {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  background: rgba(11, 31, 42, 0.92);
  border: 1px solid rgba(244, 234, 208, 0.2);
  border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}

.dropdown-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 14px 4px 4px;
  border: none;
  border-radius: 8px;
  background: none;
  color: #f4ead0;
  font: 600 15px/1.2 "Segoe UI", sans-serif;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.dropdown-menu-item:active {
  background: rgba(244, 234, 208, 0.1);
}

.dropdown-menu-item.active {
  color: #4cd964;
}

.dropdown-menu-item-icon {
  position: relative;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
}

/* Sail-cooldown readout, absorbed into the menu toggle itself now that
   there's no top-bar SailPanel to show it (see DropdownMenu.tsx) -- a green
   overlay on the medallion, blended so it tints rather than just sitting on
   top, whose opacity DropdownMenu.tsx drives directly from cooldown
   progress (0 at the start of the recharge, up to 0.7 as it nears ready).
   Once ready, .ready takes over with its own pulsing opacity/glow instead
   of the inline progress opacity. */
.dropdown-menu-sail-tint {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: #34d058;
  mix-blend-mode: color;
  pointer-events: none;
}

.dropdown-menu-sail-tint.ready {
  animation: dropdown-menu-sail-pulse 1.1s ease-in-out infinite;
}

@keyframes dropdown-menu-sail-pulse {
  0%,
  100% {
    opacity: 0.4;
    box-shadow: 0 0 0 0 rgba(76, 217, 100, 0.6);
  }
  50% {
    opacity: 0.85;
    box-shadow: 0 0 14px 4px rgba(76, 217, 100, 0.6);
  }
}

/* Sail vote button (see controller/components/SailVoteButton.tsx), pinned
   to the map view's bottom-center via .map-sail-vote-anchor below -- only
   rendered once every captain's destination pick matches. Same
   sail-ready/sail-not-ready plaque art the old top-bar SailPanel button
   used (see SailStatusBar.tsx's identical use on the screen side), without
   that widget's overlapping progress-bar image since the cooldown readout
   itself lives in DropdownMenu's toggle/Map-item glow instead. */
.map-sail-btn {
  display: block;
  width: min(140px, 38vw);
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.map-sail-btn:disabled {
  cursor: default;
  opacity: 0.6;
}

.map-sail-btn-img {
  display: block;
  width: 100%;
  height: auto;
}

.map-sail-btn.ready {
  animation: map-sail-btn-pulse 1.1s ease-in-out infinite;
}

@keyframes map-sail-btn-pulse {
  0%,
  100% {
    filter: drop-shadow(0 0 0 rgba(76, 217, 100, 0));
  }
  50% {
    filter: drop-shadow(0 0 8px rgba(76, 217, 100, 0.9));
  }
}

.crew-nav-btn {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  font-size: 18px;
  color: #0b1f2a;
  background: #d4af37;
  border: none;
  border-radius: 10px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.crew-nav-btn:active {
  background: #b8952c;
}

/* Position indicator for PageSwiper's pages -- swiping the mid section is
   the main way to switch, these dots are secondary (tap to jump directly). */
.page-dots {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
}

.page-dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(244, 234, 208, 0.3);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.page-dot.active {
  background: #d4af37;
}

.systems-panel {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  padding: 16px;
  /* .dropdown-menu/.pause-btn sit fixed at top:40px (the same 30px
     .hull-pill-bar-top height + 10px margin every fixed top element
     offsets by) and are themselves 48px tall, so their own bottom edge is
     at 88px, not 40 -- 40px alone only cleared where they *start*, not
     where they *end*, so this page's own content still rendered underneath
     their lower half. 98px = 88px + the same 10px margin used everywhere
     else here. */
  padding-top: 98px;
  /* Opaque -- .view-stack-page sits directly over the shared, always-drawing
     ship GL canvas (glSurface.ts, position:fixed behind everything at its
     own z-index), which keeps rendering the boat view regardless of which
     page is actually on screen. Without a real background here, this page's
     own text/rows just show that boat bleeding straight through instead of
     the plain dark backdrop every other non-boat page needs. */
  background: #0b1f2a;
}

.resource-summary {
  display: flex;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
  font-size: clamp(12px, 3.2vw, 15px);
  font-weight: 600;
  margin-bottom: 12px;
}

.resource-item {
  padding: 4px 10px;
  background: rgba(244, 234, 208, 0.06);
  border: 1px solid rgba(244, 234, 208, 0.15);
  border-radius: 8px;
}

.manpower-summary {
  font-size: clamp(16px, 4vw, 20px);
  font-weight: 700;
  color: #d4af37;
  text-align: center;
  margin-bottom: 16px;
}

.system-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: rgba(244, 234, 208, 0.06);
  border: 1px solid rgba(244, 234, 208, 0.15);
  border-radius: 10px;
}

.system-name {
  flex: 1 1 auto;
  font-size: clamp(14px, 3.6vw, 17px);
  font-weight: 600;
}

/* Systems page (SystemsPanel.tsx) -- one section for buying more of the
   ship-wide manpower pool, another for upgrading a system's own capacity,
   each a heading followed by .system-row entries (shared with the row
   treatment above). */
.shop-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 20px;
}

.shop-section-title {
  font-size: clamp(13px, 3.4vw, 16px);
  font-weight: 700;
  color: #d4af37;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.shop-row-info {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.shop-row-detail {
  font-size: clamp(11px, 2.8vw, 13px);
  color: rgba(244, 234, 208, 0.65);
}

.shop-empty {
  font-size: clamp(12px, 3vw, 14px);
  color: rgba(244, 234, 208, 0.5);
  font-style: italic;
}

.shop-btn {
  flex: 0 0 auto;
  padding: 8px 14px;
  font-size: clamp(13px, 3.2vw, 15px);
  font-weight: 700;
  color: #0b1f2a;
  background: #d4af37;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}

.shop-btn:active {
  background: #b8952c;
}

.shop-btn:disabled {
  background: #3a4250;
  color: #aab2bf;
  cursor: default;
}

/* Crew page (see controller/components/CrewPage.tsx) -- same page-list
   treatment as .systems-panel, one translucent card per crew member instead
   of one row per system. */
.crew-page {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  padding: 16px;
  /* Same 98px offset as .systems-panel's own identical rule -- clears
     .dropdown-menu/.pause-btn's full 48px height, not just where they start. */
  padding-top: 98px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* Opaque, same reason as .systems-panel's own identical rule -- without
     this the always-drawing shared ship GL canvas behind it shows straight
     through. */
  background: #0b1f2a;
}

.crew-page-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(244, 234, 208, 0.6);
  font-size: 15px;
}

.crew-card {
  background: rgba(244, 234, 208, 0.06);
  border: 1px solid rgba(244, 234, 208, 0.15);
  border-radius: 10px;
  padding: 10px 14px;
}

.crew-card-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.crew-card-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid rgba(212, 175, 55, 0.6);
}

.crew-card-name {
  font-size: clamp(14px, 3.6vw, 17px);
  font-weight: 700;
  color: #f4ead0;
}

.crew-card-skills {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 4px 12px;
}

.crew-skill-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 2px 0;
}

.crew-skill-name {
  font-size: clamp(11px, 2.8vw, 13px);
  color: rgba(244, 234, 208, 0.75);
}

.crew-skill-pips {
  flex: 0 0 auto;
  display: flex;
  gap: 3px;
}

.crew-skill-pip {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgba(244, 234, 208, 0.15);
  border: 1px solid rgba(244, 234, 208, 0.3);
}

.crew-skill-pip.filled {
  background: #d4af37;
  border-color: #d4af37;
}

.ship-viewport {
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
  touch-action: none;
}

/* Combat cue on the controller's own ship view (see App.tsx's
   state.activeCombat / BoatPage.tsx) -- a red inset glow around the boat
   page's edges so a captain notices they're under attack immediately,
   without needing to read the hull bars. Its own layer, on top of
   .ship-viewport's canvas, rather than a box-shadow directly on
   .ship-viewport -- that element's canvas child paints over its own
   background/shadow, which would otherwise hide it completely. */
.combat-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 0 rgba(220, 20, 20, 0);
  transition: box-shadow 0.3s ease;
}

.combat-glow.active {
  box-shadow: inset 0 0 60px 12px rgba(220, 20, 20, 0.7);
}

/* This captain's own combat glow, global (see App.tsx) -- same effect as
   .combat-glow above, fixed to the viewport instead of one boat page's own
   canvas so it stays up regardless of which view/boat is on screen. Roughly
   half the alpha of .combat-glow.active (0.35 vs 0.7) -- less overpowering
   for something that's now always-on rather than only while looking at
   your own ship. */
.combat-glow-global {
  position: fixed;
  inset: 0;
  z-index: 45;
  pointer-events: none;
  box-shadow: inset 0 0 0 0 rgba(220, 20, 20, 0);
  transition: box-shadow 0.3s ease;
}

.combat-glow-global.active {
  box-shadow: inset 0 0 60px 12px rgba(220, 20, 20, 0.35);
}

/* Travel cutscene (see TravelCutscene.tsx): fullscreen, non-interactive --
   sits above ordinary content/the combat glow but below .dialog-overlay
   (z-index 50) so an arrival dialog, if any, stays readable/usable rather
   than getting covered by the cutscene. Opacity is driven frame-by-frame
   from JS (fade in/out), not a CSS transition -- it shares the same rAF
   loop already redrawing the canvas each frame. The inner canvas is sized
   to the map's native resolution and framed via a JS-computed CSS
   transform (fitNodesFrame), the same translate+scale approach
   panZoomViewport.ts uses for the interactive map. */
.travel-cutscene {
  position: fixed;
  inset: 0;
  z-index: 46;
  pointer-events: none;
  overflow: hidden;
  /* Wood table underneath the map -- fitNodesFrame rarely scales the map
     canvas to exactly cover the viewport (its 1448x1086 aspect ratio only
     matches the screen/controller by coincidence), so this shows through
     past the map's own edges rather than flat black. */
  background-image: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url("/img/map/table.png");
  background-size: cover;
  background-position: center;
  opacity: 0;
}

.travel-cutscene-canvas {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.65);
}

/* Boat page: ShipViewport plus SystemsUI overlaid on top of it (see
   BoatPage.tsx/SystemsUIOverlay.tsx). The manpower bar used to stack below
   this as its own row -- it (and the loadout grid) now float over
   .boat-viewport-stack instead (see App.tsx's .bottom-overlay), so this
   fills the full page rather than sharing height with them. */
.boat-page {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
}

/* Holds ShipViewport/combat-glow/systems-ui-overlay -- the part of
   .boat-page that behaves as before (systems UI floats over the canvas,
   pinned to this box's own bottom, rather than taking its own space). Now
   .boat-page's only child, so this fills it entirely (edge-to-edge ship
   view) rather than flexing around a manpower-bar-row sibling. */
.boat-viewport-stack {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}

/* Pinned above .bottom-overlay (App.tsx's manpower bar + loadout grid,
   which floats over the full-screen boat viewport rather than sharing flex
   space with it -- see its own CSS) instead of flush to the very bottom --
   --bottom-overlay-height is kept live by a ResizeObserver in App.tsx, so
   this tracks that row's actual height (loadout can be 2x2 or a single
   row, manpower can be hidden) rather than sitting directly behind/under
   it the way a bare `bottom: 0` would once both are independently
   bottom-anchored overlays. Width isn't fixed either -- it follows from
   SystemsUI's own rendered height once its width is measured (same
   UNBOUNDED_HEIGHT approach as LoadoutBar/LoadoutGrid), so it can run
   taller than the boat view itself rather than being shrunk to fit.
   pointer-events: auto (and touch-action: none) since a vertical swipe on
   a system's icon column adjusts its manpower -- see
   SystemUIOverlayController.tsx. That does mean taps landing in this
   overlay's box no longer reach the ship canvas underneath for crew
   movement, even over its transparent gaps -- an accepted tradeoff of
   making this area interactive rather than a pure status display. */
/* Pinned to the bottom, full width -- height isn't fixed, it follows from
   SystemsUI's own rendered height once its width is measured (same
   UNBOUNDED_HEIGHT approach as LoadoutBar/LoadoutGrid), so it can run
   taller than the boat view itself rather than being shrunk to fit.
   pointer-events: auto (and touch-action: none) since a vertical swipe on
   a system's icon column adjusts its manpower -- see
   SystemUIOverlayController.tsx. That does mean taps landing in this
   overlay's box no longer reach the ship canvas underneath for crew
   movement, even over its transparent gaps -- an accepted tradeoff of
   making this area interactive rather than a pure status display. */
.systems-ui-overlay {
  position: absolute;
  left: 0;
  bottom: var(--bottom-overlay-height, 0px);
  width: 100%;
  display: flex;
  /* The canvas doesn't always fill the boat view's full width once its own
     scale is capped past MAX_UI_SCALE_WIDTH (see
     SystemUIOverlayController.tsx) -- left-aligns it instead of centering,
     pinning it to the boat view's own left edge. */
  justify-content: flex-start;
  pointer-events: auto;
  touch-action: none;
}

/* Viewing a teammate's ship (see TeammateBoatPage.tsx) -- no swipe gesture
   to claim here, so pointer events fall through to the ship viewport
   underneath instead, keeping pan/zoom usable even over this box. */
.systems-ui-overlay.read-only {
  pointer-events: none;
  touch-action: auto;
}

/* The paged mid-section (map/boat/systems) -- takes over the same
   fixed-header/variable-mid/fixed-bottom slot .ship-viewport used to own
   directly. All pages stay mounted side by side and are shown one at a
   time via .page-swiper-track's transform, not display:none, so each
   page's own state (e.g. the map's pan/zoom) survives switching away.
   Purely controlled by activeIndex (see PageSwiper.tsx) -- no gesture
   handling of its own, so no touch-action override needed either. */
.page-swiper {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  position: relative;
  overflow: hidden;
}

.page-swiper-track {
  display: flex;
  height: 100%;
}

.page-swiper-page {
  flex: 0 0 auto;
  height: 100%;
  overflow: hidden;
}

/* Floating counterpart to .dropdown-menu (top-left) -- boat view only, same
   fixed-to-the-corner treatment now that there's no .top-bar row for either
   of them to sit in (see App.tsx/DropdownMenu.tsx, which absorbed the old
   SailPanel). Replaces the old .boat-swipe-toggle-float (arrows + label) --
   see FleetBar.tsx. Runs vertically down the right edge, one icon per boat
   in the scene. */
.fleet-bar {
  position: fixed;
  /* 30px .hull-pill-bar-top height + the original 10px margin. */
  top: 40px;
  right: 10px;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.fleet-bar-icon {
  position: relative;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: 50%;
  border: 2px solid transparent;
  background: rgba(11, 31, 42, 0.55);
  cursor: pointer;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}

.fleet-bar-icon-self,
.fleet-bar-icon-ally {
  border-color: rgba(76, 217, 100, 0.5);
}

.fleet-bar-icon-enemy {
  border-color: rgba(220, 20, 20, 0.5);
}

.fleet-bar-icon.active {
  border-width: 3px;
}

.fleet-bar-icon-self.active,
.fleet-bar-icon-ally.active {
  border-color: #4cd964;
}

.fleet-bar-icon-enemy.active {
  border-color: #dc1414;
}

.fleet-bar-icon-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Locked-destination badge -- "M" (marked), same green the hull pips use,
   rather than new art (see FleetShipView.destinationLocked). */
.fleet-bar-icon-locked {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 14px;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: #4cd964;
  border: 2px solid #0b1f2a;
  font: 700 9px/1 "Segoe UI", sans-serif;
  color: #0b1f2a;
}

/* Deck up/down control (see controller/components/DeckToggle.tsx) -- fixed
   to the left edge, vertically centered, mirroring .fleet-bar's own
   boat-picker on the right: that one picks *which boat*, this one picks
   *which deck* of whichever boat is currently in focus. Boat view only. */
.deck-toggle {
  position: fixed;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

.deck-toggle-btn {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: 50%;
  border: 2px solid rgba(244, 234, 208, 0.35);
  background: rgba(11, 31, 42, 0.55);
  color: #f4ead0;
  font: 700 16px/1 "Segoe UI", sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.deck-toggle-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

.deck-toggle-label {
  font: 600 11px/1.2 "Segoe UI", sans-serif;
  color: rgba(244, 234, 208, 0.85);
  text-align: center;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
  max-width: 60px;
}

/* Pause/play toggle (see controller/components/PauseButton.tsx) -- top
   center, reachable from every viewMode, same z-index tier as
   .dropdown-menu/.fleet-bar. Any captain can tap it; the effect is global. */
.pause-btn {
  position: fixed;
  /* 30px .hull-pill-bar-top height + the original 10px margin. */
  top: 40px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  width: 48px;
  height: 48px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.pause-btn-img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Wraps SailVoteButton.tsx's own content -- pinned bottom-center of the map
   view via .map-sail-vote-anchor below, its only placement now that the
   fleet bar no longer shows it. */
.sail-vote {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.sail-vote-hint {
  max-width: 120px;
  text-align: center;
  font: 600 11px/1.3 "Segoe UI", sans-serif;
  color: #f4ead0;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}

/* Pins SailVoteButton.tsx to the map view's own bottom-center, same spot
   the original map-only SailButton used to sit. */
.map-sail-vote-anchor {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  z-index: 20;
}

/* Always-on hull HUD (see App.tsx/HullPillBar.tsx) -- pinned across the top
   of the screen regardless of viewMode, showing just whichever ship is
   currently in view (not one bar per ship). Fixed height (rather than one
   implied by the pills' own aspect-ratio, which would vary with maxHull as
   the captain swipes between ships) so DropdownMenu/PauseButton below it
   have a constant, reliable offset to sit below (see their own `top`).
   Below .dropdown-menu/.fleet-bar's z-index (60) so those still win if
   anything overlaps; pointer-events: none since this is a pure status
   readout, not something a tap should ever land on instead of whatever's
   underneath it. */
.hull-pill-bar-top {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 55;
  display: flex;
  align-items: center;
  gap: 3px;
  height: 30px;
  padding: 6px 12px;
  box-sizing: border-box;
  background: rgba(11, 31, 42, 0.55);
  pointer-events: none;
}

.hull-pill-bar-top-pill {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
}

/* PageSwiper (see App.tsx's boatPages) nested inside .view-stack-page,
   which is position:absolute/inset:0 rather than the flex column
   .page-swiper's own flex:1 1 auto normally expects to fill -- so unlike
   its other use (the map/boat/systems swiper that never shipped), this one
   needs an explicit height to actually fill that box. */
.boat-swiper {
  width: 100%;
  height: 100%;
}

/* Takes over the flex:1 1 auto "variable middle section" role .page-swiper
   used to have -- each view is absolutely positioned to fill it, with only
   the active one shown (.hidden), instead of being laid out side by side
   and swiped between. */
.view-stack {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  position: relative;
  overflow: hidden;
}

.view-stack-page {
  position: absolute;
  inset: 0;
}

.sail-widget {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.sail-icon {
  display: block;
  width: min(100px, 27.5vw);
  height: auto;
}

.sail-progress {
  position: relative;
  width: min(50px, 14vw);
  margin-top: -10px;
}

.sail-progress-img {
  display: block;
  width: 100%;
  height: auto;
}

.sail-progress-fill {
  position: absolute;
  top: 0;
  left: 0;
}

/* Screen page only now -- the controller's own sail readout moved into
   DropdownMenu's toggle/Map-item glow (dropdown-menu-sail-tint) plus the
   map-pinned Set Sail button (map-sail-btn below), neither of which use
   this icon+progress-bar combo, just the plaque art directly. Floats
   independently at the bottom-right of the screen. */
.sail-status-bar {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 10;
  padding: 10px 16px 6px;
}

/* Per-player weapons loadout panel (weapons-panel.png), pinned to the top
   of that player's own ship-window frame -- one per player, sized to a
   fraction of the frame the same way .ship-window-systems-ui is (see
   WEAPONS_LOADOUT_*_RATIO in ShipWindow.tsx). Flush with the top edge --
   full width, no inset. */
.ship-window-weapons-loadout {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  pointer-events: none;
}

.weapons-loadout {
  position: relative;
}

/* Stacks the manpower bar above the loadout grid, both floating over
   whatever view-stack-page is actually showing (App.tsx renders this as a
   sibling of .view-stack) instead of sharing flex space with it -- that's
   what lets the boat viewport (and map/systems/crew pages) render full
   screen behind them. pointer-events: none so its own empty margins (e.g.
   beside a narrower loadout grid, see MAX_CELL_WIDTH in LoadoutGrid.tsx)
   pass taps through to the ship canvas underneath; children that actually
   need taps (just .loadout-grid -- ManpowerBar is a pure readout) opt back
   in individually. z-index matches .map-sail-vote-anchor's tier: above the
   plain ship canvas/combat glow, below the always-on HUD (hull bar/fleet
   bar/dropdown, 55-60) and dialog/event overlays (50). */
.bottom-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
}

/* Fleet-wide manpower bar (see controller/components/BoatPage.tsx/
   ManpowerBar.tsx) -- see .bottom-overlay above for why this floats rather
   than taking its own row below .boat-viewport-stack the way it used to. */
.manpower-bar-row {
  flex: 0 0 auto;
  width: 100%;
}

/* A visibly darker panel than .top-bar/.loadout-bar's own barely-there
   divider tint -- this row has no art of its own (just bare pills), so it
   needs a backdrop of its own to read as a section rather than floating
   loose over the ship canvas. max-width is set inline by ManpowerBar.tsx
   (pills' own native pixel size, so a wide window stops growing the row
   rather than stretching pills past 100% size) -- margin: 0 auto centers
   it once width:100% actually gets capped by that. */
.manpower-bar {
  width: 100%;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 16px;
  background: rgba(11, 31, 42, 0.55);
}

.manpower-bar-pill {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  aspect-ratio: 69 / 26;
}

/* Controller's bottom bar (see LoadoutGrid.tsx) -- height isn't fixed here,
   it just follows from the grid's own rendered height once its width is
   measured, same "fixed" row role .sail-panel used to have at the bottom.
   No backdrop of its own (unlike .manpower-bar above) -- transparent by
   design, so the ship/ocean shows through beside the grid rather than
   behind a tinted strip; see .bottom-overlay for why there's something to
   actually see behind it now. */
.loadout-bar {
  flex: 0 0 auto;
  width: 100%;
  display: flex;
  justify-content: center;
}

/* 2x2 (or, once there's enough width, a single row of 4) alternative to the
   loadout-slots strip -- see LoadoutGrid.tsx, which sets grid-template-
   columns/rows inline once it knows its own container width. Sits inside
   the same .loadout-bar wrapper/slot as LoadoutBar. pointer-events: auto to
   opt back in from .bottom-overlay's pointer-events: none -- this (unlike
   its own transparent surroundings) is the one part of the bar that must
   still catch taps. */
.loadout-grid {
  display: grid;
  pointer-events: auto;
}

.loadout-grid-cell {
  position: relative;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: #f4ead0;
  cursor: default;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
}

.loadout-grid-cell.ready {
  cursor: pointer;
  filter: drop-shadow(0 0 6px rgba(76, 217, 100, 0.75));
}

.loadout-grid-cell-bg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Tune inset against the actual art -- loadout-slot.png's gold frame eats
   into the edges, so the charge bar/name/markers sit inside that border. */
.loadout-grid-cell-content {
  position: absolute;
  inset: 11% 7% 18% 7%;
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  gap: 6%;
}

.loadout-grid-charge-bar {
  flex: 0 0 auto;
  width: clamp(5px, 4%, 12px);
  display: flex;
  flex-direction: column-reverse;
  background: rgba(0, 0, 0, 0.5);
  border: 1px solid rgba(212, 175, 55, 0.6);
  border-radius: 3px;
  overflow: hidden;
  margin: 6% 0% 4% 0%;
}

.loadout-grid-charge-fill {
  width: 100%;
  background: #d4af37;
  transition: height 0.2s linear;
}

.loadout-grid-cell.ready .loadout-grid-charge-fill {
  background: #4cd964;
}

.loadout-grid-info {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.loadout-grid-name {
  font-family: "Segoe UI", system-ui, sans-serif;
  font-size: clamp(10px, 3.4vw, 15px);
  font-weight: 700;
  line-height: 1.2;
}

.loadout-grid-cell.ready .loadout-grid-name {
  color: #4cd964;
}

/* Locked-target highlight (see CombatWeaponState.targeted) -- takes over
   .ready's green the instant a target's been picked for this slot, whether
   it's about to fire right away or is still charging and will fire on its
   own once it finishes (see SetWeaponTargetMessage). Rules come after
   .ready's own so they win on the (momentary, never actually broadcast)
   overlap where both classes could apply. */
.loadout-grid-cell.targeted {
  cursor: pointer;
  filter: drop-shadow(0 0 6px rgba(255, 214, 10, 0.85));
}

.loadout-grid-cell.targeted .loadout-grid-charge-fill {
  background: #ffd60a;
}

.loadout-grid-cell.targeted .loadout-grid-name {
  color: #ffd60a;
}

/* Armed, waiting on a room tap over on an enemy's own boat page (see
   LoadoutGrid.tsx's armingSystemId/App.tsx's targetingSystemId) -- a
   pulsing white glow, distinct from .ready's static green and .targeted's
   yellow plaque swap, since this is neither "can act" nor "locked", just
   "waiting on you specifically, go look at an enemy". */
.loadout-grid-cell.arming {
  animation: loadout-grid-cell-arming-pulse 1.1s ease-in-out infinite;
}

@keyframes loadout-grid-cell-arming-pulse {
  0%,
  100% {
    filter: drop-shadow(0 0 0 rgba(244, 234, 208, 0));
  }
  50% {
    filter: drop-shadow(0 0 8px rgba(244, 234, 208, 0.95));
  }
}

.loadout-grid-manpower {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.loadout-grid-manpower-marker {
  flex: none;
  width: clamp(6px, 2vw, 11px);
  height: clamp(6px, 2vw, 11px);
  border-radius: 50%;
  border: 1px solid #d4af37;
  background: rgba(0, 0, 0, 0.4);
}

.loadout-grid-manpower-marker.filled {
  background: #4cd964;
  border-color: #4cd964;
}

/* Circular alternative to .loadout-bar/.loadout-grid -- see
   CircularWeaponLoadout.tsx. A column of medallions pinned to the screen's
   right edge instead of a grid at the bottom; not currently mounted by
   App.tsx (see its own debug harness, debug-circular-weapon-loadout.html,
   for previewing it standalone). --bottom-overlay-height (kept live by
   App.tsx's ResizeObserver -- see .systems-ui-overlay's own use of it)
   clears the existing loadout bar automatically if the two ever end up
   on screen together; falls back to a flat 10px where that variable isn't
   set (e.g. the debug harness, which doesn't render .bottom-overlay at all). */
.circular-weapon-column {
  position: fixed;
  top: 40px; /* below .hull-pill-bar-top's own fixed 30px + a little breathing room */
  bottom: calc(var(--bottom-overlay-height, 0px) + 10px);
  right: 10px;
  z-index: 20; /* same tier as .bottom-overlay/.map-sail-vote-anchor */
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;
  gap: 14px; /* CircularWeaponLoadout.tsx's own SLOT_GAP, kept in sync by hand */
  pointer-events: none; /* empty space passes through; .circular-weapon-slot opts back in */
}

/* position: relative -- CircularWeaponLoadout.tsx's pipRimStyle positions
   each manpower pip absolutely against *this* box's own top-left, sized
   exactly to the medallion itself (frameWidth x frameHeight), so a pip
   landing with its center on the circle's left rim always ends up roughly
   half outside that box -- overflow: visible (rather than the default
   clip) is what actually lets it paint out there instead of being cut off
   at the row's own edge. */
.circular-weapon-row {
  position: relative;
  flex: 0 0 auto;
  overflow: visible;
}

.circular-weapon-pip {
  position: absolute;
  display: block;
  pointer-events: none;
}

.circular-weapon-slot {
  position: relative;
  flex: 0 0 auto;
  padding: 0;
  border: none;
  background: none;
  cursor: default;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  pointer-events: auto;
}

.circular-weapon-frame {
  display: block;
  width: 100%;
  height: 100%;
}

/* width/height are set inline by CircularWeaponLoadout.tsx (cannonMaxWidth/
   cannonMaxHeight) rather than a CSS percentage of the frame -- deliberately
   computed off cannonScale, not the medallion's own (20% smaller)
   frameScale, so this stays the same size regardless of how the medallion
   around it is scaled. object-fit: contain still fits each weapon's own
   art (aspect ratios vary) into that fixed box without distorting it. */
.circular-weapon-picture {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  object-fit: contain;
  pointer-events: none;
}

/* Sits behind .circular-weapon-picture (earlier in DOM order) but in front
   of .circular-weapon-frame -- a plain colored fill, clipped to
   circular-weapon-slot-mask.png's own white disc via mask-image so it never
   bleeds onto the frame's gold ring, exactly like circular-weapon-slot.png's
   own inner disc. Sized/positioned to fill the button exactly (inset: 0),
   same coordinate space the mask image and frame art already share
   (both 348x365 natively). overflow: hidden clips the fill div itself,
   which is taller than 100% of nothing -- the mask alone would already
   confine *paint*, but this keeps the fill's own box from doing anything
   unexpected before that mask is applied. */
.circular-weapon-charge-mask {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  /* circular-weapon-slot-mask.png is a plain opaque black/white PNG (no
     transparency at all) -- mask-mode's default ("match-source") reads
     that as alpha, which is 255 everywhere on this image, so without
     forcing luminance mode explicitly the mask has no effect whatsoever
     (the fill just paints as an unclipped rectangle). White = fully
     visible, black = fully hidden, which is what luminance mode gives. */
  mask-mode: luminance;
  mask-repeat: no-repeat;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
}

/* Rises from the bottom as weaponState.charge climbs 0->1, same "column-
   reverse fill" idea as .loadout-grid-charge-fill -- green while charging,
   yellow once targeted (see the modifier below), no separate "ready" color
   the way LoadoutGrid's bar gets -- fully charged already reads as "green,
   filled all the way" here. */
.circular-weapon-charge-fill {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  background: #00a71d8a;
  transition: height 0.2s linear;
  box-shadow: 0px 5px 5px inset #ffffff55;
}

.circular-weapon-charge-mask.targeted .circular-weapon-charge-fill {
  background: #ffd60a;
}

/* Same color language as .loadout-grid-cell's own ready/targeted/arming
   states -- green "can fire", gold "locked", pulsing pale glow "waiting on
   you to pick a target" (reuses that exact keyframe animation). */
.circular-weapon-slot.ready {
  cursor: pointer;
  filter: drop-shadow(0 0 8px rgba(76, 217, 100, 0.75));
}

.circular-weapon-slot.targeted {
  cursor: pointer;
  filter: drop-shadow(0 0 8px rgba(255, 214, 10, 0.85));
}

.circular-weapon-slot.arming {
  animation: loadout-grid-cell-arming-pulse 1.1s ease-in-out infinite;
}

/* Wrecked room -- washes the whole medallion out so it visibly reads
   "broken", distinct from a merely-unmanned (full color, just no charge
   fill) weapon. */
.circular-weapon-slot.disabled {
  filter: grayscale(0.85) brightness(0.6);
}

/* A "no entry" red circle-slash, centered over the medallion -- see
   .circular-weapon-slot.disabled above for why a color wash alone isn't
   enough (an unmanned weapon already looks similarly dim). Drawn with CSS
   (a radial ring plus a rotated bar) rather than a new art asset. */
.circular-weapon-disabled-mark {
  position: absolute;
  inset: 18%;
  border-radius: 50%;
  border: 6px solid #ff2b2b;
  pointer-events: none;
}
.circular-weapon-disabled-mark::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 6px;
  height: 141%;
  background: #ff2b2b;
  transform: rotate(45deg);
}

.hull-gauge canvas {
  display: block;
}

.weapons-loadout-img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Tune this against the actual art to align with weapons-panel.png's 4 dark
   slots -- top/left/width/height only, the 4 children below always split
   it into exact quarters. */
.weapons-loadout-slots {
  position: absolute;
  top: 12%;
  left: 3%;
  width: 93%;
  height: 46%;
  display: flex;
}

.weapons-loadout-slot {
  flex: 0 0 25%;
  width: 25%;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  justify-content: space-between;
  /* gap: 4%; */
  padding: 2.2% 2.2%;
  color: #f4ead0;
  pointer-events: none;
  border-radius: 6px;
}

/* Controller only (see WeaponsLoadout's onSlotClick/LoadoutBar.tsx) --
   pointer-events stays "none" everywhere else (the screen's own copy is a
   pure status display). */
.weapons-loadout-slot.clickable {
  pointer-events: auto;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.weapons-loadout-slot.clickable.ready {
  background: rgba(212, 175, 55, 0.35);
  box-shadow: 0 0 0 2px #d4af37 inset;
}

/* Locked-target highlight (see CombatWeaponState.targeted) -- unlike
   .clickable.ready above, this isn't gated on .clickable: it's meant to be
   visible on the screen's own (non-interactive) copy just as much as the
   controller's, since that's the one place a captain's targeting isn't
   hidden behind TargetSelectOverlay's full-screen modal while it's armed
   and waiting to charge. Comes after .ready's own rules so it wins on the
   (momentary, never actually broadcast) overlap where both could apply. */
.weapons-loadout-slot.targeted {
  background: rgba(255, 214, 10, 0.35);
  box-shadow: 0 0 0 2px #ffd60a inset;
}

.weapons-loadout-slot.targeted .weapons-loadout-charge-fill {
  background: #ffd60a;
}

.weapons-loadout-slot.targeted .weapons-loadout-slot-name {
  color: #ffd60a;
}

/* Wrecked room -- see WeaponSlotState.disabled's own doc comment for why
   this is distinct from simply unmanned. */
.weapons-loadout-slot.disabled {
  position: relative;
  filter: grayscale(0.85) brightness(0.6);
}

.weapons-loadout-disabled-mark {
  position: absolute;
  inset: 30%;
  border-radius: 50%;
  border: 4px solid #ff2b2b;
  pointer-events: none;
}
.weapons-loadout-disabled-mark::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 4px;
  height: 141%;
  background: #ff2b2b;
  transform: rotate(45deg);
}

/* Vertical gauge, same fill-from-bottom idea as .combat-charge-fill, just
   oriented for a narrow slot instead of a wide bar. */
.weapons-loadout-charge-bar {
  flex: 0 0 auto;
  width: clamp(4px, 1.1vw, 10px);
  display: flex;
  flex-direction: column-reverse;
  background: rgba(0, 0, 0, 0.5);
  border: 1px solid rgba(212, 175, 55, 0.6);
  border-radius: 3px;
  overflow: hidden;
}

.weapons-loadout-charge-fill {
  width: 100%;
  background: #d4af37;
  transition: height 0.2s linear;
}

.weapons-loadout-slot-info {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

/* Segoe UI, not PiratesBay -- the decorative font loses its edges at this
   small a size (see weapon-slot-name font size history), and Segoe UI is
   already the game's go-to legible sans for small UI text elsewhere
   (.dialog-hint, .event-combat-note, .combat-ships, .name-label). */
.weapons-loadout-slot-name {
  font-family: "Segoe UI", system-ui, sans-serif;
  font-size: clamp(7.2px, 1.008vw, 12.19px);
  font-weight: 600;
  line-height: 1.2;
}

.weapons-loadout-manpower-markers {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1px, 0.3vw, 3px);
}

.weapons-loadout-manpower-marker {
  flex: none;
  width: clamp(4px, 0.8vw, 9px);
  height: clamp(4px, 0.8vw, 9px);
  border-radius: 50%;
  border: 1px solid #d4af37;
  background: rgba(0, 0, 0, 0.4);
}

.weapons-loadout-manpower-marker.filled {
  background: #4cd964;
  border-color: #4cd964;
}

