/* THE TYPE TRAVELS WITH THE STYLESHEET, not with each page that loads it.
   Found by a parallel session 2026-08-25: `public/world.html` loads this file
   and has no font link at all, so the bitmap type #1953 decided was silently
   falling through to Courier New on the actual world page. Asking every
   consumer to remember a <link> is a bug waiting to be repeated, and it had
   already been repeated once. An @import costs one request and makes any page
   that loads this file get the right faces. */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap');

/* ⭐CASCADE LAYERS (#2920, 2026-09-22): this file is the `base` layer. The
   order is declared once, below, and repeated inline by build-web.js so
   the world page fixes it before any stylesheet loads. Inside a layer,
   load order and specificity still decide between THIS file's rules;
   between files, the layer decides, always. */
@layer base, module, skin, nerm, corners;
@layer base {

/* ══════════════════════════════════════════════════════════════════════
   WORLD RAIL (GH #1927) — the inventory that lives in the letterbox.

   Every position in this file is written against the four variables the 3D
   engine publishes on <body> when it resizes:

     --fx  left edge of the render, in px      --fw  render width
     --fy  top edge of the render, in px       --fh  render height

   The picture is centred HORIZONTALLY, so the bar left equals the bar right
   and --fx doubles as a right offset. Vertically it is flush to the top
   since GH #2037: --fy is the (zero) top band and a fifth variable, --fb,
   carries the band below the render, where all the leftover black collects.
   Bottom-anchored rules read `var(--fb, var(--fy, 0px))` — the fallback is
   the old symmetric frame, so this file keeps working against an engine
   older than the flush-top change. The prototype's own HUD does exactly
   this, and copying it is deliberate: nothing here measures a canvas or
   works out a scale.

   That means the rail is correct at every window size with no JavaScript
   running. The only thing script decides is [data-edge], because a phone has
   no side bar at all (see world-rail.js).

   PLATES ARE RETIRED HERE (#1953, points 3 and 4, Wray 2026-08-25). This
   file used to paint .hg-plank, which rides `--hg-plate-btn` in
   hub-overlay.css. It no longer does, and it no longer uses the
   `--hg-plate-*` fonts or colours either. Half-and-half on one screen was
   explicitly ruled out, so a world surface takes none of it.

   WHAT REPLACES IT, AND WHY IT IS SAFE TO SHIP BEFORE THE ART EXISTS.
   The drawn-chrome module publishes generated 9-slice sources as CSS
   variables — `--wc-leather`, `--wc-parchment`, `--wc-button`, each with a
   matching `--wc-*-slice` so the slice integers are DATA and never typed
   here. Everything below asks for those with a working fallback, so:

     module present -> drawn chrome
     module absent  -> these gradients, which is a finished look, not a hole

   That second state is deliberate and is shipping first on purpose: it is
   exactly what `WorldChrome.install()` returning false has to produce, so
   proving it now means the degraded path is real rather than theoretical.

   THE FAILURE THAT IS BEING DESIGNED OUT. .hg-plank sliced its plate
   `0 300 0 300 fill`: the whole source HEIGHT squeezed into the button, only
   the left and right 300px surviving as art, and therefore one stray row in
   the source becoming a full-width line across every button in the game. The
   replacement sources use four non-zero edges so the middle tiles and the
   corners carry the detail. Nothing here should ever write a zero edge.

   TYPE IS BITMAP ON WORLD SURFACES (#1953 point 4): Press Start 2P for
   headings, VT323 for everything else, overriding Metamorphous and Alegreya.
   Press Start 2P is a display face and enormous per character, so headings
   are small and tight; VT323 is condensed and reads at label sizes where
   Press Start 2P would not.
   ══════════════════════════════════════════════════════════════════════ */

/* Component-scoped tokens. Defined HERE rather than on :root so the wider
   bitmap-type seam can define `--wc-head` / `--wc-body` globally without
   fighting this file: if it does, these inherit it; if it has not yet, the
   fallback stack applies. Nothing in this lane writes to :root. */
.wr-rail {
  --wr-head: var(--wc-head, 'Press Start 2P', 'Courier New', monospace);
  --wr-body: var(--wc-body, 'VT323', 'Courier New', monospace);
  --wr-ink: var(--wc-ink, #e6c88a);

  position: fixed;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 10px;
  box-sizing: border-box;
  color: var(--wr-ink);
  font-family: var(--wr-body);
  /* The letterbox is already dark. A rail that paints its own slab on top of
     it reads as a panel bolted over the picture, so with no drawn chrome the
     surface stays almost nothing and the pieces do the work. */
  background: var(--wc-leather, linear-gradient(180deg, rgba(20, 12, 6, .55), rgba(10, 6, 3, .8)));
  /* The tile size is the module's to decide (a 96-chrome-pixel source shown at
     2 CSS px per chrome pixel is 192px), so it arrives as data like the slices
     do. `auto` here would draw a drawn-leather source at half the size it was
     authored for. */
  background-size: var(--wc-leather-tile, auto);
  /* REQUIRED by the drawn sources: they are baked at one canvas pixel per
     chrome pixel and scaled up by a whole number, so smoothing turns the
     dither to mush. Harmless on the gradient fallback. */
  image-rendering: pixelated;
  overscroll-behavior: contain;
}

/* ── the four edges ──────────────────────────────────────────────────
   A side rail where there is a side; a bottom band where there is not.
   Same markup either way: this is a variant, not a second layout. */

.wr-rail[data-edge="left"] {
  left: 0;
  top: var(--fy, 0px);
  width: var(--fx, 200px);
  height: var(--fh, 100vh);
}

.wr-rail[data-edge="right"] {
  right: 0;
  top: var(--fy, 0px);
  width: var(--fx, 200px);
  height: var(--fh, 100vh);
}

/* A band HUGS the render rather than filling the bar. A phone's band is
   302px tall and the rail needs about 140 of it, so filling it would paint a
   third of the screen as an empty panel; sitting against the picture keeps
   the inventory next to the room and leaves the rest of the band black for
   whatever #1925 decides goes there. `max-height` is still the bar, so it can
   never grow over the render. */
.wr-rail[data-edge="top"] {
  left: var(--fx, 0px);
  bottom: calc(var(--fb, var(--fy, 0px)) + var(--fh, 0px));
  width: var(--fw, 100vw);
  height: auto;
  max-height: var(--fy, 120px);
}

.wr-rail[data-edge="bottom"] {
  left: var(--fx, 0px);
  top: calc(var(--fy, 0px) + var(--fh, 0px));
  width: var(--fw, 100vw);
  height: auto;
  max-height: var(--fb, var(--fy, 120px));
}

/* A landscape phone has almost no black space at all, so the rail has to sit
   ON the picture. It gets a real backdrop then, because it is covering the
   room rather than filling the frame around it. */
.wr-rail[data-overlay="true"] {
  background: linear-gradient(180deg, rgba(12, 7, 3, .82), rgba(12, 7, 3, .95));
  backdrop-filter: blur(2px);
}

/* Overlaid, a band can no longer hug the render (there is not enough bar to
   sit in), so it takes the window edge and a fixed height instead. */
.wr-rail[data-edge="bottom"][data-overlay="true"] {
  top: auto;
  bottom: 0;
  height: 108px;
  max-height: none;
}

.wr-rail[data-edge="top"][data-overlay="true"] {
  bottom: auto;
  top: 0;
  height: 108px;
  max-height: none;
}

.wr-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex: 0 0 auto;
}

/* A BAND IS ONE STRIP, not a stacked panel. The head above the list is right
   in a tall side rail and wrong across the bottom, where it eats the height
   the thumbnails need and cuts their labels off. Lay the whole tray out as a
   row: head on the left, pieces filling the rest. This is the shape Jack's
   shell mock uses for the tray. */
.wr-rail[data-edge="top"],
.wr-rail[data-edge="bottom"] {
  flex-direction: row;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
}

.wr-rail[data-edge="top"] .wr-head,
.wr-rail[data-edge="bottom"] .wr-head {
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  flex: 0 0 auto;
  max-width: 34%;
}

/* A side rail is narrow and tall, and a button plus a title do not fit
   across 160px without squeezing the title to an ellipsis. Stack instead: the
   rail has vertical room to spare, and a full-width button reads better. A
   band is the other way round and keeps the row. */
.wr-rail[data-edge="left"] .wr-head,
.wr-rail[data-edge="right"] .wr-head {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

/* The button does NOT stretch to the rail, even though the head is a stretch
   column. The drawn plank carries its identity in the 9-slice ENDS — an
   end-grain seam, a bevel turn and two rivets, all inside the side slice —
   and the middle is the part that stretches. Pulled to a 300px rail the ends
   are 24px each against 250px of quiet field, and it reads as a bar again;
   left at its natural width it reads as a bound plaque. Measured at both
   widths against the real sources, not guessed. */
.wr-rail[data-edge="left"] .wr-turn,
.wr-rail[data-edge="right"] .wr-turn {
  width: auto;
  align-self: center;
}

.wr-title {
  font-family: var(--wr-head, 'Press Start 2P', monospace);
  /* Small, because Press Start 2P at 13px is roughly twice the width of the
     face it replaced and would not fit a 160px rail at all. */
  font-size: 9px;
  letter-spacing: .06em;
  text-transform: uppercase;
  opacity: .8;
  white-space: nowrap;
  /* A 1600px desktop gives a 160px rail. Without these the title can grow the
     head and shove the Turn plank outside the rail entirely. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Its own line, shown only while a placed piece is in hand. */
.wr-hint {
  display: none;
  flex: 0 0 auto;
  font-size: 15px;      /* VT323 sits small; see .wr-label */
  line-height: 1.15;
  letter-spacing: .03em;
  color: rgba(230, 200, 138, .8);
}

/* THE BUTTON, OFF THE PLATE. Not .hg-plank and not a copy of it: that class
   exists to paint a retired plate, and inheriting from it would drag
   `--hg-plate-btn` back in through the side door.

   With the drawn source present this is a 9-slice of `--wc-button`. Without
   it, a carved-edge fallback that is a finished button rather than a
   placeholder — because "the module failed to load" must not look broken. */
.wr-plank {
  appearance: none;
  -webkit-appearance: none;
  font-family: var(--wr-head, 'Press Start 2P', monospace);
  /* Press Start 2P is enormous per character. 9px here is the same optical
     size as 13px in a normal face. */
  font-size: 9px;
  line-height: 1.6;
  letter-spacing: .04em;
  color: var(--wr-ink, #e6c88a);
  text-shadow: 0 2px 0 #2a1608;
  padding: 9px 12px;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
  text-align: center;
  border: 0 solid transparent;
  background: linear-gradient(180deg, #6b4423, #402711);
  box-shadow:
    inset 0 1px 0 rgba(230, 200, 138, .28),
    inset 0 -2px 0 rgba(0, 0, 0, .45),
    0 2px 0 rgba(0, 0, 0, .5);
  image-rendering: pixelated;
  transition: transform .08s, filter .08s;
}

/* The drawn source, when the module has painted. The slice integers arrive as
   DATA in `--wc-button-slice`, so they can change without this file changing
   and there is no number here to get wrong. */
.wr-plank {
  /* Three things here are load-bearing and each was got wrong first:
     - THE `fill` KEYWORD IS WRITTEN HERE AND NOWHERE ELSE. The module
       publishes bare integers (`6 12 6 12`) because numbers are data and
       `fill` is intent: whether the middle of the nine-slice gets painted is
       the consumer's decision, and a slice value carrying a keyword cannot be
       reused anywhere that wants a plain slice.
       Get this wrong in either direction and it fails silently. Two `fill`s is
       invalid and the whole shorthand is dropped, so the button keeps its
       fallback gradient while the rail around it paints drawn leather — the
       ruled-out split, reached from the inside. No `fill` at all paints a
       carved frame around a transparent hole. Both look like bad art rather
       than bad CSS.
     - The width defaults to ZERO, not to a guess. A border-image with zero
       width paints nothing, so a module that has not run — or that publishes
       sources without a width — falls back to the gradient above instead of
       wrapping it in a transparent border.
     - No @supports test. A declaration containing var() is assumed valid at
       parse time, so `@supports (border-image: var(--x) ...)` is TRUE whether
       or not the variable exists, and guards nothing. */
  border-width: var(--wc-button-width, 0);
  border-image: var(--wc-button, none) var(--wc-button-slice, 0) fill / var(--wc-button-width, 0) round;
}

.wr-plank:hover { filter: brightness(1.08); }
.wr-plank:active { transform: translateY(2px); filter: brightness(.95); }
.wr-plank:disabled,
.wr-plank[aria-disabled="true"] { opacity: .6; transform: none; pointer-events: none; }
.wr-plank:focus-visible { outline: 2px solid var(--wr-ink, #e6c88a); outline-offset: 2px; }

.wr-turn {
  flex: 0 0 auto;
}

/* ── the pieces ──────────────────────────────────────────────────────
   A grid in a side rail, a single scrolling row in a band. */

.wr-list {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  gap: 8px;
  overflow: auto;
  grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
  align-content: start;
  scrollbar-width: thin;
}

.wr-rail[data-edge="top"] .wr-list,
.wr-rail[data-edge="bottom"] .wr-list {
  grid-auto-flow: column;
  grid-auto-columns: 62px;
  grid-template-columns: none;
  overflow-x: auto;
  overflow-y: hidden;
}

.wr-item {
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid rgba(230, 200, 138, .18);
  border-radius: 3px;
  background: rgba(40, 26, 12, .5);
  color: inherit;
  font: inherit;
  padding: 5px 3px 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  cursor: grab;
  /* A drag must not also scroll the rail or pan the page under it. */
  touch-action: none;
  transition: border-color .1s, background .1s;
}

.wr-item:hover { border-color: rgba(230, 200, 138, .45); background: rgba(58, 38, 17, .6); }
.wr-item:active { cursor: grabbing; }

/* It is a <button>, so it is already a Tab stop -- it just had nothing to show
   for it. A focusable control with no focus state is worse than an unfocusable
   one: the keyboard is in there and the screen does not say so. */
.wr-item:focus-visible {
  outline: 2px solid var(--wr-ink, #e6c88a);
  outline-offset: 2px;
}

.wr-art {
  width: 100%;
  aspect-ratio: 1 / 1;
  background: rgba(0, 0, 0, .35) center / contain no-repeat;
  border-radius: 2px;
  display: grid;
  place-items: center;
}

/* Until the pieces have thumbnails, the two-letter stub keeps the rail
   legible and honestly signals that the art is not here yet. */
.wr-art[data-placeholder]::after {
  content: attr(data-placeholder);
  font-size: 15px;
  letter-spacing: .08em;
  opacity: .38;
}

.wr-label {
  /* VT323 is a condensed pixel face and sits SMALL for its declared size: at
     the 9px this was before the type switch the labels were unreadable. 14px
     here is about the optical size 9px was in the face it replaced. Caught in
     a screenshot, not in the code. */
  font-size: 14px;
  line-height: 1;
  letter-spacing: .02em;
  text-transform: uppercase;
  opacity: .62;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── the drag ────────────────────────────────────────────────────────
   The ghost is positioned from the top-left corner of the window and moved
   by transform, so it never triggers layout while the pointer moves. */

.wr-ghost {
  position: fixed;
  left: 0;
  top: 0;
  width: 64px;
  height: 64px;
  margin: -32px 0 0 -32px;
  pointer-events: none;
  z-index: 90;
  border-radius: 3px;
  background: rgba(40, 26, 12, .75) center / contain no-repeat;
  border: 1px solid rgba(230, 200, 138, .35);
  display: grid;
  place-items: center;
  opacity: .85;
}

.wr-ghost[data-placeholder]::after {
  content: attr(data-placeholder);
  font: 15px/1 var(--wr-body, 'VT323', monospace);
  letter-spacing: .08em;
  color: rgba(230, 200, 138, .5);
}

/* Over a tile it can actually take: the only feedback the player gets before
   they let go. */
.wr-ghost[data-valid="true"] {
  border-color: rgba(150, 240, 170, .85);
  box-shadow: 0 0 0 2px rgba(150, 240, 170, .25);
  opacity: 1;
}

.wr-ghost[data-valid="false"] { opacity: .5; }

/* While dragging out of the inventory, the rail steps out of the way rather
   than competing with the room for attention. */
.wr-rail[data-dragging="true"] { opacity: .72; }

/* Holding a piece that is ALREADY in the room turns the rail into the place
   you put it back. It has to look like a target, not like it is receding, so
   this overrides the fade above. */
.wr-rail[data-holding="placed"] {
  opacity: 1;
  box-shadow: inset 0 0 0 1px rgba(230, 200, 138, .5);
}

.wr-rail[data-holding="placed"] .wr-list { opacity: .3; }
.wr-rail[data-holding="placed"] .wr-hint { display: block; }

/* The hint lives inside the head, so it needs no per-edge rule: in a side
   rail it stacks under the plank, in a bottom tray it stacks under the title
   in the head's own left-hand column. */

@media (prefers-reduced-motion: reduce) {
  .wr-item { transition: none; }
}

/* ── the bag button (#2035) ──────────────────────────────────────────
   The panel mounts SHUT, and until now nothing in the served world opened it:
   the only `toggle()` in the repo was in the test harness, so purrni was
   flagged on in production and completely invisible. This is the thing that
   opens it.

   It sits in the bottom-left of the letterbox, out of the render, and it is
   `.wr-plank` so it needs no border-image of its own. It appears only in a
   room the server says you may arrange -- see world-rail-mount.js. */
.wr-bag {
  position: fixed;
  z-index: 41;                 /* over the rail, which is 40 */
  left: 14px;
  bottom: calc(var(--fb, var(--fy, 0px)) + 14px);
  width: auto;
}

.wr-bag[hidden] { display: none; }

/* THE PHONE SEAT (#2236). Under full bleed --fb publishes 0 and the corner
   above lands on the chat strip's left end. world-frame.js publishes the
   safe bands and stamps data-wf-frame on body: the bag climbs to the row
   above the strip — the band, the strip's 44px, a 14px gap — beside the
   chest button's row, far enough left that the two never touch. The
   letterbox rule above is untouched, so the desktop cannot move. */
body[data-wf-frame="fill"] .wr-bag {
  bottom: calc(var(--wf-safe-bottom, 12px) + 58px);
}

/* ══════════════════════════════════════════════════════════════════════
   THE PANEL (#1926) — the presentation furniture actually uses.

   Everything above positions a STRIP in the letterbox. A panel is the other
   thing the same component can be: a sheet over the room, open while you are
   decorating and shut the rest of the time. Asked outright on 2026-08-25, the
   bottom strip in Jack's shell mock is the combat bar and furniture comes from
   somewhere else, so this is what `mount` defaults to.

   It is positioned against the render rectangle like everything else, so it
   covers the ROOM and never the letterbox chrome around it.
   ══════════════════════════════════════════════════════════════════════ */

.wr-rail[data-edge="panel"] {
  left: var(--fx, 0px);
  top: auto;
  bottom: var(--fb, var(--fy, 0px));
  width: var(--fw, 100vw);
  /* HUGS ITS CONTENT, like the band does, and for the same reason: ten starter
     pieces are one row, and a fixed sheet would put that row at the top of 400
     empty pixels covering half the room. The cap is a share of the RENDER, not
     of the viewport — a percentage here would be of the fixed-position
     containing block, which is the window, and would be wrong at every size
     where the letterbox is not zero. */
  height: auto;
  max-height: calc(var(--fh, 100vh) * 0.45);
  background: var(--wc-parchment, linear-gradient(180deg, rgba(20, 12, 6, .92), rgba(10, 6, 3, .97)));
  background-size: var(--wc-parchment-tile, auto);
  border-top: 1px solid rgba(230, 200, 138, .28);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, .5);
  transition: transform .18s ease-out, opacity .18s ease-out;
}

/* Shut: slid off the bottom of the render, inert, and out of the tab order.
   `visibility` rather than `display` so the slide is animatable. */
.wr-rail[data-edge="panel"][data-open="false"] {
  transform: translateY(101%);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.wr-rail[data-edge="panel"][data-open="true"] {
  transform: none;
  opacity: 1;
  visibility: visible;
}

/* A panel is wide, so its pieces lay out as a grid that fills it rather than
   the single scrolling row a bottom strip uses. */
.wr-rail[data-edge="panel"] .wr-list {
  grid-auto-flow: row;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  grid-auto-columns: auto;
  overflow-y: auto;
  overflow-x: hidden;
}

.wr-rail[data-edge="panel"] .wr-head {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
}

.wr-rail[data-edge="panel"] .wr-turn { width: auto; }

/* How many you own, on the corner of the tile. Only drawn above one, so a
   panel of single pieces stays quiet. */
.wr-item { position: relative; }

.wr-qty {
  position: absolute;
  top: 2px;
  right: 3px;
  /* Same VT323 sizing correction as the label: it reads small for its size. */
  font-size: 13px;
  line-height: 1;
  letter-spacing: .02em;
  color: var(--wr-ink, #e6c88a);
  background: rgba(10, 6, 3, .78);
  border-radius: 2px;
  padding: 1px 3px;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .wr-rail[data-edge="panel"] { transition: none; }
}

/* On a phone the panel sits flush to the screen bottom (--fb is 0), so its
   last row would run under the home bar. The safe band becomes padding
   INSIDE the sheet; nothing about its position or height changes. */
body[data-wf-frame="fill"] .wr-rail[data-edge="panel"] {
  padding-bottom: var(--wf-safe-bottom, 12px);
}
}
/* end of @layer base */
