/* Helm.
 *
 * Catppuccin Mocha only, with no prefers-color-scheme block, which is the one
 * place on this site that ignores the reader's system setting. The reason is
 * legibility rather than taste: this page draws three states of the same water
 * (lit, remembered, never seen) by lightness, and in Latte the two that matter
 * most collapse into each other. A chart you cannot read is not a light theme,
 * it is a broken page. The deviation is argued in the pull request.
 *
 * The tokens below are the same Mocha values css/style.css uses in its dark
 * block, plus the accent hues this page needs and no other page does.
 * scripts/check_palette.py holds css/style.css, 404.html, DESIGN.md, the
 * theme-color metas and this file against each other. Every literal hex below
 * has to be a real Catppuccin Mocha value, and any colour this page uses that no
 * other page does has to be registered in that script's MOCHA_EXTRA, so a hue
 * invented here cannot quietly become a sixth palette.
 */

:root {
  --bg: #1e1e2e;
  --bg-alt: #181825;
  --bg-chrome: #11111b;
  --border: #585b70;
  --text: #cdd6f4;
  --text-muted: #a6adc8;
  --accent-text: #89dceb;

  /* Chart hues. Mocha, named for what they draw rather than for the colour. */
  /* Water is one hue at two strengths, so there is no --sea-deep: depth is drawn
   * as less of the same blue rather than as a second colour. The token that used
   * to be here was overlay2, chosen when the sea was a thin glyph and the problem
   * was keeping a remembered tilde off the floor. Tiles retired that problem, and
   * a separate grey-blue for the deeps read as a third kind of thing rather than
   * as more of the same water. */
  --sea-shallow: #89b4fa;
  --land: #a6e3a1;
  --harbour: #f9e2af;
  --ship: #f5e0dc;
  --raider: #f38ba8;
  --trader: #fab387;
  --crown: #cba6f7;
  --loss: #f38ba8;
  --gain: #a6e3a1;

  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;
}

* {
  box-sizing: border-box;
}

/* display: grid on .helm would otherwise beat the hidden attribute, and the
   page would show an empty chart before the module has loaded. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  padding: 1rem;
  background: var(--bg-chrome);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.875rem;
  line-height: 1.5;
}

a {
  color: var(--accent-text);
}

/* Visible only once focused, so a keyboard reader can jump past the chart to
   the log without tabbing through six direction buttons first. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: 0.5rem 0.75rem;
  background: var(--bg-alt);
  border: 1px solid var(--accent-text);
}

.skip-link:focus {
  left: 0.5rem;
  top: 0.5rem;
  z-index: 10;
}

/* Aligned to the grid below rather than centred on the window.
 *
 * Both of these keep a 70ch reading measure, and both used to centre it with
 * `margin: 0 auto`. The chart does not centre: `.helm` is a 1400px maximum that
 * only starts centring once the window is wider than that, so on every screen
 * between about 620px and 1400px the heading drifted right of the chart's left
 * edge while the chart stayed put. It was 322px adrift at 1280.
 *
 * The start margin below is the grid's own left offset, written out: nothing
 * while the window is narrower than the grid's maximum, and half the overflow
 * once it is wider. So the heading sits over the chart's left edge at every
 * width, and still centres with everything else on a very wide screen.
 */
.helm--head,
.helm--notice {
  margin-inline-start: max(0px, calc((100% - 1400px) / 2));
  margin-inline-end: auto;
}

.helm--head {
  max-width: 70ch;
  margin-bottom: 1rem;
}

.helm--head h1 {
  margin: 0;
  font-size: 1.5rem;
  letter-spacing: 0.08em;
  text-transform: lowercase;
}

.helm--sub {
  margin: 0.25rem 0 0;
  color: var(--text-muted);
}

.helm--notice {
  max-width: 70ch;
  /* Explicit, because this is a <p> and the shorthand it replaced was zeroing
     the browser's own top margin as a side effect. */
  margin-top: 0;
  margin-bottom: 1rem;
  padding: 0.75rem;
  border: 1px solid var(--border);
  background: var(--bg-alt);
  color: var(--text-muted);
}

/* Three regions: chart, status column, log. The chart and the column sit side
   by side while there is room for both and stack when there is not, because a
   status column squeezed to twelve characters is worse than a scroll. */
.helm {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: minmax(0, 1fr) minmax(20rem, 26rem);
  /* The status column is much taller than the chart and spans both rows. With
     two auto rows the browser splits its height between them, which pushed the
     log down to the middle of the left-hand side and left a hole between it and
     the chart: 427px of nothing at 1024, 306px at 1280. Sizing the second row
     as a fraction stops the spanning column contributing to the first row at
     all, so the first row is the chart's own height and the log follows
     directly under it. The surplus then lands at the bottom of the column,
     where it reads as whitespace rather than as a rendering fault. */
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "map column"
    "log column";
  align-items: start;
  max-width: 1400px;
  margin: 0 auto;
}

