/* The shell: the rail, the thin bar, the map, the board, a forum.
   Carried over from the wca-proto mockup so the two cannot drift apart.
   Everything reads its colours from tokens.css and nothing here hard-codes one,
   which is what lets the light theme be a list of replaced values. */

body {
  font-family: var(--font-ui);
  /* 15px at the browser's own default root, which is what this was written as.
     In rem so that the text size in Ustawienia — which scales the root — moves
     the body text with everything else. See applyLook in shell.js. */
  font-size: 0.9375rem;
  line-height: 1.5;
  background: var(--bg-base);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--amber-bright); text-decoration: none; }
a:hover { color: var(--amber-bright); text-decoration: underline; }

/* Prose is the board's face — Open Sans, justified — but set larger than the
   board's 13px: this page runs at 15px, so the same prose needs the size to
   match. What you type is set like what it becomes, hence the composer here. */
.post-body, .page-body, .card-prose, .composer-preview, .composer-text {
  font-family: var(--font-prose);
  font-size: 1.3em;
  line-height: 1.5;
}
.post-body, .page-body, .card-prose { text-align: justify; }
/* The reader's own size for posts, on top of the rest — see applyLook in shell.js.
   The preview too: what you type is set like what it becomes. */
.post-body, .composer-preview { font-size: calc(1.3em * var(--post-scale, 1)); }

#shell {
  display: flex;
  min-height: 100vh;
  max-width: var(--shell-max);
  margin: 0 auto;
  box-shadow: 0 0 0 1px var(--line);
}
#main { flex: 1; min-width: 0; }

/* --------------------------------------------------------------- the rail */

.sidebar {
  width: var(--sidebar-width);
  min-width: var(--sidebar-width);
  background: var(--bg-panel);
  border-right: 1px solid var(--line);
  height: 100vh;
  position: sticky;
  top: 0;
  /* The rail no longer scrolls as one piece: the navigation scrolls and the
     radio dock under it stays put, because a chat you have to scroll down to
     find is a chat nobody leaves running. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Everything that is redrawn on every navigation — see index.html. */
.sb-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Reaching the end of the nav must not start scrolling the board behind. */
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
}

.sb-brand {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--line);
}
.sb-brand-link { color: inherit; }
.sb-brand-link:hover { text-decoration: none; color: inherit; }
.sb-brand-name {
  font-family: var(--font-title);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.sb-brand-sub {
  display: block;
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  color: var(--amber-bright);
  margin-top: 2px;
}
.sb-brand-wait {
  display: block;
  margin-top: 0.3rem;
  font-family: var(--font-prose);
  font-size: 0.78rem;
  font-style: italic;
  color: var(--text-soft);
}

/* The portrait is 200×320 by rule, so the frame keeps that ratio. */
.sb-portrait { width: 100%; background: var(--bg-surface); }
/* Every avatar sits back a little. One rule, because it is one decision:
   the board is prose against a photograph of a forest, and a wall of faces at
   full strength reads as a contact sheet rather than as people in a place.
   Put here rather than on each of the dozen names a face goes by — see
   portrait() in shell.js, which is where the class is added. */
.avatar-face { opacity: 0.7; }

.portrait { display: block; width: 100%; height: auto; aspect-ratio: 200 / 320; object-fit: cover; }
.portrait-blank { display: grid; place-items: center; }
.portrait-initials {
  font-family: var(--font-title);
  font-size: 2.6rem;
  letter-spacing: 0.12em;
  color: rgba(255, 255, 255, 0.82);
}

.sb-identity { padding: 0.75rem 1rem 0.9rem; border-bottom: 1px solid var(--line); }
.sb-name {
  font-family: var(--font-title);
  font-size: 1.12rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin: 0;
}
.sb-role { color: var(--text-soft); font-size: 0.82rem; margin: 2px 0 0.6rem; }

/* Where she is standing, as one line: region and room together, in amber,
   because it is the one thing on the rail that changes when she moves. */
.sb-where {
  font-size: var(--label-sm);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--amber-bright);
  border: 1px solid var(--amber);
  padding: 4px 8px;
  display: inline-block;
}
/* It answers the cursor only when it goes somewhere. */
a.sb-where.is-link { text-decoration: none; }
a.sb-where.is-link:hover { color: var(--amber-bright); border-color: var(--amber-bright); }

.sb-switch {
  margin: 0.75rem 1rem;
  background: var(--bg-surface);
  border: 1px solid var(--line-control);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 0.82rem;
  padding: 0.3rem 0.4rem;
  border-radius: var(--radius);
}

.sb-nav { padding-bottom: 1rem; }
.nav-group-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  background: none;
  border: none;
  padding: 0.7rem 1rem 0.3rem;
  font-family: var(--font-ui);
  font-size: var(--label-sm);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-muted);
  cursor: pointer;
  user-select: none;
}
.nav-group-head:hover { color: var(--text-soft); }
.nav-chev { font-size: 0.55rem; transition: transform 0.18s; }
.nav-group.collapsed .nav-chev { transform: rotate(-90deg); }
.nav-group.collapsed .nav-group-body { display: none; }

.nav-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  color: var(--text-soft);
  font-size: 0.87rem;
  padding: 0.42rem 1rem 0.42rem 1.5rem;
  border-left: 2px solid transparent;
}
.nav-item:hover { color: var(--text); background: var(--bg-surface); text-decoration: none; }
.nav-item.active { color: var(--amber-bright); border-left-color: var(--amber); }
/* Koniec Epizodu while a form is still unsent: the one thing in the rail that
   asks for something by a date. Steady for anyone who asked for less motion. */
