/* Greedy Ivy — the page around the board. Copyright (c) 2026 Alejandro Núñez.
   All rights reserved. See LICENSE. Game mechanics are not claimed; see the notice there.

   shared/board.css draws the cells; this draws everything else, and it is written for a
   phone in portrait first. The desktop case is just this layout with more room.

   THE HEIGHT BUDGET IS THE WHOLE PROBLEM. A connectivity game is unplayable if the board
   does not fit on one screen, and the board is 9 wide by 12 tall — so it is height, not
   width, that binds on a small phone. `.stage` is a size container and the cell takes the
   smaller of the two fits, which is what keeps an iPhone SE playable at ~36px a cell.
   Do not reduce this to the width term; the board will run off the bottom. */

*{box-sizing:border-box}
/* **`hidden` is only a UA `display:none`, so any author `display` beats it.** `.sheet`
   already had to work around that with a rule of its own; `.tools button`'s
   `display:inline-flex` did not, which meant **Resign was visible in hotseat from the day
   it was added** — a button that opens a confirm for an action a local game cannot even
   perform. One rule here instead of one per component, and every future `hidden` in this
   page means what it says. */
[hidden]{display:none !important}

/* --shell is the one width every top-level piece of the page — banner, phase strip,
   board, notice, bar — agrees to line up against on a screen wide enough to have room
   to spare. none on a phone: there is no spare room, so nothing here should cap
   anything. The "wider screens" block below is the only place this is ever not none. */
:root{--shell:none}

html{
  /* svh, not vh: vh is the *largest* viewport, so a board sized to it hides under the
     browser's own chrome until the user scrolls, and this page must never scroll. */
  height:100svh;
}
body{
  margin:0; height:100svh; overflow:hidden;
  display:flex; flex-direction:column;
  background:var(--ground); color:var(--ink);
  font-family:"Karla",system-ui,-apple-system,sans-serif; font-size:15px; line-height:1.4;
  -webkit-font-smoothing:antialiased;
  /* No double-tap zoom, no pull-to-refresh, no rubber-banding into a blank page. */
  touch-action:manipulation;
  overscroll-behavior:none;
  -webkit-text-size-adjust:100%;
  -webkit-tap-highlight-color:transparent;
  padding:env(safe-area-inset-top) env(safe-area-inset-right)
          env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* ---------------------------------------------------------------- the banner
   Just the name, and the three ways to leave the board — New, Rules, Settings. Every
   other piece of the old header moved: whose turn among Feed/Grow/Choke/Decay is the
   strip below; each player's own sun, income and to-play status live in the board's own
   corners now (see render()'s corners()), not up here competing with the title. */
/* A band, not text floating on the page — the same distinction --card already draws for
   every sheet and button here, just given to the header too. The border is what actually
   separates it from the phase strip underneath; a background alone reads as one surface
   with the page once the phase strip's own family of quiet greys sits right below it. */
#banner{
  flex:none; padding:.55rem .7rem; background:var(--card); border-bottom:1px solid var(--line);
}
.bannerbar{
  display:flex; align-items:center; justify-content:space-between; gap:.6rem;
}
#turn{
  display:flex; align-items:center; gap:.4rem; min-width:0; text-decoration:none;
  font-family:"Fraunces",Georgia,serif; font-size:1.15rem; font-weight:700;
  line-height:1.1; color:var(--ink);
}
/* The name itself still truncates; the mark beside it is flex:none so it is never what
   gives way first. */
#turn b{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
/* The winner's name over the reason it won, stacked — see index.html. The column is what
   keeps the reason off the end of the headline, where the ellipsis ate it; it costs
   nothing the rest of the time, since the second line is `hidden` until there is a
   winner and the banner is sized by its own padding, not by this. */
#turn .who{display:flex; flex-direction:column; min-width:0}
/* Not Fraunces and not the title's weight: this is the caption under a headline, and the
   colour is the one every other quiet second line on this page already uses. It truncates
   too — a reason is better cut short than allowed to widen the banner. */
#turnwhy{
  font-family:"Karla",system-ui,-apple-system,sans-serif;
  font-size:.7rem; font-weight:400; line-height:1.25; color:var(--soft);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
#turn .mark{width:1.3rem; height:1.3rem; flex:none}
#turn .mark .a{fill:var(--vine)}
#turn .mark .b{fill:var(--rival)}
/* The title takes a side's colour exactly when it is naming a player rather than the
   game: once somebody has won, and — since the turn sheet stopped opening for a turn you
   watched happen — while it is your own turn to move. `data-mine` is set by render() and
   is only ever on an online seat, so a spectator and a hotseat board keep neutral ink and
   the brand.

   That colour *is* the announcement now. A player watching the opponent move live used to
   be handed a sheet saying "Your turn!" the instant they finished; the sheet cost a press
   to describe a turn already on the screen, so the banner says it instead — in place,
   without moving anything, and without asking for anything. The pulse below is the half
   that makes it noticed rather than merely true. */
body[data-phase="over"][data-side="G"] #turn{color:var(--vine)}
body[data-phase="over"][data-side="P"] #turn{color:var(--rival)}
body[data-mine="yes"][data-side="G"] #turn{color:var(--vine)}
body[data-mine="yes"][data-side="P"] #turn{color:var(--rival)}

/* Played once, when the turn becomes yours — render() adds the class and a single
   `animationend` takes it off again, so nothing here loops and nothing is still moving a
   second later. It is the banner's own line that lifts, not the board and not the bar:
   the two things under a thumb never move, which is the same rule `#notice` is kept
   absent for. Reduced motion gets the colour and no movement, which is the whole of what
   the signal actually is. */
