/* ══ shell ═══════════════════════════════════════════════════════════════
 * The frame every room is seen through: the sidebar wall, the scrolling page
 * column, the brand, the page's own header, the world ticker, and the nav.
 *
 * Below 920px the whole thing turns inside out - the wall becomes a drawer,
 * the vitals move into a fixed header, and the nav docks to the thumb. That
 * layer is at the foot of this file.
 */

/* ── sidebar + content ──────────────────────────────────────────────── */
/* The sidebar is a wall, not a floating card: it runs the full height, holds its
   own scroll, and the page beside it scrolls independently. */
.shell {
  height: 100vh;
  height: 100dvh;
  display: grid;
  grid-template-columns: 254px minmax(0, 1fr);
  align-items: stretch;
}
/* All three exist only on a phone; the desktop shell says this already. */
.topbar, .scrim, .drawer-close { display: none; }
.sidebar { height: 100%; overflow-y: auto; padding: 12px 13px 10px;
  background: var(--panel); border-right: 2px solid var(--border-soft);
  display: flex; flex-direction: column; gap: 9px; }
/* Pushes the language picker - the last child - to the foot of the wall. */
.sidebar > .side-nav { margin-bottom: auto; }
/* The page's gutter lives here, on the column itself, so no tab has to know
   the number.
 *
 * Two things this must not be, both of which it has been:
 *
 * - **Padding on the children.** Padding is *inside* an element, so a `.card`
 *   given the gutter that way paints its background and its border out to the
 *   window edge and merely insets its text. Plain headers looked right and
 *   every card looked broken.
 * - **A grid.** A `position: sticky` grid item can only travel inside its own
 *   grid area, and one child per row means that area is exactly the item's own
 *   height - zero travel. The ticker silently stopped sticking and scrolled
 *   away like anything else, which is why the top of the page looked cut off
 *   mid-scroll. Block layout gives sticky the whole scrollport to work in.
 *
 * ...and it must not space its children with margins either. A `Dialog` is
 * mounted inside the tab that opens it, so the modal overlay is one of these
 * children - and `position: fixed; inset: 0` plus an inherited `margin-top`
 * is a full-screen overlay pushed down the screen and cropped short by exactly
 * that margin. `gap` has no such reach: an out-of-flow child is not a flex
 * item, so it is not spaced at all, which is the correct answer.
 *
 * A flex column also restores sticky, for the same reason grid broke it: a
 * flex item's containing block is the whole column rather than one row of it.
 *
 * No top padding: the ticker has to start flush against the top edge for
 * `top: 0` to mean what it says. The header below it brings its own.
 */
.content { height: 100%; overflow-y: auto;
  display: flex; flex-direction: column; align-items: stretch; gap: 16px;
  padding: 0 30px 60px; min-width: 0; }
/* A column that scrolls must not squeeze its children to fit instead. */
.content > * { flex: none; }
.content > .page-head { margin-top: 6px; }
/* The one child that belongs to the window rather than to the page: it gives
   the gutter back on both sides and runs edge to edge. */
.content > .ticker-bar { margin-left: -30px; margin-right: -30px; }

/* ── the mark ───────────────────────────────────────────────────────── */
.brand { display: flex; align-items: center; gap: 9px; }
.brand-mark { width: 32px; height: 32px; flex: none; object-fit: contain;
  font-size: 24px; line-height: 1; color: var(--accent);
  /* The world turning over, slowly. It is the only thing in the sidebar that
     moves, which is what makes it read as the mark and not as an icon. */
  animation: float-y 6s ease-in-out infinite; }
/* The name, wherever it is set small. The way-in screen sets it large - see
   `.wordmark` in `entry.css` - but both are the display face. */
.brand-name { font-family: var(--font-display); font-weight: 600;
  letter-spacing: -.01em; color: var(--text-hi); }
.brand .brand-name { font-size: 22px; }
/* "Are we connected?" answered by a dot that breathes. The word beside it is
   for the times the answer is no. */
.live { margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 800; padding: 3px 9px; border-radius: var(--r-pill);
  border: 1.5px solid var(--border); color: var(--muted-2); white-space: nowrap; }
.live::before { content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; flex-shrink: 0; }
.live.on { color: var(--ok); background: rgba(66, 201, 124, .14);
  border-color: rgba(66, 201, 124, .4); }
.live.on::before { background: var(--ok-solid); animation: live-pulse 2.4s ease-in-out infinite; }
@keyframes live-pulse { 50% { opacity: .3; } }
@keyframes float-y {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50% { transform: translateY(-4px) rotate(3deg); }
}

/* ── the page's own header ────────────────────────────────────────────
   The icon at poster size, the name of the room in the display face, what the
   room is for underneath it, and - pushed to the far edge - the one live fact
   that makes this room worth opening *right now*. */
