/*
 * Served as a real stylesheet, never inlined: Kestrel's Caddy sets
 * `style-src 'self'` (kestrel/caddy/Caddyfile, the (defaultheader) snippet),
 * so a <style> block would be blocked and the page would render bare.
 *
 * Lengths are rem/em/% throughout. The `font-size` numbers inside the chart
 * SVG are user units scaled by the viewBox, not CSS px — those live in
 * dashboard.py and are correct as bare numbers.
 */

@font-face {
  font-family: "Zilla Slab";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(zilla-slab-700.woff2) format("woff2");
}
@font-face {
  font-family: Manrope;
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url(manrope-var.woff2) format("woff2");
}

:root {
  /* ---- Kanza family surfaces: identical tokens to the hub, cashbids and
         trucktracker, so a visitor arriving from kanza.cgo.app sees no jump. */
  --steel: #F4F4F5;         /* Kanza Light Gray */
  --card: #FFFFFF;
  --ink: #333333;           /* Kanza Text Dark        — 11.49:1 on steel */
  --muted: #5D5D5D;         /* Kanza Dark Gray        —  5.99:1 on steel, 6.58:1 on card */
  --rule: #C7C9CB;          /* Kanza Med Gray */
  --rule-light: #E0E0E2;

  /* ---- Interactive. Was --signal #C77B00, which measured 2.91:1 on steel and
         3.35:1 on card — it failed on every surface it was used on, and it was
         used for the range nav, the upload nav AND every focus ring. */
  --link: #184E47;          /* 8.61:1 on steel | 9.46:1 on card */
  --focus: #184E47;

  /* ---- Price direction. Polarity is deliberately the opposite of cashbids:
         there the farmer is SELLING grain so a rise is good news, here the
         customer is BUYING fuel so a rise is bad. Do not "harmonise" these. */
  --good: #1F6B45;          /* cheaper — 6.47:1 on card */
  --bad: #A8401F;           /* dearer  — 6.14:1 on card */

  /*
   * ---- The plates ----
   *
   * The plate IS the fuel: black gasoline, navy no-ethanol gasoline, green
   * diesel. This replaced one teal plate per product with a gold rail and a
   * gold price, where the colour carried no information and the product
   * identity was left to the small chip at the bottom of the card.
   *
   * Every background here sits at or below the luminance of the old --plate
   * #184E47, which is why the five foreground tokens below did not have to
   * change: they were tuned against that teal at a worst case of 4.65:1, so
   * anything that dark or darker inherits the guarantee. Keep that property if
   * you add a fourth — go darker, never lighter.
   *
   * --plate-bg-unl87-clear is #1E3A7B and not the more obvious #1B3068: the
   * darker navy lands at deltaE 10.0 against the diesel green UNDER
   * TRITANOPIA, where blue and green converge. That is the same class of
   * failure as the brand teal collapsing against near-black under deuteranopia
   * (deltaE 8.0), which is why --nozzle-roadmaster is a non-brand green. Both
   * axes are pinned by tests/test_plate_colors.py — do not retune by eye.
   *
   * CSS-only, unlike --nozzle-* below. Nothing in the SVG chart draws on a
   * plate, so there is no Python copy of these to keep in sync.
   */
  --plate-bg: #184E47;              /* fallback: a product with no colour yet */
  --plate-bg-unl87-e10: #262626;    /* black nozzle  — worst fg 7.44:1 */
  --plate-bg-unl87-clear: #1E3A7B;  /* navy, see above — worst fg 5.30:1 */
  --plate-bg-roadmaster: #155030;   /* green nozzle  — worst fg 4.65:1 */

  --plate-price: #FFFFFF;   /* 9.45:1 on the lightest plate */
  --plate-text: #F4F4F5;    /* 8.60:1 */
  --plate-dim: #C7C9CB;     /* 5.69:1 */
  --plate-up: #F0A38C;      /* 4.65:1 */
  --plate-down: #8FD3AC;    /* 5.44:1 */

  /*
   * Nozzle convention: gasoline black, diesel green. Only three products are
   * ever displayed, so these are the only three that matter; the rest are
   * recognised by the tracker but never rendered.
   *
   * These colours are drawn on a LIGHT surface ONLY — the chart and the
   * change-log marks and names on --card, and the .nozzle chip, which carries
   * its own pale background even when it sits on the dark plate. That is the
   * fix for the old palette: it was drawn on both --steel and a dark plate,
   * and roughly half the tokens failed 3:1 on one surface or the other. One
   * surface, one palette. The plates have their own family above — same three
   * hues, darker siblings — rather than these being asked to serve both.
   *
   * --nozzle-roadmaster is the one non-palette colour on the page. It is the
   * actual green of a diesel nozzle, and it is required: the official Kanza
   * teal-dark #184E47 collapses to deltaE 6.2 against #333333 under simulated
   * deuteranopia, which would make two of the three series indistinguishable.
   * Kept in sync with NOZZLE in dashboard.py, which owns the same mapping for
   * the SVG chart (presentation attributes there, which the CSP does not touch).
   */
  --nozzle-default: #5D5D5D;
  --nozzle-unl87-e10: #333333;     /* 12.63:1 on card — black nozzle */
  --nozzle-unl87-clear: #25408F;   /*  9.51:1 on card — Kanza Navy */
  --nozzle-unl89: #5D5D5D;         /*  6.58:1 */
  --nozzle-unl91: #533A6C;         /*  9.59:1 — Kanza Purple */
  --nozzle-roadmaster: #2E7D4F;    /*  5.05:1 on card — green nozzle */
  --nozzle-clear-2: #1F6B45;       /*  6.47:1 */
  --nozzle-fieldmaster: #A8401F;   /*  6.14:1 */
  --nozzle-dyed-1: #9E0263;        /*  5.98:1 — Kanza Magenta */

  --sans: Manrope, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --slab: "Zilla Slab", Georgia, serif;

  --hairline: 0.0625rem;
  --rail-w: 0.1875rem;
}