@keyframes turnrise{
  0%{transform:translateY(.22rem); opacity:.45}
  60%{transform:translateY(-.05rem); opacity:1}
  100%{transform:none; opacity:1}
}
#turn.mine{animation:turnrise .42s ease-out}
@media (prefers-reduced-motion:reduce){ #turn.mine{animation:none} }

/* The turn is Feed, Grow, Choke, Decay, and the strip is the only thing that says which
   one you are in. The engine refuses every action once a choke is spent, and without a
   phase that reads clearly at a glance the refusal looks like a broken button — so each
   of the four gets an equal share of the full row instead of sizing to its own word,
   and the active one is a solid fill, not just a tint, so it is never a second look. */
.tools{flex:none; display:flex; gap:.35rem}
.tools button, .tools a{
  min-width:2.4rem; min-height:2.4rem; padding:0 .55rem;
  border:1px solid var(--line); border-radius:.6rem;
  background:var(--card); color:var(--soft); font:inherit; font-size:.8rem;
  display:inline-flex; align-items:center; justify-content:center;
  text-decoration:none; cursor:pointer;
}
/* The white flag beside Resign / Rendirse. Its fill is a literal white rather than a
   token, and that is the one place in this project where that is right: on this icon the
   whiteness *is* the meaning, and a white flag drawn in --card would be a flag rather
   than a surrender. currentColor on the outline is what keeps it legible in light mode,
   where white otherwise sits on an almost-white ground with nothing to separate them. */
.tools button .flag{width:1.05rem; height:1.05rem; flex:none; margin-right:.32rem}

/* The gear is square and icon-only, so it gets no horizontal padding to stretch it. */
.tools button.icon{min-width:2.4rem; padding:0}
.tools button.icon svg{width:1.15rem; height:1.15rem; stroke:currentColor; stroke-width:1.6; fill:none}

/* ---------------------------------------------------------------- the board */
/* `start center`, not `center`: the board hugs the banner and whatever height is left
   over falls below it, next to the phase strip that is already a separate band. Centring
   split that slack in two and put half of it between the header and the roof, which read
   as a seam rather than as breathing room — and it only appeared on the taller phones
   where the board is width-bound and there is slack at all, so it looked like a bug that
   came and went.

   No padding either. It cost the board 0.7px a cell on every screen and bought a hairline
   of ground the board's own wall already draws better. */
/* `aspect-ratio`, and it is doing something specific: it makes .stage exactly as tall as
   the board it holds, instead of taking every spare pixel and then centring a board that
   cannot use them.

   The board is min(width/9, height/12.75), so once width binds — which it does on any
   phone taller than about 1.9:1 — extra height buys the board nothing at all. Measured:
   39px of dead space below it at 393x760, 66px at 360x740, 87px at 430x860. Giving that
   back to .stage's own flex-grow just pooled it under the board as a seam.
   9/12.75 is the board's own shape, so the stage now ends where the board does and the
   leftover falls to the two control bands below, which do have a use for it.
   `max-height:100%` is what keeps the other case working: on a short screen the ratio
   would ask for more height than exists, and the cap hands it back to the height term of
   that same min(). */
.stage{
  flex:0 1 auto; min-height:0; width:100%;
  aspect-ratio:9 / 12.75; max-height:100%;
  display:grid; place-items:start center;
  padding:0;
  container-type:size;
}
.stage .board{
  --sq:min(calc((100cqw - 2px) / 9), calc((100cqh - 2px) / 12.75));
  padding-top:calc(.75 * var(--sq));
}
#board{display:contents}

/* Each player's own status, sitting in the triangle of roof the gable leaves empty
   above their own half of the house — Green climbs from c1 on the left, Purple from g1
   on the right, so their corner is the same side their vine actually grows on. Sized in
   ems of --sq like everything else on the board, so it scales as one piece with it
   instead of needing its own breakpoints. z-index clears the roof's pitch (0) and the
   vine (1) — the corner sits in front of both, never behind either. */
.board .corner{
  position:absolute; z-index:10; top:calc(.2 * var(--sq));
  width:calc(3.9 * var(--sq)); padding:0 .2em;
  display:flex; flex-direction:column; gap:.1em;
  font-size:max(calc(var(--sq) * .48),18px); line-height:1.15;
  pointer-events:none;
}
.board .corner.g{left:calc(.08 * var(--sq)); color:var(--vine)}
.board .corner.p{right:calc(.08 * var(--sq)); text-align:right; color:var(--rival)}
/* Three sizes, each with its own floor — an em of a shrinking --sq collapses each of
   these below legible before the others, so one shared floor on the corner itself would
   still let the smallest text in it (the status word) vanish first. Same reasoning as
   the income chip's own floor; see DESIGN.md. */
.board .corner .name{display:flex; align-items:center; gap:.25em}
/* Content order does the mirroring, not this: box("G") puts the icon after the name,
   box("P") before it, so on both sides the icon lands nearer the roof's own centre
   than the outer edge — this only has to push the row itself to the far edge of a
   corner already stretched to the corner's own width. */
.board .corner.p .name{justify-content:flex-end}
.board .corner .name b{font:700 max(1.2em,20px)/1 "Fraunces",Georgia,serif}
/* The one persistent answer to "which colour am I" once the welcome sheet — the only
   other place that ever says it — is gone for good. A plain person, not a crown or any
   other icon this game already gives its own meaning to (a crown, specifically, is what
   core/engine.ts's own crown() already means — reaching the top, not "this one's mine").
   fill:currentColor, not a colour of its own, so it always matches whichever corner it
   lands in with no extra rule to keep in sync if a skin ever changes vine/rival. */
.board .corner .you{width:.55em; height:.55em; flex:none; fill:currentColor}
/* The opponent has the game open right now. It takes the same slot the "you" figure does
   in your own corner — the two can never both apply to one side — so nothing moves when
   it appears, which matters because it appears and disappears while you are looking at
   the board. currentColor for the same reason as .you: it is that side's own colour, and
   a skin changing vine/rival must not need a second rule kept in step. The soft ring is
   what stops a small dot on a busy corner reading as a speck of the board. */
.board .corner .here{
  width:.4em; height:.4em; flex:none; border-radius:50%;
  background:currentColor; box-shadow:0 0 0 .12em color-mix(in srgb,currentColor 28%,transparent);
}
.board .corner .stat{
  font:600 max(.6em,12px)/1.2 "IBM Plex Mono",monospace;
  text-transform:uppercase; letter-spacing:.03em; color:var(--faint);
}
.board .corner .stat.now{color:inherit}
.board .corner .sun{
  display:flex; align-items:center; gap:.22em; margin-top:.2em;
  font:600 max(.85em,16px)/1 "IBM Plex Mono",monospace; color:var(--soft);
}
.board .corner.p .sun{justify-content:flex-end}
.board .corner .sun b{font-size:1.15em; font-weight:700; color:var(--ink)}
.board .corner .sun em{font-style:normal; color:var(--sun)}
.sunicon{width:.85em; height:.85em; flex:none}
.sunicon circle{fill:var(--sun)}
.sunicon path{fill:none; stroke:var(--sun); stroke-width:2; stroke-linecap:round}

/* One flying sun, per client/feed.ts. Fixed and appended straight to <body>, not to any
   cell or corner — feed.ts hands it viewport coordinates from getBoundingClientRect, so
   fixed positioning is the only kind that means what those coordinates say once the
   board itself can be mid-scroll or mid-resize. feed.ts drives left/top and the actual
   flight with the Web Animations API; this only sets the size nothing else needs to. */
.feedsun{position:fixed; z-index:50; pointer-events:none; font-size:1.4rem}
.feedsun .sunicon{width:1em; height:1em;
  filter:drop-shadow(0 0 4px color-mix(in srgb,var(--sun) 70%,transparent))}

/* Feed, Grow, Choke, Decay — its own row, full width and justified, between the board
   and the bar whose buttons it explains. Tried living on the roof first, sharing the
   corners' own triangle of space; cramped there even before the corners had anything to
   say, and worse once both did. Down here it owns its row outright. Only the phase
   actually happening gets a fill, same as before — everything else is quiet text, not
   four pills fighting for the same attention. */
/* Both control bands grow into whatever .stage did not need, and each spends it the way
   that suits it: the strip centres its four labels in a taller band, and the bar's own
   `align-items:stretch` makes its buttons taller — which is the one thing on this screen
   that is strictly better for being bigger. Neither shrinks below what it had, since the
   flex-basis is still auto and the buttons keep their min-height. */
.phaserow{
  flex:1 1 auto; align-items:center; display:flex; gap:.35rem; margin:0; padding:.5rem .7rem; list-style:none;
  font:700 .74rem/1 "IBM Plex Mono",ui-monospace,monospace;
  text-transform:uppercase; letter-spacing:.04em;
}
.phaserow li{
  flex:1; text-align:center;
  padding:.5rem .3rem; border-radius:.5rem; color:var(--faint);
  border:1px solid transparent; white-space:nowrap;
}
.phaserow li.done{color:var(--faint); opacity:.55}
.phaserow li.now{
  color:var(--sun-ink); background:var(--sun); border-color:var(--sun);
  font-weight:800;
}

/* ---------------------------------------------------------------- the coach
   The tutorial's one new piece of furniture, and the whole of its design is that it does
   not move. It is unhidden once, when a lesson opens, and from then on only its words
   change — so `height` is fixed rather than `min-height`, and the text scrolls inside it
   if a translation runs longer than the four lines it reserves. A band that grew with its
   own sentence would push the board up and the action bar down every time the coach spoke,
   which is exactly why #notice below could not host this: #notice earns its keep by being
   absent when it has nothing to say.

   Four lines is measured, not guessed: .82rem on a 360px phone leaves about 52 characters
   to a line, and no lesson line is written longer than that will hold. `overflow-y:auto`
   is the safety valve, not the plan.

   A card rather than bare text on the page, like every other surface here — it is one
   voice talking, and it should look like a thing rather than like the page's own label. */
#coach{
  flex:none; display:flex; align-items:center; justify-content:center;
  margin:.35rem .7rem 0; padding:.4rem .7rem;
  height:calc(4 * 1.35 * 0.82rem + 0.9rem);
  overflow-y:auto; scrollbar-width:none;
  background:var(--card); border:1px solid var(--line); border-radius:.6rem;
}
#coach::-webkit-scrollbar{display:none}
#coach p{margin:0; font-size:.82rem; line-height:1.35; text-align:center; color:var(--ink)}

