/* The application's root stylesheet.
 *
 * It carries exactly what cannot be said from inside a view function, and
 * nothing else: eight `@font-face` blocks, the box-model reset, the
 * `font: inherit` on the four form-control selectors and the `cursor: pointer`
 * on `button`, then the root declarations and the `html` / `body` / `#app`
 * chain.
 *
 * NO COLOUR, deliberately. Colours have one source and it is `client/theme.ml`,
 * where a token names a role and the resolved mode decides the value, and
 * `just lint-brand` reconciles those values against `brand/color/tokens.json`
 * in both directions. A colour written here would be a copy outside that
 * reconciliation — the gate reads no colour out of this file — so it could
 * drift from the brand with nothing failing.
 *
 * What the gate does read here is the two font families. It strips CSS comments
 * first, so this prose cannot satisfy the check, and then requires each family
 * `brand.json`'s `.typography.families` names to appear as a quoted string in
 * what is left. That is a one-way presence check on two strings, not a
 * reconciliation of the stylesheet: everything else in this file — the eight
 * `@font-face` blocks and their `unicode-range`s, the `font-variant-numeric`
 * declaration of `docs/UPSTREAM_REGISTER.md` § N-2, the `box-sizing` reset of
 * § N-20, the `font: inherit` on `button` / `input` / `select` / `textarea`
 * and the `cursor: pointer` on `button`, `color-scheme`, the height chain — is
 * held by review rather than by machinery.
 *
 * NO `font-size` ON THE ROOT, deliberately. Feature 0006 Decision 3 leaves the
 * rem base at the browser's default so that the six steps of `Type_scale.Size`
 * are exact sixteenths and a reader who asked their browser for larger text
 * gets it. Measured on the served page: the root computes 16px untouched, and
 * `Size.hero` (2.25rem) lands as 36px.
 *
 * Served from the `dune_site` static path that already carries `index.html` and
 * `main.bc.js` — no new route (feature 0006 NFR-4). `client/dune`'s (install)
 * stanza, `lib/static_assets.ml` and the `<link>` in `index.html` are the three
 * pieces that have to agree on this filename; renaming it in one of them gives
 * a page that loads and silently looks unstyled, which is the failure this
 * whole task exists to make visible. Both halves were mutation-tested: dropping
 * the `<link>`, and dropping `app.css` from (install), each returns the served
 * page to its unstyled computed values.
 */