* { box-sizing: border-box }

body {
  margin: 0;
  background: var(--steel);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 0.9375rem;
  line-height: 1.45;
  -webkit-text-size-adjust: 100%;
}

/* The family signature bar — same three colours, same order, same height as
   the hub, cashbids and trucktracker. */
body::before {
  content: "";
  display: block;
  height: var(--rail-w);
  background: linear-gradient(90deg, #2E9486, #D19244, #F5CE3E);
}

.wrap { max-width: 58.75rem; margin: 0 auto; padding: 1.5rem 1.125rem 4rem }

/* Flex, not a float. The float made .nav overlap the h1 below it once the
   viewport was narrow enough for the eyebrow text to wrap — which is most
   phones, and this page is now public. */
.eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.25rem 1rem;
  font-size: 0.6875rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 700;
  margin: 0 0 0.25rem;
}

h1 {
  font-family: var(--slab);
  font-size: 1.625rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  margin: 0 0 0.25rem;
}

.stamp { color: var(--muted); font-size: 0.8125rem; margin: 0 0 1.375rem }
.stamp b { color: var(--ink); font-weight: 700 }

/* ---- price board ---- */

/*
 * Mobile first, and deliberately without a max-width breakpoint: the narrow
 * form is the default and the wide one is the override, so there is no width
 * at which both or neither apply.
 *
 * What this replaces: .board was a wrap-flex of 11.875rem plates, so three
 * stopped fitting below about 626px and the third dropped to a row of its own,
 * stretched to its 21.25rem cap — one plate taking ~80% of a row while two
 * shared the one above. That is most phones, and every narrow desktop window.
 */
.board { display: grid; gap: 0.5rem; margin: 0 0 1.875rem }

.plate {
  min-width: 0;   /* grid items default to min-width:auto and will not shrink */
  background: var(--plate-bg);
  border-radius: 0.375rem;
  padding: 0.875rem 1rem 0.75rem;
  color: var(--plate-text);

  /* Narrow: one compact row per plate — name/sub/chip left, price/delta right.
     Grid areas rather than a DOM change, so dashboard.py still emits them in
     reading order (name, sub, price, delta, chip). A product with no sub-line
     or no nozzle family simply leaves that area empty. */
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "name  price"
    "sub   delta"
    "chip  chip";
  column-gap: 0.75rem;
  row-gap: 0.25rem;
  align-items: baseline;
}

.plate .name { grid-area: name }
.plate .sub { grid-area: sub }
.plate .price { grid-area: price; text-align: right }
.plate .delta { grid-area: delta; text-align: right }
.plate .nozzle { grid-area: chip; justify-self: start }

