@font-face {
  font-family: "RuneScape";
  src: url("./assets/runescape.woff2") format("woff2");
  font-display: swap;
}

:root {
  color-scheme: dark;
  font-family: "RuneScape", serif;
  font-synthesis: none;
  --rs-orange: #ff981f;
  --rs-yellow: #ffff00;

  /*
   * The palette is read off the artwork the interface is cut from rather than
   * picked to sit near it, so nothing inside a page is a colour the panel
   * around it does not already contain. Counting in from `inventory-tab.png`,
   * the interior is #3e3529; `chat-btn.png` outlines its slab in near-black,
   * lights the row under the top edge at #81715d and falls away from #766856
   * underneath. Those four are the whole vocabulary.
   */
  --panel-hi: #4a4133;
  --panel-lo: #3a3227;
  --panel-sunk: #2f281f;
  --edge-black: #14110c;
  --edge-dark: #221d15;
  --edge-lit: #81715d;
  --edge-lit-dim: #5a5141;
  --edge-row: rgb(20 17 12 / 55%);

  --ink: #e9e2d4;
  --ink-dim: #b6ad9d;
  --ink-faint: #9d9384;
  --ink-off: #7d7568;

  --rs-good: #6ee275;
  --rs-bad: #ef6e68;
  --rs-burnt: #f3b558;

  /*
   * Type and spacing run off the column the same way the masthead does, so a
   * page shrinks with the frame around it instead of holding desktop sizes
   * inside a panel that has given up a third of its width. The numbers are what
   * each step came to at the full 408.
   */
  --fs-xl: calc(var(--column) * 0.0588);  /* 24 */
  --fs-lg: calc(var(--column) * 0.0466);  /* 19 */
  --fs-md: calc(var(--column) * 0.0417);  /* 17 */
  --fs: calc(var(--column) * 0.0368);     /* 15 */
  --fs-sm: calc(var(--column) * 0.0319);  /* 13 */
  --fs-xs: calc(var(--column) * 0.027);   /* 11 */

  --sp-1: calc(var(--column) * 0.0098);   /* 4 */
  --sp-2: calc(var(--column) * 0.0147);   /* 6 */
  --sp-3: calc(var(--column) * 0.0196);   /* 8 */
  --sp-4: calc(var(--column) * 0.0294);   /* 12 */

  /*
   * What the interface column comes to, for the couple of things inside the
   * inventory panel that are text rather than artwork and so cannot be a
   * percentage of their slot. It tracks the column at every width: 408px until
   * the screen is too narrow to hold it, then the screen less its gutter. This
   * counts the scrollbar, which nothing using it cares about to a pixel.
   */
  --column: min(100vw - 16px, 408px);
}

* {
  box-sizing: border-box;
}

html {
  min-height: 100%;
  background: #080705;
}

body {
  height: 100vh;
  height: 100dvh;
  margin: 0;
  overflow: hidden;
  color: #fff;
  background:
    radial-gradient(circle at 50% 38%, rgb(68 57 38 / 36%), transparent 32rem),
    repeating-linear-gradient(0deg, rgb(255 255 255 / 1%) 0 1px, transparent 1px 3px),
    #12100c;
  image-rendering: pixelated;
}

button,
a {
  font: inherit;
}

/*
 * Every part of the interface shares one column, the width the inventory panel
 * draws at: the tab strip, whichever view is open and the chatbox all run edge
 * to edge inside it. Nothing below is sized in fixed pixels across that width,
 * so the whole interface scales together and the column can simply shrink on a
 * narrow screen.
 *
 * Down the column are four sections that stay where they are. The title bar,
 * the tab strip and the chatbox hold their height and the body takes what is
 * left, scrolling inside its own frame, so reaching for a tab or the chat entry
 * never means chasing it up the page.
 */
.game {
  display: flex;
  width: min(100% - 28px, 408px);
  height: 100%;
  flex-direction: column;
  margin: 0 auto;
  padding: 14px 0;
  text-align: center;
}

.masthead,
.interface-tabs,
.chatbox {
  flex-shrink: 0;
}

/*
 * The body is the game's panel. The inventory sprite is a stone band six pixels
 * thick around a mottled interior, with a bracket at each corner, so nine-
 * slicing it at 8 keeps the brackets and stretches the band: one panel, any
 * size. It takes whatever height is left between the tab strip and the chatbox
 * and whichever view is open scrolls inside it.
 *
 * The scrollbar is hidden the way the chat log's is. A native one would take
 * its width out of the column, and the column is the one measurement every
 * other part of the interface is sized against.
 */
.view-frame {
  min-height: 0;
  flex: 1;
  overflow-y: auto;
  padding: 8px;
  border: 10px solid transparent;
  border-image: url("./assets/inventory-tab.png") 8 fill / 10px repeat;
  overscroll-behavior: contain;
  scrollbar-width: none;
}

.view-frame::-webkit-scrollbar {
  display: none;
}

/*
 * A HUD strip rather than a masthead: the name small on the left, and on the
 * right the orbs the game keeps beside its minimap, reading this one's vitals.
 *
 * Four counts fit the narrowest column the game draws at; four of five digits
 * each do not, and what a too-long row loses is the coins on the end of it. So
 * the strip wraps rather than clips: past that width the orbs come off the
 * name's line and take one of their own, still to the right.
 */
.masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 6px;
  gap: 2px 10px;
  text-shadow: 2px 2px #000;
}

h1 {
  margin: 0;
  color: #f5df97;
  font-size: calc(var(--column) * 0.062);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -1px;
}

.vitals {
  display: flex;
  align-items: center;
  margin-left: auto;
  gap: 7px;
}

.vital {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}

/* The orbs hold their native size at every width. They are chrome rather than
   the game artwork the column scales, and a screen that doubles them doubles
   them cleanly. */
.vital img {
  display: block;
  width: 24px;
  height: 24px;
}

/*
 * The boxes centre against each other exactly, but this font hangs its digits
 * high in the line box, so they read about 0.035em above the middle of an orb
 * beside them. Measured off the rendered ink and nudged back, in em so it holds
 * as the column resizes the number, and as a transform because an offset that
 * small gets rounded to a whole pixel and overshoots.
 */
.vital b {
  transform: translateY(0.035em);
  color: var(--rs-yellow);
  font-size: calc(var(--column) * 0.044);
  font-weight: 400;
  line-height: 1;
}

/* Each orb says what it is counting to a screen reader; on screen the art
   does it. */