/* How far through the lesson, beside its name in the banner. Quiet, monospaced and a
   fixed shape, so a step that takes the count from 3 to 4 does not shuffle the title
   sideways — the same reason the list's tick sits in the number's own slot. It is the
   only thing on the tutorial's screen that says a lesson has an end. */
#lessonstep{
  flex:none; color:var(--soft);
  font:600 .68rem/1 "IBM Plex Mono",ui-monospace,monospace;
}

/* The one cell a lesson is pointing at. Deliberately louder than .can above — .can is on
   many cells at once and has to stay quiet, while this is on exactly one and its whole
   job is to be found without reading. Same ring shape as .target so the two read as the
   same family (pointed at, then picked), in --sun rather than --ink because every other
   "this is the move" on the screen is already that colour: the bar's centre slot, the
   chip, the cost. It pulses, slowly, and stops for anyone who has asked it to. */
/* Three bands, not one: a hairline of ink, the sun ring, a hairline of ink. A single sun
   ring is invisible on exactly the two cells the house lesson points at hardest — the
   soil, which is dark and warm, and the cell at the top, which is drawn in --sun-soft
   with a --sun ring of its own. Ink either side of it means the ring is legible against
   any ground the board can put under it, without giving up the colour that says "this is
   the move" everywhere else in the client. */
/* A fourth band, and it is outside the cell: the two the lesson points at hardest are
   the root and the top, and both fill their own middle with something large — the root
   with its oversized node and its --card ring, the sky cell with a ring of its own. An
   inset-only mark on those reads as part of the art rather than as a mark on it, which
   is exactly what the root looked like: ringed, and not noticeably ringed. The outer
   ring and its glow sit where nothing else on a cell ever draws, so the mark is legible
   whatever the cell itself is holding — and z-index lifts it over the neighbours, since
   an outer shadow is drawn under any cell painted after it. */
.cell.teach{z-index:3; box-shadow:
    inset 0 0 0 1px color-mix(in srgb,var(--ink) 70%,transparent),
    inset 0 0 0 3.5px var(--sun),
    inset 0 0 0 4.5px color-mix(in srgb,var(--ink) 70%,transparent),
    0 0 0 1.5px var(--sun),
    0 0 9px 2px color-mix(in srgb,var(--sun) 50%,transparent);
  animation:teach 1.9s ease-in-out infinite}
@keyframes teach{
  0%,100%{box-shadow:
    inset 0 0 0 1px color-mix(in srgb,var(--ink) 70%,transparent),
    inset 0 0 0 3.5px var(--sun),
    inset 0 0 0 4.5px color-mix(in srgb,var(--ink) 70%,transparent),
    0 0 0 1.5px var(--sun),
    0 0 9px 2px color-mix(in srgb,var(--sun) 50%,transparent)}
  50%{box-shadow:
    inset 0 0 0 1px color-mix(in srgb,var(--ink) 45%,transparent),
    inset 0 0 0 3.5px color-mix(in srgb,var(--sun) 55%,transparent),
    inset 0 0 0 4.5px color-mix(in srgb,var(--ink) 45%,transparent),
    0 0 0 1.5px color-mix(in srgb,var(--sun) 55%,transparent),
    0 0 9px 2px color-mix(in srgb,var(--sun) 25%,transparent)}
}
@media (prefers-reduced-motion:reduce){.cell.teach{animation:none}}