.nav-item.is-due { color: var(--danger); border-left-color: var(--rust); animation: nav-due 1.6s ease-in-out infinite; }
@keyframes nav-due { 50% { background: color-mix(in srgb, var(--rust) 18%, transparent); } }
@media (prefers-reduced-motion: reduce) { .nav-item.is-due { animation: none; } }
.nav-badge {
  font-family: var(--font-mono);
  /* 0.62rem is under 10px, which is the size the caps labels were pulled up
     off in tokens.css. The same argument applies harder here: the labels at
     least spell a word you can guess at, and this is a number you either read
     or do not. */
  font-size: 0.72rem;
  background: var(--rust);
  color: #fff;
  padding: 1px 6px;
}

.sb-foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.7rem 1rem 1rem;
  border-top: 1px solid var(--line);
  font-size: 0.72rem;
  color: var(--text-muted);
}
.sb-account { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ------------------------------------------------------------- the topbar */

.topbar {
  display: flex;
  align-items: center;
  gap: 1rem;
  /* The insets are 0 on everything without a notch, so max() is the ordinary
     padding there and the cutout's width on a phone held sideways — which is
     where the search button would otherwise sit underneath it. */
  padding: 0.55rem 1.25rem;
  padding-left: max(1.25rem, env(safe-area-inset-left));
  padding-right: max(1.25rem, env(safe-area-inset-right));
  border-bottom: 1px solid var(--line);
  background: var(--bg-panel);
  position: sticky;
  top: 0;
  z-index: 20;
}
/* Beside the buttons rather than in the middle of the bar, and only when it
   has been asked for — see renderTopbar. */
.topbar-form { flex: 0 1 16rem; display: flex; }
/* `display: flex` above would otherwise win over the attribute. */
.topbar-form[hidden] { display: none; }
.topbar-search {
  flex: 1;
  background: var(--bg-surface);
  border: 1px solid var(--line-control);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 0.85rem;
  padding: 0.35rem 0.7rem;
  border-radius: var(--radius);
}
.topbar-search::placeholder { color: var(--text-muted); }
.topbar-spacer { flex: 1; }
.tbtn {
  background: none;
  border: 1px solid var(--line-control);
  color: var(--text-soft);
  font-family: var(--font-ui);
  font-size: var(--label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius);
  cursor: pointer;
}
.tbtn:hover { color: var(--text); border-color: var(--text-soft); }
.tbtn.quiet { border-color: transparent; cursor: default; }
/* Square, because what is in it is a shape rather than a word. */
.tbtn-icon { display: grid; place-items: center; padding: 0.35rem 0.5rem; }

/* A magnifying glass, drawn: a ring and a stroke off its corner. Borders, not
   an image — the same choice .dekor and .map-dot make, and it inherits the
   button's colour so it answers hover without a second rule. */
.icon-search {
  display: block;
  position: relative;
  width: 9px;
  height: 9px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
}
.icon-search::after {
  content: '';
  position: absolute;
  top: 7px;
  left: 7px;
  width: 5px;
  height: 1.5px;
  background: currentColor;
  transform: rotate(45deg);
  transform-origin: left center;
}

/* ---------------------------------------------------------------- the map */

/* A band across the top of the board.
   
   The map runs the full width and the strip is only as tall as the pins need —
   both numbers come from map.js, which measures where the pins actually are.
   The empty north and south of the drawing are cropped rather than paid for in
   height; nothing is scaled unevenly, and a pin cannot drift, because the
   frame is still the whole picture and a pin is still a percentage of it.
   
   The fallback ratio applies for the moment before the image has loaded, and
   on the Mapa screen, which shows the drawing whole. */
/* The banner: the drawn map, and what has been happening on it, side by side.
   The panel is a fixed column so the map keeps the width it had; below the
   breakpoint the two stack and the panel becomes a short list under the map. */
.banner-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 22rem;
  border-bottom: 1px solid var(--line);
}
@media (max-width: 60rem) {
  .banner-row { grid-template-columns: minmax(0, 1fr); }
}

.map-wrap {
  position: relative;
  aspect-ratio: var(--map-aspect, 2);
  max-height: 72vh;
  background: var(--bg-surface);
  overflow: hidden;
}

/* Paper, not a photograph. A pale rule set in from the edge and a soft shadow
   falling inwards from it, so the drawing reads as a sheet lying on the board
   rather than a picture that happens to stop. Drawn over the map with a
   pointer-transparent overlay — the frame under it is the click target for the
   whole map and must stay reachable. */
.map-wrap::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border: 10px solid rgba(238, 232, 218, 0.82);
  box-shadow:
    inset 0 0 0 1px rgba(60, 50, 38, 0.45),
    inset 0 0 22px 10px rgba(28, 24, 20, 0.42);
}
html[data-theme='light'] .map-wrap::after {
  border-color: rgba(252, 250, 245, 0.9);
  box-shadow:
    inset 0 0 0 1px rgba(90, 78, 62, 0.35),
    inset 0 0 22px 10px rgba(120, 105, 85, 0.18);
}
/* The drawn map, and the box every pin is a percentage of — the same element,
   so the two cannot disagree. Slid so the pins' own band sits in the middle of
   the strip rather than the drawing's geometric centre. */