.vital-name {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/*
 * The body frame is the panel now, so the inventory draws no frame of its own:
 * the hover line sits at the top of it, the way the game puts it at the top of
 * the screen, and the grid flows down from under it.
 */
.inventory-stage {
  width: 100%;
}

.hover-action {
  min-height: 1.25em;
  margin: 0 0 4px;
  overflow: hidden;
  color: #fff;
  font-size: calc(var(--column) * 0.0441);   /* 18 / 408 */
  line-height: calc(var(--column) * 0.0539); /* 22 / 408 */
  text-align: left;
  text-overflow: ellipsis;
  text-shadow: 1px 1px #000;
  white-space: nowrap;
}

/*
 * Every item grid on the site is this: a number of slots across, each one the
 * shape a slot is in the artwork, filling whatever width it has been given.
 * How many across is JavaScript's to say, since the inventory rounds its own
 * size to whole rows of that same number and two copies of the figure are two
 * chances to disagree; see `itemsAcross` in game-state.js.
 *
 * The inventory used to be a fixed slab of 4 by 7 sized to whatever the panel
 * could spare, which meant a stack was a different size on a tall window than
 * on a short one, and a different size again on a shop's shelf. One width and
 * one slot shape everywhere settles all three.
 */
.inventory-grid,
.shop-grid {
  display: grid;
  width: 100%;
  grid-template-columns: repeat(var(--slots-across, 6), 1fr);

  /*
   * Panning has to reach the frame underneath, or a finger anywhere on the grid
   * could not scroll the page, and the grid is most of it. A drag on a touch
   * screen arms on a held press instead, and blocks the scroll itself from
   * there; see inventory.js.
   */
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/*
 * 43 by 36 is a slot in the inventory artwork, and the slot is its own query
 * container so everything drawn in one is a share of it rather than of the grid
 * it happens to be standing in. That is what lets the same sprite and the same
 * corner number come out the same size on a page of six slots and a page of
 * thirty.
 */
.slot {
  position: relative;
  display: grid;
  width: 100%;
  aspect-ratio: 43 / 36;
  padding: 0;
  place-items: center;
  border: 0;
  outline: 0;
  background: transparent;
  container-type: inline-size;
  cursor: default;
}

.slot:focus-visible {
  box-shadow: inset 0 0 0 2px var(--rs-yellow);
}

.slot.has-item {
  cursor: grab;
}

.slot.dragging {
  opacity: 0.38;
}

.slot.drop-target::after {
  position: absolute;
  inset: 4px;
  border: 2px solid rgb(255 255 255 / 85%);
  background: rgb(255 255 255 / 15%);
  content: "";
  pointer-events: none;
}

/*
 * Every sprite is scaled by the same factor, so each one is drawn at its own
 * native width over the 43px a slot measures in the panel artwork. A single
 * percentage would not do: the piles run from 12px to 31px wide, and object-fit
 * would blow the small ones up to the size of the large ones.
 */
.item-sprite {
  display: block;
  width: calc(100cqw * var(--sw, 31) / 43);
  height: auto;
  image-rendering: pixelated;
  pointer-events: none;
  -webkit-user-drag: none;
}

/* Stack sizes sit in the slot's top-left corner, coloured by magnitude. */
.stack-count {
  position: absolute;
  z-index: 2;
  top: 3.5%;
  left: 4.5%;
  color: var(--rs-yellow);
  font-size: 22cqw;   /* 9.5 of the 43 a slot is across in the artwork */
  line-height: 1;
  pointer-events: none;
  text-shadow: 1px 1px #000;
}

.stack-count.large {
  color: #fff;
}

.stack-count.huge {
  color: #00ff80;
}

.drag-ghost {
  position: fixed;
  z-index: 1001;
  transform: translate(-50%, -50%);
  pointer-events: none;
  opacity: 0.72;
  filter: drop-shadow(2px 2px 0 rgb(0 0 0 / 75%));
}

/* Width comes from the slot the item was picked up from, set when the drag
   starts, since the slots resize with the page. */
.drag-ghost img {
  display: block;
  height: auto;
  image-rendering: pixelated;
}

.context-menu {
  position: fixed;
  z-index: 1100;
  min-width: 188px;
  padding: 2px 3px 3px;
  border: 1px solid #000;
  color: #fff;
  background: #5d5447;
  box-shadow:
    inset 0 0 0 1px #8c8376,
    2px 2px 0 rgb(0 0 0 / 45%);
  font-size: 18px;
  line-height: 18px;
  text-align: left;
  text-shadow: 1px 1px #000;
}

.menu-title {
  padding: 1px 3px 3px;
  color: #000;
  text-shadow: none;
  white-space: nowrap;
}

.menu-title span {
  color: #904010;
}

.menu-option {
  display: block;
  width: 100%;
  padding: 1px 3px;
  border: 0;
  color: #fff;
  background: transparent;
  line-height: inherit;
  text-align: left;
  text-shadow: 1px 1px #000;
  white-space: nowrap;
  cursor: pointer;
}

.menu-option:hover,
.menu-option:focus-visible {
  color: var(--rs-yellow);
  outline: none;
}

.menu-option .target {
  color: var(--rs-orange);
}


/*
 * The view switcher is the game's own tab strip. Both stones are a single
 * 33x36 sprite rebuilt from a screenshot of the interface, and every icon is
 * padded to that same 33x36, so each one lands where the game puts it without
 * any per-tab nudging. Stone and icon are both sized in percentages, so the
 * pair scales together to whatever an eighth of the strip comes to.
 */
/* Eight stones spanning the inventory panel below, every one of them a page.
   The game's own row is seven, overhanging a proportionally narrower panel,
   which would put each stone at 16% of the panel's width against 12.5% here;
   eight sized to the panel keeps them from reading as oversized on a page this
   wide.

   Eight is also where it stops. The strip divides a column that shrinks with
   the screen, so a stone is already down to 36px at 320 and a ninth would take
   it to 32, which under a finger is the tightest thing on the page. That is why
   the HiScores sit on the Progress page rather than on a stone of their own:
   the count is the thing that has to give as pages are added, and a board of
   everyone's numbers is the other half of a page about yours. */
.interface-tabs {
  display: grid;
  width: min(100%, 408px);
  margin: 6px auto 0;
  grid-template-columns: repeat(8, 1fr);
}

.interface-tab {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  aspect-ratio: 33 / 36;
  background: url("./assets/tab-stone.png") 0 0 / 100% 100% no-repeat;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.interface-tab.active {
  background-image: url("./assets/tab-stone-active.png");
}

/* The game does not light a tab up under the cursor, but a pointer on a page
   needs the affordance. A finger has no use for it: by the time :hover lands
   the tab is open, and the stone says so itself. */
@media (hover: hover) {
  .interface-tab:hover:not(.active) {
    filter: brightness(1.2);
  }
}

.interface-tab:disabled {
  cursor: default;
  filter: brightness(0.76);
}

.interface-tab:disabled img {
  filter: grayscale(1);
  opacity: 0.4;
}

.interface-tab:focus-visible {
  outline: 2px solid var(--rs-yellow);
  outline-offset: -4px;
}

.interface-tab img {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/*
 * The chat log has a page to itself, so the game can now say something where
 * nobody is looking — including a refusal, which is the kind of thing a player
 * should not have to go and find. So the stone flashes when a line lands there.
 *
 * It flashes between the strip's own two sprites rather than wearing a mark of
 * its own, so nothing here is invented: the plain stone and the red one the open
 * tab wears. `step-end` holds each for half the cycle, which is a flash rather
 * than a throb — pixel art has no in-between frames to dissolve through, and a
 * crossfade would blur the stone grain the whole strip is cut from.
 *
 * Red already says a tab is open. A steady red and a flashing one read as
 * different things, and the open one cannot be the flashing one anyway, since
 * opening the page is what clears it.
 */
@keyframes tab-unread {
  0%,
  100% {
    background-image: url("./assets/tab-stone.png");
  }

  50% {
    background-image: url("./assets/tab-stone-active.png");
  }
}

/* Infinite here because chat.js decides how long a flash runs: it takes the
   class off after three cycles, so its FLASH_MS is three of these 1.1s. */
.interface-tab.unread:not(.active) {
  animation: tab-unread 1.1s step-end infinite;
}

/* The strip carries icons only, the way the game draws it. The name stays in
   the markup unseen, so each button still has an accessible name. */
.tab-name {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* =========================================================== page parts === */

/*
 * Everything below is content inside the body frame, so it is built out of the
 * same materials the frame, the tab strip and the chat row are: the panel's own
 * interior colour, hard one-pixel edges, and the chat button sprite. Nothing
 * here uses a browser 3D border. `groove`, `outset` and `inset` draw a soft
 * desaturated bevel that anti-aliases at any width, which is the one thing a
 * page of pixel art cannot have running through it.
 */

.activity-view,
.exchange-view {
  width: 100%;
}

.activity-window,
.skills-window,
.exchange-window,
.shop-window,
.account-window,
.community-window {
  width: 100%;
  color: var(--ink);
  font-size: var(--fs);
  text-align: left;
  text-shadow: 1px 1px #000;
}

/* ------------------------------------------------------------ surfaces --- */

/*
 * The two surfaces every page is assembled from, cut the way the sprites cut
 * them: a black outline, one lit row under the top edge and one dark row above
 * the bottom for something raised, the lighting the other way up for something
 * sunk into the panel. Counting in from the top of `chat-btn.png`, that lit row
 * is #81715d over a fill that falls away underneath; `chat-btn-active.png` is
 * the same slab with the fall reversed.
 *
 * The sprites themselves do this at button size. At panel size their 24px
 * gradient would have to stretch into a vignette, so these draw the same edge
 * in CSS, where it stays one pixel at any height.
 */
.rs-raised {
  border: 1px solid var(--edge-black);
  background: linear-gradient(var(--panel-hi), var(--panel-lo));
  box-shadow:
    inset 0 1px 0 var(--edge-lit),
    inset 0 -1px 0 var(--edge-dark);
}

.rs-sunken {
  border: 1px solid var(--edge-black);
  background: var(--panel-sunk);
  box-shadow:
    inset 0 1px 0 var(--edge-dark),
    inset 0 -1px 0 var(--edge-lit-dim);
}

/* ------------------------------------------------------------- heading --- */

/*
 * One heading for every page, with the icon off its own tab stone so the page
 * is topped by the thing that opened it. The stones pad every icon to 33x36, so
 * they all hang the same way here with no per-page nudging, which is what the
 * two bespoke title bars this replaces each needed.
 *
 * It sits on the panel rather than on a bar of its own. A bar would be a slab
 * floating in the frame's own padding with a moat of panel around it, which is
 * what the pages used to do and what read as a window inside a window.
 */
.page-head {
  display: flex;
  align-items: center;
  padding-bottom: var(--sp-2);
  margin-bottom: var(--sp-3);
  border-bottom: 1px solid var(--edge-black);
  box-shadow: 0 1px 0 var(--edge-lit-dim);
  gap: var(--sp-3);
}

.page-head-icon {
  display: block;
  width: calc(var(--column) * 0.108);  /* the tab stone's 33, a third over */
  height: auto;
  flex-shrink: 0;
  image-rendering: pixelated;
}

.page-head-text {
  min-width: 0;
  flex: 1;
}

.page-head h2 {
  margin: 0;
  color: var(--rs-orange);
  font-size: var(--fs-xl);
  font-weight: 400;
  line-height: 1;
}

.page-head p {
  margin: 0 0 1px;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1.1;
}

/* The level a skill page reports is a reading rather than a control, so it is
   sunk into the panel the way the other readouts are. */
.level-badge {
  flex-shrink: 0;
  padding: var(--sp-2) var(--sp-3) calc(var(--sp-2) - 2px);
  border: 1px solid var(--edge-black);
  background: var(--panel-sunk);
  box-shadow:
    inset 0 1px 0 var(--edge-dark),
    inset 0 -1px 0 var(--edge-lit-dim);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1;
  white-space: nowrap;
}

.level-badge b {
  color: var(--rs-yellow);
  font-size: var(--fs-md);
  font-weight: 400;
}

/* A panel heading with something beside it: the market's own History button,
   the stall's coin pouch. The page head does this for a whole page; a panel
   inside one has the same need and none of the page head's weight. */
.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
}

.panel-head .section-head {
  min-width: 0;
}

/* A page's own sections say what they are in the same orange, a step down. */
.section-head {
  margin: 0 0 var(--sp-2);
  color: var(--rs-orange);
  font-size: var(--fs-lg);
  font-weight: 400;
  line-height: 1;
}

.section-note {
  margin: calc(var(--sp-1) * -1) 0 var(--sp-3);
  color: var(--ink-faint);
  font-size: var(--fs-xs);
  line-height: 1.35;
}

/* ------------------------------------------------------------- buttons --- */

/*
 * Every button on every page is the chat row's slab: the same four sprites,
 * nine-sliced at 4 so the stepped corners land on whole pixels however wide the
 * button comes out. The pages used to draw five different buttons in CSS, one
 * of which sat an inch above the chat row drawing the real thing.
 */
.rs-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: calc(var(--column) * 0.083);
  padding: var(--sp-1) var(--sp-3) calc(var(--sp-1) - 1px);
  border: 4px solid transparent;
  border-image: url("./assets/chat-btn.png") 4 fill / 4px stretch;
  color: var(--ink);
  /* The art fills the slab, and the corners it leaves out have to show the
     panel rather than the browser's own button grey. */
  background: none;
  font: inherit;
  font-size: var(--fs);
  gap: var(--sp-2);
  line-height: 1.1;
  text-align: center;
  text-decoration: none;
  text-shadow: 1px 1px #000;
  cursor: pointer;
  /* A tap on a button is only ever a tap, so there is no second one to hold the
     first back waiting for, and the grey rectangle the browser would draw over
     the press has stepped corners of its own art to sit wrong on. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* A finger leaves :hover behind on whatever it touched last, so lit lettering
   would sit on a button long after the tap and be read as a state. */
@media (hover: hover) {
  .rs-button:hover:not(:disabled) {
    color: var(--rs-yellow);
  }
}

/* Inside the outline, so the ring does not square off the stepped corners. */
.rs-button:focus-visible {
  outline: 2px solid var(--rs-yellow);
  outline-offset: -4px;
}

/*
 * The sunken slab belongs to the on-state, so a button that has one keeps it:
 * a toggle borrowing it for the press looked on from the moment a finger
 * landed, and since the browser drops :active on the lift a few frames before
 * the click lands, turning one on flashed the raised slab back in between. A
 * toggle says a finger is down by moving under it instead, which is not
 * something its state ever does and so cannot be mistaken for it.
 */
.rs-button.active,
.rs-button:not([aria-pressed]):active:not(:disabled) {
  border-image-source: url("./assets/chat-btn-active.png");
}

.rs-button[aria-pressed]:active:not(:disabled) {
  transform: translateY(1px);
}

.rs-button:disabled {
  color: var(--ink-off);
  border-image-source: url("./assets/chat-btn-off.png");
  cursor: not-allowed;
}

.rs-button img {
  width: calc(var(--column) * 0.059);
  height: auto;
  flex-shrink: 0;
  image-rendering: pixelated;
}

/* A full-width button with an icon in the corner needs its label to take the
   middle rather than sit against the icon. Nothing else does, and an arrow
   drawn out of borders would stretch into a bar if it did. */
.account-button > span {
  flex: 1;
}

.rs-button-primary {
  padding-top: var(--sp-2);
  padding-bottom: calc(var(--sp-2) - 1px);
  color: var(--rs-orange);
  font-size: var(--fs-md);
}

/* A button that is the only thing on its line takes the line, the way the
   chat row and the tab strip both fill the column. */
.rs-button-block {
  display: flex;
  width: 100%;
}

.rs-button-danger {
  color: #f0a79a;
  border-image-source: url("./assets/chat-btn-report.png");
}

/* Under a pointer only, like the rest of them: a finger leaving the yellow
   behind would take the warning off a button whose whole job is to carry one. */
@media (hover: hover) {
  .rs-button-danger:hover:not(:disabled) {
    color: var(--rs-yellow);
  }
}

/* A row of buttons that fills the width, the way the chat row and the tab strip
   both do. */
.rs-button-row {
  display: grid;
  gap: var(--sp-2);
  grid-auto-columns: 1fr;
  grid-auto-flow: column;
}

/* The same row with nothing between the slabs, for a set of choices where only
   one is on: the sprites already carry their own edges, so a gap would read as
   three buttons rather than one control. */
.rs-segment {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-flow: column;
}

.rs-segment .rs-button {
  min-width: 0;
  padding-right: var(--sp-1);
  padding-left: var(--sp-1);
  font-size: var(--fs-sm);
}

.rs-segment .rs-button img {
  width: calc(var(--column) * 0.044);
}

.rs-segment .rs-button.active {
  color: var(--rs-yellow);
}

/* ---------------------------------------------------------- data rows --- */

/*
 * Label on the left, value on the right, one to a line. Six places on five
 * pages were drawing this list, and each drew its own rule under a row.
 */
.rs-rows {
  margin: 0 0 var(--sp-3);
  padding: 0;
  font-size: var(--fs);
  list-style: none;
}

.rs-rows > div,
.rs-rows > li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: var(--sp-1) 0;
  border-bottom: 1px solid var(--edge-row);
  gap: var(--sp-4);
}

.rs-rows > div:last-child,
.rs-rows > li:last-child {
  border-bottom: 0;
}

/* A row carrying a sprite centres on it instead: a 23px turtle sitting on the
   text baseline drops the number beside it onto a line of its own. */
.rs-rows > div:has(img) {
  align-items: center;
}

/* A row is a pair, however the markup spells it: a term and its definition, or
   a label and the number beside it. */
.rs-rows dt,
.rs-rows > div > span,
.rs-rows .row-label {
  flex-shrink: 0;
  color: var(--ink-dim);
}

.rs-rows dd,
.rs-rows > div > b {
  margin: 0;
  overflow-wrap: anywhere;
  color: var(--rs-yellow);
  font-weight: 400;
  text-align: right;
}

.rs-rows .positive {
  color: var(--rs-good);
}

.rs-rows .negative {
  color: var(--rs-bad);
}

/* --------------------------------------------------------------- meter --- */

/*
 * One trough for every bar on the site: the skills XP bars, the drift net's
 * fill and the bar under an action in progress. They were three troughs with
 * three borders; the difference between them is now only how tall they are.
 */
.rs-progress,
.action-progress {
  height: calc(var(--column) * 0.034);
  padding: 1px;
  border: 1px solid var(--edge-black);
  background: var(--panel-sunk);
  box-shadow: inset 0 1px 0 var(--edge-dark);
}

.rs-progress span,
.action-progress span {
  display: block;
  width: 0;
  height: 100%;
  background:
    repeating-linear-gradient(90deg, transparent 0 10px, rgb(0 0 0 / 18%) 10px 11px),
    #46a449;
  box-shadow: inset 0 1px #82df76;
}

/* Thinner, and orange rather than green, so a thing being done now does not
   read as a level being gained. */
.action-progress {
  height: calc(var(--column) * 0.0245);
  margin-top: var(--sp-2);
}

.action-progress span {
  background: var(--rs-orange);
  box-shadow: inset 0 1px #ffe064;
}

/* ---------------------------------------------------------------- text --- */

.page-text {
  margin: 0 0 var(--sp-3);
  color: var(--ink);
  font-size: var(--fs);
  line-height: 1.35;
}



.coin {
  display: inline-block;
  width: 0.62em;
  height: 0.62em;
  margin-right: 0.2em;
  border: 1px solid #542f08;
  border-radius: 50%;
  vertical-align: 0.02em;
  background: radial-gradient(circle at 35% 30%, #ffff75 0 20%, #e6a21a 24% 62%, #85500c 66%);
  box-shadow: 1px 1px #1b1207;
}

a {
  color: var(--rs-orange);
  text-decoration: none;
}

@media (hover: hover) {
  a:hover {
    color: var(--rs-yellow);
  }
}


[hidden] {
  display: none !important;
}

/* ============================================================= fishing === */

/*
 * The four methods are one control, so they are the segmented slab: the open
 * one is the pressed sprite, which is how the chat row and the tab strip both
 * say which of a set is on. It used to turn blue, a hue that appears nowhere
 * else on the site.
 */
.fishing-modes {
  margin-bottom: var(--sp-3);
}

/* The size strip sits under the method strip and belongs to the reef, so it is
   tucked up against it and reads as a second line of the same control rather
   than a row of its own. Three sizes leave room the four methods do not. */
.reef-sizes {
  margin-top: calc(var(--sp-3) * -1 + 2px);
  margin-bottom: var(--sp-3);
}

.reef-sizes .rs-button {
  font-size: var(--fs-xs);
  white-space: nowrap;
}

/* A fourth method puts each name in a quarter of the strip rather than a third,
   which is not room enough at the size the segment sets. Everything on the
   button gives up a little for it rather than the name being cut off. */
.fishing-modes .rs-button {
  padding-right: 2px;
  padding-left: 2px;
  font-size: var(--fs-xs);
  gap: var(--sp-1);
}

.fishing-mode img {
  width: calc(var(--column) * 0.047);
  height: auto;
  flex-shrink: 0;
  image-rendering: pixelated;
}

.fishing-mode span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.water-scene {
  position: relative;
  height: calc(var(--column) * 0.52);
  overflow: hidden;
  /* The game's own 128x128 water texture, tiled and drifting. */
  background:
    linear-gradient(rgb(8 26 40 / 28%), rgb(4 16 26 / 58%)),
    url("./assets/water-texture.png") 0 0 / 128px 128px repeat,
    #3d5f7a;
  animation: water-drift 24s linear infinite;
}

@keyframes water-drift {
  from {
    background-position:
      0 0,
      0 0,
      0 0;
  }

  to {
    background-position:
      0 0,
      128px 128px,
      0 0;
  }
}

/*
 * The splash is one frame of a screenshot, and one frame of splash laid on the
 * water sits there like a sticker. What the sprite has instead of frames is
 * reach: it is a wide scatter of spray with ink across most of its middle, so a
 * window onto part of it is a frame of its own. Six windows cut from the parts
 * that carry ink, each held for a tick, and the water is working. It is the
 * same trick the tab stones went through, which is to say: take what the
 * artwork has and get the missing thing out of it rather than drawing one.
 *
 * Held rather than tweened on purpose. Bubbles in the game come and go on the
 * tick; spray that slides is a texture scrolling past, which is a current
 * rather than a spot.
 *
 * All six windows sit in the bottom half of the sprite, which is the half that
 * is only bubbles. The top of it carries the rain the screenshot was taken in,
 * and rain popping in and out on the tick reads as a fault rather than weather.
 */
@keyframes water-churn {
  0% {
    background-position: 18% 76%;
  }

  17% {
    background-position: 58% 100%;
  }

  34% {
    background-position: 86% 82%;
  }

  50% {
    background-position: 34% 94%;
  }

  67% {
    background-position: 70% 72%;
  }

  84% {
    background-position: 94% 98%;
  }
}

.water-churn,
.fishing-spot {
  background: url("./assets/fishing-spot.png") 18% 76% / 300% auto no-repeat;
  animation: water-churn 0.84s steps(1, end) infinite;
}

.fishing-spot {
  position: absolute;
  width: calc(var(--column) * 0.36);
  height: calc(var(--column) * 0.207);
  padding: 0;
  border: 0;
  opacity: 0.85;
  transform: translate(-50%, -50%);
  cursor: pointer;
}

/* A net in the water is working whether or not anyone is watching, so the water
   over it says so for as long as it is down. */
.net-churn {
  position: absolute;
  /* Clear of the readout chip above it and the net's own panel below. */
  top: 44%;
  left: 50%;
  width: calc(var(--column) * 0.42);
  height: calc(var(--column) * 0.24);
  opacity: 0.72;
  pointer-events: none;
  transform: translate(-50%, -50%);
}

@media (hover: hover) {
  .fishing-spot:hover {
    opacity: 1;
  }
}

/* Worked harder while it is being fished: the frames come faster, and the whole
   patch rises and falls under them. */
.fishing-spot.active {
  opacity: 1;
  animation:
    water-churn 0.58s steps(1, end) infinite,
    spot-splash 1.1s ease-in-out infinite;
}

.fishing-spot:focus-visible {
  outline: 2px solid var(--rs-yellow);
  outline-offset: 4px;
}

@keyframes spot-splash {
  0%,
  100% {
    opacity: 0.75;
    transform: translate(-50%, -52%) scale(0.97);
  }

  50% {
    opacity: 1;
    transform: translate(-50%, -48%) scale(1.03);
  }
}

/*
 * The rig is the whole of the water rather than a strip across the middle of
 * it: the bar hangs in the centre and the run's counts sit in a corner, and
 * neither is in the other's way. Nothing in it is ever clicked, since the
 * button under the water and the space bar are what strike, so it lets the
 * pointer straight through to whatever is behind it.
 */
.strike-rig {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/*
 * Centred off its own height rather than with a translate, because a miss
 * knocks the bar sideways and a keyframe that sets `transform` would take the
 * centring with it: the bar would drop half its height for the length of the
 * flinch. Whatever holds the bar in place has to be something the animations
 * are not already using.
 */
.strike-bar {
  --strike-bar-height: calc(var(--column) * 0.093);

  position: absolute;
  top: calc(50% - var(--strike-bar-height) / 2);
  right: 7%;
  left: 7%;
  height: var(--strike-bar-height);
  border: 1px solid var(--edge-black);
  background: #15242c;
  box-shadow: inset 0 1px 0 #0a1418;
}

.strike-zone,
.strike-rare {
  position: absolute;
  top: 0;
  bottom: 0;
}

/* The window is somewhere else nearly every time the marker turns, and it
   slides there rather than cutting, so the eye can follow where it went. Only
   then: the rest of the time it is breathing or drifting, and activities.js
   draws that exactly, since an eased window would trail the one a strike is
   judged against. The length is RELOCATE_MS there. */
.strike-bar.relocating .strike-zone,
.strike-bar.relocating .strike-rare {
  transition:
    left 140ms ease-out,
    width 140ms ease-out;
}

.strike-zone {
  background: rgb(64 190 96 / 45%);
}

.strike-rare {
  background: rgb(255 199 51 / 80%);
}

.strike-marker {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 3px;
  background: #fff;
  box-shadow: 0 0 7px rgb(255 255 255 / 85%);
  opacity: 0;
}

.strike-bar.sweeping .strike-marker {
  opacity: 1;
}

/* ------------------------------------------------ what a strike looks like --- */

/*
 * A strike is over in a frame, so the bar has to say what happened after the
 * fact: it lights green for one turtle and gold for the whopper, and a miss
 * knocks it sideways in the red the Report button is cut from. The marker is
 * held still by activities.js for as long as each of these runs.
 */
.strike-bar.landed {
  animation: strike-landed 320ms ease-out;
}

.strike-bar.whopper {
  animation: strike-whopper 460ms ease-out;
}

.strike-bar.slipped {
  animation: strike-slipped 260ms steps(3, end);
}

@keyframes strike-landed {
  from {
    background: #2f6b40;
    box-shadow: inset 0 0 0 1px rgb(110 226 117 / 90%);
  }

  to {
    background: #15242c;
    box-shadow: inset 0 1px 0 #0a1418;
  }
}

@keyframes strike-whopper {
  0% {
    background: #7a5a16;
    box-shadow: inset 0 0 0 2px var(--rs-yellow);
  }

  40% {
    background: #4a3a14;
    box-shadow: inset 0 0 0 1px var(--rs-orange);
  }

  to {
    background: #15242c;
    box-shadow: inset 0 1px 0 #0a1418;
  }
}

@keyframes strike-slipped {
  0%,
  100% {
    transform: translateX(0);
    background: #15242c;
  }

  25% {
    transform: translateX(-3px);
    background: #5c1f1a;
  }

  75% {
    transform: translateX(3px);
    background: #5c1f1a;
  }
}

/*
 * The game's own splash, dropped on the bar where the strike landed. The whole
 * sprite here rather than a window onto it: what the spot cannot use is exactly
 * what this wants, since the streaks across the top of it read as spray thrown
 * outwards once there is a scale and a fade carrying them.
 */
.strike-splash {
  position: absolute;
  top: 50%;
  width: calc(var(--column) * 0.32);
  aspect-ratio: 831 / 478;
  background: url("./assets/fishing-spot.png") center / contain no-repeat;
  /* The art is white spray on nothing, cut for open water rather than for a
     bar this dark, so it is lifted to read against it. */
  filter: brightness(1.7);
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -50%);
}

.strike-splash.burst {
  animation: strike-splash 460ms ease-out forwards;
}

@keyframes strike-splash {
  from {
    opacity: 0.95;
    transform: translate(-50%, -50%) scale(0.45);
  }

  to {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.5);
  }
}

/* What the strike was worth, rising off the bar the way a hitsplat leaves. */
.strike-pop {
  position: absolute;
  bottom: 100%;
  color: var(--rs-good);
  font-size: var(--fs-md);
  opacity: 0;
  pointer-events: none;
  text-shadow: 1px 1px #000;
  transform: translateX(-50%);
  white-space: nowrap;
}

.strike-pop.rare {
  color: var(--rs-yellow);
  font-size: var(--fs-xl);
}

.strike-pop.rising {
  animation: strike-pop 760ms ease-out forwards;
}

@keyframes strike-pop {
  from {
    opacity: 1;
    transform: translate(-50%, 4px);
  }

  70% {
    opacity: 1;
  }

  to {
    opacity: 0;
    transform: translate(-50%, -22px);
  }
}

/* Bottom-anchored, so it clears the readout chip in the opposite corner. */
.drift-net {
  position: absolute;
  right: var(--sp-3);
  bottom: var(--sp-3);
  left: var(--sp-3);
  display: flex;
  align-items: center;
  padding: var(--sp-3);
  border: 1px solid var(--edge-black);
  background: rgb(10 8 5 / 72%);
  box-shadow:
    inset 0 1px 0 rgb(0 0 0 / 60%),
    inset 0 -1px 0 rgb(160 148 124 / 35%);
  gap: var(--sp-3);
}

.drift-net > img {
  width: calc(var(--column) * 0.103);
  height: auto;
  flex-shrink: 0;
  image-rendering: pixelated;
}

.net-readout {
  min-width: 0;
  flex: 1;
}

/* The count and what the net is doing read as one line, with the bar under
   both, rather than a number sitting on a line of its own. */
.net-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--sp-1);
  gap: var(--sp-3);
}

.net-readout b {
  flex-shrink: 0;
  color: var(--rs-yellow);
  font-size: var(--fs-xl);
  font-weight: 400;
  line-height: 1;
}

.net-readout p {
  margin: 0;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  text-align: right;
}

/* ------------------------------------------------------------ the reef --- */

/*
 * The board is cut from the two surfaces every page here is: an unswept patch
 * is the chat row's slab, raised, and a swept one is a hole in it with the
 * water underneath showing through. A net presses its patch in, which is what
 * being switched on looks like everywhere else on the site.
 */

/*
 * A board is no good half on the screen, so this one method fills the panel and
 * gives the water whatever the rest of the page leaves, the way the inventory
 * grid takes what the hover line leaves. Under the floor the frame scrolls
 * instead: a page that moves is the lesser of the two against patches too small
 * to hit.
 */
#fishing-view:has(#reef-board:not([hidden])) {
  height: 100%;
}