/* The tier lines, turned up for the one lesson step that names the bands by row number.
   The board draws no coordinates, so "rows 5 to 8" is a thing a player cannot find
   unless the floors between the bands say where each one starts and stops — and the
   board has drawn those floors all along, quietly, in --floor-line.

   It restyles `.floor`'s own ::before rather than adding an overlay, so the count of
   rules wanting a cell's single pseudo-element stays at four (see shared/board.css).
   z-index goes over the vine on purpose: the line is the subject for these few seconds,
   and a floor drawn under the stems is exactly the thing nobody noticed was there. */
#board.tiers .cell.floor::before{top:-3px; height:6px; z-index:5;
  background:var(--sun);
  box-shadow:0 0 0 1px color-mix(in srgb,var(--ink) 55%,transparent),
    0 0 10px 2px color-mix(in srgb,var(--sun) 55%,transparent);
  animation:tiers 1.9s ease-in-out infinite}
@keyframes tiers{0%,100%{opacity:1} 50%{opacity:.4}}
@media (prefers-reduced-motion:reduce){#board.tiers .cell.floor::before{animation:none}}

/* The tutorial's front door, which is the one sheet that is not laid over something the
   reader can act on. Every other sheet interrupts a game and lets it show through on
   purpose; this one *is* the page, and at .sheet's own 74% the board behind it reads as
   clutter nobody can touch. Heavier scrim, and a centred title with room under it —
   there is no body paragraph here to make the gap the way the other sheets have one. */
#lessons{background:color-mix(in srgb,var(--ground) 90%,transparent)}
#lessons .card>b{text-align:center; margin-bottom:.25rem}
/* What the list is, in one line: how many lessons, and that they are played rather than
   read. Centred under the title and carrying the space the title used to. */
#lessons .card p{text-align:center; margin:0 0 .85rem}
/* The way out, under a list rather than beside it. `.sheet .row` has no top margin of its
   own, because a sheet's row normally follows a paragraph that carries one — and a list
   ends in a tight .35rem stack that the row would otherwise join, which is Back drawn
   flush against the last thing it is not. Keyed on the *list*, not on #lessons: three
   sheets end this way now — the lessons, the rungs and the game list — and the first of
   them having the fix as a sheet of its own is what left the other two without it. */
.lessons + .row{margin-top:.75rem}

/* The lesson list. Numbered, because the order is the argument — each lesson is the one
   thing the next one is written on top of — and a tick on whatever has been finished, so
   coming back to the list a week later says where you stopped without a word about it. */
.lessons{margin:0; padding:0; list-style:none; display:flex;
  flex-direction:column; gap:.35rem}
/* A row has to read as a control, and it did not: --card on a --card sheet with a hairline
   border is the same rectangle the card itself is drawn as, five times over. Three cheap
   things fix it and none of them is a colour the palette does not already have — a tint of
   ink so the row sits *on* the card, a chevron at the end, and a hover that lifts both. */
/* The geometry is measured, not chosen. The blurb under each name is the widest thing in
   this sheet and the card is capped at 20rem on every screen, so the row's padding, its
   two gaps, the number's slot and the chevron all come out of one line of Spanish: 212px
   of it as the chevron first landed, 223px now. Four blurbs that wrapped to two lines fit
   on one at 223 — see DESIGN.md. Anything added to this row is paid for the same way. */
.lessons button{
  width:100%; display:flex; align-items:center; gap:.4rem; text-align:left;
  padding:.5rem .5rem; border:1px solid var(--line); border-radius:.6rem;
  background:color-mix(in srgb,var(--ink) 5%,var(--card));
  color:var(--ink); font:inherit; cursor:pointer;
}
.lessons button:hover{background:color-mix(in srgb,var(--ink) 9%,var(--card))}
.lessons button:hover .chev{color:var(--ink); transform:translateX(2px)}
.lessons button:active{transform:translateY(1px)}
/* The hub's Invite row for the moment between the press and the page it navigates to.
   `aria-disabled`, not `disabled` — nothing is unavailable, something is under way — and
   it has to *look* like a wait: without this it kept the hover fill, which a phone leaves
   stuck on the last thing touched, so a row that had gone quiet read as one lit up and
   refusing to move. */
.lessons button[aria-disabled="true"],
.lessons button[aria-disabled="true"]:hover{
  background:color-mix(in srgb,var(--ink) 5%,var(--card));
  color:var(--soft); cursor:default;
}
.lessons button[aria-disabled="true"]:active{transform:none}
.lessons button[aria-disabled="true"] .chev{opacity:.4}
.lessons .chev{
  flex:none; width:.85rem; height:.85rem; color:var(--soft);
  fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round;
  stroke-linejoin:round; transition:transform .15s ease, color .15s ease;
}
@media (prefers-reduced-motion:reduce){.lessons .chev{transition:none}}
.lessons .no{
  flex:none; width:1.2rem; text-align:center; color:var(--soft);
  font:700 .72rem/1.4 "IBM Plex Mono",ui-monospace,monospace;
  align-self:flex-start; margin-top:.15rem;
}
/* The tick replaces the number rather than sitting beside it — the slot is the same width
   either way, so a finished lesson does not shift the words next to it. */
.lessons .done .no{color:var(--vine-text)}
/* The hub and the game list reuse these rows whole — see client/index.html. Two things
   they need that a lesson does not:

   `.now` is the dot on a game waiting for you. It sits in the number's slot, which is
   why the slot stays a fixed width even when it is empty: a row that wants something
   must not be indented differently from one that does not.

   `.drop` is the per-row Remove. It is deliberately quiet and deliberately outside the
   row's own button — nesting a button inside a button is invalid, and the row is a
   navigation while this is not. `li` becomes the flex row so the two sit side by side. */
.lessons .now{color:var(--vine-text)}
/* `.todo` is the mark on the hub's Learn row, in that same slot and there for the same
   reason: it must not indent the row or shift a word of it, because the row beside it is
   the one it is being compared with. --sun, like every other "this is the one" in the
   client, and it is gone the moment every lesson has been finished — a mark that stayed
   would be decoration, and this one is a thing to do. */
.lessons .todo{
  display:block; width:.8rem; height:.8rem; margin:.1rem auto 0;
  color:var(--sun); fill:currentColor;
}
/* The game list is the one of these that has no fixed length — five lessons is five
   lessons, and a person can be in any number of games. So it scrolls inside the card
   rather than growing the card past the window, which is what ten rows did. The padding
   keeps the scrollbar off the rows' own border. */
/* The margin is not decoration: a scrolling list that ends flush against Back reads as
   one more row, one you cannot press. */
#gamelist{
  max-height:min(56vh,24rem); overflow-y:auto; padding-right:.2rem;
  margin-bottom:.7rem;
}