/* The eight vendored faces, one `@font-face` per file.
 *
 * IBM Plex Sans at 400/500/600 and IBM Plex Mono at 400 — the three weights
 * `Type_scale.Weight` names and no others, because BRAND.md § 4 permits 400,
 * 500 and 600 and says "Nothing else". Each family-weight appears twice, once
 * per subset: `latin` and `latin-ext`. That is not an optimisation. `₡`
 * (U+20A1) lives in `latin-ext` and nowhere else, so a latin-only build loses
 * the colón from every currency amount while still occupying the cell with a
 * fallback glyph — FR-7's silent failure. Measured on the files as vendored,
 * with the same `fontTools` reader `just lint-font-subset` uses:
 *
 *   no  U+20A1  ibm-plex-sans-latin-{400,500,600}-normal.woff2, mono-latin-400
 *   YES U+20A1  ibm-plex-sans-latin-ext-{400,500,600}-normal.woff2, mono-latin-ext-400
 *
 * Both families, because BRAND.md § 4 requires the colón in each: the
 * verification screen renders transcribed money in mono.
 *
 * The files are vendored under `client/fonts/`, not fetched at build time
 * (feature 0006 Decision 7) and not loaded from a CDN (NFR-3) — a third-party
 * request is a second failure domain for a page whose whole point is that it
 * renders, which is ADR 0002's argument pointing the other way. They are under
 * `client/` rather than `brand/` because `brand/` is in `.dockerignore`: a font
 * left there gives a local `just run` that works and a deployed page that 404s.
 * Verified out of the running container rather than off this tree: `docker exec
 * comalito ls /app/share/comalito/static/fonts` lists all nine, and each file
 * fetched from the container hashes equal to the one committed here.
 *
 * Where they came from — package, version and a `sha256` per file — is in
 * `client/fonts/PROVENANCE.txt`, committed beside them. Nothing in this tree can
 * rebuild a font, so that record is the only thing that makes a re-vendoring
 * auditable.
 *
 * `unicode-range` is copied verbatim from Fontsource 5.3.0's own `400.css`,
 * where it is what tells the browser which subset to fetch for the text on the
 * page. Every `latin` block here carries the same range and every `latin-ext`
 * block the same one; checked across all eight rather than assumed from one.
 * Dropping it would not break rendering — it would make the browser fetch both
 * subsets of a face whenever either was wanted, which is the fetch the split
 * exists to avoid. Reasoned from the property `unicode-range` has, not executed
 * here; what was executed is that the page as shipped requests exactly one
 * .woff2, the 600 `latin` one, and none of the other seven.
 *
 * `src` names `woff2` only, with no `woff` fallback, though Fontsource ships
 * both. A fallback URL is a second file to vendor, licence and gate, bought
 * against browsers that cannot read woff2 — which is a support-matrix claim
 * rather than a property of this tree, and is not executed anywhere here. It is
 * asserted on the ground that such a browser could not run a js_of_ocaml bundle
 * either, so it would have no application to render the font into.
 *
 * `font-display: swap` on all eight: text paints immediately in the fallback
 * and re-paints in IBM Plex when the file arrives. The alternative, `block`,
 * shows nothing for up to three seconds. For a reporting tool read in a kitchen
 * on whatever connection the restaurant has, a figure visible in the wrong face
 * beats no figure — and the reflow it costs is bounded, since the fallback
 * stack in `:root` below resolves to a system sans of similar metrics.
 *
 * Paths are absolute under `/static/`, for the same reason index.html's are:
 * this page is served at `/` and the assets from `/static`. But note the
 * difference — a URL in a stylesheet resolves against the *stylesheet's* URL,
 * not the document's, so a relative `fonts/x.woff2` here would in fact resolve
 * correctly to `/static/fonts/x.woff2`. Absolute anyway, so that every asset
 * URL in the project reads the same way and none of them depends on where the
 * file quoting it happens to live. */

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-sans-latin-400-normal.woff2")
    format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-sans-latin-ext-400-normal.woff2")
    format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-sans-latin-500-normal.woff2")
    format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-sans-latin-ext-500-normal.woff2")
    format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-sans-latin-600-normal.woff2")
    format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-sans-latin-ext-600-normal.woff2")
    format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Mono at 400 only. It marks machine-read text — invoice numbers, timestamps,
 * the verification screen — and nothing in that role is emphasised or headed,
 * so the other two weights would be two more files nothing names. */

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-latin-400-normal.woff2")
    format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-latin-ext-400-normal.woff2")
    format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* The box-model reset, and `docs/UPSTREAM_REGISTER.md` § N-20's own named fix.
 *
 * nopal's web backend emits a `Fill` width as `width: 100%` and emits padding as
 * its own declaration, and it emits no `box-sizing` anywhere — one hit for the
 * word in the whole pinned repository, in nopal's example harness rather than in
 * `backends/` or `lib/`. The CSS initial value is `content-box`, so the two add,
 * and every full-width box in this application overflows its parent by the sum
 * of its horizontal padding. Measured over the DevTools protocol at feature
 * 0009's browser oracle, at a 1280px viewport:
 * `document.documentElement.scrollWidth` read 1310 against an `innerWidth` of
 * 1280 — exactly `client/page.ml`'s 15px frame padding, on both sides of which
 * the rightmost 15px of every screen was clipped, the sign-out control's right
 * edge included. It has been true of every screen since feature 0007.
 *
 * It is here rather than in a view function for the reason everything else in
 * this file is: no view can name a universal selector, and a per-element
 * `box-sizing` is not a field `Nopal_style.Style.paint` or `.layout` carries.
 * That the example harness applies the same reset is what makes this the
 * host page's job by upstream's own convention rather than a workaround
 * invented here.
 *
 * The three pseudo-element selectors are part of the same rule and not
 * decoration: `::before` and `::after` do not inherit `box-sizing` from the
 * element that generates them, so a reset naming `*` alone leaves them on
 * `content-box`.
 *
 * NO `margin: 0` and NO `padding: 0` alongside it, though the harness's line
 * carries both. Every box in this application is laid out by nopal from a
 * `Style.layout`, which emits the padding it was told to; zeroing padding here
 * would be this file overriding view code rather than normalising the box model
 * underneath it. `body`'s margin is the one UA default that needs removing and
 * it is removed by name at the bottom of this file, where the reason for it is
 * written down. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* The control font, and the pointer cue. Feature 0014 Task 6a, and both are
 * here for exactly the reason the reset above is: a view function cannot say
 * either of them.
 *
 * `font: inherit` first, and it is a real defect rather than a nicety. The
 * family is declared once on `:root` below and inherited (feature 0006
 * Decision 4), but a form control does NOT inherit font — the user agent
 * stylesheet sets its own — so every button, input, select and textarea in this
 * application was rendering in the UA's own face. Measured over the DevTools
 * protocol at feature 0014 Task 6, on exactly what nopal emits for a navigation
 * tab, in the Chromium on this machine:
 *
 *   before  {"ff":"Arial","fs":"13.3333px"}
 *
 * so the navigation, every button and every form field on every screen has been
 * in Arial at 13.33px since feature 0007, against IBM Plex Sans at the step the
 * view asked for everywhere else on the same page. The `font` shorthand is what
 * covers it rather than `font-family` alone: the size, the weight and the line
 * height are all set by the UA sheet too, and `font: inherit` sets every
 * longhand the shorthand governs to `inherit`, `font-variant-numeric` included
 * — so the tabular figures the root asks for reach a control instead of being
 * reset to `normal` under it.
 *
 * It does not fight view code. nopal emits a per-element class rule
 * (backends/nopal_web/style_css.ml at the pinned 735d259c), and a class
 * selector outranks the type selectors here, so a control whose style names a
 * size or a weight still gets it and only the unstated properties inherit.
 *
 * `cursor: pointer` on `button` is now the WHOLE of the pointer affordance,
 * which it was not when it was written. Task 6a paired it with a text cue — a
 * `:hover` rule nopal emits from `Nopal_style.Interaction.hover`, raising the
 * label to the brand's heaviest weight — and Task 6c deleted that half: IBM
 * Plex Sans is a static family with one vendored face per weight, so the
 * heavier label was a wider label and the row slid under the pointer by 0.80px
 * to 3.31px depending on the word. `client/control_style.mli` § No hover
 * carries the four alternatives that were measured and why each was rejected.
 * The cue is here because it cannot be there: `Nopal_style.Cursor` exists at
 * the pin, but the only element that accepts one is `Element.canvas`'s
 * `?cursor` (element.ml:222, beside `~width ~height scene`) — `Element.button`
 * takes none. `button` alone and not the other three selectors above: a text
 * field's cursor is a caret and the UA already gives it one, and a `select`
 * already gets the UA's own.
 *
 * NO COLOUR AND NO WEIGHT HERE, which is the rule that decided WHERE the
 * deleted text cue had lived rather than whether it should exist: a
 * `font-weight` in this file would be as much an unreconciled brand copy as a
 * hex, since scripts/lint-brand.sh reads neither out of it. */