#fishing-view:has(#reef-board:not([hidden])) .activity-window {
  display: grid;
  height: 100%;
  /* Page head, the method strip, the size strip, the water, and the controls
     under it. The board comes out about 354px across on a 420px page, which is
     30px a patch at twelve across and 22px at sixteen; on a 360px phone those
     are nearer 25px and 19px. Sixteen is under what a finger reliably hits,
     which is why the sizes are a choice rather than one bigger board. */
  grid-template-rows: auto auto auto minmax(calc(var(--column) * 0.7), 1fr) auto;
}

.water-scene:has(#reef-board:not([hidden])) {
  display: grid;
  height: auto;
  padding: var(--sp-2);
  container-type: size;
  place-items: center;
}

.reef-board {
  display: grid;
  /* Square, and never taller than the water it is laid over. */
  width: min(100%, 100cqh);
  aspect-ratio: 1;
  /* How many patches across is activities.js's to say; everything inside the
     board is a share of one patch rather than a step off the column, so the
     grid can be made bigger or smaller in the one place. */
  container-type: inline-size;
  grid-template-columns: repeat(var(--patches-across), 1fr);
  grid-template-rows: repeat(var(--patches-across), 1fr);

  /* A finger has to be able to scroll the frame the board covers, so panning
     reaches it and a net is set on a held press instead; see activities.js. */
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/* The rows are there to be read out, not to be laid out. */
.reef-board > [role="row"] {
  display: contents;
}

.reef-patch {
  display: grid;
  min-width: 0;
  padding: 0;
  border: 4px solid transparent;
  border-image: url("./assets/chat-btn.png") 4 fill / 4px stretch;
  color: var(--ink);
  background: none;
  font: inherit;
  font-size: calc(100cqw / var(--patches-across) * 0.54);
  line-height: 1;
  place-items: center;
  text-shadow: 1px 1px #000;
  cursor: pointer;
}

/* The game does not light anything up under the cursor, but a pointer on a page
   needs to know which patch it is about to open. A finger never is about to: it
   leaves :hover on whatever it touched last, so the board kept one patch lit
   for good, and a lit patch on a board read as a state of its own. */
@media (hover: hover) {
  .reef-patch:hover {
    filter: brightness(1.16);
  }
}

.reef-patch:focus-visible {
  outline: 2px solid var(--rs-yellow);
  outline-offset: -4px;
}

/*
 * With nets on, every patch still covered shows the net a tap would drop on it,
 * far enough down to be a ghost of one rather than one. The button under the
 * board was the only thing saying which way a tap would go, and it is not where
 * anyone is looking when they take one: a tap meant for a net that sweeps
 * instead wakes the shoal and ends the reef, which is the dearest mistake in
 * the game and was the one the board gave no warning of. It doubles as the
 * answer to forgetting nets are off, since a board with nothing on it is now
 * the board that sweeps.
 */
.reef-board.netting .reef-patch.covered::after {
  width: calc(100cqw / var(--patches-across) * 0.62);
  content: "";
  opacity: 0.22;
  aspect-ratio: 1;
  background: url("./assets/fishing-net.png") center / contain no-repeat;
  image-rendering: pixelated;
}

/* A patch whose tap has gone to the server and not come back. Signed in, what
   is under one is the server's to say, so a round trip sits between the tap and
   the reveal and there is nothing to draw in between; what there is to say is
   that the tap landed, and a board where you are unsure of that is a board you
   tap twice. It is held down rather than pressed and released, which is what it
   is doing. */
.reef-patch.pending {
  border-image-source: url("./assets/chat-btn-active.png");
  filter: brightness(0.9);
}

/* A patch under a net is pressed in; one that turned out to hold nothing wears
   the off slab, which is the site's way of saying a button did nothing. */
.reef-patch.netted,
.reef-patch.landed {
  border-image-source: url("./assets/chat-btn-active.png");
}

.reef-patch.empty {
  border-image-source: url("./assets/chat-btn-off.png");
}

/*
 * Swept, and the sea is back. The lighting is the sunken one the panel uses,
 * thinned so the water drifting underneath still reads through it, the way the
 * drift net's own readout sits on the water.
 */
.reef-patch.swept,
.reef-patch.woken,
.reef-patch.escaped {
  border: 1px solid var(--edge-black);
  background: rgb(6 20 30 / 34%);
  box-shadow:
    inset 0 1px 0 rgb(0 0 0 / 55%),
    inset 0 -1px 0 rgb(160 148 124 / 24%);
  cursor: default;
}

/* The patch the net went over: the turtle you woke, and the reason the rest of
   the shoal is gone. */
.reef-patch.woken {
  background: rgb(122 30 22 / 60%);
}

.reef-patch img {
  width: calc(100cqw / var(--patches-across) * 0.62);
  height: auto;
  image-rendering: pixelated;
}

.reef-patch.escaped img,
.reef-patch.empty img {
  opacity: 0.45;
}

.reef-board.over .reef-patch {
  cursor: default;
}

/*
 * The count climbs the ladder the game itself uses for a number getting worse:
 * green, yellow, orange, red. Past four it is out of ladder and onto the three
 * chat colours it has not spent, which comes to a five on something like half
 * the reefs, a six on very few, and the two above it as arithmetic rather than
 * anything you will meet.
 */
.reef-patch[data-count="1"] { color: var(--rs-good); }
.reef-patch[data-count="2"] { color: var(--rs-yellow); }
.reef-patch[data-count="3"] { color: var(--rs-orange); }
.reef-patch[data-count="4"] { color: var(--rs-bad); }
.reef-patch[data-count="5"] { color: #f0f; }
.reef-patch[data-count="6"] { color: #0ff; }
.reef-patch[data-count="7"] { color: #fff; }
.reef-patch[data-count="8"] { color: var(--rs-burnt); }

/* The two counts that matter while fishing, kept on the water in the corner
   the game keeps its own counters. */
.water-hud {
  position: absolute;
  z-index: 1;
  top: var(--sp-2);
  left: var(--sp-2);
  min-width: calc(var(--column) * 0.42);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--edge-black);
  background: rgb(10 8 5 / 68%);
  box-shadow: inset 0 -1px 0 rgb(160 148 124 / 30%);
  font-size: var(--fs-xs);
}

.water-hud > div {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
}

.water-hud b {
  color: var(--rs-yellow);
  font-weight: 400;
}

/*
 * What a cast & strike run is up to: the same panel as the counts above it,
 * in the opposite corner and out of the flow entirely. It is on the water from
 * the moment the method is picked, so the strikes a cast is worth are read
 * before the first one rather than after it, and nothing on the page moves when
 * the numbers start changing. Every rule here has to come after `.water-hud`'s
 * own, which is why it sits down here rather than up with the rig.
 */
.strike-readout {
  top: auto;
  bottom: var(--sp-2);
}

.activity-content {
  padding-top: var(--sp-3);
}

.fishing-blurb,
.cook-hint {
  min-height: 2.7em;
  margin: 0 0 var(--sp-3);
  color: var(--ink);
  font-size: var(--fs);
  line-height: 1.3;
  text-align: center;
}

.activity-content b {
  color: var(--rs-yellow);
  font-weight: 400;
}

.activity-buttons {
  display: flex;
  justify-content: center;
  margin-top: var(--sp-3);
  gap: var(--sp-2);
}

.activity-buttons .rs-button {
  min-width: calc(var(--column) * 0.27);
}

/* Netting mode changes what a tap on the board does, which is too much to hang
   on the slab alone, so the button lights its lettering the way the strip above
   it lights the method you are on. */
#reef-nets.active:not(:disabled) {
  color: var(--rs-yellow);
}

.stop-button {
  color: #f0a79a;
}

/* ============================================================= cooking === */

.cooking-workbench {
  display: grid;
  height: calc(var(--column) * 0.36);
  background:
    radial-gradient(circle at center, rgb(184 89 22 / 20%), transparent 60%),
    #2b2620;
  grid-template-columns: 1fr auto 1fr;
  place-items: center;
}

.cook-item {
  display: grid;
  justify-items: center;
  color: var(--rs-orange);
  font-size: var(--fs-sm);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  /* A held press on a picture is a menu on iOS, and a menu is the end of the
     drag it interrupted. */
  -webkit-touch-callout: none;
}

.cook-item.draggable {
  cursor: grab;
}

.cook-item.draggable:active {
  cursor: grabbing;
}

.cook-item img {
  width: calc(var(--column) * 0.152);
  height: auto;
  image-rendering: pixelated;
}

.cook-item b {
  color: var(--rs-yellow);
  font-size: var(--fs-lg);
  font-weight: 400;
}

.range-target {
  display: grid;
  padding: var(--sp-2);
  border: 1px solid transparent;
  place-items: center;
}

.range-target.drop-target {
  border-color: rgb(255 255 255 / 85%);
  background: rgb(255 255 255 / 14%);
}

.official-range {
  display: block;
  width: calc(var(--column) * 0.25);
  height: auto;
  mix-blend-mode: screen;
}

.cook-results {
  display: flex;
  gap: var(--sp-4);
}

.cook-results div {
  display: grid;
  justify-items: center;
  color: var(--rs-yellow);
  font-size: var(--fs-md);
}

.cook-results img {
  width: calc(var(--column) * 0.115);
  height: auto;
  image-rendering: pixelated;
}

.burn-readout {
  display: flex;
  justify-content: center;
  margin: 0;
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  gap: var(--sp-2);
}

.burn-readout span {
  color: var(--ink-faint);
}

/* ============================================================== skills === */

.skill-cards {
  display: grid;
  margin-bottom: var(--sp-3);
  gap: var(--sp-2);
}

.skill-card {
  display: grid;
  min-width: 0;
  padding: var(--sp-3);
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--sp-3);
}

.skill-card > img {
  display: block;
  width: calc(var(--column) * 0.093);
  height: auto;
  image-rendering: pixelated;
}

.skill-main {
  min-width: 0;
}

.skill-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--sp-1);
}