/* **The card is the `li` here, not the button** — the one place the game list departs
   from the lesson rows it otherwise borrows whole, and it departs for a reason.

   Only some rows can be removed, so a Remove button *beside* the row made those rows
   narrower than the rest and the list read as ragged — two widths, alternating, with the
   ✕ floating outside the card in its own box. Reserving an empty slot on every row would
   line them up again at the cost of 2.4rem of the one line each row gets to say anything
   in, which is the wrong thing to spend it on.

   So the border and the background move up to the `li`, and the two buttons sit inside
   one card with no chrome of their own. Every row is the same width, the ✕ is plainly
   part of the row it removes, and the rows that cannot be removed give up nothing.
   Nested buttons are invalid, which is why this is two siblings in a box rather than a
   button inside a button. */
#gamelist li{
  display:flex; align-items:center;
  border:1px solid var(--line); border-radius:.6rem;
  background:color-mix(in srgb,var(--ink) 5%,var(--card));
}
#gamelist li:hover{background:color-mix(in srgb,var(--ink) 9%,var(--card))}
#gamelist li > button{
  flex:1; min-width:0; width:auto; border:none; border-radius:.6rem;
  background:transparent;
}
/* The ✕ keeps a 44px target and shows almost nothing of it: a dismiss control that is
   only as big as its glyph is a miss on a phone, and one that looks as heavy as the row
   it sits in is louder than the thing it removes.

   It was a `border-left` on a stretched button and a typed `×`, and both were wrong. The
   divider ran the full height of the row, so it met the row's own rounded corner and drew
   a T through the card at each end, with the chevron pressed up against it — the end of a
   row read as three marks crowded into one corner. And `×` in Karla is a small letter
   sitting off the optical centre of its box, a lowercase x next to a stroked chevron.

   So: no line at all, and the mark in a hairline ring instead. A ring says "its own
   control" the way the divider was meant to, without cutting anything, and it leaves the
   chevron the air it needs. The cross itself is drawn in the chevron's own stroke family
   so the two marks at the end of a row read as one set. Both the ring and the cross are
   this button's alone — the ring is a pseudo-element rather than a wrapper, since the
   row's markup is already two siblings in a box for reasons of its own. */
#gamelist .drop{
  position:relative;
  /* `align-self:stretch` so the tap target is the row's full height, not a 44px box
     floating inside a taller one. The 44 stays as a floor. */
  flex:none; align-self:stretch; width:2.6rem; min-height:44px; padding:0;
  border:none; border-radius:0 .6rem .6rem 0;
  display:flex; align-items:center; justify-content:center;
  background:transparent; color:var(--faint);
  font:inherit; cursor:pointer;
}
#gamelist .drop::before{
  content:""; position:absolute; width:1.5rem; height:1.5rem;
  border:1px solid var(--line); border-radius:50%;
}
#gamelist .drop svg{
  position:relative; width:.7rem; height:.7rem; fill:none; stroke:currentColor;
  stroke-width:2; stroke-linecap:round;
}
#gamelist .drop:hover{color:var(--ink)}
#gamelist .drop:hover::before{
  border-color:color-mix(in srgb,var(--ink) 30%,var(--line));
  background:color-mix(in srgb,var(--ink) 8%,transparent);
}

.lessons .name{flex:1; min-width:0}
.lessons .name b{display:block; font-size:.9rem; font-weight:700}
.lessons .name small{display:block; font-size:.74rem; color:var(--soft); line-height:1.3}

/* The relocated notice — see index.html's comment on #notice. Same idea as .hint used
   to be (soft ink, red on a real complaint) but shown only when it has something to say,
   so it never claims a permanent line the way an always-on instruction did. */
#notice{
  flex:none; margin:0; padding:.3rem .7rem 0; color:var(--soft); font-size:.78rem;
  line-height:1.3; text-align:center;
}
#notice.bad{color:var(--rival)}

/* ---------------------------------------------------------------- overlays
   The loud layer. shared/board.css already defines .target, .doomed and .offer; only the
   quiet "you could move here" hint is new, and it must stay quiet — it is on many cells
   at once, and the board still has to read as a board underneath it. */
/* A background-image, NOT ::after — shared/board.css spends ::after on shade, and a
   legal cell under your own vine is shaded far more often than not. Painting it into the
   cell's background also puts it correctly *under* the shade rather than over it. */
.cell.can{
  background-image:radial-gradient(circle at 50% 50%,
    color-mix(in srgb,var(--ink) 26%,transparent) 0 9%, transparent 9.5%);
}
/* Soil is the one cell whose lightness inverts opposite to --ink between themes — dark
   brown in light mode, pale tan in dark mode, in every skin shared/skins.json defines —
   so an ink dot lands close to soil's own lightness in both and nearly disappears in
   either. --card inverts the same way --ink does, but relative to soil the two end up on
   opposite sides in both themes (measured ~8.5:1 light, ~9.8:1 dark, against ink-on-soil's
   ~1.9:1 / ~1.4:1) — the actual contrast partner soil has, not a coincidence of hue.
   First attempt here still mixed --card down to 45% opacity, chasing the same quiet
   translucency the ink dot uses elsewhere — with ~8.5:1 of headroom to spend and the
   result still reported barely visible, quiet was the wrong thing to preserve. Solid
   fill, no transparency, a hair larger than the 9% ink dot: soil is the one cell where
   the hint has to win the argument with a busy, contrasty background, not stay quiet
   next to a calm one. */
/* The actual bug both earlier attempts at this had, neither one about colour: .soil sets
   its own background-size (6px 6px, 9px 9px, for its two-dot texture) and this rule only
   ever replaced background-image, never background-size — so the dot below was being
   sized against a 6px tile instead of the cell, then tiled across it by the default
   background-repeat, landing as a field of sub-pixel specks. Reset both explicitly. */
.cell.soil.can{
  background-image:radial-gradient(circle at 50% 50%,
    var(--card) 0 11%, transparent 11.5%);
  background-size:100% 100%; background-repeat:no-repeat;
}
/* A second cell the plain centred dot never had a chance on, for a different reason:
   Choke's own legal-target cells are always the mover's own living vine — legal_cut()
   offers nothing else — and the dead centre a vine cell puts on screen is already busy:
   shared/board.css's .node covers close to half the cell, and .link runs a full-width
   band out to each edge the vine actually connects through. A ring around the node,
   tried first, crossed every one of those bands exactly where a link leaves the cell,
   and on a leaf (.lf is 84% of the cell, centred) sat almost flush against the leaf's
   own outline instead of framing it. The one corner nothing else claims is where
   .pay — the leaf's own income chip — is not: shared/board.css already owns
   bottom-right for that, so this takes top-left. */