.map-wrap .map-frame {
  position: absolute;
  left: 50%;
  top: 50%;
  /* The strip shows one band of the map, so the whole map is that band's
     height divided by how much of it we are showing. Sizing the frame off the
     strip rather than off the page is what makes the cap below behave: when
     max-height bites, the map gets smaller and the band stays whole, instead
     of the band getting cropped and a pin falling off the edge. */
  height: calc(100% / var(--map-band, 1));
  width: auto;
  /* The band puts the right slice of the drawing in the strip — across as well
     as down, since the empty east and west are cropped for the same reason the
     empty north and south are. --pan-* moves it from there, and only ever
     within what actually overflows — see panOn. */
  transform: translate(
    calc(var(--map-band-across, 0.5) * -100% + var(--pan-x, 0px)),
    calc(var(--map-band-centre, 0.5) * -100% + var(--pan-y, 0px))
  );
}
.map-frame { position: relative; }
.map-wrap .map-art { height: 100%; width: auto; display: block; }
.map-art[hidden] { display: none; }
.map-empty { position: absolute; inset: auto 1rem 1rem; color: var(--text-soft); font-size: 0.8rem; }

.map-dot {
  position: absolute;
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  background: var(--amber);
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.55);
  pointer-events: none;
}
html[data-theme='light'] .map-dot { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7); }

.map-pin-label {
  position: absolute;
  transform: translate(-50%, 0);
  display: flex;
  gap: 5px;
  background: rgba(12, 14, 13, 0.55);
  border: none;
  padding: 1px 5px;
  font-family: var(--font-title);
  font-size: var(--label-sm);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #f1e9d8;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
  white-space: nowrap;
  cursor: pointer;
}
html[data-theme='light'] .map-pin-label {
  color: #1e2926;
  text-shadow: none;
  background: rgba(255, 255, 255, 0.6);
}
.map-pin-label .cs { color: var(--amber-bright); }

.map-stack {
  position: absolute;
  transform: translate(-50%, -100%);
  display: flex;
  align-items: flex-end;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
.map-face {
  display: block;
  width: 34px;
  height: 40px;
  background: var(--paper);
  padding: 3px 3px 9px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.55);
  margin-left: -13px;
  transition: transform 0.15s, margin-left 0.15s;
}
.map-face:first-child { margin-left: 0; }
.map-face-img { width: 100%; height: 100%; display: block; object-fit: cover; aspect-ratio: auto; }
.map-face-img .portrait-initials { font-size: 0.8rem; }

/* Point at the pin and the hand fans out.
 *
 * Every face is drawn — see `stack` in map.js — so at rest a room of nine is a
 * pin-sized pile of photographs, and only hovering costs the width to lay them
 * out. Margin rather than transform: the faces have to actually take up the
 * room, or the ones on the right would still be underneath each other while
 * looking apart, and the stack is centred on its pin so it opens both ways.
 *
 * After the rot- rules on purpose. Same specificity, so source order is what
 * lets the fan straighten the photographs it lifts. */
.map-stack:hover .map-face,
.map-stack:focus-visible .map-face {
  margin-left: 3px;
  transform: translateY(-4px) rotate(0deg);
}
.map-stack:hover .map-face:first-child,
.map-stack:focus-visible .map-face:first-child { margin-left: 0; }
/* The one being fanned goes over its neighbours rather than under the next pin. */
.map-stack:hover, .map-stack:focus-visible { z-index: 3; }

.map-weather {
  position: absolute;
  right: 1rem;
  top: 1rem;
  text-align: right;
  background: rgba(12, 14, 13, 0.86);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.6rem 0.8rem;
  font-size: 0.68rem;
  color: var(--text-soft);
  backdrop-filter: blur(2px);
}
html[data-theme='light'] .map-weather { background: rgba(248, 248, 244, 0.9); }
.weather-title {
  font-size: var(--label-sm);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 0.35rem;
}
.weather-date { font-family: var(--font-title); font-size: 0.9rem; letter-spacing: 0.05em; color: var(--text); }

/* -------------------------------------------------------------- the board */

/* The forest runs behind the board — fixed, so scrolling moves the cards and
   not the trees. Width is not what this class is for any more: every screen is
   full width, and this one is about the wall behind it. */
.view.wide {
  padding: 1.75rem 1.25rem 4rem;
  background: url('/img/forum/tlofora.jpg') center / cover fixed no-repeat;
  box-shadow: inset 0 0 0 100vmax rgba(14, 16, 14, 0.72);
}
/* The light theme keeps more of the wash than the dark one: pale cards and a
   lit photograph sit at the same brightness, so the trees have to step further
   back here for the board to read as being in front of them. */
html[data-theme='light'] .view.wide { box-shadow: inset 0 0 0 100vmax rgba(233, 236, 229, 0.84); }

.turnbar {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  flex-wrap: wrap;
  padding: 0.7rem 1rem;
  margin-bottom: 1.4rem;
  background: var(--bg-surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--amber);
  border-radius: var(--radius);
}
.turnbar-label { font-size: var(--label-sm); letter-spacing: 0.18em; text-transform: uppercase; color: var(--amber-bright); }
.turn-chip {
  display: flex;
  gap: 0.4rem;
  align-items: baseline;
  font-size: 0.8rem;
  color: var(--text);
  border: 1px solid var(--line-control);
  border-radius: var(--radius);
  padding: 3px 9px;
}
.turn-chip:hover { border-color: var(--amber); text-decoration: none; }
.turn-chip small { color: var(--text-muted); }

/* The board index itself lives in forum.css — it is the forum's own CSS.
   What stays here is only what wraps it. */

/* --------------------------------------------------------------- a forum */

.forum h1 { font-family: var(--font-title); letter-spacing: 0.06em; text-transform: uppercase; margin: 0.4rem 0 0.2rem; }

.subforum { margin: 1.75rem 0; }