.skill-heading h3 {
  margin: 0;
  color: var(--rs-orange);
  font-size: var(--fs-lg);
  font-weight: 400;
}

.skill-heading strong {
  color: var(--rs-yellow);
  font-size: var(--fs-xl);
  font-weight: 400;
}

.skill-main p {
  margin: var(--sp-1) 0 0;
  overflow: hidden;
  color: var(--ink-dim);
  font-size: var(--fs-xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.skill-main p b {
  color: #fff;
  font-weight: 400;
}

.collection-log {
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
}

.burnt-log {
  color: var(--rs-burnt);
}

.burnt-log .row-label {
  display: flex;
  align-items: center;
  color: inherit;
  gap: var(--sp-2);
}

.burnt-log img {
  width: calc(var(--column) * 0.076);
  height: auto;
  image-rendering: pixelated;
}

/* ============================================================ hiscores === */

/*
 * A card per player rather than a table. The column is 408 at its widest and a
 * table of rank, name, two skills and a total would be five columns of nothing;
 * this is the skill card's own slab, carrying the same three things a hiscores
 * line carries and putting the rest under the name.
 */
.board-list {
  display: grid;
  margin: 0 0 var(--sp-3);
  padding: 0;
  gap: var(--sp-2);
  list-style: none;
}

.board-row {
  display: grid;
  min-width: 0;
  align-items: center;
  padding: var(--sp-2) var(--sp-3);
  grid-template-columns: auto 1fr auto;
  gap: var(--sp-3);
}

/* Your own line, marked on the name rather than down the edge of the slab. The
   slab's edge is the one the sprites cut, lit along the top and dark along the
   bottom; an accent laid into it is a fourth side the artwork does not have.
   The chat marks your own name by colouring it, so this does the same. */
.board-row.self .board-who b {
  color: var(--rs-orange);
}

.board-rank {
  display: flex;
  min-width: calc(var(--column) * 0.075);
  flex-direction: column;
  align-items: center;
  color: var(--ink-faint);
  font-size: var(--fs-md);
  gap: 1px;
  line-height: 1;
}

.board-rank img {
  display: block;
  image-rendering: pixelated;
}

/* The number under the crown takes the crown's own metal, so a row says which
   of the three it is without the sprite having to be read at 13 pixels. */
.board-rank-gold { color: var(--rs-yellow); }
.board-rank-silver { color: #d8d8d8; }
.board-rank-bronze { color: #d99a5e; }

.board-who {
  min-width: 0;
}

.board-who b {
  display: block;
  overflow: hidden;
  color: var(--ink);
  font-weight: 400;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The name is a button that raises the apps its profile opens in, so it takes
   the colour of the line around it and only shows it can be pressed when the
   pointer is on it. */
.board-name {
  display: block;
  max-width: 100%;
  overflow: hidden;
  padding: 0;
  border: 0;
  color: inherit;
  background: transparent;
  font: inherit;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}

.board-name:hover,
.board-name[aria-expanded="true"] {
  text-decoration: underline;
}

.board-name:focus-visible {
  outline: 1px dotted var(--rs-yellow);
  outline-offset: 1px;
}

.board-activity {
  display: block;
  color: var(--ink-faint);
  font-size: var(--fs-xs);
}

/* Here now, in the green the experience bars are drawn in. Presence is a window
   with an end on it rather than a flag somebody switched on, so this goes out by
   itself when the window does and nothing has to write again to clear it. */
.board-activity.here {
  color: var(--rs-good);
}

.board-score {
  color: var(--ink-off);
  font-size: var(--fs-sm);
  text-align: right;
  white-space: nowrap;
}

.board-score b {
  color: var(--rs-yellow);
  font-weight: 400;
}

/* The two skills under the total, in the dim the activity line uses, so the
   total stays the thing the eye lands on down a column of them. */
.board-split {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  color: var(--ink-faint);
  font-size: var(--fs-xs);
  gap: var(--sp-3);
}

/* Each icon stays against its own number, so the space between the pairs is
   what separates them rather than two numbers running together. */
.board-pair {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.board-pair img {
  width: auto;
  height: calc(var(--column) * 0.032);
  image-rendering: pixelated;
}

/*
 * The apps a name on the board opens in, in the same brown box and white text
 * as the names an @ offers in the chat, with the one under the pointer in the
 * menu's hover yellow. waypoints.js places it under the name, or over it when
 * there is no room below. Each app's mark paints in the text's own colour, so
 * it turns yellow with its name, and takes the same hard shadow.
 */
.waypoint-list {
  position: fixed;
  z-index: 1100;
  min-width: 140px;
  margin: 0;
  padding: 2px 3px 3px;
  border: 1px solid #000;
  color: #fff;
  background: #5d5447;
  box-shadow:
    inset 0 0 0 1px #8c8376,
    2px 2px 0 rgb(0 0 0 / 45%);
  list-style: none;
  font-size: 18px;
  line-height: 20px;
  text-shadow: 1px 1px #000;
}

.waypoint-option {
  display: flex;
  align-items: center;
  padding: 3px 3px;
  color: inherit;
  gap: 6px;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

/* A groove between each app and the next: a dark line with the box's own light
   edge under it, so it reads as cut into the panel rather than drawn on it. */
.waypoint-list li + li {
  border-top: 1px solid rgb(0 0 0 / 40%);
  box-shadow: inset 0 1px 0 rgb(140 131 118 / 45%);
}

.waypoint-option:hover,
.waypoint-option:focus-visible {
  color: var(--rs-yellow);
  outline: none;
}

.waypoint-icon {
  display: inline-flex;
  flex: none;
  filter: drop-shadow(1px 1px 0 #000);
}

.waypoint-icon svg {
  display: block;
  width: 16px;
  height: 16px;
}

/* ========================================================== login gate === */

/*
 * The prompt a game action raises while nobody is signed in. It is the Account
 * tab's login form lifted out over the page, on the same raised slab, so
 * signing in reads the same whichever door it happens at.
 */
.login-gate {
  width: min(calc(100vw - 32px), calc(var(--column) * 0.9));
  max-width: none;
  padding: 0;
  border: 0;
  color: var(--ink);
  background: none;
  box-shadow: 3px 3px 0 rgb(0 0 0 / 55%);
}

.login-gate::backdrop {
  background: rgb(12 10 7 / 70%);
}

.login-gate-form {
  padding: var(--sp-4);
  border-color: #000;
}

.login-gate-title {
  margin: 0 0 var(--sp-3);
  color: var(--rs-orange);
  font-size: var(--fs-lg);
  font-weight: 400;
  text-shadow: 1px 1px #000;
}

.login-gate-label {
  display: block;
  margin-bottom: var(--sp-2);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
}

.login-gate-status:empty {
  display: none;
}

/* ============================================================== about === */

/* The Jagex notice, at the foot of Account's About panel since the Info page
   it used to close went. */
.legal {
  margin: 0;
  color: var(--ink-faint);
  font-size: var(--fs-xs);
  line-height: 1.4;
}

/* ====================================================== grand exchange === */

/*
 * The real panel is 484x304 and was scaled up with `zoom` to fill a page that
 * no longer exists. It takes the column now and stacks down it, which is the
 * layout a phone was always falling back to, so there is one of it rather than
 * a wide one and an override underneath.
 */
.exchange-status {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-3);
  font-size: var(--fs-sm);
  gap: var(--sp-3);
}

.market-status {
  color: var(--ink-dim);
}

.market-status.live {
  color: var(--rs-good);
}

.market-status.fallback {
  color: var(--rs-burnt);
}

.gp-balance {
  flex-shrink: 0;
  color: var(--rs-yellow);
}

.exchange-prompt {
  margin: 0 0 var(--sp-3);
  color: var(--rs-orange);
  font-size: var(--fs-sm);
  text-align: center;
}

.market-card {
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
}

.item-heading {
  display: flex;
  align-items: center;
  padding-bottom: var(--sp-2);
  margin-bottom: var(--sp-2);
  border-bottom: 1px solid var(--edge-row);
  gap: var(--sp-3);
}

.item-heading img,
.offer-intro img {
  display: block;
  width: calc(var(--column) * 0.104);
  height: auto;
  flex-shrink: 0;
  image-rendering: pixelated;
}

.item-heading h3,
.offer-description h3 {
  margin: 0;
  color: var(--rs-orange);
  font-size: var(--fs-lg);
  font-weight: 400;
  line-height: 1.1;
}

.item-heading span {
  color: var(--ink-faint);
  font-size: var(--fs-xs);
}

.quote-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: var(--sp-2);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  gap: var(--sp-3);
}

.quote-row b {
  color: var(--rs-yellow);
  font-weight: 400;
}

/* The three windows sit on the graph they change, close enough to read as its
   own control rather than the card's. */
.chart-ranges {
  margin-bottom: var(--sp-2);
}

/*
 * The graph used to be the one thing on the site drawn the way a chart library
 * draws one: a smoothed line in colours of its own, laid over rules at three
 * arbitrary heights. Everything else here is sprite art scaled up whole, so it
 * read as a screenshot of another program sitting in the panel.
 *
 * It is cut to the same rules as the rest now. The viewBox is half the width
 * the card comes to at the full column, so one unit is two pixels and the whole
 * graph is drawn on the grid the artwork is: `crispEdges` keeps the browser
 * from smoothing an edge off it, the line runs in flats and jumps so every edge
 * it has is square to that grid, and it takes its colours from the same four
 * the panel around it does.
 */
.price-chart {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 196 / 66;
  shape-rendering: crispEdges;
}

/* A window that is still coming down the wire keeps the last one up and dims
   it, which says more than an empty box does about what is about to appear. */
.price-chart.loading {
  opacity: 55%;
}

.chart-grid {
  stroke: var(--edge-lit-dim);
  stroke-width: 1;
}

.chart-grid-minor {
  stroke: var(--edge-dark);
  stroke-width: 1;
}

/* The one part of the graph that is type rather than art, so it is the page's
   own face at the page's own smallest size and is left to anti-alias. */
.chart-label {
  fill: var(--ink-off);
  font-size: 5.5px;
  shape-rendering: auto;
}

/*
 * Outlined in near-black the way every sprite on the page is, so the line sits
 * on the grid rather than in it. One pixel of it either side of two of line, at
 * the full column: the outline a sprite this thin would carry. Twice that and
 * it stopped being an outline, closing the gap between two steps that only
 * happen to be next to each other into one blob.
 */
.chart-line-shadow {
  fill: none;
  stroke: var(--edge-black);
  stroke-width: 2;
}

.chart-line {
  fill: none;
  stroke: var(--ink-dim);
  stroke-width: 1;
}

.chart-fill {
  fill: var(--ink-dim);
  opacity: 16%;
}

.chart-mark {
  fill: var(--ink-dim);
  stroke: var(--edge-black);
  stroke-width: 1;
}

.price-chart.rising .chart-line {
  stroke: var(--rs-good);
}

.price-chart.rising .chart-fill,
.price-chart.rising .chart-mark {
  fill: var(--rs-good);
}

.price-chart.falling .chart-line {
  stroke: var(--rs-bad);
}

.price-chart.falling .chart-fill,
.price-chart.falling .chart-mark {
  fill: var(--rs-bad);
}

/* Nothing to draw, or nothing to draw it from: the box says which rather than
   sitting empty and reading as a graph still on its way. */
.chart-notice {
  fill: var(--ink-off);
  font-size: 6px;
  shape-rendering: auto;
  text-anchor: middle;
}

.trend {
  margin: var(--sp-2) 0 var(--sp-3);
  color: var(--ink-dim);
  font-size: var(--fs-xs);
  text-align: center;
}

.trend.positive {
  color: var(--rs-good);
}

.trend.negative {
  color: var(--rs-bad);
}

/*
 * Buy and sell carry the chat scrollbar's arrow: two borders and a third
 * coloured one, which is a triangle on whole pixels at any size. The pair used
 * to be an Arial glyph on an orange tile, the one piece of type on the site set
 * in something other than the game's own face.
 */
.offer-icon {
  display: block;
  width: 0;
  height: 0;
  flex: none;
  border-right: 0.32em solid transparent;
  border-left: 0.32em solid transparent;
}

.ge-action-label {
  flex: none;
}

.ge-action.buy .offer-icon {
  border-top: 0.38em solid currentcolor;
}

.ge-action.sell .offer-icon {
  border-bottom: 0.38em solid currentcolor;
}

.portfolio-card {
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
}

.trade-history {
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-3);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
}

.trade-history p {
  margin: 0;
  color: var(--ink-faint);
  text-align: center;
}

.trade-history ul {
  display: grid;
  margin: 0;
  padding: 0;
  gap: 1px var(--sp-4);
  list-style: none;
}

.trade-history .buy-trade {
  color: var(--rs-good);
}

.trade-history .sell-trade {
  color: var(--rs-burnt);
}

/* --- the offer form --- */

.offer-intro {
  display: flex;
  align-items: center;
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
  gap: var(--sp-3);
}

.offer-type {
  color: var(--rs-orange);
  font-size: var(--fs-sm);
}

.offer-description {
  min-width: 0;
  flex: 1;
}

.offer-description p {
  margin: 0;
  color: var(--rs-yellow);
  font-size: var(--fs-sm);
}

.offer-description span {
  color: var(--ink-dim);
  font-size: var(--fs-sm);
}

.offer-controls {
  display: grid;
  margin-bottom: var(--sp-3);
  gap: var(--sp-3);
}

.offer-controls fieldset {
  min-width: 0;
  padding: var(--sp-3);
  border: 0;
  margin: 0;
}

/* A legend sits inside the border it labels, which cuts a line through the
   lit row along the top of the slab. Floated it is an ordinary block again,
   and the control under it clears the float. */
.offer-controls legend {
  float: left;
  width: 100%;
  padding: 0;
  margin-bottom: var(--sp-2);
  color: var(--rs-orange);
  font-size: var(--fs-sm);
}

.offer-controls legend + * {
  clear: both;
}

.stepper {
  display: grid;
  margin-bottom: var(--sp-2);
  grid-template-columns: auto 1fr auto;
  gap: var(--sp-2);
}

.stepper .rs-button {
  min-width: calc(var(--column) * 0.088);
  padding-right: 0;
  padding-left: 0;
}

.stepper input {
  min-width: 0;
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--edge-black);
  outline: 0;
  color: var(--rs-yellow);
  background: var(--panel-sunk);
  box-shadow: inset 0 1px 0 var(--edge-dark);
  font: inherit;
  font-size: var(--fs-md);
  text-align: center;
  appearance: textfield;
}

.stepper input::-webkit-inner-spin-button {
  appearance: none;
}

.stepper input:focus-visible {
  outline: 1px solid var(--rs-yellow);
  outline-offset: -2px;
}

.quick-buttons {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-flow: column;
  gap: var(--sp-2);
}

.quick-buttons .rs-button {
  padding-right: var(--sp-1);
  padding-left: var(--sp-1);
  font-size: var(--fs-sm);
}

.price-display {
  padding: var(--sp-2);
  margin-bottom: var(--sp-2);
  color: var(--rs-yellow);
  font-size: var(--fs-md);
  text-align: center;
}

.offer-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-3);
  gap: var(--sp-3);
}

.offer-total span {
  color: var(--ink-dim);
  font-size: var(--fs-sm);
}

.offer-total strong {
  color: var(--rs-yellow);
  font-size: var(--fs-md);
  font-weight: 400;
}

.offer-footer {
  display: flex;
  align-items: stretch;
  gap: var(--sp-2);
}

.back-button {
  min-width: calc(var(--column) * 0.14);
}

.confirm-button {
  flex: 1;
}

/* =================================================== account management === */

.account-tabs,
.progress-tabs,
.market-tabs,
.community-tabs,
.board-tabs {
  margin-bottom: var(--sp-3);
}

.account-status {
  margin: 0 0 var(--sp-3);
  color: var(--rs-orange);
  font-size: var(--fs);
  overflow-wrap: anywhere;
}

.account-login label {
  display: block;
  margin-bottom: var(--sp-2);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
}

.account-input {
  display: block;
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-2);
  border: 1px solid var(--edge-black);
  outline: 0;
  color: #fff;
  background: var(--panel-sunk);
  box-shadow: inset 0 1px 0 var(--edge-dark);
  font: inherit;
  font-size: var(--fs);
  text-align: center;
}

.account-input::placeholder {
  color: var(--ink-off);
  opacity: 1;
}

.account-input:focus-visible {
  outline: 1px solid var(--rs-yellow);
  outline-offset: -2px;
}

.account-button {
  display: flex;
  width: 100%;
  margin-bottom: var(--sp-2);
}

/* Pressed toggles go yellow, the way an active tab stone does. */
.account-button.active {
  color: var(--rs-yellow);
}

/* The setting sits at the end of the button rather than sharing the middle
   with the label, which is what `.account-button > span` would give it. */
.account-button > span.toggle-state {
  flex: 0 0 auto;
  margin-left: var(--sp-2);
  color: var(--ink-dim);
}

.account-button.active > span.toggle-state {
  color: inherit;
}

.account-pouch {
  margin-top: var(--sp-4);
}

.account-note {
  margin: 0 0 var(--sp-3);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1.35;
}

/* A list of links reads as rows, so it is the same list: the destination on
   the left and what it does on the right. */
.account-links {
  margin: 0 0 var(--sp-3);
  padding: 0;
  font-size: var(--fs-sm);
  list-style: none;
}

.account-links li {
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--edge-row);
  color: var(--ink-dim);
}