.cell.vine.can{background-image:none}
.cell.vine.can::before{
  content:""; position:absolute; z-index:6;
  top:8%; left:8%; width:20%; height:20%; border-radius:50%;
  background:color-mix(in srgb,var(--ink) 55%,transparent);
}
.cell.target{box-shadow:inset 0 0 0 2.5px var(--ink); z-index:3}

/* The income chip is 0.22em of a cell — about 8px on a small phone, with a numeral in
   it. Below ~34px cells the em scaling stops being legible, so it gets a floor. Measured
   on a device before any scored colour is touched; see DESIGN.md. */
.pay{min-width:max(1.3em,11px); height:max(1.3em,11px); font-size:max(.22em,7.5px)}
.count{font-size:max(.42em,11px)}

/* ---------------------------------------------------------------- the action bar */
#bar{
  flex:1 1 auto; display:flex; gap:.4rem; align-items:stretch;
  padding:.4rem .6rem calc(.5rem + env(safe-area-inset-bottom));
  overflow-x:auto; scrollbar-width:none;
}
#bar::-webkit-scrollbar{display:none}
#bar button{
  /* Three slots, never equal. Position, not label length, decides width — a slot that
     resized with its own text would move the other two under a thumb already travelling,
     since the note changes on every tap. `flex-basis:0` with `min-width:0` is what makes
     the width come from the bar rather than the words in it. Height is a floor, not a
     fixed number, and `align-items:stretch` on #bar keeps the three buttons equal height
     even when one of them wraps — but wrapping is the catch, not the plan. A string that
     does not fit does not widen its button, it grows it taller and shoves the two beside
     it and the board above them, which is the one thing the sizing above exists to
     prevent. So every label and note is written to one line each at 360px; the rule and
     the measurements are in shared/strings.json's `_bar`. */
  flex:1 1 0; min-width:0; min-height:3.1rem;
  padding:.4rem .6rem;
  display:flex; flex-direction:column; justify-content:center; gap:.05rem;
  border:1px solid var(--line); border-radius:.7rem;
  background:var(--card); color:var(--ink); font:inherit; text-align:left;
  cursor:pointer;
}
/* The centre slot is the phase's one action — Creep/Leaf, Choke, Wither — and it is the
   button a thumb should find without looking, so it gets more of the bar and a larger
   label than Undo/Back and the phase-advance button either side of it. */
#bar button:nth-child(2){flex-grow:1.9}
#bar button:nth-child(2) b{font-size:1.05rem}
#bar button:nth-child(2) small{font-size:.72rem}
#bar button b{font-size:.92rem; font-weight:700; line-height:1.15}
/* The cost in sun, not a bare digit — inline-flex only for the baseline alignment a raw
   <svg> next to text never gets on its own; .sunicon's own em sizing already tracks
   whichever button label it lands in, wide slot included, with nothing to override here. */
.cost{display:inline-flex; align-items:center; gap:.15em; vertical-align:-.08em}
#bar button small{
  font-size:.68rem; color:var(--soft); line-height:1.2;
  overflow-wrap:break-word;
}
#bar button:active{transform:translateY(1px)}
/* A slot with nothing to offer is greyed rather than removed, so the bar never reflows
   under a thumb. Opacity rather than a colour: it dims border, label and note together,
   in both themes, and adds nothing for playtest/palette.py to score. */
#bar button:disabled{opacity:.4; cursor:default}
#bar button:disabled:active{transform:none}
/* The one exception to "greyed means nothing to offer": Waiting is not idle chrome, it
   is the actual answer to why the other two slots either side of it are dimmed at all.
   Full strength text and a border that actually shows, so it reads as "this is what is
   happening" rather than "this button is broken too". */
#bar button.wait:disabled{opacity:1; border-color:var(--soft)}
/* Creep and Leaf used to fall through to the bar's own neutral default, leaving Choke
   and Wither as the only phase actions that read as "the one thing to press here" —
   an asymmetry nothing about the rules calls for. All four spend sun or cut a vine on
   the same footing: this is the phase's actual move, not incidental chrome. */
#bar .cut, #bar .grow, #bar .leaf{border-color:var(--sun)}
#bar .cut b, #bar .grow b, #bar .leaf b{color:var(--sun)}
#bar .go{background:var(--ink); color:var(--ground)}
#bar .go b{color:var(--ground)}
#bar .un{color:var(--soft)}

/* ---------------------------------------------------------------- the chip
   The centre slot's offer again, on the cell it would act on. Same words, same colours,
   built by the same offerAt() — this is a second place to press it, not a second thing.
   It exists because the bar is pinned to the bottom of the screen and the cell is under
   the thumb that just tapped it: the first playtest asked three times in four minutes
   for the second tap to be next to the first.

   Anchored above the cell (.up), because below is where the hand is; the top two rows
   flip (.down) because there is nothing above e12 to hang it from. Horizontal placement
   is clamped in app.ts against the board's own width — the chip is several cells wide.

   No :active nudge, unlike the bar's buttons: the transform is what positions this, and
   a second one would fight it. */
.chip{
  position:absolute; z-index:6;
  max-width:min(14rem,calc(7*var(--sq)));
  padding:.35rem .55rem;
  display:flex; flex-direction:column; gap:.05rem;
  border:1px solid var(--sun); border-radius:.7rem;
  background:var(--card); color:var(--ink); font:inherit; text-align:left;
  cursor:pointer;
  /* It floats over the board rather than sitting in the page, and the board is a busy
     surface: without this the cells behind it read as part of it. */
  box-shadow:0 .25rem .7rem color-mix(in srgb,var(--ground) 55%,transparent);
}
.chip.up{transform:translate(-50%,calc(-100% - .14*var(--sq)))}
.chip.down{transform:translate(-50%,calc(.14*var(--sq)))}
.chip b{font-size:.92rem; font-weight:700; line-height:1.15; color:var(--sun)}
.chip small{font-size:.68rem; color:var(--soft); line-height:1.2; overflow-wrap:break-word}
/* A chip with nothing to press is the answer to "why not", so it is not dimmed the way
   an idle bar slot is — same reasoning as #bar button.wait:disabled. It must also let
   the tap through: it hangs over cells, and the next thing a player does after reading
   "no vine here to choke" is tap a different cell, quite possibly one underneath it. */