/* The plate IS the swatch now — black gasoline, navy no-ethanol, green diesel.
   The markup already carries the product slug (class="plate unl87_e10"), so
   this needs nothing from dashboard.py. Anything unrecognised keeps the
   fallback teal, which is the same graceful path PUBLIC takes for a name. */
.plate.unl87_e10 { background: var(--plate-bg-unl87-e10) }
.plate.unl87_clear { background: var(--plate-bg-unl87-clear) }
.plate.roadmaster { background: var(--plate-bg-roadmaster) }

/* Sits under the grade name and carries the disambiguation, so it must stay
   legible rather than decorative — --plate-dim is 5.69:1 on the darkest plate.
   Margins are zero because the compact grid spaces these with row-gap; the
   wide layout puts them back. */
.plate .sub {
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: 0.01em;
  color: var(--plate-dim);
  margin: 0;
}

.plate .name {
  font-family: var(--slab);
  font-size: 0.9375rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--plate-text);
  margin: 0;
}

.price {
  font-family: var(--sans);
  font-size: 2.125rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--plate-price);
}

/* The pole-sign tenth-of-a-cent. Keep it — it is the detail that makes the
   board read as a fuel sign rather than a table of numbers.
   These are `em`, not `rem`, so they track the price through the clamp() the
   wide layout puts on it: 0.475em and 0.6em reproduce the original 1.1875rem
   and 1.5rem against a 2.5rem price. */
.price .tenth { font-size: 0.475em; vertical-align: super; letter-spacing: 0; margin-left: 0.03em }
.price.word { font-size: 0.6em; letter-spacing: 0.04em; color: var(--plate-dim) }

.delta { font-size: 0.75rem; font-weight: 600; color: var(--plate-dim); font-variant-numeric: tabular-nums }
.delta.up { color: var(--plate-up) }
.delta.down { color: var(--plate-down) }

/*
 * The nozzle chip names the fuel family in words instead of asking anyone to
 * infer it from a colour. It matters more now that the plate itself is the
 * nozzle colour: both gasoline grades read "Gasoline" whatever their plate, so
 * a navy card never implies navy is a real-world nozzle colour. It is a
 * legibility split, not a convention.
 *
 * It also carries its own pale background, which is what lets the dot stay
 * visible against a plate of nearly its own colour.
 */
.nozzle {
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  margin-top: 0.25rem;
  padding: 0.125rem 0.4375rem;
  border-radius: 999rem;
  background: var(--steel);
  color: var(--ink);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.nozzle i {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--nozzle-default);
}

.nozzle.unl87_e10 i   { background: var(--nozzle-unl87-e10) }
.nozzle.unl87_clear i { background: var(--nozzle-unl87-clear) }
.nozzle.unl89 i       { background: var(--nozzle-unl89) }
.nozzle.unl91 i       { background: var(--nozzle-unl91) }
.nozzle.roadmaster i  { background: var(--nozzle-roadmaster) }
.nozzle.clear_2 i     { background: var(--nozzle-clear-2) }
.nozzle.fieldmaster i { background: var(--nozzle-fieldmaster) }
.nozzle.dyed_1 i      { background: var(--nozzle-dyed-1) }

/*
 * Wide: the tall pole-sign plate, all of them on one row.
 *
 * grid-auto-flow:column puts every plate on a single row whatever the count,
 * so this cannot reproduce the 2+1 break — that came from flex wrapping once
 * three fixed-width plates stopped fitting. Past four products the row wants a
 * rethink rather than a narrower column.
 *
 * The price has to be fluid, not 2.5rem: at this breakpoint a column is about
 * 141px and a fixed 2.5rem "5.09⁹" needs ~151px, so it would overflow the
 * first pixel after the layout switches. clamp() reaches 2.5rem at roughly
 * 45rem and holds there.
 *
 * Must stay after the base plate rules — the overrides below are same
 * specificity, so source order is what decides.
 */
@media (min-width: 30rem) {
  .board { grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.625rem }

  .plate { display: block }
  .plate .name { margin-bottom: 0.375rem }
  .plate .sub { margin: -0.25rem 0 0.375rem }
  .plate .price { text-align: left; font-size: clamp(2.125rem, 5.5vw, 2.5rem) }
  .plate .delta { text-align: left; margin-top: 0.4375rem }
  .nozzle { margin-top: 0.625rem }
}