@media (max-width: 60rem) {
  .helm {
    grid-template-columns: minmax(0, 1fr);
    /* Reset: nothing spans anything once the three regions are stacked, so the
       fraction above would only misdescribe the middle one. */
    grid-template-rows: auto auto auto;
    grid-template-areas:
      "map"
      "column"
      "log";
  }
}

.helm--map {
  grid-area: map;
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 0.25rem;
  min-width: 0;
}

#chart {
  display: block;
  width: 100%;
  height: auto;
}

.helm--column {
  grid-area: column;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 0;
}

.helm--chronicle {
  grid-area: log;
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 0.5rem 0.75rem;
  min-width: 0;
}

.panel {
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 0.5rem 0.75rem;
}

.panel h2,
.helm--chronicle h2 {
  margin: 0 0 0.4rem;
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent-text);
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.25rem;
}

.panel--sub {
  margin: 0.75rem 0 0.25rem;
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.dim {
  color: var(--text-muted);
  margin: 0.35rem 0 0;
}

.bad {
  color: var(--loss);
}

.good {
  color: var(--gain);
}

/* who: a two-column fact list, label left, value right. */
.panel--facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 0.75rem;
  margin: 0;
}

.panel--facts dt {
  color: var(--text-muted);
}

.panel--facts dd {
  margin: 0;
  text-align: right;
}

/* The same rule as the one in css/style.css, restated because game.html loads
 * this stylesheet and no other. `display: none` would take the element out of
 * the accessibility tree, which is the one thing it must stay in: the only use
 * here is the market's last column header, which names a column of buttons that
 * needs no visible caption but must still have a name to be a header at all. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* here: the market. A table, because it is one. */
.market {
  width: 100%;
  border-collapse: collapse;
  margin-top: 0.35rem;
}

.market th,
.market td {
  padding: 0.15rem 0.3rem;
  text-align: right;
  border-bottom: 1px solid var(--bg-alt);
  white-space: nowrap;
}

.market th {
  color: var(--text-muted);
  font-weight: normal;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.market th:first-child,
.market td:first-child {
  text-align: left;
  white-space: normal;
}

.market--scroll {
  max-height: 16rem;
  overflow-y: auto;
}

/* On a 320px screen the market wanted 285px inside a 262px box, so it scrolled
   sideways inside a box that already scrolls down and put the buy buttons off
   the edge. Four columns of horizontal cell padding is the cheapest 23 pixels
   on the page, and the table is the last thing that should be the reason a
   phone has to scroll two ways at once. */
@media (max-width: 22rem) {
  .market th,
  .market td {
    padding: 0.15rem 0.1rem;
  }
}

button {
  font: inherit;
  color: var(--text);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  padding: 0.2rem 0.5rem;
  cursor: pointer;
}

button:hover {
  border-color: var(--accent-text);
  color: var(--accent-text);
}

/* Never removed, only restyled. A focus ring is the only way a keyboard reader
   knows where they are, and this page has a lot of controls. */
:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

kbd {
  font: inherit;
  color: var(--accent-text);
}

input[type="number"] {
  font: inherit;
  width: 4.5rem;
  color: var(--text);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  padding: 0.1rem 0.25rem;
}

/* Every control on this page was between 26 and 29 pixels tall, which is fine
 * for a mouse and wrong for a finger: the guideline floor is 44, and a phone
 * visitor is precisely the one who cannot fall back on the keyboard this game
 * is really played with. The block is behind `pointer: coarse` rather than a
 * width query because it is the input device that matters, not the screen. A
 * narrow window on a laptop keeps the dense layout; a tablet at 1024 gets the
 * larger targets.
 *
 * Only the block-level controls grow. The links inside the heading are inline
 * text, where enlarging the target means changing the line height of the prose
 * around it, and where the guideline makes an exception for exactly that
 * reason. */
@media (pointer: coarse) {
  button,
  input[type="number"] {
    min-height: 44px;
  }

  .market td button,
  .panel--ports button {
    min-width: 44px;
  }

  /* The ports list is a column of two-word links; without this the taller
     targets overlap into one undifferentiated block. */
  .panel--ports li {
    padding: 0.15rem 0;
  }
}

.helm--rose {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.25rem;
}

.helm--orders {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-top: 0.5rem;
}

/* The shipyard. A list rather than a row of buttons because each hull needs
   two lines of prose next to its price: the player is choosing between six
   ships at once and the choice is not made on cost alone. No colours of its
   own, so check_palette.py has nothing new to police here. */
.helm--ships {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
}

.helm--ships li {
  padding: 0.4rem 0;
  border-top: 1px solid var(--border);
}

.helm--ships button {
  display: flex;
  width: 100%;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  text-align: left;
}

.helm--ships .ship--name {
  font-weight: 600;
}

.helm--ships .ship--price {
  white-space: nowrap;
}

/* An unbuyable hull is still legible: this is a shown-but-locked row, not a
   hidden one, so it dims rather than disappears. */
.helm--ships button[aria-disabled="true"] {
  opacity: 0.62;
}

.helm--ships .ship--blurb,
.helm--ships p {
  margin: 0.2rem 0 0;
}

.panel--ports {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 12rem;
  overflow-y: auto;
}

.panel--ports li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.1rem 0;
}