.page-head { display: flex; align-items: center; gap: 13px; }
.page-icon { font-size: 30px; line-height: 1; flex: none; }
img.page-icon { width: 32px; height: 32px; object-fit: contain; }
.page-head-id { flex: 1; min-width: 0; }
.page-head h2 { font-size: 27px; font-weight: 600; color: var(--text-hi); }
.page-head p { margin-top: 1px; }
/* The room's pulse. Deliberately the same shape as the connection pill - both
   answer "is anything happening here?" - and the only thing in a page header
   that changes without being asked. */
.page-flag { display: inline-flex; align-items: center; gap: 8px; flex: none;
  padding: 8px 13px; border-radius: var(--r-pill);
  background: var(--panel-2); border: 1.5px solid var(--border);
  font-size: 12.5px; font-weight: 700; color: var(--muted-hi); }
.page-flag::before { content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--ok-solid); flex: none; animation: live-pulse 2.2s ease-in-out infinite; }

/* ── the ticker ───────────────────────────────────────────────────────
   The world talking over the top of whatever room you are standing in: things
   that just happened somewhere else, and the handful of facts about your own
   city that change on their own. It is the width of the window because it does
   not belong to the page under it.

   The strip holds the list several times over and slides by exactly one copy,
   which is what makes the loop seamless - at the moment the animation resets,
   the next copy is sitting precisely where this one started.

   *How many* copies is not a number this file can hold. It is `window / copy`
   rounded up, and a copy is however much the world happens to be saying, in
   whatever font arrived; `ui::mod.rs::Ticker` measures it and hands the answer
   down as `--ticker-shift`, which is why the shift is not written here. Two
   copies was hard-coded once and it is worth saying what that cost: with a
   quiet city and a wide window one copy fell a thousand pixels short of the
   frame, so the last stretch of every pass was blank and the reset read as a
   teleport.

   The travel is exact only if each copy is *exactly* one share of the track,
   which is why the 40px between copies is a trailing pad on the copy and not a
   `gap` on the track. A gap belongs to neither copy: with one, copy two started
   at `run + 40` while the animation travelled `run + 20`, and the strip snapped
   20px - half a gap - backwards at every wrap. Padding is measured inside the
   copy, so the two numbers are the same number again. */
/* Stuck to the top of the page column: what the world is saying is not a
   headline you scroll past, it is the state of the place you are standing in. */
.ticker-bar { position: sticky; top: 0; z-index: 5;
  overflow: hidden; padding: 8px 0;
  background: var(--panel); border-bottom: 2px solid var(--border-soft); }
.ticker-track { display: flex; width: max-content;
  animation: marquee var(--ticker-secs, 38s) linear infinite; }
.ticker-run { display: flex; gap: 40px; padding-right: 40px; }
.ticker-item { display: flex; align-items: center; gap: 7px; white-space: nowrap;
  font-size: 12px; font-weight: 600; color: var(--muted-hi); }
.ticker-item .ticker-icon { font-size: 12.5px; line-height: 1; }
img.ticker-icon { width: 14px; height: 14px; object-fit: contain; }
/* The part of the line worth reading if you only read one part of it. */
.ticker-hi { font-weight: 800; color: var(--accent-2); }
.ticker-hi.money { color: var(--gold); }
.ticker-hi.good { color: var(--ok); }
.ticker-hi.magic { color: var(--magic); }
.ticker-hi.loud { color: var(--err); }
.ticker-hi.cool { color: var(--cool); }
/* Text does not stop dead at the window edge; it fades into it at both ends. */
.ticker-bar::before, .ticker-bar::after { content: ""; position: absolute;
  top: 0; bottom: 0; width: 60px; pointer-events: none; z-index: 1; }
.ticker-bar::before { left: 0; background: linear-gradient(90deg, var(--panel), transparent); }
.ticker-bar::after { right: 0; background: linear-gradient(270deg, var(--panel), transparent); }
/* The fallback is the two-copy case, so a strip that somehow renders before it
   has been measured still loops rather than sitting still. */
@keyframes marquee {
  from { transform: translateX(0); }
  to { transform: translateX(var(--ticker-shift, -50%)); }
}
/* A moving strip of text is the single worst thing on this page for anybody
   who has asked for less motion. It stops, and wraps instead of overflowing.
   Keyed off `body.calm` - the in-game ✨ toggle - and NOT the OS media query,
   for the reason `juice.css` spells out: Windows reports reduced motion on
   machines whose owners never chose it, and a media query would out-rank the
   toggle and quietly park the marquee forever. (It did.) */