.chip:disabled{border-color:var(--line); cursor:default; pointer-events:none}
.chip:disabled b{color:var(--soft)}
/* The tutorial's read-step chip (`shownOnly()` in app.ts): nothing to press, but it is
   the action itself rather than a refusal, so it keeps the colours of one. */
.chip.shown:disabled{border-color:var(--sun)}
.chip.shown:disabled b{color:var(--sun)}

/* Naming a side or the goal in prose gets the one colour treatment art.html and
   rules.html already use — not a new one invented here. `--vine-text`/`--rival-text`,
   not the board's own `--vine`/`--rival`: playtest/palette.py audits those two
   specifically for word-in-a-sentence contrast (`("vine-text","ground","the word
   Green",4.5)`), which the board's own saturated fill was never checked against. */
.green{color:var(--vine-text)}
.purple{color:var(--rival-text)}
.top{color:var(--top)}

/* ---------------------------------------------------------------- sheets
   One overlay pattern, every sheet on the page: Settings, the invitation, the two
   confirms, the announcement. Centred, because on a phone the thumb that opened any of
   them is already near the middle of the screen, not at an edge. Tokens only — nothing here is a colour playtest/palette.py has not already
   scored on the board. */
.sheet{
  position:fixed; inset:0; z-index:20;
  display:flex; align-items:center; justify-content:center; padding:1.2rem;
  background:color-mix(in srgb,var(--ground) 74%,transparent);
}
.sheet .card{
  width:min(20rem,100%); padding:1.1rem 1.1rem 1rem;
  border:1px solid var(--line); border-radius:1rem; background:var(--card);
  box-shadow:0 1.2rem 2.4rem color-mix(in srgb,var(--ground) 60%,transparent);
}
/* Centred, every sheet, because they are titles of a card and not the first line of its
   paragraph — the body text under them stays ranged left, where a line of prose belongs. */
.sheet .card>b{
  display:block; text-align:center;
  font-size:1.05rem; line-height:1.2; margin-bottom:.2rem;
}
.sheet .card p{margin:.35rem 0 .9rem; color:var(--soft); font-size:.82rem; line-height:1.35}
.sheet .row{display:flex; gap:.5rem}
/* `a` alongside `button`: the welcome sheet's Rules slot is a real link (opens the
   manual in its own tab, works without JS, middle-click still works), not a button that
   only pretends to be one — it just has to fill the same slot the same way. */
.sheet .row button, .sheet .row a{
  flex:1 1 0; min-height:44px; padding:.5rem;
  border:1px solid var(--line); border-radius:.7rem;
  background:var(--card); color:var(--ink); font:inherit; font-weight:700;
  cursor:pointer; text-decoration:none;
  display:inline-flex; align-items:center; justify-content:center; text-align:center;
}
/* The welcome sheet's primary. Full width above the quiet pair rather than a third of a
   row beside them — Start is what almost everyone in that sheet wants, and three equal
   buttons is also genuinely tight on a 360px phone. */
.sheet .go.wide{
  display:block; width:100%; min-height:44px; margin-bottom:.5rem; padding:.5rem;
  border:1px solid var(--ink); border-radius:.7rem;
  background:var(--ink); color:var(--ground); font:inherit; font-weight:700;
  cursor:pointer;
}
.sheet .row .go{background:var(--ink); color:var(--ground)}
.sheet .row .un{color:var(--soft)}

/* The quiet third control, under the row rather than in it — see index.html's own comment
   on why. No border and no fill: it has to read as an aside to the two answers above it,
   not as a third answer, and the only sheet that has one so far is the turn sheet's
   "never again". Full width so the tap target is honest at 44px even though the words are
   small; centred, because everything else in a card is. */
.sheet .quiet{
  display:block; width:100%; min-height:44px; margin-top:.35rem; padding:.5rem;
  border:0; background:none; color:var(--soft);
  font:inherit; font-size:.78rem; font-weight:400; text-align:center; cursor:pointer;
}

/* Settings is a list of rows rather than a question, so it gets its own inner layout
   instead of .row: a label on the left, one control on the right, repeated. */
#settings .card>b{margin-bottom:.4rem}
.setrow{
  display:flex; align-items:center; justify-content:space-between; gap:.6rem;
  padding:.6rem 0; border-top:1px solid var(--line); font-size:.88rem;
}
.setrow[aria-disabled]{color:var(--faint)}
/* The one line in Settings that is an explanation rather than a control: why a row cannot
   be pressed. It hangs under its own row with no border of its own, so it reads as part of
   that row rather than as a fourth setting. */
.setnote{margin:.4rem 0 0; font-size:.78rem; line-height:1.35; color:var(--soft)}
.setctl{
  min-width:4.4rem; min-height:38px; padding:0 .7rem;
  border:1px solid var(--line); border-radius:.6rem;
  background:var(--ground); color:var(--ink); font:inherit; font-weight:700; font-size:.8rem;
  cursor:pointer;
}
/* The language row. Two flags, one of them the page you are on — see the markup in
   index.html for why it is drawn rather than written, and why both are anchors.

   Heights match and widths do not: the Union Flag is 2:1 and Spain's is 3:2, and one box
   for both would mean distorting one of them. Sized off .setctl's own 38px tap target
   rather than the flag artwork, so the row lines up with Theme beside it and a thumb has
   the same amount to aim at. */
.langs{display:flex; align-items:center; gap:.5rem}
.langs a, .langs button{
  display:block; line-height:0; padding:.55rem .45rem;
  border:1px solid var(--line); border-radius:.6rem; background:var(--ground);
  opacity:.45; cursor:pointer; transition:opacity .15s;
}
.langs a:active, .langs button:active{opacity:.8}
/* No href: the current language. Inert, and it says so by being the bright one. */
.langs a:not([href]){opacity:1; border-color:var(--soft); cursor:default}
/* The invite sheet's pair are buttons, since choosing the link's language goes nowhere.
   Pressed is the language the link will be sent in — the same bright-means-this rule. */
.langs button[aria-pressed="true"]{opacity:1; border-color:var(--soft)}
.linklang{
  display:flex; align-items:center; justify-content:space-between; gap:.6rem;
  padding:.1rem 0 .4rem; font-size:.82rem; color:var(--soft);
}
.langs svg{height:.95rem; width:auto; display:block; border-radius:.1rem}
/* A greyed row still names what it will be, so "not yet" reads as a promise, not a dead
   end — the same reasoning as a disabled action-bar button naming the action it isn't. */