.panel--ports button {
  border: none;
  background: none;
  padding: 0.1rem 0.2rem;
  color: var(--accent-text);
  text-decoration: underline;
}

.chronicle--lines {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 12rem;
  overflow-y: auto;
  display: flex;
  flex-direction: column-reverse;
}

.chronicle--lines li {
  padding: 0.1rem 0;
  border-bottom: 1px solid var(--bg-alt);
}

.chronicle--lines li:first-child {
  color: var(--text);
}

.chronicle--lines li:not(:first-child) {
  color: var(--text-muted);
}

/* The chart itself.
 *
 * Two channels, and keeping them apart is the whole design. The tile is a filled
 * hexagon and says what the sea floor is; the glyph on top says what is standing
 * on it. Before the tiles existed both shared one character per hex, so a raider
 * on deep water drew an X and the water underneath simply vanished -- and whether
 * that water is deep or shallow is precisely what you need to know when you are
 * deciding whether to run for it.
 *
 * Three states of knowledge cut across both, told apart by opacity first so the
 * distinction survives every kind of colour blindness: lit, remembered, and never
 * seen. Never-seen hexes draw nothing at all, tile included, so the blank is the
 * fog: it means unexplored and nothing else.
 *
 * That only reads if remembered is unmistakably *drawn*, so the dim state has a
 * contrast floor rather than a pleasing opacity. The floor is 3:1, the figure for
 * a graphical mark, and the binding case is a raider on the brightest tile: dim
 * glyph over dim tile compresses toward the background from both sides at once.
 * That case is what set the fill alphas below, rather than any preference about
 * how blue the sea should look.
 *
 * Every figure in this file was measured by rasterising these rules -- fill
 * alpha, element opacity and the saturate() filter together -- into a canvas and
 * reading the composited pixels back, not by compositing on paper. Doing it on
 * paper got the background wrong once already: the chart sits on --bg, not on
 * --bg-chrome, and the darker assumption flattered every tile by roughly a
 * factor of two and hid a real failure at 2.79:1.
 *
 * What the tiles are NOT is the terrain cue. No two Catppuccin accents are more
 * than 1.83:1 apart -- the palette is near-iso-luminant by construction -- so a
 * land fill and a water fill cannot clear 3:1 against each other and no choice of
 * hues would fix it. Measured at the alphas below, land against the shallows is
 * 1.08:1 and the shallows against the deeps 1.22:1; sweeping the land alpha from
 * 0.10 to 0.24 never got land past 1.26:1 of the shallows, because it crosses them
 * in luminance around 0.19 rather than staying clear of them. The tiles
 * are therefore a redundant second channel, in the same sense as the saturate()
 * below: terrain keeps its glyph (~ shallow, = deep, # land) and a click reports
 * it in words. The one hex where a mark hides the terrain glyph is the reason
 * write_look() reports depth as text. */
.tile,
.glyph {
  user-select: none;
}

.glyph {
  font-family: var(--font-mono);
  font-size: 15px;
  text-anchor: middle;
  dominant-baseline: central;
}

.tile {
  /* The hex is the click target as well as the fill, so the cursor lives here.
   * A stroke in the page background separates neighbours: without it the water
   * tiles merge into one wash and the grid the game is played on disappears. */
  cursor: pointer;
  stroke: var(--bg-chrome);
  stroke-width: 1;
}

.tile.seen-0,
.glyph.seen-0 {
  opacity: 0;
}