body.calm .ticker-track { animation: none; flex-wrap: wrap; width: auto; }
/* The wrapping has to be asked of the copy, not the track: the track's children
   are the two copies, and wrapping *those* only ever moved copy two onto a
   second line - which is the line that is hidden anyway. The lines a reader
   sees are inside the copy. The trailing pad goes with the animation it was
   for. */
body.calm .ticker-run { flex-wrap: wrap; padding-right: 0; }
/* Every copy but the first. Not `:last-child` - there were exactly two copies
   when that was written, and there is now however many it takes to fill the
   window, so hiding the last one would leave the rest of them stacked up. */
body.calm .ticker-track .ticker-run:not(:first-child) { display: none; }
body.calm .brand-mark { animation: none; }

/* The two whole-page states: waiting for the socket, and the end of a life. */
.connecting, .death { display: grid; gap: 14px; text-align: center; justify-items: center;
  padding: 44px 20px; }

/* ── side nav ───────────────────────────────────────────────────────── */
.side-nav { display: grid; gap: 2px; }
/* A nav row is not a pressable object: it has no drop and no border, because
   what it does is move you, not change the world. That distinction is worth
   protecting - if navigation looked like the buttons that spend orbs, the
   buttons that spend orbs would stop looking like anything. */
.nav-btn { display: flex; align-items: center; gap: 10px; padding: 8px 11px;
  background: transparent; border: 0; border-radius: var(--r-md);
  box-shadow: none;
  color: var(--muted-hi); font-size: 14px; font-weight: 700; text-align: left; }
button.nav-btn:active:not(:disabled) { transform: none; }
@media (hover: hover) {
  .nav-btn:hover:not(:disabled) { background: var(--panel-3); color: var(--text-hi);
    border-color: transparent; }
}
/* Where you are: lit, not painted. The accent is spent on actions, not on the
   room the player already knows they are standing in. */
.nav-btn.active { color: var(--on-accent); background: var(--panel-3); font-weight: 800; }
.nav-icon { font-size: 15px; width: 20px; text-align: center; flex: none; }
img.nav-icon { width: 18px; height: 18px; object-fit: contain; }

/* ══ the phone ════════════════════════════════════════════════════════
 *
 * Same markup, different shape. Below 920px the sidebar stops being a wall
 * beside the page and becomes a drawer over it; the parts of it a player may
 * never lose - the vitals, the purse, the bell - move into a fixed header; and
 * the nav docks to the thumb.
 *
 * One rule governs the whole layer: on a screen this size the page is the game,
 * so nothing permanent may cost more than the two bars at its edges.
 */