.thread-list { display: grid; margin-top: 0.6rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.thread-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.2rem 1rem;
  padding: 0.6rem 0.9rem;
  background: var(--bg-surface);
  border-bottom: 1px solid var(--line);
  color: inherit;
}
.thread-row:last-child { border-bottom: none; }
.thread-row:hover { background: var(--bg-raised); text-decoration: none; }
.thread-title { font-family: var(--font-title); letter-spacing: 0.04em; font-size: 0.92rem; }
.thread-meta { font-size: 0.72rem; color: var(--text-muted); grid-column: 1; }
.thread-when { font-size: 0.7rem; color: var(--text-muted); grid-row: 1 / span 2; align-self: center; text-align: right; }

/* ------------------------------------------------------------ narrow view */

@media (max-width: 60rem) {
  /* A height and an aspect ratio together are a contradiction, and the ratio
     won: the box took its *width* from the 260px, came out 443px wide, and
     the page grew sideways to hold a map nobody could see the right-hand
     third of. The strip is a fixed height here, so the ratio has to go. */
  .map-wrap { height: 260px; aspect-ratio: auto; }
}

/* ------------------------------------------------------------- the drawer */

/* Below 60rem the rail is a drawer over the page, opened from the ≡ in the
   thin bar. It used to become one only on a phone (46rem) and, between the
   two, stack over the top of every screen — a tablet or a phone held sideways
   scrolled past the whole portrait and menu before reaching the board. The
   rest of the phone layout still waits for 46rem; only the rail comes early.
   DRAWER in shell.js is the same number. */
@media (max-width: 60rem) {
  /* ------------------------------------------------------- the rail, shut */

  /* Stacked, the rail was 1191px of portrait, name and navigation on top of
     every screen — you scrolled past the whole of yourself before reaching
     the board. It is a panel over the page now, and it starts shut. */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    width: var(--drawer-width);
    height: 100dvh;
    z-index: 60;
    border-right: 1px solid var(--line);
    border-bottom: none;
    /* As a drawer it is a fixed-height panel again, so the dock is pinned to
       the bottom of it exactly as on a wide screen. */
    overflow: hidden;
    transform: translateX(-100%);
    transition: transform 0.2s ease-out;
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
    /* The dock is pinned to the bottom of a 100dvh panel, and on a phone with
       a home indicator the bottom of the screen is not somewhere you can put
       a button — the gesture bar takes the press. */
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* `rail-shut` is the *desktop* preference, remembered per browser. It has no
     say here: on a phone the drawer starts shut whatever the last window did,
     and `rail-open` is the only thing that opens it. */
  html.rail-shut .sidebar { display: flex; }
  html.rail-open .sidebar { transform: none; }

  /* A 100dvh box with `overflow: hidden`: without this a drawer taller than
     the screen is simply cut off with no way to reach the bottom of it. */
  .sb-body { overflow-y: auto; }

  .rail-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgba(10, 12, 11, 0.55);
    border: none;
    padding: 0;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease-out;
  }
  html.rail-open .rail-scrim { opacity: 1; pointer-events: auto; }
  /* The page behind a drawer does not scroll. */
  html.rail-open, html.rail-open body { overflow: hidden; }

}

/* ------------------------------------------------------------- the trail */

/* Forum › region › location › thread, in the thin bar. Screens push their own
   trail — see setCrumbs in shell.js. */
.crumbs {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  font-size: var(--label-sm);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
}
/* Back and forward, beside the trail rather than inside it — the trail says
   where this screen sits in the board, and these say where the reader has
   been, which is not the same journey. Joined into one control so the pair
   reads as one thing at a glance. */
.nav-steps { display: flex; flex: none; }
.nav-step {
  background: none;
  border: 1px solid var(--line-control);
  color: var(--text-soft);
  font-family: var(--font-ui);
  font-size: 1rem;
  line-height: 1;
  padding: 0.28rem 0.5rem;
  cursor: pointer;
}
.nav-step + .nav-step { border-left: none; }
.nav-step:hover:not(:disabled) { color: var(--amber-bright); border-color: var(--text-soft); }
/* Dimmed rather than gone: a control that disappears takes the other one's
   position with it, and the pair is meant to sit still. */
.nav-step:disabled { opacity: 0.35; cursor: default; }

.crumb { color: var(--text-muted); }
.crumb:hover { color: var(--amber-bright); text-decoration: none; }
.crumb.current { color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.crumb-sep { color: var(--text-muted); opacity: 0.6; }

/* ------------------------------------------------------------- a thread */

/* Read on one side, write on the other. Below the breakpoint they stack, with
   the composer under the posts where it has always been. */
.scena { display: grid; gap: 1.5rem 2rem; align-items: start; }
.scena-head { grid-column: 1 / -1; }

/* The plate at the head of a thread — see the header in app.js.
   The board already has a way of saying "this is a place": the opaque panel and
   the amber tick a room's heading wears on the region page (.lokacja-name in
   forum.css). A thread stands in one of those rooms, so it is built out of the
   same two things rather than out of an h1 and a grey line, and the room is the
   line the plate is built around instead of the third item in a list of four. */
.scena-plate {
  position: relative;
  padding: 0.85rem 1.1rem 0.9rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--amber);
  border-radius: var(--radius);
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--fora-panel) 88%, var(--bg-base)),
    var(--fora-panel));
}

/* Region › room, over the title. Mono and letterspaced, which is how the board
   sets anything that is a label rather than a sentence. */