.account-links li:last-child {
  border-bottom: 0;
}

/* A permission is the same row: what it allows, in the yellow the session
   rows above give their values, and what that covers under it, dim. */
.account-permissions li {
  display: grid;
  gap: var(--sp-1);
}

.permission-title {
  color: var(--rs-yellow);
  font-size: var(--fs);
}

.permission-detail {
  overflow-wrap: anywhere;
}

/* =========================================================== community === */

/*
 * The friends list is the game's own: a sunken box with a name on the left in
 * white, and on the right where that player is, yellow for here and red for
 * Offline. What each one is to you goes under the name, small, since the game
 * has nothing like it to copy and it should not compete with the name. A
 * clan's member list is the same list.
 */
.friends-list {
  display: grid;
  margin: 0 0 var(--sp-3);
  padding: var(--sp-1) 0;
  list-style: none;
}

.person {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-1) var(--sp-3);
  border: 0;
  color: inherit;
  background: none;
  font: inherit;
  gap: var(--sp-3);
  text-align: left;
  text-shadow: inherit;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
  .person:hover {
    background: rgb(255 255 255 / 5%);
  }
}

.person:focus-visible {
  outline: 1px solid var(--rs-yellow);
  outline-offset: -1px;
}

.person-who {
  display: flex;
  min-width: 0;
  flex-direction: column;
}