@media (max-width: 920px) {
  .shell {
    grid-template-columns: 1fr;
    height: auto;
    padding:
      calc(var(--top-h) + env(safe-area-inset-top) + 14px)
      max(12px, env(safe-area-inset-right))
      calc(var(--nav-h) + env(safe-area-inset-bottom) + 24px)
      max(12px, env(safe-area-inset-left));
  }
  /* The shell already supplies the phone's side margins, so the page's own
     gutter would double them. Down here `.content` is not a scroll box either. */
  .content { height: auto; overflow-y: visible; padding: 0; gap: 14px; }
  .content > .page-head { margin-top: 0; }
  /* With the column's gutter gone, so are the negatives the ticker was
     cancelling. The strip escapes the *shell's* padding instead, mirroring the
     exact expression it pads with so the two cancel whatever the safe-area
     turns out to be. And "top" now means the window: the ticker has to clear
     the fixed header itself rather than sliding under it. */
  .content > .ticker-bar {
    margin-left: calc(max(12px, env(safe-area-inset-left)) * -1);
    margin-right: calc(max(12px, env(safe-area-inset-right)) * -1);
    margin-bottom: 14px; border-radius: 0;
    top: calc(var(--top-h) + env(safe-area-inset-top));
  }
  .ticker-bar::before, .ticker-bar::after { width: 28px; }

  /* The header stacks: on a phone the flag would squeeze the title into two
     words and a hyphen. */
  .page-head { flex-wrap: wrap; gap: 10px; }
  .page-head h2 { font-size: 22px; }
  .page-icon { font-size: 26px; }
  .page-flag { order: 3; flex-basis: 100%; justify-content: center; }

  /* ── the header: vitals that can never be scrolled away ─────────────
     This is a permadeath game. A bar hitting zero ends the character, so the
     three bars stay on screen on every tab, at every scroll position. They are
     drawn bare - the color and the length are the whole reading - and a bar
     under a fifth turns urgent by pulsing rather than by waiting to be read.
     The bar itself, and `@keyframes pulse`, come from `character.css`. */
  .topbar {
    display: flex; align-items: center; gap: 10px;
    position: fixed; top: 0; left: 0; right: 0; z-index: 45;
    height: calc(var(--top-h) + env(safe-area-inset-top));
    padding: env(safe-area-inset-top) max(12px, env(safe-area-inset-right))
             0 max(12px, env(safe-area-inset-left));
    background: rgba(26, 30, 48, .94); backdrop-filter: blur(14px);
    border-bottom: 2px solid var(--border-soft);
  }
  .topbar-me { padding: 0; border: none; background: none; border-radius: 50%;
    flex-shrink: 0; min-height: 0; }
  .topbar-me .avatar.sm { width: 32px; height: 32px; font-size: 14px; }
  .topbar-vitals { flex: 1; min-width: 0; display: grid;
    grid-template-columns: repeat(3, 1fr); gap: 8px; }
  .mini-bar { display: flex; align-items: center; gap: 4px; min-width: 0; }
  .mini-bar-icon { font-size: 11px; line-height: 1; flex-shrink: 0; }
  .mini-bar .bar { flex: 1; min-width: 0; }
  .mini-bar.danger .bar-fill { background: var(--err); animation: pulse 1s infinite; }
  .topbar-orbs { font-family: var(--font-num); font-size: 13px; font-weight: 600;
    color: var(--gold); white-space: nowrap; flex-shrink: 0; }
  .topbar .bell { margin: 0; align-self: center; flex-shrink: 0; }

  /* ── the drawer: everything else the sidebar was ────────────────────
     Slid with `left`, never `transform`: a transformed ancestor would drag the
     fixed bottom nav inside it off the screen along with the panel. */
  .sidebar {
    position: fixed; top: 0; bottom: 0; left: -100%; z-index: 55;
    width: min(320px, 86vw); height: auto;
    border-right: 1px solid var(--border-hi);
    padding:
      calc(14px + env(safe-area-inset-top)) 14px
      calc(var(--nav-h) + env(safe-area-inset-bottom) + 24px);
    overflow-y: auto; overscroll-behavior: contain;
    transition: left .24s cubic-bezier(.2, .8, .2, 1);
  }
  .drawer-open .sidebar { left: 0; box-shadow: 30px 0 70px rgba(0, 0, 0, .55); }
  /* The nav is fixed to the screen down here, so it is no longer between the
     drawer's contents - there is nothing left for it to push to the bottom. */
  .sidebar > .side-nav { margin-bottom: 0; }
  /* The header already carries the bell; a second one here would be two places
     to look for the same number. */
  .sidebar .bell { display: none; }
  .drawer-close { display: inline-flex; align-items: center; justify-content: center;
    margin-left: 8px; min-width: 38px; min-height: 38px; }

  .scrim { display: block; position: fixed; inset: 0; z-index: 50;
    background: rgba(4, 6, 12, .6); opacity: 0; pointer-events: none;
    transition: opacity .24s ease; }
  .drawer-open .scrim { opacity: 1; pointer-events: auto; }

  /* ── the nav, docked ────────────────────────────────────────────────
     Seven tabs across the narrowest phone: the label shrinks, the icon does
     not, and the badge stops being a third row and sits on the icon. */
  .side-nav { position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
    display: flex; gap: 2px; background: rgba(26, 30, 48, .94); backdrop-filter: blur(14px);
    border-top: 2px solid var(--border-soft);
    padding: 5px max(4px, env(safe-area-inset-left)) calc(5px + env(safe-area-inset-bottom))
             max(4px, env(safe-area-inset-right)); }
  .nav-btn { position: relative; flex: 1 1 0; min-width: 0; justify-content: center;
    flex-direction: column; gap: 3px; padding: 4px 1px; border-radius: var(--r-md);
    height: calc(var(--nav-h) - 12px); min-height: 0; }
  /* Docked, the active tab is marked by a bar over it rather than by a filled
     slab: seven filled slabs in a row is a stripe, not a nav. */
  .nav-btn.active { background: transparent; border-color: transparent; color: var(--accent-2);
    box-shadow: inset 0 2.5px 0 var(--accent); }
  /* An emoji's line box is far taller than its point size. Left to shrink, the
     icon eats the label's height and the word under it disappears - so neither
     gives ground, and the button is sized to fit both. */
  .nav-icon, .nav-label { flex: none; }
  .nav-icon { width: auto; font-size: 18px; line-height: 1.15; }
  .nav-label { font-size: 10px; line-height: 1.25; letter-spacing: -.1px; max-width: 100%;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .side-nav .badge.count { position: absolute; top: 1px; left: 50%; margin: 0 0 0 5px;
    font-size: 9.5px; min-width: 15px; padding: 0 4px; box-shadow: 0 0 0 2px var(--bg); }
}

/* Sideways: the bars keep their jobs and give back their padding; the tab icons
   are learned by then anyway. */
@media (max-width: 920px) and (max-height: 480px) {
  .nav-label { display: none; }
}