.scena-where {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.5rem;
  margin: 0 0 0.15rem;
  font-family: var(--font-mono);
  font-size: var(--label);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.scena-region { color: var(--ink-dim); text-decoration: none; }
.scena-region:hover { color: var(--accent); }
.scena-region::after { content: '›'; margin-left: 0.5rem; opacity: 0.6; }
.scena-place-name { color: var(--accent); font-weight: 600; }

/* The thread's name, set the way the board sets the head of a panel: centred,
   uppercase and tracked, with the barbell under it — see OFFLINE on the front
   page. Uppercase in the stylesheet rather than in the markup, because that
   would be a lie about what somebody called their thread. */
.scena-head h1 {
  font-family: var(--font-title);
  font-size: 1.5rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
  margin: 0;
}
.scena-dekor {
  height: 1px;
  max-width: 260px;
  margin: 0.5rem auto 0.7rem;
  background: linear-gradient(90deg, transparent, currentColor 22%, currentColor 78%, transparent);
  color: var(--text-soft);
  opacity: 0.5;
}

/* The four facts, in as many columns as the plate has room for. `auto-fit`
   rather than a fixed count: on a phone this is one column and on a wide
   screen it is four, and nothing in between needs deciding. */
.scena-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.5rem 1.4rem;
  align-items: start;
}
.scena-fact { display: grid; gap: 0.2rem; align-content: start; min-width: 0; }
.scena-fact-label {
  font-family: var(--font-mono);
  font-size: var(--label-sm);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Names and things: a run of short items on one wrapping line, not a list.
   Four bulleted lists in a row would be a table of contents at the head of a
   thread nobody has started reading. */
.scena-who, .scena-things {
  margin: 0;
  font-size: 0.85rem;
  line-height: 1.45;
}
.scena-things { color: var(--text-soft); }
.scena-who.is-empty, .scena-things.is-empty { color: var(--text-muted); font-style: italic; }
.scena-name { color: var(--green, var(--text)); text-decoration: none; }
.scena-name.is-second { color: var(--amber-bright); }
.scena-name.is-npc { color: var(--rust, var(--text-soft)); }
.scena-name:hover { text-decoration: underline; }
/* The board's own separator between two names on a line — the tracking on a
   name eats the space either side, so the gap is drawn rather than typed. */
.scena-name + .scena-name::before {
  content: '·';
  color: var(--text-muted);
  margin: 0 0.35rem;
  text-decoration: none;
  display: inline-block;
}

/* Date, time of day and — only when it is not an ordinary scene — the kind.
   The same bordered chips the karta prints its facts on, so the two mastheads
   on this board say "here is a fact about this" in one voice. */
.scena-stamps { margin: 0; }
/* Inside a fact the chips are the fact, so the row's own bottom margin would
   be a gap under one column and not the others. */
.scena-fact .stamp-row { margin-bottom: 0; }

/* The queue sits in a fact now, so it gives back the margins it had when it
   was a line of its own under the plate. */
.scena-fact .turns { margin: 0; font-size: 0.85rem; gap: 0.35rem; }

/* What the room is. Set narrow and quiet — it is the label on the door, read
   once on the way in, not the thread. */
.scena-place {
  margin: 0.6rem 0 0;
  max-width: 62ch;
  color: var(--text-soft);
  font-size: 0.9rem;
  line-height: 1.55;
}
.scena-place p { margin: 0 0 0.4rem; }
.scena-place p:last-child { margin-bottom: 0; }

/* The star and the record shelf, on one line and of a size.
   They were stacked down the full width of the page, which made the head of a
   thread taller than the first post in it. `align-items: start` matters: the
   shelf grows downward when it is opened and must not stretch the star with
   it. */
.scena-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  gap: 0.5rem;
  margin-top: 0.8rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--line);
}
/* Two controls of one size. They already shared their padding, border and
   corner and still did not match, because one was as wide as the words in it
   and the other carried a 12rem floor — so the width is the container's to
   decide and neither gets an opinion. */
.scena-tools > * { flex: 0 0 12rem; max-width: 100%; }
/* Named rather than left to `> *`: the shelf's own top margin is set further
   down this file at the same specificity, and would win on order. */
.scena-tools .playlist { margin-top: 0; }
.scena-tools .scena-mark { justify-content: center; }
/* Level as well as equal. The two carry the same padding, but the shelf's ♪
   was set at the body's 15px against the star's label size, and six pixels of
   line box is the difference between a pair and a near-miss. */
.scena-tools .playlist > summary { justify-content: center; font-size: var(--label-sm); }
/* Opened, the shelf is a list of records rather than a button, and a list of
   records in a 12rem column is a column of broken titles. */
.scena-tools .playlist[open] { flex: 1 1 100%; }
.scena-tools .playlist[open] > summary { justify-content: flex-start; }

/* Two columns as soon as both fit, which is sooner than it used to be.

   The rail takes 268px before this grid gets any width at all, so a breakpoint
   read off the viewport is always that much meaner than it looks — 82rem meant
   a 13" MacBook at its own default scaling (1280 CSS px) stacked the composer
   under a thread sixty posts long, which is the one place it is least use. So
   the column arrives at 72rem, narrower, and grows at 82. */