button,
input,
select,
textarea {
  font: inherit;
}

button {
  cursor: pointer;
}

:root {
  /* Upstream register N-2, which is `wontfix` there precisely because this is
   * app-shell configuration rather than a styling decision inside a component:
   * `Nopal_style.Text.t` has no `font_variant_numeric` field — checked against
   * the pinned source, whose record carries twelve fields and no numeric one —
   * and view code cannot emit a CSS string, so the only place this can be said
   * is here.
   *
   * On the root because the property inherits and every number in Comalito
   * wants column alignment — a report whose figures do not line up is harder to
   * scan, and there is no case in the product for proportional figures.
   * `lining-nums` alongside it because the two axes are independent and only
   * one of them is implied: asking for tabular fixes the advance width and
   * leaves the figure *style* to whatever the face defaults to. Naming both
   * pins the rendering to the app rather than to the font that happens to
   * load, which matters across a fallback stack whose members need not agree.
   * No claim is made here about which figures IBM Plex Sans ships; the point
   * is that this rule does not have to know. */
  font-variant-numeric: tabular-nums lining-nums;

  /* Feature 0006 Decision 4: the family is declared once here and inherited,
   * not set per element via `Text.font_family`. `Font.family_to_css_string`
   * quotes a `Custom` name (`Custom name -> "\"" ^ name ^ "\""`, read from the
   * pinned `nopal_style/font.ml`), so `Font.Custom "IBM Plex Sans, system-ui,
   * sans-serif"` emits one quoted family rather than a stack — a view can name
   * the webfont but cannot name what happens when it fails to load. Inheriting
   * from the root is a web-cascade property and not a portable one; that cost
   * is paid knowingly by a product with one backend.
   *
   * "IBM Plex Sans" resolves as of the `@font-face` blocks above. The two
   * fallbacks stay and are not decoration: `font-display: swap` paints the
   * first frame in `system-ui` while the woff2 is in flight, and a fetch that
   * never completes leaves the page in `system-ui` permanently rather than in
   * whatever the browser's default happens to be — which on this workstation
   * was a serif before this rule existed.
   *
   * Measured on the served page rather than assumed. Chromium's
   * `CSS.getPlatformFontsForNode` on the greeting node:
   *
   *   before the fonts  [{"postScriptName":"NotoSans-SemiBold","isCustomFont":false}]
   *   after             [{"postScriptName":"IBMPlexSans-SemiBold","isCustomFont":true}]
   *
   * `isCustomFont` is the half that matters — it separates a webfont this
   * application shipped from a face that happened to be installed on the
   * machine, which is the difference a screenshot cannot show.
   *
   * DO NOT verify this with `document.fonts.check('1em "IBM Plex Sans"')`. It
   * is not merely uninformative here, it is anti-correlated with the truth, and
   * both readings were taken on this page:
   *
   *   with no IBM Plex on the machine and no @font-face   ->  true
   *   with the eight faces vendored and the page rendering
   *     the greeting in IBMPlexSans-SemiBold              ->  false
   *
   * `FontFaceSet.check` answers "can this sample be rendered without a further
   * download", so before it said yes via the fallback, and now it says no
   * because the query's implied weight is 400 and only the 600 face had loaded
   * when it was asked. Whoever vendored these fonts would have reported a
   * working build as broken had they trusted it. */
  font-family: "IBM Plex Sans", system-ui, sans-serif;

  /* Not a colour, and the distinction is what lets this live in a file whose
   * first rule is that colours come from `client/theme.ml`. `color-scheme`
   * names no value; it tells the user agent which of its own two canvases to
   * paint underneath the document and which defaults to hand form controls and
   * scrollbars. Every colour on this page still comes from `Theme.color`.
   *
   * Without it, a browser whose user asked for dark keeps a white UA canvas and
   * black UA ink regardless of what `prefers-color-scheme` reports, so the
   * area outside the app's own painted surface stays white under the dark
   * palette. The related claim that the *first frame* — before the bundle has
   * mounted anything — flashes white is **assumed**, reasoned from the same
   * canvas rule and not executed: measuring it wants a paint-timing trace
   * rather than the computed-style read everything else here uses.
   * `light dark` says the page renders correctly in either and lets the UA
   * follow the reader's system preference for its own canvas and chrome.
   *
   * WHAT THIS DECLARATION MEANT CHANGED AT FEATURE 0014, and the sentence that
   * stood here is corrected rather than left to contradict the code. It used to
   * say that the UA and the application were "answering the same question",
   * because `client/main.ml` seeded the palette from the same system preference
   * the UA reads. Feature 0014 Decision 1 deleted that read: the application now
   * starts at light for every reader and moves only when the theme control is
   * pressed, so the two no longer answer the same question, and on a
   * dark-preferring machine a light page gets a dark UA canvas behind it and
   * dark scrollbars beside it.
   *
   * RE-READ AT FEATURE 0015 FR-22, WHICH CHANGED THE FINDING'S SHAPE WITHOUT
   * CLOSING IT. The palette a reader presses now survives the tab, so the two
   * agree for the reader who chose the mode their machine also asks for — which
   * is most readers who have chosen at all — and the disagreement moves to the
   * reader who chose the other one. It is DURABLE now rather than lasting until
   * the next reload, which is the half that got worse. Neither static value is
   * any more honest than it was: the mode is still a field in `client/app.ml`
   * and CSS still cannot see it. Left as it is, owner and moment unchanged, and
   * restated in feature 0015's § Deferred.
   *
   * The declaration is left as it is anyway, and this is a reported finding
   * rather than a settled one: the honest value depends on the mode the model
   * currently holds, which CSS cannot see, and both static alternatives are
   * wrong somewhere — `light` gives the dark palette a white UA canvas, which is
   * the defect this declaration was added to remove, and dropping it entirely
   * gives that defect back plus the pre-0006 `normal` reading. Owner: Miguel,
   * at the next observation gate that runs on a dark-preferring machine, and
   * recorded in feature 0014's § Deferred rather than only here.
   *
   * Measured on the served page under headless Chromium, `getComputedStyle`
   * on the root element with `colorScheme` emulation on both settings:
   * `color-scheme` read `normal` before this declaration and `light dark`
   * after. `normal` is the reading that names the defect — the UA is told
   * nothing, so it answers the dark preference with its light canvas. */
  color-scheme: light dark;
}

