/* The public pages' one stylesheet.
 *
 * # Why the tokens are copied from the local UI and not invented
 *
 * A person meets these pages first and the daemon's own page (localapi/
 * page.html) minutes later, on the same machine, in the same browser. Two
 * palettes would read as two products — the same reason UI.md §12-9 forbids the
 * tray and the browser disagreeing. So the :root block below is that file's
 * :root, value for value, including the dark half.
 *
 * It is a copy rather than a shared file because the two are served by different
 * programs from different machines: the daemon embeds its page into the binary
 * (go:embed) and these are static files on the cloud host. Nothing can import
 * across that gap. The copy is small and this comment says where it came from,
 * which is what makes a divergence findable.
 *
 * # The layout is not the local UI's
 *
 * The daemon's page is a dashboard for one machine and lives at 760px. These are
 * a product front page: a wide hero, a card grid, and a lot of air. Same colours,
 * different room.
 *
 * # Nothing here carries meaning in colour alone
 *
 * Every state on these pages is a word first — the platform rows say "Windows"
 * and "Linux", the cards are headed in text, the warnings are prose with a
 * heading. Colour only ever repeats what a word already said.
 */

/* # Light, and only light — which is a departure from the local UI
 *
 * The daemon's page follows prefers-color-scheme because it is a tool that sits
 * open on a desktop: it should look like the rest of that desktop. This is the
 * page someone meets before they have anything installed, and it was rendering
 * near-black on a machine set to dark — which is what the first screenshot of it
 * showed (2026-08-07). 대장님 asked for bright, and a front page that changes its
 * character with a system setting is a front page with two first impressions.
 *
 * So color-scheme is pinned. It has to be declared, not merely left out: without
 * it the browser still styles form controls dark under a dark system setting, and
 * the sign-in card would come back half inverted.
 *
 * The hues are the local UI's, warmed. Same blue, same ink, same greens and reds
 * — a background at #f7f6f3 rather than #fbfbfd, because a faintly warm paper
 * reads as brighter than a faintly blue one at the same lightness. */
:root {
  color-scheme: light;
  --bg: #f7f6f3; --fg: #1c1c1e; --muted: #64646a;
  --line: #e4e2dd; --card: #ffffff; --sunken: #f0eeea;
  --bad: #b3261e; --warn: #8a5a00; --ok: #1a6b3c;
  --accent: #2f5bd7; --relay: #6366f1;

  /* The call-to-action is ink, not brand blue. The blue is load-bearing inside
     the product (it marks the thing you can press in a menu full of things you
     cannot); on a page where almost everything is pressable it would stop
     meaning anything.
   *
   * Lightened from #0f172a on 2026-08-16. 대장님 read the button and the band
   * as "the dark mode background", which is the one thing a page pinned light
   * must not look like — near-black on warm paper reads as an inverted panel
   * rather than as a filled control. Four steps of a ladder were built and
   * looked at side by side; this is the second, chosen for being clearly out
   * of black while the band above it stays the darkest thing on the page.
   *
   * ⚠ This fill is also what says a platform tile is the chosen one on the
   * download page (D370 ⑷ — the word "Chosen" was taken out, so the fill is
   * the only channel left saying it). White on it is 10.35:1 here, and the
   * ladder's lightest step was 7.58:1; below that the tile stops announcing
   * itself and the check in tools/uicolor is what would catch it. */
  --ink: #334155; --ink-fg: #ffffff;

  /* The one tinted panel in the strip below, which is the brand mark's own
     indigo rather than a new colour. It marks where to start reading; it carries
     no meaning the heading beside it does not already carry. */
  --feature: #eef0ff; --feature-line: #c9cffb;

  /* The announcement band. Dark on a page that is pinned light, and that is not
     a contradiction: the band is chrome, not the page. It reads as an edge above
     the product rather than as the product changing character — which is the
     thing §0's "one first impression" was protecting.
     --band-on is the band's own foreground for the link you are standing on,
     white rather than a brighter grey so the difference is luminance and not a
     hue nobody can rely on seeing.

     Lightened from #14161a on 2026-08-16 alongside --ink, and for the same
     reason: chrome is meant to read as an edge, and at near-black it read as a
     dark theme leaking in. It is still the darkest surface here — that is the
     part that has to survive any further lightening, because it is what makes
     the row an edge rather than a second page.

     ⚠ --band-fg came up with it (#a8b0ba → #b6bdc6). It has to: the text sits
     on this background and nothing else, so raising the background alone spends
     the contrast rather than the darkness. 6.71:1, against the 4.5 the sentence
     needs at 14px. */
  --band: #2e333a; --band-fg: #b6bdc6; --band-on: #ffffff;
}

* { box-sizing: border-box; }

body {
  margin: 0; min-height: 100vh;
  background: var(--bg); color: var(--fg);
  font: 16px/1.6 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
  -webkit-font-smoothing: antialiased;
}

code, .mono { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; }
a { color: inherit; }

/* ── frame ────────────────────────────────────────────────────────────── */

.wrap { max-width: 1200px; margin: 0 auto; padding: 0 28px 110px; }

/* The gutter is 28 because 1200 of page needs a margin to sit in. A phone has
 * no page to speak of: at 360 those two gutters are 56px, a sixth of the
 * window, spent on nothing. 20 gives the words back 16px, which is what buys
 * the headline its third line below, and it is still wide enough that a line of
 * text does not run into the edge of the glass.
 *
 * The foot padding comes down with it. 110px under the last thing on the page
 * is air for a scroll wheel that has overshot; a thumb stops where it stops. */