.tile.seen-1,
.glyph.seen-1 {
  opacity: 0.65;
  /* Colour drains out of a chart drawn from memory. This is the second cue, and
   * it exists because opacity alone cannot carry both jobs: once the dim state
   * is lifted clear of the background it is necessarily closer to the lit one,
   * and the two would be told apart by brightness alone. Saturation is free
   * here -- the filter matrix is built on the same luminance coefficients WCAG
   * measures with, so draining the hue moves every contrast figure above by
   * less than 0.1 -- and it is redundant rather than load-bearing, so a visitor
   * who cannot see the hue shift still has the opacity step. */
  filter: saturate(0.3);
}

.tile.seen-2,
.glyph.seen-2 {
  opacity: 1;
}

/* Terrain, as an area.
 *
 * Water is one hue at two strengths, because depth is a quantity and drawing it as
 * a quantity of the same blue is what makes the shelf legible at a glance.
 *
 * Every tile here is a light colour at a low alpha, so every tile is brighter than
 * the page it sits on. That is not decoration: unexplored water draws no tile at
 * all, so bare --bg is what a tile is most often adjacent to, and it is also the
 * colour dimming moves a tile toward. Both of those only behave if the tiles stay
 * above the background. A land fill *below* it was tried -- crust at 0.6, on the
 * theory that light marks want a dark tile under them -- and it did buy the marks
 * headroom, 3.72:1 for a remembered raider. It also measured 1.09:1 against
 * unexplored, so the most common edge on the chart became almost invisible, ran the
 * grid stroke to 1.05:1 because that stroke *is* crust, and inverted the fog: with
 * land under the background, dimming made land brighter, so remembered meant
 * fainter on water and bolder on land. Three regressions for one gain.
 *
 * So the alphas are set by what stands on them, from above. Land is green at 0.13
 * rather than the 0.24 it started at, because 0.24 put a remembered raider on a
 * shore at 2.79:1, under the floor; the ceiling is about 0.18, where the raider hits
 * 3.02:1. 0.13 is a step back from that edge and reads 1.35:1 against unexplored,
 * which is better than the dark version managed. Water is 0.19, where a remembered
 * raider over the shallows measures 3.11:1 and is the worst case anywhere on the
 * chart. Depth is therefore the faintest distinction of the three, and of the two
 * channels the fill is the one that can afford to give way, because a fill one step
 * too faint still only reinforces what the glyph already said, whereas a mark under
 * its floor is a ship you cannot see. */
.tile.terrain-0 { fill: var(--sea-shallow); fill-opacity: 0.19; }
.tile.terrain-1 { fill: var(--sea-shallow); fill-opacity: 0.10; }
.tile.terrain-2 { fill: var(--land); fill-opacity: 0.13; }

/* Terrain texture: one neutral ink, drawn small.
 *
 * Not the terrain's own hue, because the tile underneath is already that hue and a
 * green # on a green tile measures about 1:1. One ink also buys the thing the
 * chart most needed, which is that colour in a glyph now means something is
 * *there*.
 *
 * These are meant to recede so the traffic stands out, and the honest way to do
 * that is size rather than a fainter ink. Ink is floor-bound: --text-muted is the
 * quietest Catppuccin value that still clears 3:1 over all three tiles, at 3.40:1
 * dim and 5.05:1 lit, and the next step down the ramp -- overlay2 -- measures
 * 2.84:1 dim on the shallows and stops being a cue at all. Since the fills are
 * near-iso-luminant, this glyph is the only thing telling land from water without
 * colour vision, so it cannot be the channel that gives way. Size is not
 * floor-bound in the same way: 10px against the marks' 15px reads as texture under
 * traffic while every contrast figure above holds exactly. */
.glyph.mark-0,
.glyph.mark-1,
.glyph.mark-2 {
  fill: var(--text-muted);
  font-size: 10px;
}

.glyph.mark-3 { fill: var(--harbour); }
.glyph.mark-4 { fill: var(--ship); }
.glyph.mark-5 { fill: var(--raider); }
.glyph.mark-6 { fill: var(--trader); }
.glyph.mark-7 { fill: var(--crown); }

/* More than one on the hex. Same colour as the single, because the colour says
 * what it is and the doubled mark says how many; a stack is a quantity of a
 * thing the reader already knows, not a new thing to learn. Two glyphs are
 * wider than one, so these draw a size down to keep the pair inside its hex. */
.glyph.mark-8,
.glyph.mark-9,
.glyph.mark-10 {
  font-size: 11px;
  letter-spacing: -0.06em;
}

.glyph.mark-8 { fill: var(--raider); }
.glyph.mark-9 { fill: var(--trader); }
.glyph.mark-10 { fill: var(--crown); }

.cell--mark {
  fill: none;
  stroke: var(--accent-text);
  stroke-width: 1.5;
}