@media (min-width: 72rem) {
  .scena { grid-template-columns: minmax(0, 1fr) 24rem; }

  /* Bounded to what is on screen, and scrolling inside itself past that.

     A sticky column taller than the window is a column whose send button
     nobody can reach without scrolling the whole thread to the end — which,
     with the roll and kit block open, is what it was. Only here: these are the
     widths where the column has the screen to itself. Stacked under the posts
     it is just the bottom of the page, and bottoms of pages scroll. */
  .scena-write {
    max-height: calc(100vh - 5.5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
  }
}

@media (min-width: 82rem) {
  .scena { grid-template-columns: minmax(0, 1fr) 30rem; }
}

/* The composer keeps its place on screen while the thread scrolls past it. */
.scena-write { position: sticky; top: 4.5rem; }
.scena-write .composer { position: static; }

/* Or, if the reader asked for it in Ustawienia, under the thread at any width:
   the bottom of the page, full width, scrolling with it. */
[data-writer='pod'] .scena { grid-template-columns: minmax(0, 1fr); }
[data-writer='pod'] .scena-write { position: static; max-height: none; overflow-y: visible; }

/* The preview only takes half the desk once it is asked for. */
.composer-panes { grid-template-columns: 1fr; }
@media (min-width: 60rem) {
  .composer-panes.with-preview { grid-template-columns: 1fr 1fr; }
}

/* --------------------------------------------------------- shutting the rail */

/* The rail is a panel, not furniture: it goes away when the screen is wanted
   for reading, and the handle that brings it back lives in the thin bar so it
   is reachable when the rail itself is not. */
.rail-toggle {
  background: none;
  border: 1px solid var(--line-control);
  color: var(--text-soft);
  font-size: 0.8rem;
  line-height: 1;
  padding: 0.35rem 0.5rem;
  cursor: pointer;
}
.rail-toggle:hover { color: var(--text); border-color: var(--text-soft); }

html.rail-shut .sidebar { display: none; }

/* The dark sheet behind the drawer. Nothing above the phone breakpoint — the
   rail is furniture there and has nothing to be dismissed from. */
.rail-scrim { display: none; }

/* Who is speaking, stated rather than chosen — the rail decides it. */
.composer-speaker {
  font-family: var(--font-title);
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text);
}

/* ----------------------------------------------------------- turning pages */

/* A screen arrives rather than snapping in — short enough not to be in the way
   of someone clicking through several. */
@keyframes view-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}
#view > * { animation: view-in 0.22s ease-out both; }
.map-wrap:not([hidden]) { animation: view-in 0.3s ease-out both; }

@media (prefers-reduced-motion: reduce) {
  #view > *, .map-wrap:not([hidden]) { animation: none; }
}

/* The banner shows a band of the map, and the map is dragged to see the rest
   of it. The pins keep their own clicks — see panOn. */
.map-frame.is-pannable { cursor: grab; touch-action: none; }
.map-frame.is-panning { cursor: grabbing; }
/* The pins are the things to point at; the ground under them is the handle. */
.map-frame.is-pannable button,
.map-frame.is-pannable a { cursor: pointer; }

/* ------------------------------------------------- ogólne wydarzenia */

/* The panel beside the map. It is the map's height and scrolls inside, so the
   banner is one band whatever has been going on underneath it.
 *
 * That only holds if the panel cannot make the grid row taller than the map,
 * and a max-height cannot promise it: 72vh was a guess at the map's height,
 * and on any window where the map came out shorter the list ran on past the
 * bottom of the drawing. So the panel is an empty box the row ignores, and its
 * contents are lifted out of flow inside it. The map — which has an aspect
 * ratio and therefore a height of its own — is left as the only thing in the
 * row with an opinion about how tall it is. */
.events-panel {
  position: relative;
  overflow: hidden;
  background: var(--bg-surface);
  border-left: 1px solid var(--line);
}
.events-inner {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 0.9rem 1rem 1rem;
}
/* Stacked under the map, there is no row to match: the panel is given a height
   so the box inside it still has one to fill. */
@media (max-width: 60rem) {
  .events-panel { height: 20rem; border-left: none; border-top: 1px solid var(--line); }
}

.events-head {
  margin: 0;
  font-family: var(--font-title);
  font-size: 0.95rem;
  letter-spacing: 2px;
  text-transform: uppercase;
}

/* Whose events these are. Wears the same left band as the rows below it, so
   the colour says which region without repeating the name on every line. */
.events-scope {
  margin: 0.35rem 0 0.6rem;
  padding-left: 0.55rem;
  border-left: 3px solid var(--line);
  font-size: 0.8rem;
  color: var(--text-soft);
}

.events-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  display: grid;
  gap: 0.5rem;
  align-content: start;
}

/* One deed. The colour down the left is its region — that is the whole reason
   a mixed list is readable, so it is the one thing that is never subtle. */
.event {
  display: grid;
  gap: 0.15rem;
  padding: 0.35rem 0 0.35rem 0.6rem;
  border-left: 3px solid var(--line);
  font-size: 0.82rem;
  line-height: 1.35;
}
.event-who { font-weight: 600; }
.event-what { color: var(--text); }
.event-where {
  display: flex;
  justify-content: space-between;
  gap: 0.6rem;
  color: var(--text-soft);
  font-size: 0.75rem;
}
.event-when { flex: none; }
/* The place is the way into the thread. Underlined only on hover: eight of
   these down the margin, permanently underlined, would read as a list of
   links rather than a list of things that happened. */
.event-place { color: var(--text-soft); text-decoration: none; }
.event-place:hover { color: var(--amber-bright); text-decoration: underline; }
.event-empty { padding: 0.4rem 0; font-size: 0.82rem; }

/* --------------------------------------------------- which page you are on */

/* Two marks under the banner, for the two panes of the swipe. Nothing at all
   above the phone breakpoint, where both panes are on screen at once and
   there is nothing to be on page one or page two of.

   Above that query on purpose: the two rules set display at the same
   specificity, so the later one wins and this has to be the earlier. */