@media (max-width: 420px) {
  .wrap { padding: 0 20px 72px; }
}

/* The reading measure, inside a page that is wider than it.
 *
 * Every page is 1200 now, which is the width of the chrome and of the front
 * page's card grid — not the width of a paragraph. At 16px a 1200 column is
 * about 143 characters to the line and prose stops being readable somewhere
 * around 75. So the page is wide and the words inside it are not, which is what
 * the front page already did in three separate inline rules before this class
 * existed. */
.column { max-width: 780px; margin-left: auto; margin-right: auto; }

/* ── the two bands ────────────────────────────────────────────────────── */

/* Both bands span the window and their contents do not, which is what makes a
   bar rather than a strip. .inner and .bar therefore carry the same measure as
   .wrap, and the elements sit outside it in the markup.
 *
 * ⚠ That measure was 780 on three of the four pages until 2026-08-16, so the
 * logo moved sideways when you walked from /download/ to /login/. One number
 * now, and it is .wrap's. */

/* Band 1 — the announcement and the standing links. */
.topbar { background: var(--band); color: var(--band-fg); }
.topbar .inner {
  display: flex; align-items: center; gap: 24px;
  max-width: 1200px; margin: 0 auto; padding: 15.8px 28px;
  font-size: 13px;
}
/* The announcement is the thing that gives way when there is no room: the links
   are the way out of the page and the sentence is news about it.
 *
 * ⚠ Not `.notice` — that class is the warning card further down this file, and
 * for one render this span wore its sunken background and its warn stripe. A
 * name reused across two unrelated things in one stylesheet is a collision
 * waiting for whichever one is written second.
 *
 * Sentence case while the links beside it are uppercase. That reason survived a
 * day of the name being spelled PINMESH and then being spelled Pinmesh again,
 * because it never depended on the name: news and navigation set differently is
 * what stops the band reading as one long menu. The sentence names the product
 * and the links name pages, and one of those is prose. */
/* A point larger than the links it sits beside, which is what keeps the two
   looking the same size. 대장님 read the sentence as the smaller of the two and
   asked whether the case was doing it; it was. Type set in capitals shows its
   full cap-height on every letter, while lowercase shows mostly x-height —
   around half the em in this face — so at one font-size the capitals win by
   sight. Setting the shouted half a step down is the usual answer; here the
   links are the size 대장님 approved, so the sentence comes up instead.
 *
 * ⚠ Paired with .inner's padding: the taller line was given back by taking
 * 0.7px off the top and bottom, so the band stays at the 54px it was set to. */