.tag{
  padding:.15rem .5rem; border-radius:1rem; border:1px solid var(--line);
  font-size:.7rem; letter-spacing:.02em; color:var(--faint);
}
/* The one link the invite sheet hands out — the link itself as selectable text
   (tap-and-hold to copy needs no button on a phone), and a Copy button for anyone whose
   thumb prefers that over a long-press. */
/* The one-seat warning above the link. Not styled as an error — nothing has gone wrong,
   this is how the link works — so it borrows the sun accent the phase strip and the
   kicker already use for "read this", rather than anything red. */
.sheet p.warn{
  margin:.1rem 0 .2rem; font-size:.78rem; font-weight:700; color:var(--sun);
}
/* Two buttons now, and the link has to stay the thing that gives way — a token is long
   and both buttons are short, so `code` keeps flex:1 and they keep flex:none. Wrapping is
   allowed for the narrowest phones, where a link plus Copy plus New link genuinely does
   not fit on one line. */
.linkrow{display:flex; align-items:center; gap:.5rem; padding:.35rem 0}
/* One row under the link, split by what each control is for rather than by size: the
   first child is the way *out* of this game and everything after it acts on the link, so
   `margin-right:auto` on that one pushes the pair to the right with no spacer element and
   no rule naming an id. */
.linkbtns{display:flex; align-items:center; gap:.5rem; padding:.35rem 0}
.linkbtns > :first-child{margin-right:auto}
.linkrow code{
  flex:1 1 auto; min-width:0; overflow-x:auto; white-space:nowrap;
  font:.72rem "IBM Plex Mono",ui-monospace,monospace; color:var(--soft);
  background:var(--ground); border:1px solid var(--line); border-radius:.4rem;
  padding:.35rem .5rem; user-select:all;
}
.linkbtns button, .linkbtns a{
  flex:none; min-height:34px; padding:0 .7rem;
  border:1px solid var(--line); border-radius:.5rem; background:var(--ground);
  color:var(--ink); font:inherit; font-size:.72rem; font-weight:700; cursor:pointer;
  display:inline-flex; align-items:center; text-decoration:none;
}
/* Copy is what this screen is for, so it is the one filled control in it. */
.linkbtns .go{background:var(--ink); color:var(--ground); border-color:var(--ink)}
#settingsdone{
  width:100%; margin-top:.9rem; min-height:44px; padding:.5rem;
  border:1px solid var(--line); border-radius:.7rem; font:inherit; font-weight:700;
  cursor:pointer; background:var(--ink); color:var(--ground);
}

/* ---------------------------------------------------------------- wider screens
   Nothing here changes the game; it only stops the board being a stamp on a monitor. */
@media (min-width:34rem){
  :root{--shell:46rem}
  body{font-size:16px}
  /* The 3rem this used to cap --sq at meant the board stopped growing past a phone-ish
     cell size the instant a window crossed this very breakpoint — every desktop window,
     not just an implausibly huge monitor, since nothing between here and infinity ever
     raised the cap again. --shell/9 replaces it: not a number of its own, but the same
     width everything else on the page is about to line up against, so the board can
     still claim all the height it wants without ending up wider than the banner and bar
     around it — which, uncapped entirely, is exactly what made it "look ridiculous". */
  .stage .board{
    --sq:min(calc((100cqw - 2px) / 9), calc((100cqh - 2px) / 12.75), calc(var(--shell) / 9));
  }
  /* .stage gets the same treatment as the chrome around it, not a bigger one: at
     --shell's own width the cqw term above and this max-width agree, so capping --sq by
     hand and capping .stage's own box are the same board width read two ways, not two
     competing rules that could drift apart later. */
  /* #coach belongs in this list for the same reason #notice does, and was missed when it
     arrived: a band of text the full width of a monitor, under a board that stops at
     46rem, reads as part of the browser rather than part of the game. */
  .stage, .bannerbar, .phaserow, #notice, #coach, #bar{
    max-width:var(--shell); width:100%; margin-left:auto; margin-right:auto;
  }
  /* Phase strip and action bar were still sized in the same rem as a phone, so once the
     board itself grew to fill a tall window they read as a caption under it rather than
     the controls for it. Both scale up here, and the bar gets real breathing room below
     it instead of sitting on env(safe-area-inset-bottom) alone, which is 0 on a desktop
     and left the row hugging the very edge of the window. */
  .phaserow{font-size:.85rem; padding:.65rem .7rem}
  .phaserow li{padding:.6rem .3rem}
  #bar{justify-content:center; padding-bottom:calc(1.4rem + env(safe-area-inset-bottom))}
  #bar button{flex:0 1 11rem; min-height:3.6rem; padding:.55rem .7rem}
  #bar button b{font-size:1rem}
  #bar button small{font-size:.74rem}
  #bar button:nth-child(2) b{font-size:1.15rem}
  #bar button:nth-child(2) small{font-size:.78rem}
}

/* ---------------------------------------------------------------- short screens

   The board is `min(width/9, height/12.75)`, so on a portrait phone it is almost always
   *height* that binds — and when height binds, every pixel the chrome spends is a pixel
   off the board's own width. Measured on a 393-wide viewport: it becomes width-bound at
   about 714px of height, and below that the side margins grow fast — 17px of margin at
   700, 55px at 645, 87px at 600. A phone showing a URL bar and a bottom tab strip lands
   squarely in that range, which is exactly where the board should be *least* willing to
   give up room.

   So this block spends less height on chrome precisely when height is the constraint.
   Above the threshold it never applies, and the roomier spacing everywhere else is kept.

   **It sits after the wider-screens block on purpose.** The two overlap — a landscape
   phone and a short desktop window are both wide *and* short — and they set the same
   paddings, that one inflating what this one trims. Last rule wins, and the constraint
   that is actually binding should be the one that does: when there is height to spare the
   roomier spacing is free, and when there is not it is coming straight off the board.

   Only padding is touched, never a tap target: `#bar button`'s 3.1rem and `.tools
   button`'s 2.4rem are what a thumb aims at and are already at the small end of what
   that should be. Trading those for board size would be trading the wrong thing. */
@media (max-height:715px){
  #banner{padding:.4rem .7rem}
  .phaserow{padding:.3rem .7rem}
  .phaserow li{padding:.35rem .3rem}
  /* .5rem below, not .35: on a phone env(safe-area-inset-bottom) adds to this, but on a
     short desktop window it is 0 and the bar ended up sitting on the very edge. Two and
     a half pixels of board width is the right price for that. */
  #bar{padding:.25rem .6rem calc(.5rem + env(safe-area-inset-bottom))}
}