.banner-dots { display: none; }
.banner-dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 1px solid var(--line-control);
  border-radius: 50%;
  background: none;
  cursor: pointer;
}
.banner-dot.on { background: var(--amber); border-color: var(--amber); }

/* ============================================================ the phone ===
 *
 * 46rem and down. The number is decided in tokens.css — see the note there
 * before changing it.
 *
 * Everything above this line assumes a screen wide enough to put two things
 * beside each other. On a phone there is only ever one column, and three
 * pieces of the shell stop being furniture: the rail becomes a drawer, the
 * drawn map goes away and leaves a link, and the trail shortens to the step
 * you are standing on. */

@media (max-width: 46rem) {

  /* --------------------------------------------------------- the thin bar */

  .topbar { gap: 0.5rem; padding: 0.5rem 0.75rem; }
  /* Who you are logged in as is at the foot of the drawer already, and on a
     375px bar it is the one thing there that is never acted on. */
  .topbar .tbtn.quiet { display: none; }
  .tbtn { padding: 0.3rem 0.5rem; letter-spacing: 0.06em; }
  /* Open, the search box is the bar: a 16rem basis beside four other things
     left it three characters wide. */
  .topbar-form { flex: 1 1 auto; }
  /* …and when it is open, the trail and the arrows have nowhere to be. */
  .topbar:has(.topbar-form:not([hidden])) .crumbs,
  .topbar:has(.topbar-form:not([hidden])) .nav-steps { display: none; }

  /* Narrower, but never smaller: this is the way back on a phone, so it keeps
     a thumb's worth of height whatever the bar has to fit around it. */
  .nav-step { padding: 0.28rem 0.45rem; min-height: 1.9rem; }

  /* Only the step you are standing on. The ancestors are a row of truncated
     words at this width, and the way back is the arrow beside them and the
     drawer, neither of which is the trail. */
  .crumbs { flex: 1 1 auto; }
  .crumbs > :not(.crumb.current) { display: none; }

  /* ------------------------------------------------------- the two panes */

  /* What has been happening, and the map, as two pages of one swipe.

     Scroll snapping does all of it: the browser's own gesture, its own
     momentum, its own snap and its own accessibility. Nothing here listens
     for a finger, which is the whole reason to do it this way — a hand-rolled
     swipe is a week of edge cases and it still fights the page underneath. */
  .banner-row {
    display: flex;
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    /* A swipe that runs out of pane must not start dragging the page behind. */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .banner-row::-webkit-scrollbar { display: none; }
  .banner-row > * { flex: 0 0 100%; scroll-snap-align: start; }

  /* What has been happening is where you land; the map is what you swipe to.
     Order rather than markup, so a wide screen keeps the map on the left where
     it has always been. */
  .events-panel { order: 1; height: 19rem; border-top: none; border-left: none; }
  .map-wrap { order: 2; height: 19rem; }

  .banner-dots {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.5rem 0;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--line);
  }

  /* The whole drawing, fitted — not the band of it the wide banner shows.
     A band is for a strip that is wider than it is tall and has room to crop;
     this pane is nearly square, and a reader swiping to "the map" wants the
     map, not the middle third of it with the pins cropped off the side. */
  .map-wrap .map-frame {
    left: 50%;
    top: 50%;
    height: auto;
    width: 100%;
    transform: translate(-50%, -50%);
  }
  .map-wrap .map-art { width: 100%; height: auto; }

  /* The paper edge comes off.

     It is a 10px cream border with a shadow falling inwards from it, and the
     point of it is that the drawing reads as a sheet lying on the board rather
     than a picture that happens to stop. On a 375px pane it is a white frame
     eating twenty pixels of a map that has none to spare, and the pane's own
     edges already say where the map ends. */
  .map-wrap::after { display: none; }

  /* The polaroids come off instead — drawPins is told not to draw them, so
     this is only what is left to say about the names.

     A name is 113 to 154 pixels wide and the pins are 56 and 64 apart, so
     they do overlap across — but they are pinned at their own places, and
     those are 63 and 90 pixels apart down the map, which is four times a
     label's height. They pass each other rather than collide. Tightened a
     little here so they pass with more room. */
  .map-wrap .map-pin-label {
    font-size: var(--label-sm);
    letter-spacing: 0.06em;
    padding: 1px 4px;
  }

  /* The weather, laid along the bottom edge instead of pinned to the corner.

     In the corner it was a 137×81 card on a 375×304 pane — an eighth of the
     map — and it sat directly on top of the third pin. The rail already
     carries the date and the time of day; the weather itself is only ever
     written here, so it moves rather than goes. */
  .map-weather {
    inset: auto 0 0 0;
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 0.6rem;
    padding: 0.25rem 0.6rem;
    border: none;
    border-top: 1px solid var(--line);
    text-align: center;
    font-size: 0.7rem;
  }
  /* The word "Pogoda" over a line that says what the weather is. */
  .map-weather .weather-title { display: none; }
  .map-weather .weather-date { font-size: 0.72rem; }

  /* ------------------------------------------------------------ the page */

  /* The forest is a 6000×4000 photograph — 1.5MB, and on a phone it was most
     of what the board weighed. There is barely any of it to see between the
     cards at this width, it sits behind a 72% wash regardless, and
     background-attachment: fixed repaints it on every scroll frame. So the
     wash's own result becomes the ground: the same colour, nothing fetched.

     `background` rather than `background-image`, so the shorthand up the file
     cannot leave the URL behind for the browser to go and get. */
  .view.wide {
    padding: 1rem 0.75rem 3rem;
    background: var(--board-ground);
    box-shadow: none;
  }
  /* The light theme sets the wash with one more selector than .view.wide has,
     so it has to be told separately or it wins and darkens a flat colour. */
  html[data-theme='light'] .view.wide { box-shadow: none; }

  /* The composer follows the thread down on a wide screen because there is a
     column spare for it. Here it would be most of the screen, permanently. */
  .scena-write { position: static; }
}