.person-name {
  overflow: hidden;
  color: #fff;
  font-weight: 400;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.person-note {
  color: var(--ink-faint);
  font-size: var(--fs-xs);
}

.person-where {
  flex-shrink: 0;
  color: var(--rs-yellow);
  font-size: var(--fs-sm);
  text-align: right;
}

.person-where.offline {
  color: var(--rs-bad);
}

.person-empty {
  padding: var(--sp-1) var(--sp-3);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1.35;
}

/*
 * A clan per slab, the HiScores card with the name where the player's goes.
 * The one whose details are open is pressed into the panel the way a picked
 * segment is, and the one // talks in says so on the right.
 */
.clan-list {
  display: grid;
  margin: 0 0 var(--sp-3);
  padding: 0;
  gap: var(--sp-2);
  list-style: none;
}

.clan-row {
  display: grid;
  width: 100%;
  align-items: center;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--edge-black);
  color: var(--ink);
  background: linear-gradient(var(--panel-hi), var(--panel-lo));
  box-shadow:
    inset 0 1px 0 var(--edge-lit),
    inset 0 -1px 0 var(--edge-dark);
  font: inherit;
  column-gap: var(--sp-3);
  grid-template-columns: minmax(0, 1fr) auto;
  text-align: left;
  text-shadow: 1px 1px #000;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.clan-row[aria-pressed="true"] {
  background: var(--panel-sunk);
  box-shadow:
    inset 0 1px 0 var(--edge-dark),
    inset 0 -1px 0 var(--edge-lit-dim);
}

.clan-row:focus-visible {
  outline: 2px solid var(--rs-yellow);
  outline-offset: -3px;
}

.clan-row-name {
  overflow: hidden;
  font-weight: 400;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.clan-row[aria-pressed="true"] .clan-row-name {
  color: var(--rs-yellow);
}

.clan-row-note {
  grid-column: 1;
  color: var(--ink-faint);
  font-size: var(--fs-xs);
}

.clan-row-note.failed {
  color: var(--rs-bad);
}

.clan-row-talking {
  grid-column: 2;
  grid-row: 1 / span 2;
  color: var(--rs-orange);
  font-size: var(--fs-sm);
}

.clan-detail,
.clan-start {
  margin-bottom: var(--sp-3);
}

.clan-actions {
  margin-bottom: var(--sp-3);
}

/* The space's own at:// address is for anybody who wants to look it up, and
   long; it is there to be copied more than read. */
.clan-space {
  font-size: var(--fs-xs);
}

.clan-owned .section-head {
  font-size: var(--fs-md);
}

.clan-form {
  margin-bottom: var(--sp-3);
}

.clan-form label {
  display: block;
  margin-bottom: var(--sp-2);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
}

.clan-form-row {
  display: flex;
  gap: var(--sp-2);
}

.clan-form-row .account-input {
  min-width: 0;
  flex: 1;
  margin-bottom: 0;
}

.clan-form-row .rs-button {
  min-width: calc(var(--column) * 0.2);
  flex-shrink: 0;
}

/* ---------------------------------------------------------------- shop --- */

/* The fishing page's line of what you are carrying that is helping, in the
   orange a page uses for the things it wants read. */
.fishing-gear {
  margin: calc(var(--sp-2) * -1) 0 var(--sp-3);
  color: var(--rs-orange);
  font-size: var(--fs-sm);
  line-height: 1.35;
  text-align: center;
}

.shop-shelf {
  padding: var(--sp-2);
  margin-bottom: var(--sp-3);
}

/*
 * A counter, drawn the way the game draws one: a shelf of goods with the number
 * left in the corner of each, and a line over it saying what a click would do.
 * The grid itself is the inventory's, up with it above, so a stack on a shelf
 * is the same size as the same stack in the panel.
 */
.shop-grid .slot {
  cursor: pointer;
}

.shop-grid .slot:not(.has-item) {
  cursor: default;
}

/* A shop draws what is left on the shelf in green, rather than running the
   bank's yellow-white-green scale over it. */
.stack-count.in-stock {
  color: var(--rs-good);
}

/* An empty shelf still draws its item. Sold out is not the same as gone, and
   the picture is how you know what is coming back. */
.shop-grid .slot.out-of-stock .item-sprite {
  opacity: 0.32;
  filter: grayscale(0.8);
}

.shop-grid .slot.out-of-stock .stack-count {
  color: var(--rs-bad);
}

/* Which slot the amount buttons underneath are pointed at. */
.shop-grid .slot.picked::after {
  position: absolute;
  inset: 1px;
  border: 1px solid var(--rs-yellow);
  content: "";
  pointer-events: none;
}

.shop-detail {
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
}

.shop-detail-head {
  display: flex;
  align-items: center;
  padding-bottom: var(--sp-2);
  margin-bottom: var(--sp-2);
  border-bottom: 1px solid var(--edge-row);
  gap: var(--sp-3);
}

/* Sized off the column like every other loose sprite on a page, at its own
   native width rather than a single size that would stretch the narrow ones. */
.shop-detail-head .item-sprite {
  width: calc(var(--column) * var(--sw, 31) * 0.0039);
  flex-shrink: 0;
}

.shop-detail-text h3 {
  margin: 0;
  color: var(--rs-orange);
  font-size: var(--fs-md);
  font-weight: 400;
  line-height: 1.1;
}

.shop-detail-text span {
  color: var(--rs-yellow);
  font-size: var(--fs-sm);
}

.shop-boon {
  margin: 0 0 var(--sp-3);
  color: var(--ink-dim);
  font-size: var(--fs-sm);
  line-height: 1.35;
}

.shop-amounts {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--sp-2);
}

/* ------------------------------------------------------- chat interface --- */

.chatbox {
  width: 100%;
  margin: 6px auto 0;
  text-align: left;
}

/*
 * The chat is a page. It used to be a strip under whichever view was open, in
 * one of three sizes, because it was always on screen and therefore always in
 * the body's way; a stone of its own settles that. On every other page the box
 * is not there at all, neither the log nor the row of controls, and the body
 * takes the whole column.
 *
 * It swaps with the body frame rather than sitting inside it. The chatbox wears
 * the same nine-sliced stone the body does, so putting one in the other is the
 * frame-inside-a-frame this interface spent a rewrite getting rid of.
 */
.game:not([data-view="chat"]) .chatbox {
  display: none;
}

.game[data-view="chat"] .view-frame {
  display: none;
}

.game[data-view="chat"] .chatbox,
.game[data-view="chat"] .chat-frame {
  display: flex;
  min-height: 0;
  flex: 1;
  flex-direction: column;
}

.game[data-view="chat"] .chat-log-wrap {
  min-height: 0;
  flex: 1;
}

/*
 * The chatbox wears the same frame as the body, so the two read as one make of
 * panel. No fill this time: what it holds is the game's parchment, a 64px crop
 * of the real one blended against a mirror of itself so its edges meet and it
 * tiles without showing a grid.
 */
.chat-frame {
  padding: 5px 5px 3px;
  border: 10px solid transparent;
  border-image: url("./assets/inventory-tab.png") 8 / 10px repeat;
  background: url("./assets/chat-parchment.png") 0 0 / 128px 128px repeat;
}

.chat-log-wrap {
  position: relative;
  display: flex;
  padding-top: 3px;
  gap: 3px;
}

.chat-log {
  display: flex;
  flex: 1;
  flex-direction: column;
  overflow-y: auto;
  padding: 0 5px;
  color: #000;
  font-size: 18px;
  line-height: 20px;
  scrollbar-width: none;
  overflow-wrap: anywhere;
}

.chat-log::-webkit-scrollbar {
  display: none;
}

/* Messages hug the bottom of the box until there are enough to scroll. */
.chat-log > :first-child {
  margin-top: auto;
}

.chat-log:focus-visible {
  outline: 2px solid var(--rs-yellow);
  outline-offset: -2px;
}

.chat-msg {
  margin: 0;
  padding: 0;
}

.chat-msg-system .chat-msg-text {
  color: #6b0b0b;
}

.chat-msg-public .chat-msg-text {
  color: #0000b4;
}

.chat-msg-name {
  color: #00007f;
}

.chat-msg-name.self {
  color: #7a4b00;
}