/* The height chain, and `body`'s margin.
 *
 * `Style.Fill` on the root column becomes `height: 100%`, and a percentage
 * height resolves against the parent's height rather than against the
 * viewport. `html`, `body` and `#app` have no height of their own, so the
 * chain terminates immediately and the column collapses to its content: the
 * themed surface renders as a band across the top of the page and the rest is
 * the UA canvas. Measured before this rule existed, under headless Chromium at
 * 1280x720: the painted element was 1264x47, and `body`'s computed
 * `margin-top` was the UA's `8px` — which is where the missing 16px of width
 * went.
 *
 * `height`, not `min-height`, on `html` and `body`, and that is the whole
 * subtlety: a percentage height resolves only against a parent whose height is
 * *definite*, and `min-height` does not make one. The first attempt at this
 * rule was `html { height: 100% }` over `body`/`#app` at `min-height: 100%`, on
 * the reasoning that a screen taller than the viewport should grow rather than
 * overflow. Measured under headless Chromium at 1280x720 it left the painted
 * element at **1280x47** — the band, unchanged, because `#app`'s `min-height`
 * gave the column's `100%` nothing to read. With `height` on all three the same
 * measurement read **1280x720**.
 *
 * THE LIMIT THAT PARAGRAPH LEFT ASSUMED HAS NOW FIRED, AND THIS RULE IS WHAT
 * CLOSED IT. It said the trade was unmeasured "because this screen has one line
 * of text and cannot overflow", and that content past the viewport would spill
 * with the themed surface not extending behind it — a real limit belonging to
 * "the first screen with more content than fits, which is also the first screen
 * that can measure it". That screen is the dish form on *Menú*. Measured on the
 * served page under headless Chromium at 1280x720, signed in, with the create
 * form open, `#app` and all three at `height: 100%`:
 *
 *   innerHeight 720   documentElement.scrollHeight 1219
 *   painted surface   720px   ->   499px of content with NO surface behind it
 *   html/body background   rgba(0, 0, 0, 0)  (both transparent)
 *
 * so the bottom 499px fell through to the UA canvas — which `color-scheme:
 * light dark` above makes BLACK on a reader whose machine prefers dark, under a
 * light page, with `Ink_1` text on it. The band is the defect; the dark is what
 * made it unmissable rather than merely unpainted.
 *
 * THE FIX IS A FLEX COLUMN ON `#app`, NOT A HEIGHT ANYWHERE. `#app` keeps a
 * definite floor (`min-height: 100%`, resolved against `body`'s definite
 * height) and is free to grow past it, and the root column is a flex item that
 * takes the space with `flex-grow: 1` instead of claiming `height: 100%`. That
 * is why `client/shell.ml`'s and `client/login.ml`'s `surface_style` set
 * `flex_grow = Some 1.0` and set NO height: `Style.Fill` on the height is
 * exactly the `height: 100%` that pinned the surface to the viewport. It had to
 * move to view code rather than be overridden here, because nopal emits its
 * styles INLINE — `style="display: flex; flex-direction: column; width: 100%;
 * height: 100%; background-color: rgb(248, 238, 232);"` read off the live
 * element — and an inline declaration outranks every selector, so the `#app >
 * *` rule that would have said this from the stylesheet was measured to change
 * nothing at all. (Note that this also dates the `font: inherit` comment
 * above, which says nopal "emits a per-element class rule" — true at the
 * 735d259c it cites, and the current pin emits inline. `font: inherit` is
 * unaffected: it sets properties the inline style never names.)
 *
 * Re-measured after the change, on the same page, all three arrangements:
 *
 *   Ventas (short)       scrollHeight 720    surface 720    uncovered 0
 *   Menú + form (tall)   scrollHeight 1180   surface 1180   uncovered 0
 *   the same at 390px    scrollHeight 1093   surface 1093   uncovered 0
 *
 * The short case is the one that had to be checked as well as the tall one: it
 * is the band bug this whole rule was written for, and `min-height` is what
 * keeps it closed while `height: auto` lets the tall case grow.
 *
 * NO VIEWPORT UNITS, which is feature 0015 NFR-7 and not a preference here —
 * `100vh` would have been the one-line version of this and it is both banned
 * and wrong, since it ignores the mobile URL bar and measures the wrong box.
 *
 * `margin: 0` on `body` is the same defect in its second form: the UA's default
 * 8px margin is unpainted by the app, so it frames the themed surface in the
 * UA canvas colour on all four sides. It is here rather than in a view because
 * `body` is not an element any view function owns — `client/index.html` opens
 * it and `#app` is where the bundle mounts. */
html,
body {
  height: 100%;
}

#app {
  display: flex;
  flex-direction: column;
  min-height: 100%;
}

body {
  margin: 0;
}