/* The drawer slides; a reader who has asked for less of that gets none. */
@media (prefers-reduced-motion: reduce) {
  .sidebar, .rail-scrim { transition: none; }
}

/* ------------------------------------------- wearing somebody's account -- */

/* Rust, full width, above the trail: this is not part of the board, and it is
   not supposed to look like it is. Not sticky — the topbar is, and two things
   glued to the top fight each other. What stays on screen is the chip in the
   bar, which turns rust and says whose account this is. */
.worn {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.45rem 1.25rem;
  background: var(--rust);
  color: var(--paper);
  font-family: var(--font-ui);
  font-size: 0.85rem;
}
/* `display: flex` above would otherwise win over the attribute. */
.worn[hidden] { display: none; }
.worn .tbtn {
  border-color: currentColor;
  color: inherit;
  margin-left: auto;
}
.worn .tbtn:hover { background: rgba(0, 0, 0, 0.15); }
/* The chip in the topbar, while the account is not the reader's own. */
.tbtn.is-worn { border-color: var(--rust); color: var(--danger); }

/* ------------------------------------------------------------- playlista */

/* A shelf of records in the head of a thread. Folded by default and silent
   until pressed — the JS makes no iframe until then, so there is nothing here
   that could start singing on its own. */
.playlist {
  margin-top: 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-surface);
}
.playlist > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.8rem;
  cursor: pointer;
  list-style: none;
}
.playlist > summary::-webkit-details-marker { display: none; }
.playlist > summary::before {
  content: '♪';
  color: var(--text-muted);
}
.playlist[open] > summary { border-bottom: 1px solid var(--line); }
.playlist-label {
  font-family: var(--font-title);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-size: var(--label-sm);
}
.playlist-body { padding: 0.6rem 0.8rem 0.8rem; display: grid; gap: 0.6rem; }
.playlist-body > .muted { margin: 0; font-size: 0.76rem; }
.playlist-tracks { display: grid; }
.playlist-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.25rem 0;
  border-bottom: 1px solid var(--line);
}
.playlist-row:last-child { border-bottom: none; }
.playlist-play {
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.8rem;
  padding: 0;
}
.playlist-play:hover { color: var(--amber-bright); }
.playlist-title { font-size: 0.85rem; overflow-wrap: anywhere; }
.playlist-who { font-size: 0.7rem; color: var(--text-muted); }
.playlist-drop, .playlist-stop { font-size: 0.7rem; }
/* Small, but on the page — see the note in playlist.js. Never offscreen and
   never display:none: a frame nobody can see is a frame nobody can press play
   on when YouTube declines to start by itself, and a frame that is not being
   rendered is one the browser is free to stop. */
.playlist-player:empty { display: none; }
.playlist-player iframe {
  display: block;
  width: 100%;
  max-width: 260px;
  aspect-ratio: 16 / 9;
  border: 0;
}
.playlist-now { display: flex; gap: 0.6rem; align-items: baseline; margin: 0; font-size: 0.78rem; }
.playlist-now-title { color: var(--amber-bright); }
.playlist-row.is-playing .playlist-title { color: var(--amber-bright); }
.playlist-add { display: grid; gap: 0.4rem; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; }
@media (max-width: 40rem) {
  .playlist-add { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------- the cookie notice ---- */

/* Bottom of the page, full width, once per browser — see cookies.js. Fixed
   rather than in the flow: it is not part of the board and must not push it
   about on the one load it appears. Above everything, but dismissible, so it
   cannot be what a keyboard's focus ends up hiding behind. */
.cookie-note {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.6rem 1rem;
  padding: 0.85rem 1.2rem;
  /* The gesture bar on a phone sits under this; the notice stays above it. */
  padding-bottom: max(0.85rem, env(safe-area-inset-bottom));
  background: var(--bg-panel);
  border-top: 1px solid var(--line-bright);
  color: var(--text-soft);
  font-family: var(--font-ui);
  font-size: 0.85rem;
}
.cookie-note p { margin: 0; max-width: 60ch; }
/* The one control on it, and it is a thumb target rather than a caption. */
.cookie-note button { min-height: 2.75rem; padding: 0.4rem 1.4rem; }

/* ---------------------------------------------------------- a finger -----
   The shell's half of the coarse-pointer block at the end of app.css, which
   says why 44px and why `pointer: coarse` rather than the breakpoint. These
   two live here because the phone rules above would otherwise win: same
   specificity, later file. */
@media (pointer: coarse) {
  /* Width as well as height for the pager: two arrows side by side at 22px
     across are a coin toss as to which one you get. */
  .nav-step { min-height: 44px; min-width: 44px; }
  .rail-toggle { min-height: 44px; min-width: 44px; }
}

/* Scrollbars in the board's own colours, everywhere. The standard pair is what
   current Chrome, Edge and Firefox read; the ::-webkit- rules are for older
   Chromium builds — Opera among them — which ignore the standard pair and
   drew a wide white bar down the sidebar. */
* { scrollbar-width: thin; scrollbar-color: var(--line-bright) transparent; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line-bright); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--line-control); }
::-webkit-scrollbar-corner { background: transparent; }