/* ---- cards ---- */

.card {
  background: var(--card);
  border: var(--hairline) solid var(--rule);
  border-radius: 0.375rem;
  padding: 0.875rem 0.875rem 0.75rem;
  margin: 0 0 1.375rem;
  box-shadow: 0 0.0625rem 0.1875rem rgba(51, 51, 51, 0.06);
  /* Backstop, not the plan. The changes table is sized to fit unaided from
     about 360px up; below that this keeps the overflow inside the card instead
     of letting the whole PAGE scroll sideways, which hides the Change column
     with nothing on screen to say it is there. */
  overflow-x: auto;
}

@media (min-width: 30rem) {
  .card { padding: 1.125rem 1.125rem 0.75rem }
}

.card h2 {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.25rem 0.75rem;
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 0.875rem;
  font-weight: 700;
}

/* Range nav. Was a row of 0.75rem text links with no hit area worth the name —
   on a public page that is mostly opened on a phone, these are the only
   controls, so they get a real 2.75rem target. The selected one is a filled
   pill, not just a colour change, so the state survives greyscale. */
.ranges { display: flex; flex-wrap: wrap; gap: 0.25rem; font-size: 0.75rem; font-weight: 700; letter-spacing: 0; text-transform: none }
.ranges a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0 0.625rem;
  border-radius: 999rem;
  color: var(--muted);
  text-decoration: none;
}
.ranges a:hover { color: var(--ink); background: var(--steel) }
.ranges a.on { background: var(--link); color: #FFFFFF }
.ranges a:focus-visible { outline: 0.125rem solid var(--focus); outline-offset: 0.125rem }

svg { display: block; width: 100%; height: auto }

/* Caps how far the chart's SVG user units can scale. Everything inside an SVG
   scales with its viewBox, so an uncapped width makes the axis text unreadable
   on a phone and oversized on a desktop. See svg_chart's docstring — the
   numbers there assume this cap. */
.chart-wrap { max-width: 34rem; margin: 0 auto }

/*
 * Ties a change-log row back to its line on the chart. Same dash shape as the
 * stroke it refers to, not a dot.
 *
 * The colour is on the NAME as well as the mark, because the mark alone did
 * not work. It was 0.6875rem x 0.1875rem — 11x3px, 33 square pixels — and at
 * that size a patch reads as "small dark thing" no matter how far apart the
 * hues are. And they are far apart: measured pairwise these three are deltaE
 * 47-86 under normal vision, where 2-3 is the just-noticeable threshold. The
 * problem was never the palette, it was that perceived saturation falls away
 * as a colour patch shrinks, so everyone loses hue discrimination at 3px.
 *
 * So: the mark is now 1rem x 0.4375rem (3.4x the area) and the product name
 * carries the same colour, which is the widest field available in the cell.
 * Note unl87_e10's colour IS --ink #333333, so E10 rows still read as ordinary
 * text and only two of the three names visibly change — the table does not
 * turn into a colouring book. All three clear 4.5:1 on --card, pinned by
 * tests/test_nozzle_colors.py, and the name is still the actual answer, so the
 * colour is reinforcement rather than the only cue.
 */
.prod { display: inline-flex; align-items: center; gap: 0.4375rem }
.swatch { width: 1rem; height: 0.4375rem; border-radius: 0.25rem; background: var(--nozzle-default); flex: none }

.prod.unl87_e10   { color: var(--nozzle-unl87-e10) }
.prod.unl87_clear { color: var(--nozzle-unl87-clear) }
.prod.unl89       { color: var(--nozzle-unl89) }
.prod.unl91       { color: var(--nozzle-unl91) }
.prod.roadmaster  { color: var(--nozzle-roadmaster) }
.prod.clear_2     { color: var(--nozzle-clear-2) }
.prod.fieldmaster { color: var(--nozzle-fieldmaster) }
.prod.dyed_1      { color: var(--nozzle-dyed-1) }

.prod.unl87_e10 .swatch   { background: var(--nozzle-unl87-e10) }
.prod.unl87_clear .swatch { background: var(--nozzle-unl87-clear) }
.prod.unl89 .swatch       { background: var(--nozzle-unl89) }
.prod.unl91 .swatch       { background: var(--nozzle-unl91) }
.prod.roadmaster .swatch  { background: var(--nozzle-roadmaster) }
.prod.clear_2 .swatch     { background: var(--nozzle-clear-2) }
.prod.fieldmaster .swatch { background: var(--nozzle-fieldmaster) }
.prod.dyed_1 .swatch      { background: var(--nozzle-dyed-1) }

/* ---- tables ---- */

table { width: 100%; border-collapse: collapse; font-size: 0.75rem }

th {
  text-align: left;
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 700;
  padding: 0 0 0.5rem;
  border-bottom: var(--hairline) solid var(--rule);
}

td { padding: 0.5625rem 0; border-bottom: var(--hairline) solid var(--rule-light); font-variant-numeric: tabular-nums }

/* Cells carry no horizontal padding, which is invisible while the columns are
   wide enough to keep their content apart and becomes "EFFECTIVEPRODUCT" the
   moment they are not. A gutter between neighbours, not inside the outer edges,
   so the table still sits flush with the card. */
th + th,
td + td { padding-left: 0.375rem }
/* The heading follows its values, not the reverse. `th` above sets left for the
   text columns, which strands "Price" and "Change" at the far left of a wide
   table — metres from the digits they label, and reading as though they belong
   to the column before. Numeric headers right-align with their own numbers. */
th.num,
td.num { text-align: right; font-variant-numeric: tabular-nums }
tr:last-child td { border-bottom: none }

/* Four columns in a 248px table on a 320px phone. "4.999 -> 5.099" is 90px of
   it, and holding that on one line is what pushed the whole table — and so the
   page — into horizontal scroll: the Change pill, the column people actually
   came for, ended up off-screen with no indication it was there. Letting the
   pair break across two lines costs nothing (the arrow still reads) and is
   what keeps the page itself from ever scrolling sideways. Restored to one
   line at the same breakpoint the plates use. */
@media (min-width: 30rem) {
  table { font-size: 0.8125rem }
  th.num,
  td.num { white-space: nowrap }
}

/* Change pill — the same component cashbids ships. The +/- sign carries the
   meaning on its own, so the colour is reinforcement, never the only cue.
   Polarity is inverted relative to cashbids on purpose: a rise in a fuel price
   is bad news for the person reading this, where a rise in a grain bid is good
   news for the person reading that. */
.chg {
  display: inline-block;
  padding: 0.0625rem 0.375rem;
  border-radius: 0.25rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.chg.up { background: #F7E4DE; color: var(--bad) }    /* dearer  — 5.00:1 */
.chg.down { background: #E0F2EF; color: var(--good) } /* cheaper — 5.58:1 */

.up { color: var(--bad) }
.down { color: var(--good) }

.empty { color: var(--muted); padding: 0.5rem 0 1rem }

.foot {
  color: var(--muted);
  font-size: 0.75rem;
  margin-top: 1.75rem;
  border-top: var(--hairline) solid var(--rule);
  padding-top: 0.75rem;
}
.foot a { color: var(--link) }
.foot a:focus-visible { outline: 0.125rem solid var(--focus); outline-offset: 0.125rem }

/* ---- upload page ---- */

/*
 * The board is public but /fuel/upload is IP-gated, so for most visitors this
 * link leads to a 403. The padlock says so before the tap rather than after —
 * same cue the hub uses for its staff cards, so the two pages agree about what
 * "you need to be staff" looks like. Shape first, colour second: the glyph
 * carries it, and .sr-only spells it out for assistive tech.
 */
.nav { font-size: 0.75rem; font-weight: 700 }
.nav a {
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  min-height: 2.75rem;
  color: var(--link);
  text-decoration: none;
}
.nav a:hover { text-decoration: underline; text-underline-offset: 0.1875rem }
.nav a:focus-visible { outline: 0.125rem solid var(--focus); outline-offset: 0.125rem }

/*
 * Sized off the link's own class, never off a container. This padlock appears
 * in .nav on the board and in .foot on both write pages; when the rule was
 * `.nav .lock` the .foot copies matched nothing, and an <svg> with a viewBox
 * and no CSS width falls back to the replaced-element default of 300x150px.
 * Not a subtle failure -- the glyph swallowed most of the page.
 */
a.staff {
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  min-height: 2.75rem;
  color: var(--link);
  text-decoration: none;
}
a.staff:hover { text-decoration: underline; text-underline-offset: 0.1875rem }
a.staff:focus-visible { outline: 0.125rem solid var(--focus); outline-offset: 0.125rem }

.staff .lock {
  width: 0.8125rem;
  height: 0.8125rem;
  flex: none;
  stroke: currentColor;
  stroke-width: 2.25;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.sr-only {
  position: absolute;
  width: 0.0625rem;
  height: 0.0625rem;
  padding: 0;
  margin: -0.0625rem;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.drop {
  border: 0.125rem dashed var(--rule);
  border-radius: 0.375rem;
  padding: 1.75rem 1.25rem;
  text-align: center;
  background: var(--steel);
}

.drop input[type="file"] { font: inherit; font-size: 0.8125rem; max-width: 100% }
.drop input[type="file"]:focus-visible { outline: 0.125rem solid var(--focus); outline-offset: 0.125rem }

button {
  font: inherit;
  font-weight: 700;
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--plate-text);
  background: var(--plate-bg);
  border: none;
  border-radius: 0.25rem;
  padding: 0.625rem 1.25rem;
  margin-top: 1.125rem;
  cursor: pointer;
  min-height: 2.75rem;
}
button:hover { background: #123A35 }
button:focus-visible { outline: 0.125rem solid var(--focus); outline-offset: 0.125rem }

.hint { color: var(--muted); font-size: 0.75rem; margin: 0.75rem 0 0 }

/* Per-file receipt. The status word carries the meaning, so it is never
   colour alone — `logged` / `duplicate` / `failed` read fine in grayscale. */
.result td { vertical-align: top }
.result .tag { font-weight: 700; text-transform: uppercase; font-size: 0.6875rem; letter-spacing: 0.08em }
.result .tag.new { color: var(--good) }
.result .tag.duplicate { color: var(--muted) }
.result .tag.conflict,
.result .tag.failed { color: var(--bad) }
.result .detail { color: var(--muted); font-size: 0.75rem }
.result .detail div { white-space: pre-wrap }

.banner {
  border-radius: 0.375rem;
  padding: 0.75rem 1rem;
  margin: 0 0 1.375rem;
  font-size: 0.8125rem;
  border: var(--hairline) solid var(--rule);
  background: var(--card);
}
.banner.bad { border-color: var(--bad); color: var(--bad) }

/* ---- typed entry ---- */

/*
 * Same .card / .banner / button furniture as the upload page on purpose: the
 * two write paths differ in what they take, not in what they are. One column on
 * a phone, two above the same 30rem breakpoint the plates use, with the source
 * note spanning both.
 */
.fields { display: grid; gap: 0.875rem; grid-template-columns: 1fr }
@media (min-width: 30rem) { .fields { grid-template-columns: 1fr 1fr } }

.field { display: flex; flex-direction: column; gap: 0.3125rem }
.field.wide { grid-column: 1 / -1 }
.field label {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.field input {
  font: inherit;
  /* 1rem is a floor, not a preference: iOS Safari zooms the page in on focus
     for any input under 16px, which on the office phone leaves the form
     scrolled sideways mid-entry. */
  font-size: 1rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--card);
  border: var(--hairline) solid var(--rule);
  border-radius: 0.25rem;
  padding: 0.5rem 0.625rem;
  min-height: 2.75rem;
}
.field input:focus-visible { outline: 0.125rem solid var(--focus); outline-offset: 0.125rem }

/* The last reading, shown as ghost text. It must read as absent rather than as
   a faint value -- it is not submitted, and a placeholder that looks filled in
   is exactly the confusion that made prefilling the wrong call. */
.field input::placeholder { color: var(--rule); opacity: 1 }

/* .price is no longer here: the compact row is the base and the wide layout
   clamps it, so both ends are already handled by the 30rem breakpoint. */
@media (max-width: 26.25rem) {
  h1 { font-size: 1.375rem }
}

@media (forced-colors: active) {
  .plate { border: var(--hairline) solid CanvasText }
  .nozzle { border: var(--hairline) solid CanvasText }
  .card { border: var(--hairline) solid CanvasText }
}