/* Somebody else's name puts an @mention of them in the line, once signed in. */
.chat-log.can-mention .chat-msg-name[data-handle] {
  cursor: pointer;
}

.chat-log.can-mention .chat-msg-name[data-handle]:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
}

/*
 * A mention keeps the colour and effect of the line it is in, and is marked
 * out by an underline alone, which is how the game's own chat shows nothing at
 * all and a web page shows something that goes somewhere.
 */
.chat-msg-mention {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

/* A line that mentions you, in the brown your own name is drawn in. */
.chat-msg-mentions-you {
  background: rgb(122 75 0 / 14%);
  box-shadow: inset 2px 0 #7a4b00;
  padding-left: 4px;
}

/* What to type, at the head of a line of the ::help legend. */
.chat-msg-key {
  color: #00007f;
}

/*
 * A clan line, in the colours the game gives a line from a chat-channel on the
 * opaque chatbox: the channel's name in blue between black brackets, and the
 * words in a dark red. The values are the friends chat defaults as RuneLite
 * records them in its JagexColors; the wiki does not list the clan chat's own,
 * so these are borrowed from the nearest channel that is written down.
 */
.chat-msg-channel-name {
  color: #0000ff;
}

.chat-msg-clan .chat-msg-text {
  color: #7f0000;
}

/*
 * The crown before a name, drawn at the 13x11 it was cut at. Everything else in
 * the chatbox is sized in fixed pixels too, because the parchment behind it is
 * a 64px crop that does not scale, and a sprite this small is the last thing
 * that could take being stretched to a percentage of the column.
 *
 * The baseline nudge is what puts it where the game puts it: sitting on the
 * line with the name rather than hanging off the top of an 18px cap height.
 */
.chat-badge {
  display: inline-block;
  width: 13px;
  height: 11px;
  vertical-align: -1px;
  image-rendering: pixelated;
}

/* --- scrollbar --- */

.chat-scrollbar {
  display: flex;
  width: 16px;
  flex-direction: column;
  flex-shrink: 0;
  background: #61553e;
  box-shadow: inset 1px 1px 0 #4a3f2b;
}

.chat-scrollbar.idle .chat-scroll-thumb {
  visibility: hidden;
}

.chat-scroll-arrow {
  height: 16px;
  flex-shrink: 0;
  padding: 0;
  border: 0;
  background: linear-gradient(#8a7859, #6b5c41);
  box-shadow:
    inset 1px 1px 0 #c3b391,
    inset -1px -1px 0 #4a3f2b;
  cursor: pointer;
}

.chat-scroll-arrow::after {
  display: block;
  width: 0;
  height: 0;
  margin: 0 auto;
  border-right: 4px solid transparent;
  border-left: 4px solid transparent;
  content: "";
}

.chat-scroll-arrow.up::after {
  border-bottom: 5px solid #17130c;
}

.chat-scroll-arrow.down::after {
  border-top: 5px solid #17130c;
}

.chat-scroll-arrow:active {
  background: linear-gradient(#7b6c4e, #9e8e6c);
}

.chat-scroll-track {
  position: relative;
  flex: 1;
  cursor: pointer;
}

.chat-scroll-thumb {
  position: absolute;
  left: 0;
  width: 100%;
  background: linear-gradient(#8a7859, #6b5c41);
  box-shadow:
    inset 1px 1px 0 #c3b391,
    inset -1px -1px 0 #4a3f2b;
  cursor: grab;
}

.chat-scroll-thumb:active {
  cursor: grabbing;
}

/* --- entry line --- */

.chat-entry {
  display: flex;
  align-items: baseline;
  margin-top: 3px;
  gap: 5px;
  border-top: 2px groove #9d8d6c;
  padding: 4px 5px 2px;
  font-size: 18px;
  line-height: 22px;
}

.chat-entry-name {
  flex-shrink: 0;
  color: #000;
  white-space: nowrap;
}

.chat-entry-field {
  position: relative;
  display: block;
  min-width: 0;
  flex: 1;
}

/*
 * The names an @ offers, in the Choose Option menu's brown box and white
 * text, sitting just over the line so it covers the log rather than the words
 * being typed. The one Enter would pick is in the menu's hover yellow.
 */
.chat-mention-list {
  position: absolute;
  z-index: 20;
  bottom: calc(100% + 4px);
  left: 0;
  max-width: 100%;
  margin: 0;
  padding: 2px 3px 3px;
  border: 1px solid #000;
  color: #fff;
  background: #5d5447;
  box-shadow:
    inset 0 0 0 1px #8c8376,
    2px 2px 0 rgb(0 0 0 / 45%);
  list-style: none;
  font-size: 18px;
  line-height: 20px;
  text-shadow: 1px 1px #000;
}

.chat-mention-option {
  overflow: hidden;
  padding: 1px 3px;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}

.chat-mention-option:hover,
.chat-mention-option[aria-selected="true"] {
  color: var(--rs-yellow);
}

.chat-entry-input {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  margin: 0;
  color: #00007f;
  background: transparent;
  caret-color: transparent;
  font: inherit;
  outline: 0;
}

.chat-entry-input::placeholder {
  color: #6f6047;
  opacity: 1;
}

/* A placeholder stays in a focused field until something is typed, which left
   nothing on this line to show that a click had landed. This one steps aside
   on focus instead, and the asterisk takes its place. */
.chat-entry-input:focus::placeholder {
  color: transparent;
}

/* Shut only while a sign-in typed here is on its way out of the page. */
.chat-entry-input:disabled {
  color: #6f6047;
  pointer-events: none;
}

/* A line on its way to the clan is typed in the clan's colour. */
.chat-entry[data-to="clan"] .chat-entry-input,
.chat-entry[data-to="clan"] .chat-caret {
  color: #7f0000;
}

/* On the entry line the clan's name gives way before the input does. */
.chat-entry-name .chat-msg-channel-name {
  display: inline-block;
  max-width: 6em;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: bottom;
}

/* Off-screen mirror of the input, so the blinking asterisk can sit exactly
   where the typed text ends the way the game's caret does. */
.chat-entry-measure {
  position: absolute;
  top: 0;
  left: 0;
  font: inherit;
  visibility: hidden;
  white-space: pre;
}

/*
 * The asterisk is the only caret the line has, since the browser's own is made
 * transparent above, so it keeps to what a caret means: typing lands here. At
 * rest it holds its place, still and in the placeholder's brown, because
 * nothing typed reaches the field until it has focus. With focus it turns blue
 * and blinks. The colour carries the signal as much as the blink does, because
 * anyone who has asked for less motion never sees it blink.
 */
.chat-caret {
  position: absolute;
  top: 0;
  left: 0;
  color: #6f6047;
  pointer-events: none;
}

.chat-entry-input:focus:enabled ~ .chat-caret {
  color: #0000c8;
  animation: chat-caret-blink 1s steps(1, end) infinite;
}

/* Logged out and empty, the placeholder holds the start of the line, and the
   asterisk would sit on its first letter. */
.chat-entry-input:placeholder-shown:not(:focus) ~ .chat-caret {
  display: none;
}

@keyframes chat-caret-blink {
  0%,
  50% {
    opacity: 1;
  }

  51%,
  100% {
    opacity: 0;
  }
}

/* --- tabs --- */

.chat-tabs {
  display: grid;
  margin-top: 2px;
  grid-template-columns: repeat(6, 1fr);
}

/*
 * Slabs cut the same way as the tab stones above them: a black outline that
 * steps in 4-2-2-1 at the corners rather than curving, a lit row under the top
 * edge, and a dip toward the outline down both sides. The art carries all of
 * that so the corners land on whole pixels at any button width; only the fill
 * changes between states, and which way it is lit says whether the slab is
 * proud or pressed in.
 */
.chat-tab {
  display: flex;
  min-width: 0;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 1px;
  border: 4px solid transparent;
  border-image: url("./assets/chat-btn.png") 4 fill / 4px stretch;
  color: #dfd6c2;
  /* The art already fills the slab, and the corners it leaves out have to show
     the page rather than the browser's own button grey. */
  background: none;
  font-size: 15px;
  line-height: 15px;
  text-shadow: 1px 1px #000;
  cursor: pointer;
}

.chat-tab-state {
  max-width: 100%;
  overflow: hidden;
  color: #35c635;
  font-size: 13px;
  line-height: 14px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * A layer switched off. The stone keeps its lit art, because one that can
 * still be pressed should not look like one that cannot, so the state line is
 * what says so: red against the green of a layer that is on, which is the same
 * pair the game's own chat tabs use. The name dims a shade with it, for the
 * glance that does not stop to read the word underneath.
 */
.chat-tab-state.off {
  color: #ef6e68;
}
.chat-tab.off .chat-tab-name {
  color: #a79d8c;
}

/* The lit yellow outranks the cream an open tab wears, so a finger leaving it
   behind on the tab it just tapped hid which one was actually on. */
@media (hover: hover) {
  .chat-tab:hover:not(:disabled) {
    color: var(--rs-yellow);
  }
}

/* Inside the outline, so the ring does not square off the stepped corners. */
.chat-tab:focus-visible {
  outline: 2px solid var(--rs-yellow);
  outline-offset: -4px;
}

.chat-tab.active {
  color: #ffe7b0;
  border-image-source: url("./assets/chat-btn-active.png");
}

.chat-tab:disabled {
  color: #8d8577;
  border-image-source: url("./assets/chat-btn-off.png");
  cursor: not-allowed;
}

.chat-tab.report {
  color: #ffdede;
  border-image-source: url("./assets/chat-btn-report.png");
}

@media (hover: hover) {
  .chat-tab.report:hover {
    color: var(--rs-yellow);
  }
}

/* ------------------------------------------------- chat colours & effects --- */

/*
 * OSRS chat takes `colour:effect:message`. The flat colours below are the
 * documented ones; the flash, glow and rainbow cycles are approximations,
 * since the wiki records which codes exist but not the values Jagex animates
 * between. Every coloured line gets a dark outline so it stays readable on
 * the parchment.
 */
.chat-msg-text[class*="fx-"] {
  text-shadow:
    1px 0 rgb(0 0 0 / 45%),
    -1px 0 rgb(0 0 0 / 45%),
    0 1px rgb(0 0 0 / 45%),
    0 -1px rgb(0 0 0 / 45%);
}

.chat-msg-text.fx-yellow { color: #ff0; }
.chat-msg-text.fx-red { color: #f00; }
.chat-msg-text.fx-green { color: #0f0; }
.chat-msg-text.fx-cyan { color: #0ff; }
.chat-msg-text.fx-purple { color: #f0f; }
.chat-msg-text.fx-white { color: #fff; }

.fx-flash1 { animation: fx-flash1 0.5s steps(1, end) infinite; }
.fx-flash2 { animation: fx-flash2 0.5s steps(1, end) infinite; }
.fx-flash3 { animation: fx-flash3 0.5s steps(1, end) infinite; }
.fx-glow1 { animation: fx-glow1 3s linear infinite; }
.fx-glow2 { animation: fx-glow2 3s linear infinite; }
.fx-glow3 { animation: fx-glow3 3s linear infinite; }
.fx-rainbow { animation: fx-rainbow 2s linear infinite; }

@keyframes fx-flash1 {
  0% { color: #f00; }
  50% { color: #ff0; }
}

@keyframes fx-flash2 {
  0% { color: #0ff; }
  50% { color: #00f; }
}

@keyframes fx-flash3 {
  0% { color: #0f0; }
  50% { color: #063; }
}

@keyframes fx-glow1 {
  0%, 100% { color: #f00; }
  50% { color: #ff0; }
}

@keyframes fx-glow2 {
  0%, 100% { color: #00f; }
  50% { color: #0ff; }
}

@keyframes fx-glow3 {
  0%, 100% { color: #0f0; }
  50% { color: #0ff; }
}

@keyframes fx-rainbow {
  0% { color: #f00; }
  17% { color: #ff0; }
  33% { color: #0f0; }
  50% { color: #0ff; }
  67% { color: #00f; }
  83% { color: #f0f; }
  100% { color: #f00; }
}

.fx-glyph {
  display: inline-block;
  white-space: pre;
}

.fx-wave .fx-glyph {
  animation: fx-wave 0.9s ease-in-out infinite;
  animation-delay: calc(var(--i) * -80ms);
}

.fx-wave2 .fx-glyph {
  animation: fx-wave2 0.9s ease-in-out infinite;
  animation-delay: calc(var(--i) * -80ms);
}

.fx-shake .fx-glyph {
  animation: fx-shake 0.24s steps(2, end) infinite;
  animation-delay: calc(var(--i) * -37ms);
}

.fx-slide .fx-glyph {
  animation: fx-slide 1.4s ease-in-out infinite;
  animation-delay: calc(var(--i) * -55ms);
}

@keyframes fx-wave {
  0%, 100% { transform: translateY(-3px); }
  50% { transform: translateY(3px); }
}

@keyframes fx-wave2 {
  0%, 100% { transform: translate(-2px, -3px); }
  50% { transform: translate(2px, 3px); }
}

@keyframes fx-shake {
  0% { transform: translate(-1px, 1px); }
  50% { transform: translate(1px, -1px); }
}

@keyframes fx-slide {
  0%, 100% { transform: translateY(-5px); }
  50% { transform: translateY(5px); }
}

.chat-msg-scrolling {
  display: flex;
  align-items: baseline;
  gap: 0.3em;
}

.chat-msg-scrolling .chat-msg-name {
  flex-shrink: 0;
}

.fx-scroll {
  display: block;
  min-width: 0;
  flex: 1;
  overflow: hidden;
  white-space: nowrap;
}

.fx-scroll-inner {
  display: inline-block;
  padding-left: 100%;
  animation: fx-scroll 7s linear infinite;
}

@keyframes fx-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-100%); }
}

/* A phone keeps a thinner gutter, and the sections either side of the body give
   up a little of their padding to leave it more of the screen. */
@media (max-width: 480px) {
  .game {
    width: min(100% - 16px, 408px);
    padding: 8px 0;
  }

  .interface-tabs {
    margin-top: 5px;
  }

  .view-frame {
    padding-bottom: 6px;
  }

  .chatbox {
    margin-top: 5px;
  }

  /* The type steps down on a narrow screen; the height does not, because the
     log is a page of its own now and takes whatever the page has. */
  .chat-log {
    font-size: 16px;
    line-height: 18px;
  }

  .chat-entry {
    font-size: 15px;
  }

  .chat-tab {
    font-size: 12px;
    line-height: 13px;
  }

  .chat-tab-state {
    font-size: 11px;
    line-height: 12px;
  }
}

/*
 * iOS zooms the whole page into any text field set under 16px the moment it
 * takes focus, and leaves it zoomed after. On a touch screen every field the
 * game has is held at 16px or more; `max` keeps the ones already bigger as they
 * are. Only the field changes, not the line around it: the chat's "Log in:"
 * label keeps its own size.
 */
@media (hover: none) and (pointer: coarse) {
  /* Set on the field rather than the input, so the mirror and the asterisk in
     it grow with the text; on the input alone the asterisk fell short of the
     text's end and sat on its last letter. 1em is the entry line's size, which
     is what the field would have had. */
  .chat-entry-field {
    font-size: max(16px, 1em);
  }

  .account-input {
    font-size: max(16px, var(--fs));
  }

  .stepper input {
    font-size: max(16px, var(--fs-md));
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
  }

  .chat-caret,
  .chat-msg-text[class*="fx-"],
  .fx-glyph,
  .fx-scroll-inner {
    animation: none !important;
    transform: none !important;
  }

  .water-scene,
  .fishing-spot {
    animation: none !important;
  }

  /* The unread stone holds the lit half of its flash for as long as the three
     blinks would have taken, rather than losing the signal: the same red, said
     once instead of three times. */
  .interface-tab.unread:not(.active) {
    animation: none !important;
    background-image: url("./assets/tab-stone-active.png");
  }
}

/* ================================================================ desk === */

/*
 * Past a phone the column gets company. The body becomes a stage on the left,
 * the chat a sidebar beside it that is up whatever page is open, which is the
 * one thing the column could never afford, and the stones stand in a rail
 * down the far right with their names under them. It is the RuneScape client's
 * own arrangement: the game in the big space, and the chat and the stones
 * along the right of it, the stones on the outside the way the client keeps
 * its side panel's tabs.
 *
 * Nothing moves in the markup. The same four sections are laid out on a grid
 * instead of down a column, which is why the chat's page and the chat's
 * sidebar are one element and cannot disagree about what has been said.
 *
 * The query has to match the one in layout.js word for word: the scripts ask
 * that copy whether the chat is on screen, and this one decides whether it is.
 */
@media (min-width: 1200px) and (min-height: 600px), (min-width: 1024px) and (min-height: 720px) {
  .game {
    display: grid;
    width: 100%;
    max-width: 1680px;
    padding: 16px;
    /* The title bar belongs to the stage, so it sits over it and no further:
       the name lines up with the stage's left edge and the orbs with its right.
       The chat and the rail both start where the stage does, so everything
       shares a top edge rather than the chat running up past the rest. The
       order is also the markup's, so tabbing walks the page left to right. */
    grid-template-areas:
      "mast  .    ."
      "stage chat rail";
    /* The chat is a sidebar, not a second stage: tall, and only as wide as a
       line of talk reads well at. The rail is exactly a stone wide, so the one
       gap between each of the three is the same gap to the eye as well: a
       wider column would centre its stones and pad the space beside the chat
       out past the space beside the stage. */
    grid-template-columns: minmax(0, 1fr) clamp(300px, 23vw, 380px) 48px;
    grid-template-rows: auto minmax(0, 1fr);
    gap: 8px 16px;
  }

  .masthead {
    grid-area: mast;
    margin-bottom: 0;
  }

  /* Both the page and the log are always on a desk, so neither hides the other. */
  .game[data-view] .view-frame {
    display: block;
    grid-area: stage;
  }

  .game[data-view] .chatbox,
  .game[data-view] .chat-frame {
    display: flex;
    min-height: 0;
    flex: 1;
    flex-direction: column;
  }

  .game[data-view] .chatbox {
    grid-area: chat;
    margin: 0;
  }

  .game[data-view] .chat-log-wrap {
    min-height: 0;
    flex: 1;
  }

  /*
   * The rail, down the far right. Each stone keeps the size it is cut at, a
   * little over, and wears its name underneath, which runs a few pixels past
   * the stone either side for the longest names and is not counted in the gap: seven icons in a row are a strip you learn, but seven
   * stacked down the edge of a big screen are a menu, and a menu says what is
   * on it.
   */
  .interface-tabs {
    display: flex;
    width: auto;
    grid-area: rail;
    flex-direction: column;
    align-items: center;
    margin: 0;
    gap: 22px;
  }

  .interface-tab {
    width: 48px;
    flex-shrink: 0;
  }

  /* The chat is not a page here, so its stone has nothing to open. */
  .interface-tab[data-view="chat"] {
    display: none;
  }

  .tab-name {
    top: calc(100% + 2px);
    left: 50%;
    width: auto;
    height: auto;
    overflow: visible;
    clip-path: none;
    transform: translateX(-50%);
    color: var(--ink-dim);
    font-size: 13px;
    line-height: 14px;
    text-shadow: 1px 1px #000;
  }

  .interface-tab.active .tab-name {
    color: var(--rs-yellow);
  }

  @media (hover: hover) {
    .interface-tab:hover .tab-name {
      color: var(--rs-yellow);
    }
  }

  /*
   * The stage scrolls the way the body did, with more room to do it in.
   *
   * Every page was drawn against the 408px column, and its type and spacing are
   * shares of it, so the stage gets a column of its own a little wider than a
   * phone's and everything on it comes up together rather than a page of
   * phone-sized lettering sitting in the middle of a big screen. The shares are
   * worked out on :root, and a custom property that is itself a calculation is
   * inherited already worked out, so moving the column is not enough by itself:
   * the steps have to be said again here, against the new one.
   */
  .view-frame {
    --column: clamp(440px, 34vw, 520px);
    --fs-xl: calc(var(--column) * 0.0588);
    --fs-lg: calc(var(--column) * 0.0466);
    --fs-md: calc(var(--column) * 0.0417);
    --fs: calc(var(--column) * 0.0368);
    --fs-sm: calc(var(--column) * 0.0319);
    --fs-xs: calc(var(--column) * 0.027);
    --sp-1: calc(var(--column) * 0.0098);
    --sp-2: calc(var(--column) * 0.0147);
    --sp-3: calc(var(--column) * 0.0196);
    --sp-4: calc(var(--column) * 0.0294);
    padding: 12px 16px;
  }

  /*
   * A page of reading is kept to a width a line of it reads at, and centred on
   * the stage; the two pages that are pictures rather than reading, the water
   * and the exchange, take all of it. The inventory is narrower again, since
   * its sprites are drawn at a share of their slot and a slot a third of the
   * stage across would draw a turtle the size of a hand.
   */
  .view-frame > [data-game-view] {
    max-width: calc(var(--column) * 1.5);
    margin: 0 auto;
  }

  .view-frame > #fishing-view,
  .view-frame > #market-view {
    max-width: none;
  }

  .view-frame > #inventory-view {
    max-width: calc(var(--column) * 1.12);
  }

  /*
   * The exchange side by side: the quote and its graph on the left, where the
   * eye starts, and what you hold and what you have done with it on the right.
   * On the column the two stack, and the graph was the thing a portfolio had
   * to be scrolled past to reach.
   */
  #exchange-overview {
    display: grid;
    grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
    grid-template-rows: auto auto auto minmax(0, 1fr);
    column-gap: var(--sp-4);
  }

  #exchange-overview > .exchange-status,
  #exchange-overview > .exchange-prompt {
    grid-column: 1 / -1;
  }

  #exchange-overview > .market-card {
    grid-row: 3 / span 2;
    margin-bottom: 0;
  }

  #exchange-overview > .portfolio-card,
  #exchange-overview > .trade-history {
    grid-column: 2;
  }

  /*
   * The water, as big as the stage allows. The page fills the stage whichever
   * method is open, and the water takes whatever height the rest leaves, where
   * on the column only the reef did: a desk has the height to spare, and the
   * strike bar and the net read better in a sea than in a strip of one.
   *
   * `#game` is only here to outrank the column's own grid for the reef, which
   * is written against the board being on the page and is otherwise the more
   * particular of the two.
   */
  #game #fishing-view:has(#water-scene) {
    height: 100%;
  }

  #game #fishing-view:has(#water-scene) .activity-window {
    display: grid;
    height: 100%;
    grid-template-areas:
      "head"
      "modes"
      "sizes"
      "water"
      "content";
    grid-template-columns: minmax(0, 1fr);
    /* The water has a floor, the way the column's reef does, and a stage too
       short for it scrolls rather than laying the controls over the water. */
    grid-template-rows: auto auto auto minmax(calc(var(--column) * 0.6), 1fr) auto;
    column-gap: var(--sp-4);
  }

  #fishing-view .page-head { grid-area: head; }
  #fishing-view .fishing-modes { grid-area: modes; }
  #fishing-view .reef-sizes { grid-area: sizes; }
  #fishing-view .activity-content { grid-area: content; }

  #fishing-view .water-scene {
    height: auto;
    grid-area: water;
  }

  #fishing-view .activity-buttons {
    flex-wrap: wrap;
  }

  /*
   * A patch stops growing at 36px. The board takes all the water it is given,
   * which on a big screen made an eight across board of patches the size of a
   * thumbnail, each a slab of stone with a number rattling around in it. The
   * cap is a little over what twenty-four across comes to on a 1080p screen,
   * so every size draws its patches at about the same size and a smaller reef
   * reads as a smaller board rather than a bigger one of the same thing.
   */
  .reef-board {
    max-width: calc(var(--patches-across) * 36px);
  }

  /*
   * Wide enough, and the reef's size strip and everything under the water move
   * into a column beside it, so the water runs from under the method strip to
   * the foot of the stage. A desk has width to spare and height to want; that
   * is what puts a twenty-four across board at thirty-odd pixels a patch on an
   * ordinary laptop. Narrower than this the column beside the water costs the
   * board more width than it gives back in height, and the strip stays on top.
   */
  @media (min-width: 1200px) {
    #game #fishing-view:has(#reef-board:not([hidden])) .activity-window {
      grid-template-areas:
        "head  head"
        "modes modes"
        "water sizes"
        "water content";
      grid-template-columns: minmax(0, 1fr) calc(var(--column) * 0.5);
      grid-template-rows: auto auto auto minmax(0, 1fr);
    }

    /* Beside the water the size strip is the top of a column, not a second
       line under the method strip, so it stands apart from it. */
    #fishing-view .reef-sizes {
      margin-top: 0;
    }

    #fishing-view:has(#reef-board:not([hidden])) .activity-content {
      padding-top: 0;
    }
  }
}