.topbar .msg {
  flex: 1 1 auto; min-width: 0;
  font-size: 14px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ⚠ …and giving way used to mean the end of the sentence was simply gone. One
   line, clipped, no tooltip and no wrap: on a window narrower than the sentence
   there was no way left to read it. That cost nothing while the line was four
   words; it is a promise now (deploy/www/_chrome.html), and a promise the reader
   cannot finish is worse than no line.
 *
 * So below the width where it stops fitting, the band stacks instead: the
 * sentence takes a row of its own and wraps, the links sit under it. The band
 * grows a line on narrow screens, which is the trade, and nothing is hidden.
 * The breakpoint is where the sentence and the three links stop sharing a row —
 * re-measure it if either gets longer. */
@media (max-width: 900px) {
  .topbar .inner { flex-wrap: wrap; gap: 10px 24px; }
  .topbar .msg {
    flex-basis: 100%;
    overflow: visible; text-overflow: clip; white-space: normal;
  }
}
.topbar nav {
  flex: none; display: flex; gap: 22px;
  letter-spacing: .07em; text-transform: uppercase;
}
.topbar a { color: var(--band-fg); text-decoration: none; }
.topbar a:hover { color: var(--band-on); }

/* A link set at 13px is 21px tall, which is a mouse's target and not a thumb's.
 * The usual floor for a finger is around 44px, and these three are the way out
 * of the page — on a phone DOWNLOAD is where the visitor was going.
 *
 * Padding rather than a larger type size: the row is news and navigation set
 * small on purpose (see .msg above), and growing the letters to grow the target
 * would restate the band. The gap comes in a little to pay for the taller rows
 * without the three links wrapping. */
@media (max-width: 420px) {
  .topbar nav { gap: 18px; }
  .topbar a { display: inline-block; padding: 11px 0; }
  .topbar .inner { padding-top: 12px; padding-bottom: 4px; }
}

/* Which page you are on. Set from the body class because the band is one shared
   fragment and cannot know (deploy/www/_chrome.html). Underlined as well as
   lightened — the state is not carried by colour alone.

   ⚠ `$=` and not `=`. There are two bands: the phone's links at /m/download/
   and /m/docs/, the desktop's at /download/ and /docs/, and both end in the
   same path. Written as an exact match this rule quietly stopped firing on the
   phone site the day /m/download/ was built — the page said DOWNLOAD in the
   band and the band did not say you were on it. Nothing could see that: the
   stylesheet is shared, so the desktop kept passing every check there is. */
body.p-download .topbar a[href$="/download/"],
body.p-docs     .topbar a[href$="/docs/"] {
  color: var(--band-on); text-decoration: underline; text-underline-offset: 3px;
}

/* Band 2 — the brand, the (empty) menu, and the two doors. */
header.site { background: var(--card); border-bottom: 1px solid var(--line); }
header.site .bar {
  display: flex; align-items: center; gap: 24px;
  max-width: 1200px; margin: 0 auto; padding: 17.5px 28px;
}
header.site .brand {
  display: flex; align-items: center; gap: 11px;
  color: var(--fg); text-decoration: none;
}
header.site .mark { width: 32px; height: 32px; flex: none; }
/* The wordmark is drawn in capitals here and nowhere else.
 *
 * The name itself is "Pinmesh" (internal/config/config.go), which is what the
 * installer, the tray and every sentence in the product say. 대장님 tried the
 * name in capitals for an afternoon and took it back out — it reads as force in
 * a logo and as noise inside a sentence — and this is the half that wanted the
 * force. So the capitals live in the stylesheet: a drawing decision about one
 * element, not a second spelling of the name.
 *
 * ⚠ text-transform, not a second copy of the word in the markup. The fragment
 * carries the token like every other page (deploy/www/_chrome.html), so the day
 * the name changes it changes here too, and nothing has to remember that the
 * header holds its own copy.
 *
 * Positive tracking follows the capitals and not the name: caps have no
 * descenders or x-height to interlock, so the negative tracking a title-case
 * wordmark wants would close their counters and turn the word into a block. */
header.site .name {
  font-weight: 650; font-size: 20px;
  text-transform: uppercase; letter-spacing: 0.02em;
}

/* …and on a phone the wordmark is the half that goes.
 *
 * Measured rather than guessed (2026-08-22): the second band stops fitting at a
 * viewport of 397px. At 405 it fits, at 395 it does not, and below that the row
 * does not shrink at all — it simply keeps its 397 and the window scrolls
 * sideways. What hangs off the right edge there is the Sign up button, cut in
 * half, on all six pages. A phone is 360–412 wide, so this was every phone.
 *
 * Three ways out were built and looked at side by side; 대장님 chose this one.
 * The other two were tightening the padding and the type until the row fits
 * (7px to spare at 360, so an iPhone SE breaks it again, and so does any one of
 * these five words getting longer) and wrapping the bar onto two rows (nothing
 * lost, but the header goes 76px → 111px, and the band above it is already two
 * lines on a phone — the top 187px of the first screen would all be furniture).
 *
 * # The name does not leave the page
 *
 * Which is what makes this the cheap one. The band directly above says "Pinmesh
 * is coming soon — …" (deploy/www/_chrome.html), so the product is still named
 * on screen, in prose, one row up. The mark stays and it is still the link
 * home. What is dropped is the *second* place the name appears, in the row
 * where the space is owed to a door.
 *
 * # Why the breakpoint is 420 and not 400
 *
 * The measured edge is 397 and this is 23px above it, which costs the 412-wide
 * phones their wordmark. That is deliberate, because the two mistakes are not
 * the same size: folding early costs a word that is legible one row up, and
 * folding late costs a button that is cut in half. 397 is also today's number —
 * it is five words long, and any of them growing moves it. So the margin is the
 * room for that, not caution about the measurement.
 *
 * ⚠ Re-measure if the words change. The check is the page's scrollWidth against
 * the viewport at 360 and at 412; the row is the only thing on these pages that
 * has ever exceeded it. */
@media (max-width: 420px) {
  header.site .name { display: none; }
}
/* Empty today. It is what pushes .end to the right edge, and it is where the
   first real menu goes. */
header.site nav.main { flex: 1 1 auto; display: flex; justify-content: center; gap: 26px; }
header.site nav.end { flex: none; display: flex; align-items: center; gap: 20px; font-size: 14.5px; }
header.site nav.end a { color: var(--muted); text-decoration: none; }
header.site nav.end a:hover { color: var(--fg); }
header.site nav.end .btn { padding: 8px 16px; font-size: 14px; color: var(--ink-fg); }
header.site nav.end .btn:hover { color: var(--ink-fg); }

/* Same floor as the band above, for the same reason and on the two doors that
 * matter most. Console is a bare link at 23px and Sign up a button at 38px;
 * both come up to a thumb's size, and the mark — the only thing left of the
 * brand once the wordmark goes — comes up with them, because a 32px square is
 * the whole target for the link home.
 *
 * The bar's own padding comes down by more than the links grew, so the header
 * gets *shorter*: 76px → 63px, measured. That is the trade taken deliberately —
 * on a phone the two bands are already 130px of furniture before a word of the
 * page, and the row is now taller inside and shorter outside. */
@media (max-width: 420px) {
  header.site .bar { padding: 8px 20px; }
  header.site .brand { padding: 6px 0; }
  header.site nav.end a { display: inline-block; padding: 10px 0; }
  header.site nav.end .btn { padding: 11px 16px; }
}

/* The foot lines up with whatever the page put above it — 1200 on the front
   page, the reading column on the pages that use one. It is the page's foot and
   not the window's, unlike the two bands, which are full width because they are
   chrome. A footer flush to the window under a column of text indented 200px
   reads as belonging to some other page. */
footer.site {
  margin-top: 72px; padding-top: 20px; border-top: 1px solid var(--line);
  color: var(--muted); font-size: 13px;
  display: flex; flex-wrap: wrap; gap: 6px 20px;
}
footer.site a { text-decoration: none; }
footer.site a:hover { color: var(--fg); }

/* And the same on a phone. These are 21px rows sitting 20px apart, which on a
   thumb is three targets inside one contact patch. The row gap opens with the
   padding so the taller rows do not touch when the foot wraps to two lines. */
@media (max-width: 420px) {
  footer.site { gap: 2px 22px; }
  footer.site a { display: inline-block; padding: 11px 0; }
}

/* ── hero ─────────────────────────────────────────────────────────────── */

/* Centred, and the headline is allowed the full column.
 *
 * ⚠ It was `max-width: 22ch` here for one round and that shipped a headline
 * broken one word per line. ch is the width of a "0" in *this* element's font,
 * and the hero section inherits the 16px body size — so 22ch was about 180px
 * while the h1 inside it was set at 56px. A width limit on a block whose child
 * is much larger text is a limit in units of the wrong font. Where a measure is
 * wanted it is on the paragraph, in the paragraph's own size. */
/* ⚠ These numbers are a budget, not a taste. 대장님 asked for the four blocks to
   be readable without scrolling, and the target that was measured against is a
   1440x780 viewport — his laptop at 200% scale, minus browser chrome.
 *
 * What fits there, measured in the browser rather than reasoned about:
 *
 *	.topbar        0 -  54
 *	header.site   54 - 130
 *	h1           178 - 301      two lines at 52px
 *	.lede        321 - 405
 *	.cta         431 - 479
 *	.blocks      539 - 731      ← 49px to spare
 *
 * It arrived there by giving up four things at once, all of them 대장님's call:
 * the headline dropped 60px to 52px and then lost a whole line, the "What it is
 * for" heading was removed outright (the tailscale page he pointed at has no
 * heading over its block row either), and the four blocks' bodies were cut from
 * a paragraph plus an example to one sentence each.
 *
 * ★ The last of those is why the padding here reads 48 and not 24. Losing the
 * headline's third line paid 61px, and that was spent on air rather than
 * banked: the hero's padding went back up 84→24→48 and the section gap 90→34→56
 * within one round. Space bought by cutting words is worth more as space than
 * as a shorter page.
 *
 * ★ The fifth block was added after this budget was written and the screen got
 * *roomier*, which is worth understanding before the next one. Five columns in
 * 1200px are 216px wide against four columns' 285, and the first attempt
 * measured 20px over the fold — not because of the new block but because the
 * other four grew: at 216px two headings wrapped to a second line and two
 * bodies to a fourth, and a grid row is as tall as its tallest cell. Cutting
 * every heading to one line and every body to three took the row from 261px to
 * 192px. The cost of an item is paid by all of them.
 *
 * ⚠ There is 49px of slack and no more. Anything that adds a line — a sixth
 * block, a longer heading, a second CTA — pushes the row under the fold again,
 * and nothing in the page will say so. Re-measure rather than
 * eyeball it: copy bin/www/index.html, append a script that prints
 * getBoundingClientRect for these elements, and load it in headless Chromium at
 * 1440x780. Eyeballing a screenshot was 6px wrong the first time. */
.hero { padding: 48px 0 4px; text-align: center; }
.hero h1 {
  margin: 0 auto; max-width: 17em;
  font-size: clamp(34px, 4.8vw, 52px);
  /* 1.06 was set when this was one sentence that happened to wrap, and at that
     job it was right: tight leading is what keeps a wrapped headline reading as
     one block rather than as a list. It is now three lines the writer chose,
     and lines that are meant to be read as separate need air between them —
     대장님 read the first two as stuck together. Loosened for that. */
  line-height: 1.18; letter-spacing: -0.035em; font-weight: 660;
}

/* …and 34px is too big for a phone, which is not a matter of taste — it is the
 * rule below this one being broken. Each headline line is a break the writer
 * chose, and at 34px in a 360-wide window the first of them wraps: "The easiest
 * way to" becomes two lines, and a headline written as three is read as four
 * with one of them orphaned. The browser is undoing the writer's break, which
 * is the exact thing the comment under here forbids.
 *
 * 30 is where it stops. Measured across pad × size at 345 (2026-08-22): at
 * 20px of gutter, 32 still wraps and 30 does not. The headline block goes
 * 160px → 106px and the page 3000 → 2871, but the size was chosen by the wrap
 * and the shorter page is change from it, not the reason.
 *
 * ⚠ It is a plain size and not a smaller clamp, because clamp's floor governs
 * everything below ~708px and this is only true of the narrow end. Re-measure
 * if the headline's words change: the longest line is what sets this. */
@media (max-width: 420px) {
  .hero h1 {
    font-size: 30px;
    /* At 30px the second half — "Private and Secure Access", one run of text
       rather than lines the writer set — breaks as 303px, then "Access" alone
       on 109. A headline that ends in one orphaned word reads as having run out
       rather than as having finished. balance re-cuts it to 184 / 228 at the
       same height, which is "Private and / Secure Access".
     *
     * ⚠ Scoped to the phone deliberately, though it changes nothing at 1440
       (measured: 516 / 729 either way). The hero above is budgeted to the pixel
       for a 1440x780 fold, and a property that re-cuts headlines is not
       something to switch on globally without re-measuring that budget. */
    text-wrap: balance;
  }
  .hero { padding-top: 36px; }
}
/* Each headline line is its own block, because a break the writer chose is not
   a break the browser may undo. Left to wrap on its own the words would re-flow
   at every window width, and the last line — the one that names the product —
   would stop being a line at all.
 *
 * ⚠ There was a third line and a `.up` class for the middle one until
 * 2026-08-16. Both are gone; if a headline ever needs three again, this is one
 * rule, not a pattern to rebuild from scratch. */
/* The quiet line, dimmed rather than shrunk or spaced. It is a half of one
   thought and not a subtitle, so it keeps the headline's size and weight and
   gives up only colour.
 *
 * ⚠ Named for what it does and not for where it sits: it was `.then` and it was
 * the last line, and then it became the first (deploy/www/index.html.template).
 * A class named after a position is a class that lies the first time the writer
 * reorders the lines.
 *
 * ⚠ It carried margin-top: .28em until 대장님 saw the three lines together and
 * asked for one spacing. The gap was written when the headline was two lines
 * and had to be stopped from reading as one run-on sentence; at three lines it
 * had become the odd one out, and an extra gap on exactly one of three evenly
 * set lines reads as a mistake rather than as a turn. Colour already marks the
 * turn, and it marks it without touching the rhythm. */
.hero h1 .quiet { display: block; color: var(--muted); }
.hero .lede {
  max-width: 60ch; margin: 20px auto 0;
  font-size: 17.5px; color: var(--muted);
}
.hero .cta {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 12px; margin-top: 26px;
}

.btn {
  display: inline-block; padding: 11px 22px; border-radius: 8px;
  font-size: 15px; font-weight: 600; text-decoration: none;
  background: var(--ink); color: var(--ink-fg); border: 1px solid var(--ink);
}
.btn:hover { opacity: .88; }
.btn.ghost { background: transparent; color: var(--fg); border-color: var(--line); }
.btn.ghost:hover { border-color: var(--muted); opacity: 1; }

/* ── section headings ─────────────────────────────────────────────────── */

.section { margin-top: 56px; }
.section > h2 {
  margin: 0 0 7px; font-size: 12.5px; font-weight: 650;
  letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
}
.section > .say {
  margin: 0 0 30px; font-size: 25px; font-weight: 620;
  letter-spacing: -0.025em; max-width: 34ch;
}
.section.mid, .section.mid > .say { text-align: center; }
.section.mid > .say { max-width: none; }

/* A heading with no second line under it, so it takes that line's size.
 *
 * The pattern above is a small uppercase label and a big sentence beneath it —
 * two lines doing two jobs. When the sentence goes (deploy/www/index.html.template
 * says why it went), the label cannot stay 12.5px: it would be the only thing
 * introducing a section and it would be the smallest type on the page.
 *
 * It matches the .say line's size and weight rather than inventing a third, so
 * every section on the page is introduced at one size whether it uses one line
 * or two. The uppercase and the tracking go with the label job — this is the
 * section's own name now, not a category stamped above it. */
.section > h2.solo {
  margin: 0 0 24px;
  font-size: 25px; font-weight: 620; letter-spacing: -0.025em;
  text-transform: none; color: var(--fg);
}

h1.page { font-size: 34px; letter-spacing: -0.02em; margin: 48px 0 10px; }
.lede { color: var(--muted); margin: 0 0 32px; max-width: 60ch; }

/* ── the block grid ───────────────────────────────────────────────────── */

/* The things this product is for, as one strip across the page.
 *
 * ⚠ auto-fit was wrong here: at this page width it makes three columns and
 * leaves the rest alone on a second row, which reads as "and one more" rather
 * than as a set. The count is stated instead, and a media query steps it down —
 * so adding an item means changing the number here, which is a deliberate edit
 * rather than a surprise at one particular window width.
 *
 * It was 4 until 2026-08-16 and is 5 now (대장님 added IoT access, partly to
 * prove the row is not a fixed four). ⚠ Changing this number is half the edit:
 * the columns get narrower, the text in *every* block rewraps, and the row can
 * grow past the fold. The budget above says how to re-measure. */
.blocks {
  display: grid; gap: 16px;
  grid-template-columns: repeat(5, 1fr);
}
@media (max-width: 980px) { .blocks { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .blocks { grid-template-columns: 1fr; } }

/* ⚠ text-align is restated here because .section.mid centres its subtree, and a
   centred paragraph of five lines is markedly harder to read than a ragged-right
   one — the centring is meant for the two heading lines above, not for prose.
   Setting it on the section and undoing it here keeps that intent in one place
   each rather than tagging every heading. */
.block {
  display: flex; flex-direction: column;
  padding: 18px 18px; border-radius: 14px;
  background: transparent; border: 1px solid transparent;
  text-align: left;
}
/* One panel is tinted, to say where the row starts. It is decoration and not a
   category: everything that distinguishes these four is in their headings. */
.block.lead { background: var(--feature); border-color: var(--feature-line); }

.block .icon { width: 24px; height: 24px; margin-bottom: 10px; color: var(--relay); }
.block h3 { margin: 0 0 7px; font-size: 16px; letter-spacing: -0.015em; }
.block p { margin: 0; color: var(--muted); font-size: 14.5px; line-height: 1.5; }
.block p .mono { color: var(--fg); }

/* ⚠ There was a second paragraph in each block, `.eg`, pinned to the bottom
   with margin-top:auto — the concrete line under the abstract one, and the
   comment here called it "the line that stops these from being four ways of
   saying networking". It was right about which line mattered and wrong about
   which one to keep: on 2026-08-16 the abstract paragraph went instead and the
   concrete sentence became the whole body.
 *
 * Two things went with it. The blocks were ~400px tall and the front page did
 * not show one pixel of them above the fold. And the pinned-to-bottom rule put
 * a hole in every block shorter than the tallest, because the row's height is
 * the longest paragraph's — visible in the 1440 screenshot as a gap under the
 * middle two.
 *
 * ⚠ The block is still a flex column, which now costs nothing and buys the next
 * item that needs to sit at the bottom. If nothing ever does, it can go. */

/* ── command blocks ───────────────────────────────────────────────────── */

/* The commands a reader is meant to copy, drawn as one object. They were bare
   text on paper for one round, which left a twelve-line apt recipe looking like
   prose that happened to be monospaced — nothing said where the thing to select
   started and stopped. Scrollable rather than wrapped: a line break inserted
   into a shell command by a stylesheet is a line break somebody pastes. */
pre.mono {
  margin: 0 0 18px; padding: 16px 18px; border-radius: 12px;
  background: var(--sunken); border: 1px solid var(--line);
  font-size: 13px; line-height: 1.75;
  overflow-x: auto;
}
pre.mono code { font-size: inherit; }

/* ── the platform picker ─────────────────────────────────── */

/* A row of tiles, one per platform, and only the chosen one's instructions
 * below it. The page used to stack every platform at once, which meant a person
 * on Linux scrolled through four screens of Windows warnings to reach one line
 * that concerned them.
 *
 * # Radio inputs, not script
 *
 * The switching is a checked radio and sibling selectors, so the page works with
 * script disabled and keyboard focus moves through the tiles as a radio group
 * for free. Script does one thing only: tick the radio that matches the visitor
 * before first paint. If it does not run, Windows stays chosen and every tile is
 * still one click away — a worse guess, not a broken page.
 *
 * # Nothing here is chosen by colour alone
 *
 * The selected tile is filled *and* carries a check mark *and* the panel under
 * it is headed with the platform's name. The unbuilt ones are dimmed *and*
 * dashed *and* say "Not yet" in words. 대장님 cannot verify a colour difference
 * by eye (UI.md §12), so each state is carried by at least two of fill, shape
 * and word. ⚠ tools/uicolor does not read this file — it checks the daemon's
 * page.html and the Android resources — so this rule is held by review here. */
.tiles {
  display: grid; gap: 12px; margin: 40px 0 38px;
  grid-template-columns: repeat(5, 1fr);
}

/* # Once the row folds, the order starts meaning something
 *
 * Five tiles in one row have no first: the eye takes the row in at once and
 * lands on the filled one. Folded to two columns on a phone, they become a
 * list, and a list has a top — which on 2026-08-22 was macOS (Not yet) and iOS
 * (Not yet). A person on a phone had to walk past both platforms we cannot
 * give them to reach the one we can, and theirs was already chosen and already
 * scrolled past by the time they saw it.
 *
 * So below the fold width the unbuilt two go to the end. Nothing else moves,
 * and on a wide screen the order is untouched.
 *
 * ⚠ order only moves what is drawn, not what is in the document — which is
 * exactly right here and would not be if these were focusable. The two dimmed
 * tiles are divs; the three real ones are labels in a radio group, and they
 * keep both their document order and their tab order. */
@media (max-width: 640px) {
  .tiles .tile.off { order: 1; }
}
@media (max-width: 640px) { .tiles { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 400px) { .tiles { grid-template-columns: repeat(2, 1fr); } }

/* Off screen rather than display:none, which would take the radios out of the
   focus order and with them the keyboard path to the whole picker. */
.pick { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

.tile {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 20px 8px 16px; border-radius: 14px;
  background: var(--card); border: 1px solid var(--line);
  font-size: 14.5px; font-weight: 600; text-align: center;
  cursor: pointer; user-select: none;
}
.tile:hover { border-color: var(--muted); }
.tile .icon { width: 30px; height: 30px; color: var(--fg); }
/* Only the unbuilt tiles carry a word now — the chosen one is told by its fill,
   which is a luminance difference and therefore not the "colour alone" §12
   forbids. It was `✓ Chosen`, and 대장님 took it out: a mark that repeats what a
   filled box already says is noise on five tiles to explain one.
 *
 * The three real tiles keep an empty one so the row's baselines stay level. */
.tile .state { font-size: 11.5px; font-weight: 600; color: transparent; letter-spacing: .02em; }
.tile .state:empty::before { content: "\200b"; }

/* Not built. Dashed and dimmed and labelled — and not a label, so there is
   nothing to click and nothing to focus. */
.tile.off {
  background: transparent; border-style: dashed; color: var(--muted);
  cursor: default;
}
.tile.off:hover { border-color: var(--line); }
.tile.off .icon { color: var(--muted); opacity: .65; }
.tile.off .state { color: var(--muted); }

/* ── the panes ────────────────────────────────────────────────────────── */

.pane { display: none; }

/* One pair of rules per platform rather than a generated selector: three
   platforms, spelled, so a mistyped id fails visibly at review instead of
   quietly selecting nothing. */
#os-windows:checked ~ .tiles label[for="os-windows"],
#os-linux:checked   ~ .tiles label[for="os-linux"],
#os-android:checked ~ .tiles label[for="os-android"] {
  background: var(--ink); border-color: var(--ink); color: var(--ink-fg);
}
#os-windows:checked ~ .tiles label[for="os-windows"] .icon,
#os-linux:checked   ~ .tiles label[for="os-linux"]   .icon,
#os-android:checked ~ .tiles label[for="os-android"] .icon { color: var(--ink-fg); }

#os-windows:checked ~ .panes #pane-windows,
#os-linux:checked   ~ .panes #pane-linux,
#os-android:checked ~ .panes #pane-android { display: block; }

/* The keyboard ring has to land on the tile, because the input it belongs to is
   parked off screen where nobody can see it. */
#os-windows:focus-visible ~ .tiles label[for="os-windows"],
#os-linux:focus-visible   ~ .tiles label[for="os-linux"],
#os-android:focus-visible ~ .tiles label[for="os-android"] {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* The big button under the tiles, and the line that says what it will fetch.
   Full width because it is what a person who has picked their platform has left
   to do. Windows and Android have one; Linux has three ways in and so has
   three, which is why the gap below is a rule and not a one-off. */
.grab {
  display: block; width: 100%; max-width: 520px; margin: 0 auto;
  text-align: center; padding: 16px 22px; font-size: 16.5px;
}
.req { margin: 14px 0 0; text-align: center; color: var(--muted); font-size: 14px; }
.req .mono { color: var(--fg); word-break: break-all; }

/* Stacked bars need air or they read as one control with several rows. The
   space is put above the next bar rather than below whatever ended the last
   one, so a bar carrying no lines is spaced like one that does. */
.grab + .grab { margin-top: 14px; }
.req + .grab, .hash + .grab { margin-top: 30px; }

/* A command block you can take with you. The button is not in the markup — the
   script below the page adds it — so a browser that runs no script shows the
   commands exactly as this page always showed them, to be selected by hand.
   That is the same rule the tile radios follow: script improves the page, it is
   never what makes it work.

   ⚠ "Copied" is a word and not a colour. The button has to say what happened
   for a reader who cannot tell the two fills apart (UI.md §12). */
.cmd { position: relative; }
.cmd .copy {
  position: absolute; top: 8px; right: 8px;
  padding: 7px; border-radius: 7px; cursor: pointer; line-height: 0;
  background: var(--sunken); color: var(--fg); border: 1px solid var(--line);
}
.cmd .copy svg { display: block; width: 16px; height: 16px; }
.cmd .copy:hover { border-color: var(--muted); }
.cmd .copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cmd .copy.done { background: var(--ink); color: var(--ink-fg); border-color: var(--ink); }

/* The block has to keep its hands off the button's corner, or a long command
   runs underneath it.

   ⚠ And it has to wrap rather than scroll. A command that runs off the right
   edge is a command the reader cannot check before pasting it into a root
   shell — measured 2026-08-21, where the first line ended at "…/apt/se" with
   the rest of the URL out of sight. Wrapping here changes nothing about what
   the Copy button hands over: this is a CSS line break, not a character.

   White rather than sunken, because this block stands next to a download bar
   and has to be told apart from it at a glance — the bar is a filled surface
   you click, this is a page-white field you copy out of. */
.cmd pre.mono {
  padding-right: 50px;
  white-space: pre-wrap; overflow-wrap: anywhere;
  background: var(--card);
}

/* A command standing where a download bar stands, and told apart from it by
   fill, typeface and wording rather than by colour (see the Linux pane).

   It sizes to the command instead of filling the column: a short line in a wide
   box reads as a box with something in it, where the same line in a box its own
   size reads as the line. The button sits beside the field rather than on top
   of it, because a button inside would have to be reserved for in padding on
   every command block, short ones included — and that reserved strip is exactly
   the width this is trying not to have. max-width keeps a long command inside
   the column; past that the field wraps rather than pushing the page sideways. */
.cmd.bar { width: fit-content; max-width: 100%; margin: 14px auto 0; }
.cmd.bar pre.mono { margin: 0; padding: 14px 50px 14px 18px; font-size: 13px; }

/* A command too long for the column is cut with an ellipsis rather than wrapped
   (decided 2026-08-21). Two reasons it is done here and not in the markup:
   the field stays one line high beside its neighbour, and the text that the
   Copy button reads is still the whole command — textContent does not know that
   CSS stopped drawing at the edge. So the visible string and the copied string
   are never two different strings; one is a window onto the other.

   ⚠ This is the opposite ruling to the wrapping one above, and the difference
   is the ellipsis. A line that just ends is a line the reader thinks they have
   read. A line that ends in "…" says what it is doing. */
.cmd.bar pre.mono {
  white-space: pre; overflow: hidden; text-overflow: ellipsis;
}
.grab + .cmd.bar { margin-top: 14px; }
.cmd.bar + .req { margin-top: 12px; }

/* The gaps say which field belongs to the bar above it. .tight is the same
   download in the form a terminal takes and sits closer to the bar than the
   next field would; anything after it is a different act and stands apart from
   both. Proximity does the work a caption would otherwise have to do in words.

   ⚠ 9px at first, and it read as one control that had sprouted a second row
   rather than as two ways to ask for one file. Related is not the same as
   joined — the gap has to be small enough to group and large enough that the
   two are still two things. */
.grab + .cmd.bar.tight { margin-top: 18px; }
.cmd.bar + .cmd.bar { margin-top: 30px; }

/* The way in for the Linux nobody else on this page is talking to. Quieter than
   the two above it, but a link and not a footnote. */
.req.alt { margin-top: 16px; }

/* Long, monospace, allowed to wrap anywhere: on a phone it otherwise pushes the
   page sideways. Centred under the button rather than tucked into a card, so
   the checksum sits beside the file it describes. */
.hash {
  margin: 8px 0 0; text-align: center;
  font-size: 12px; color: var(--muted); word-break: break-all;
}

/* The heading that repeats the tile's choice in words, so the selection is
   never carried by the fill alone. */
h2.chosen {
  margin: 40px 0 14px; font-size: 20px; letter-spacing: -0.015em;
}

/* ── plain cards (download, notices) ──────────────────────────────────── */

.card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: 14px; padding: 20px 22px;
}
.card + .card { margin-top: 12px; }

/* Cards get their top margin from the heading above them on every other pane.
   The Linux pane ends its offers with a line rather than a heading, so the card
   under it had nothing to stand off from and sat flush against the text. */
.req + .card { margin-top: 26px; }

/* ── download rows ────────────────────────────────────────────────────── */

.dl { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.dl .what { flex: 1 1 16em; min-width: 0; }
.dl .what strong { display: block; font-size: 16px; }
.dl .what .note { color: var(--muted); font-size: 14px; }
.dl .file {
  display: block; margin-top: 6px; font-size: 12px; color: var(--muted);
  word-break: break-all;
}
.dl .get { flex: none; }

/* Long, monospace, and allowed to wrap anywhere: on a phone it otherwise pushes
   the page sideways. */
.sum {
  flex-basis: 100%;
  margin-top: 4px; padding-top: 12px; border-top: 1px solid var(--line);
  font-size: 12px; color: var(--muted); word-break: break-all;
}

/* ── notices ──────────────────────────────────────────────────────────── */

/* What a tester has to know before the file lands: the SmartScreen prompt, the
   elevation prompt, where the first sign-in happens. Prose, not an icon — this
   is the text that stops a blue full-screen warning from reading as malware. */
.notice { border-left: 4px solid var(--warn); background: var(--sunken); }
.notice h3 { margin: 0 0 8px; font-size: 15px; }
.notice p { margin: 0 0 10px; font-size: 15px; }
.notice p:last-child { margin-bottom: 0; }
/* The facts that are not warnings.
 *
 * Each platform's pane used to end with two or three more cards — administrator
 * prompts, where sign-in happens, that updates do not arrive by themselves. They
 * are true and short and none of them is a *warning*, so when the warning cards
 * were cut to one (2026-08-22) these had no card left to be in.
 *
 * They are a paragraph rather than another card because a card is a thing the
 * reader is meant to stop at, and there is nothing here to stop at — it is what
 * you read once on the way to pressing the button. Set quieter and narrower than
 * the notice above it, so the eye takes the warning first.
 *
 * ⚠ It sits inside .panes, which carries the reading measure on the desktop
 * page; the margin is its own because it follows a card and not a heading. */
.also {
  margin: 16px 0 0; color: var(--muted);
  font-size: 14.5px; line-height: 1.65;
}
.also code { font-size: 13.5px; }

.notice .quote {
  display: inline-block; padding: 1px 7px; border-radius: 5px;
  background: var(--card); border: 1px solid var(--line);
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 12.5px;
}

/* ── forms (signup, login) ────────────────────────────────────────────── */

/* Deliberately the daemon's #gate card, down to the numbers: a person who signs
   up here meets the same card again when the daemon asks them to sign in. */
.gate { min-height: 68vh; display: flex; align-items: center; justify-content: center; padding: 32px 0; }
.gate form {
  width: 100%; max-width: 390px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 14px; padding: 30px;
}
.gate h1 { margin: 0 0 4px; font-size: 21px; letter-spacing: -0.01em; }
.gate .sub { margin: 0 0 24px; color: var(--muted); font-size: 14px; }
.gate label { display: block; margin-bottom: 14px; font-size: 13px; color: var(--muted); }
.gate input {
  display: block; width: 100%; margin-top: 5px; padding: 10px 12px;
  font: inherit; color: var(--fg);
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
}
.gate input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
.gate button {
  width: 100%; padding: 11px; margin-top: 4px;
  font: inherit; font-weight: 600; color: var(--ink-fg);
  background: var(--ink); border: 0; border-radius: 8px; cursor: pointer;
}
.gate button:disabled { opacity: .5; cursor: default; }
.gate .after { margin: 18px 0 0; font-size: 14px; color: var(--muted); text-align: center; }
.gate .after a { color: var(--accent); }

/* The form does not submit yet, and this says so where the button is rather
   than letting a press do nothing — an inert control with no explanation is the
   defect D109 spent a rebuild removing from the tray. */
.gate .pending {
  margin: 18px 0 0; padding: 12px 14px; border-radius: 8px;
  background: var(--sunken); border: 1px solid var(--line);
  font-size: 13px; color: var(--muted);
}

/* # The one place the daemon's numbers are not copied
 *
 * `font: inherit` on the input takes the label's 13px, which is the daemon's
 * card and is fine on a desktop. On a phone it is two defects at once. iOS
 * Safari zooms the whole page when a field under 16px takes focus — the page
 * jumps, the layout is suddenly wider than the window, and nothing scrolls it
 * back — and 13px is small to read a password being typed into.
 *
 * The copy is broken here rather than everywhere on purpose: the reason for
 * copying is that the same person meets the daemon's card minutes later on the
 * same machine (see the comment at the top of this section), and there is no
 * daemon on the phone. Nobody sees both of these.
 *
 * The label stays 13px. It is a label, it is not typed into, and it is what
 * makes the field the larger of the two.
 *
 * # And the card comes up the screen by the padding, which is all there was
 *
 * ⚠ This block first also set `min-height: 0`, on the reasoning that 68vh of
 * centring is what a phone cannot afford. Measured, that line moved nothing:
 * the sign-up form is 607px tall and 68vh of a 780 phone is 530, so the
 * min-height had never once applied on a phone. It is not in the rule below,
 * because a declaration that does nothing is a declaration the next person has
 * to disprove.
 *
 * What actually moved the card was the padding — 32 → 22 here and 30 → 22 on
 * the form, together worth 22px of screen and a wider field inside. The form
 * is taller than the window either way; this is the top of it arriving sooner,
 * not the whole of it fitting. */
@media (max-width: 420px) {
  .gate { padding: 22px 0 8px; }
  .gate form { padding: 22px; }
  .gate input { font-size: 16px; }
}
