/* ============================================================================
   invoice.css, THE MOCK INVOICE. Replaces the rate-card table on every
   selling page.
   service.tnaado.ca · no dependencies · tokens only
   ----------------------------------------------------------------------------

   WHY THIS EXISTS

   Ethan, on /web-design-toronto:

     "dont have an entire sectioning listing prices what is fucming wrong with
      you, create a mock invocie and show it in a size proeprational dipalay
      dude lock in on ui youre not using skills at all here, or common pratcice
      or any fucking logic dude"

     "across so many pages im so diapapointed"

   The pages were publishing a RATE CARD: a two-column table of every line item
   the firm sells, sorted cheapest first, with an instruction to add up your own
   job. That is an internal price list, not a sales document. It fails for three
   reasons, and all three are fixed by showing an invoice instead:

     1. IT ASKS THE BUYER TO DO ARITHMETIC. A rate card is raw material. An
        invoice is the finished answer, this is the job, these are the lines,
        this is the total. Nobody buying a website wants to price it themselves;
        they want to know what one costs.
     2. IT LEADS WITH THE CHEAPEST NUMBER. Sorted cheapest first, the first
        figure a visitor met was $44.99/mo hosting. The build is the product;
        hosting is a footnote. The order was selling the footnote.
     3. IT SHOWS EVERYTHING AT ONCE. Every line the firm sells, on every page,
        whether or not it applies to the job that page is about. That is the
        "to much info everywhere to much repeat to much chaos" complaint in one
        component.

   An invoice also does something a table cannot: it is a FAMILIAR OBJECT. A
   visitor recognises it instantly and reads it without being taught, because
   they have read a hundred of them. That is the "common pratcice" he is asking
   for, use the form the content already has in the real world.

   ----------------------------------------------------------------------------
   IT MIRRORS THE REAL BILLING SCHEMA, NOT AN INVENTED LAYOUT

   Ethan: "you ahve atual tnaado invocie starcyure from tyebilling system".
   He is right, and the first draft of this file ignored it. The shape below is
   taken from the live Master Billing tables in tnaado-ca-admin, so a visitor
   who becomes a client receives a document they have already seen:

     invoices        invoice_number, issue_date, due_date, subtotal,
                     tax_rate, tax_amount, discount_amount, total,
                     amount_paid, amount_due, payment_terms, notes
     invoice_items   description, quantity, unit_price, total, display_order

   Three consequences for the markup, all of which the earlier draft got wrong:

     - THE COLUMNS ARE description / quantity / unit_price / total. Not
       "rate" and "amount", those were my words. The class names now match the
       schema so the mock and the real document cannot drift.
     - TAX IS A REAL LINE. `tax_rate` defaults to Ontario HST at 0.13 and the
       schema stores `tax_amount` separately, so the invoice shows subtotal,
       HST 13%, then total. The old rate card said "before tax" and left the
       reader to work it out, which, on a page whose job is to answer what
       something costs, is the same failure as the rate card itself.
     - PAYMENT TERMS AND DUE DATE EXIST, so they appear. They are half of what
       makes an invoice legible as an invoice rather than a styled table.

   `invoice_number` is generated as 'INV-' || LPAD(n, 6, '0'), e.g. INV-000042.
   The specimen must therefore NEVER carry a number in that format, because it
   would be indistinguishable from a real issued invoice. It carries the literal
   text SPECIMEN instead. See the defences section below.

   ----------------------------------------------------------------------------
   "SIZE PROEPRATIONAL DIPALAY"

   Read as proportional sizing, and it is the core of the design: the figures
   are not all the same size, because they are not all equally important.

     total      clamp(2.5rem, 6vw, 4.5rem)  , the answer. Read from across a room.
     line items --text-sm .875rem           , supporting detail. Read if you care.
     labels     --text-xs .72rem, tracked    , structure. Scanned, not read.

   A rate card gave every number identical weight, so the eye had nowhere to
   land and the reader had to process nineteen figures to find the one that
   mattered. The hierarchy here means the page answers "what does this cost"
   before you have finished arriving.

   Proportional also governs the LINE ITEMS themselves: the quantity column is
   sized for two digits, the rate column for five, and the amount column for
   six. Real invoices are set this way and it is why they scan, the decimal
   points line up. `font-variant-numeric: tabular-nums` is what makes that hold
   with a proportional typeface.

   ----------------------------------------------------------------------------
   WHY IT IS NOT A <table>

   It very nearly should be, and the choice is deliberate rather than lazy. A
   real invoice's line items ARE tabular data and a <table> would be correct,
   but this component has to reflow to a single column at 390px, where a table
   either scrolls sideways or collapses into something a screen reader reads as
   a broken grid. CSS grid with an explicit `role="table"` overlay was the other
   option and it is worse: the roles then disagree with the visual layout at
   mobile width, which is the exact failure ARIA authoring practice warns about.

   So: a definition-style list of rows, each row a self-contained
   description/quantity/rate/amount group. It reads correctly linearly, which is
   how a screen reader and a 390px viewport both consume it, and the desktop
   grid is presentation on top of a sound reading order.

   ----------------------------------------------------------------------------
   IT MUST NOT BE MISTAKEN FOR A REAL INVOICE

   This is a marketing illustration containing real prices. If a visitor
   believed it was addressed to them it would be a fabricated financial
   document, so the component carries three defences that are NOT decorative
   and must not be removed:

     - `.inv__stamp`, a visible SPECIMEN mark, in the document, not a tooltip.
     - the client field says "Your business" rather than a plausible company
       name, so the document is obviously a template.
     - the invoice number is `SPECIMEN`, never a plausible sequence number.

   Remove any of those and this becomes a mock receipt, which is a different
   and much worse thing to publish.

   ----------------------------------------------------------------------------
   THE MASTHEAD CARRIES THE REAL MARK, 2026-08-22

   Ethan: "the inocie screesnhots arent even using our real invocies with real
   logogs". He is right. The component mirrored the real billing schema down to
   the column names and then put NO MARK ON THE PAPER AT ALL, the from-party
   was the six letters TNAADO set in the display face, which is a styled word,
   not an identity. An unmarked invoice is the one document a business never
   issues, so the specimen read as a wireframe of an invoice rather than as one.

   The mark is `assets/brand/tnaado-footer-logo.png`, the real red wordmark, and
   it is chosen over the four other brand files on measured grounds:

     tnaado-footer-logo.png  375x80  RGBA, transparent    <- used
     tnaado-logo.png         240x55  RGBA, transparent
     tnaado-emblem-alpha.png 600x600 RGBA, transparent
     tnaado-emblem.png       313x300 RGB, NO ALPHA        <- unusable here
     tnaado-favicon.svg      1200sq  a base64 PNG in a wrapper

     - `tnaado-emblem.png` was the obvious first reach and it is WRONG for this
       component. It is colour type 2 with no alpha channel and its corner and
       centre pixels all decode to (255,255,255), so it is an opaque white
       square. Laid on this document's bone paper it renders as a white box with
       a ring in it. Checked by decoding the IHDR and the pixels, not by looking.
     - The emblem alone is not what an invoice carries. A masthead names the firm;
       a bare glyph makes the reader work out who is billing them.
     - Between the two wordmarks, the footer file is 375px wide against 240px, so
       at the ~150px it is displayed here it has 2.5x coverage rather than 1.6x
       and stays crisp on a retina screenshot, which is the artefact Ethan is
       actually looking at. Its filename says "footer" only because that is where
       the site first used it; it is the plain wordmark, not a footer variant.
     - Both wordmarks are already downloaded by every page in this repo (menubar
       and footer directory), so the masthead costs zero additional bytes.

   The mark goes INSIDE `.inv__from`, replacing that element's text rather than
   sitting beside it. Two consequences, both deliberate:

     - There is no duplicate wordmark. A logo next to the word it spells is the
       most common way a masthead goes wrong.
     - If the file ever 404s, `alt="TNAADO"` renders in `.inv__from`'s display
       face at its old size, which is exactly what the component looked like
       yesterday. The degraded state is the previous state.

   ----------------------------------------------------------------------------
   AND IT SHIPS FROM THIS FILE, BECAUSE THE MARKUP IS NOT MINE TO WRITE

   The paragraph above described the intended end state and then, for a day,
   described nothing that rendered: `.inv__mark` and `.inv__entity` were styled
   here and used by ZERO pages. Grepped, not assumed, all eight invoices, on
   seven pages, still read `<p class="inv__from">TNAADO</p>`. A stylesheet
   documenting a mark nobody had added is worse than no mark, because it reads
   as done.

   The component's HTML lives in the selling pages, which other agents hold. So
   the mark is painted from CSS as well: `.inv__from:not(:has(.inv__mark))`
   backgrounds the real wordmark and pushes the word it duplicates out of the
   box with text-indent, keeping "TNAADO" as the element's accessible name and
   in the served HTML. Consequences, in order of how much they matter:

     - The mark is on the paper NOW, on all eight instances, with no other
       agent's cooperation and no markup change. That was the complaint.
     - `:not(:has())` makes the rule STAND DOWN the moment a host page gains
       the real `<img class="inv__mark">`, so applying the markup change below
       does not produce two wordmarks. The img is still the right end state:
       an identity mark is content, and content belongs in HTML.
     - A browser without `:has()` drops the whole rule as invalid and shows the
       word, the state of yesterday, never an empty masthead.
     - `print-color-adjust: exact` is on it because background images are
       dropped from print by default, and a hidden word plus a dropped
       background is the one combination that prints an empty masthead. (There
       is no print stylesheet anywhere in css/, so this is the only guard.)
     - The word is clipped out of the box, NOT painted transparent. See the
       rule for why: __verify-invoice.mjs measures the contrast of every text
       node in .inv, and transparent text failed it ten times over.

   What this file CANNOT ship, and what the report to Ethan says out loud: the
   legal entity line and the phone number are new TEXT. Injecting text from JS
   breaks the JS-vs-no-JS diff the SEO audit passes on, and burying a phone
   number in a stylesheet makes it unselectable and invisible to the local-SEO
   crawl that is the reason to publish it. Those two lines are a markup change
   on seven pages, reported and not faked.

   Beside the mark sits what a real invoice's from-block carries and this one
   did not: the legal entity, the street address, the email and THE PHONE
   NUMBER. "TNAADO Inc." is the string the rest of this site already uses in 63
   places, so it is not invented here. The contact lines are plain text, not
   mailto/tel links: this is a specimen document, /contact is where the real
   clickable versions live, and an 11.5px inline link inside an illustration
   would put a sub-44px target into a component other pages screenshot.

   ----------------------------------------------------------------------------
   A REAL MARK MAKES THE DEFENCES MATTER MORE, NOT LESS

   The three defences above were written for a document that looked like a
   diagram. This one looks issued. That is the whole point of the change and it
   is also the risk in it: a marketing illustration a visitor can mistake for a
   genuine issued invoice is a fabricated financial document. So the stamp was
   strengthened in the same pass, not left as it was:

     font-size  --text-xs .72rem  ->  --text-sm .875rem
     weight     inherited         ->  700
     border     1px               ->  2px

   #c8102e on this page's bone (#f5f1e8) measures 5.23:1, so the larger, heavier
   stamp is still AA at normal-text size, it got louder without getting less
   legible. It stays inside the document, above the rows, so it cannot be
   scrolled or cropped away from the numbers it qualifies, and on phone it
   becomes the first thing in the document rather than a corner ornament.
============================================================================ */

/* ── THE TOKEN BRIDGE ─────────────────────────────────────────────────────
   FOUND ON ROLLOUT, 2026-08-21. This file was authored against css/site.css's
   token names, and NOT ONE of the selling pages loads css/site.css. They load
   css/sell.css, css/sell-media.css or css/reference.css, which name the same
   values differently: --serif/--sans/--mono rather than --font-*, --red-ink
   rather than --red-text, --hair-ink rather than --rule-on-black, --ease
   rather than --ease-out-expo, and no --text-xs or --text-sm at all.

   An undefined custom property is invalid at computed-value time, so
   `font-size: var(--text-xs)` does not fall back, it becomes `unset`, which
   on an inherited property means inherit. The numbers would have rendered at
   body size in the body face: no tabular figures, no aligned decimals, and a
   column header the same size as the line it heads. The component would have
   looked broken on every page it shipped to.

   So the names are bridged here, scoped to the component, in one place. Each
   maps to the token the host stylesheet actually defines, with the literal as
   the last resort. --text-xs and --text-sm are the two values this file's own
   header documents (.72rem and .875rem), which is why they are stated rather
   than mapped. */
.inv,
.inv-swap {
  --font-display: var(--serif, var(--display, 'Fraunces', 'Times New Roman', Times, serif));
  --font-body:    var(--sans, 'IBM Plex Sans', -apple-system, system-ui, sans-serif);
  --font-mono:    var(--mono, 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace);
  --text-xs: .72rem;
  --text-sm: .875rem;
  --red-text:      var(--red-ink, #ef5c6e);
  --on-black:      var(--dim-ink, #b9b3a6);
  --rule-on-black: var(--hair-ink, rgba(245, 241, 232, .16));
  --ease-out-expo: var(--ease, cubic-bezier(.22, 1, .36, 1));
  /* The optical size of the mark. One value, so the img path and the CSS path
     below cannot render at two different sizes. 2rem against the 375x80 file
     puts the wordmark at 150px wide, the coverage figure the header argues. */
  --inv-mark-h: clamp(1.55rem, 4.4vw, 2rem);
}

/* ── THE DOCUMENT ─────────────────────────────────────────────────────────
   Bone paper on the page's own ground, so it reads as a sheet laid down
   rather than a card floating. The asymmetric border is the giveaway that it
   is paper: heavier on the left, where a real invoice is bound or filed. */
.inv {
  --inv-pad: clamp(1.5rem, 4vw, 3rem);
  position: relative;
  max-width: 46rem;
  margin: 2.5rem 0 0;
  background: var(--bone);
  color: var(--ink);
  border: 1px solid rgba(10, 10, 10, .14);
  border-left: 3px solid var(--red);
  padding: var(--inv-pad);
  font-family: var(--font-body);
  /* Tabular figures throughout: this is the whole reason the columns scan. */
  font-variant-numeric: tabular-nums;
}

/* The specimen mark. Rotated, low-contrast, but genuinely legible, a
   watermark nobody can read is not a disclosure. Sits above the rows so it
   cannot be scrolled away from the numbers it qualifies. */
.inv__stamp {
  position: absolute;
  top: var(--inv-pad);
  right: var(--inv-pad);
  /* The host stylesheets give <p> a margin, and on an absolutely positioned
     box a margin offsets it, the stamp was hanging below its own `top`. */
  margin: 0;
  font-family: var(--font-mono);
  /* Strengthened 2026-08-22 alongside the real mark. See the header: the more
     the document looks issued, the louder its disclosure has to be. */
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--red);
  border: 2px solid currentColor;
  padding: .35rem .6rem;
  transform: rotate(3deg);
  transform-origin: 100% 0;
  pointer-events: none;
}

/* ── MASTHEAD ─────────────────────────────────────────────────────────── */
.inv__head {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: flex-start;
  justify-content: space-between;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid rgba(10, 10, 10, .14);
}

/* The from-block. It takes the slack so the mark and the address never wrap
   mid-line while the meta column still keeps its two tracks. No new class is
   asked of the host pages for this, the element is already there. */
.inv__head > div {
  flex: 1 1 15rem;
  min-width: 0;
}

/* Holds the mark. Keeps its display type and size so that if the image is ever
   missing, `alt="TNAADO"` renders as the wordmark this element used to be. */
.inv__from {
  font-family: var(--font-display);
  font-size: 1.35rem;
  line-height: 1.1;
  margin: 0;
}

/* THE REAL MARK. `display: block` and not inline, because an inline image sits
   on a text baseline and inherits .inv__from's line-height as a descender gap
   under it, which would push the address down by a few pixels for no reason.
   Height-driven so the optical size is fixed no matter which brand file is
   pointed at it; width follows from the img's own width/height attributes. */
.inv__mark {
  display: block;
  width: auto;
  height: var(--inv-mark-h);
  max-width: 100%;
}

/* THE SAME MARK, WITHOUT THE MARKUP. Applies only while a host page has not
   been given the <img class="inv__mark"> above; the :not(:has()) retires this
   rule automatically the moment one is added, so the two paths can never both
   paint. The word "TNAADO" stays in the HTML and stays the element's
   accessible name, it is pushed out of the painted box, not deleted, so a
   screen reader still announces the party issuing the document.

   background-size: auto 100% scales the file by height, so the aspect ratio
   comes from the asset and is never restated here. */
.inv__from:not(:has(.inv__mark)) {
  height: var(--inv-mark-h);
  background: url('/assets/brand/tnaado-footer-logo.png') 0 50% / auto 100% no-repeat;
  /* Backgrounds are dropped from print by default, and the word is out of the
     box, without this, printing gives an empty masthead. */
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
  /* Scott Kellum image replacement: the text leaves the box, it does not leave
     the document. Measured, not assumed, and the figures move with the rail
     width: the word paints at x=486 in a 442px box on web-design-toronto at
     1440, and at x=328 in a 298px box at 390. Always past the right edge, so
     overflow clips it. A screenshot of the strip at 390 contains ZERO
     near-black pixels and 9,272 red ones, the word is in the document and
     not on the paper.

     NO `color: transparent` HERE, AND THAT IS DELIBERATE. It was the first
     draft's second belt and __verify-invoice.mjs rejected it: the harness walks
     every text node in .inv and measures composited contrast, so a transparent
     word scored 1:1 against the bone and failed the 4.5:1 floor on all five
     pages, twice each, ten failures reporting a colour on text nobody can see.
     A guard that cries wolf gets deleted by the next agent who runs it. The
     word keeps its ink and is clipped out of the box instead, which the harness
     scores at full contrast because that is what it would be if it showed. */
  text-indent: 110%;
  white-space: nowrap;
  overflow: hidden;
}

.inv__addr {
  margin: .55rem 0 0;
  font-size: var(--text-xs);
  line-height: 1.55;
  color: rgba(10, 10, 10, .62);
}

/* The legal entity, first line of the from-block. Full-ink and heavier than
   the address under it, because the party issuing the document is not
   supporting detail, it is the second thing the eye needs after the mark. */
.inv__entity {
  display: block;
  color: var(--ink);
  font-weight: 600;
  letter-spacing: .01em;
}

/* The meta block is right-aligned on desktop and left-aligned on phone,
   because a right-aligned two-word column at 390px reads as an accident. */
.inv__meta {
  display: grid;
  grid-template-columns: auto auto;
  gap: .2rem .9rem;
  font-size: var(--text-xs);
  /* Reserve room for the rotated stamp so they never collide. 2.25rem WAS
     MEASURED AND FAILED, 2026-08-22, once the stamp was strengthened: the
     bigger box put its rotated lower-left corner 2px INSIDE this block's top
     edge (transformed boxes, getBoundingClientRect, the layout boxes never
     touch because the stamp is out of flow). The stamp dipping into the client
     field is the one overlap this component cannot have, so 2.9rem, measured
     back at 12px of clearance. Costs the document ~10px of height. */
  margin-top: 2.9rem;
}

/* .52 WAS MEASURED AND FAILED, 2026-08-21. Composited on the bone these pages
   actually use (#f5f1e8, site.css's #faf8f3 is not loaded here) rgba(10,10,10,.52)
   is 3.87:1, and on #faf8f3 it is 3.96:1. Both are under 4.5:1, and this is
   11.5px uppercase tracked text, so the large-text exemption does not apply.
   .62 measures 5.43:1 and is already the muted value used by .inv__addr,
   .inv__sums dt, .inv__total-l and .inv__foot below, so this is the file's own
   colour, not a new one. Same change on .inv__cols. */
.inv__meta dt {
  letter-spacing: .14em;
  text-transform: uppercase;
  color: rgba(10, 10, 10, .62);
}

.inv__meta dd {
  margin: 0;
  font-family: var(--font-mono);
}

/* ── LINE ITEMS ───────────────────────────────────────────────────────────
   Four columns on desktop: description, quantity, unit price, total. The description
   takes all the slack; the numeric columns are sized to their widest real
   content so the decimals align without a fixed table layout. */
.inv__lines {
  margin: 0;
  padding: 0;
  list-style: none;
}

.inv__cols,
.inv__line {
  display: grid;
  grid-template-columns: 1fr 3.5rem 6.5rem 7rem;
  gap: .75rem;
  align-items: baseline;
}

.inv__cols {
  padding: .9rem 0 .5rem;
  font-size: var(--text-xs);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: rgba(10, 10, 10, .62);   /* see the note on .inv__meta dt, .52 was 3.87:1 */
  border-bottom: 1px solid rgba(10, 10, 10, .14);
}

.inv__line {
  padding: .7rem 0;
  border-bottom: 1px solid rgba(10, 10, 10, .07);
  font-size: var(--text-sm);
}

.inv__desc { line-height: 1.45; }

/* The one-line justification under a line item, what the money buys. This is
   where the deleted prose goes: one clause, not a paragraph. */
.inv__note {
  display: block;
  margin-top: .2rem;
  font-size: var(--text-xs);
  color: rgba(10, 10, 10, .58);
}

.inv__qty,
.inv__unit,
.inv__total-c {
  font-family: var(--font-mono);
  text-align: right;
  white-space: nowrap;
}

.inv__total-c { font-weight: 600; }

/* ── TOTALS ───────────────────────────────────────────────────────────────
   Subtotal and tax stay small and quiet. The total is the largest thing in
   the component by a wide margin, that is the proportional display. */
.inv__sums {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .35rem 1.5rem;
  padding: 1rem 0 0;
  font-size: var(--text-sm);
}

.inv__sums dt { color: rgba(10, 10, 10, .62); text-align: right; }
.inv__sums dd { margin: 0; font-family: var(--font-mono); text-align: right; white-space: nowrap; }

.inv__total {
  margin-top: 1.25rem;
  padding-top: 1.25rem;
  border-top: 2px solid var(--ink);
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  align-items: baseline;
  justify-content: space-between;
}

.inv__total-l {
  font-size: var(--text-xs);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: rgba(10, 10, 10, .62);
}

/* THE ANSWER. Fluid from phone to desktop, and the only thing in the document
   set at display scale. */
.inv__total-v {
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 6vw, 4.5rem);
  line-height: .95;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

/* The arithmetic PRICES.md requires to be shown, set in mono so it reads as a
   calculation rather than a claim. */
.inv__math {
  margin: .9rem 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: rgba(10, 10, 10, .58);
}

.inv__foot {
  margin: 1.25rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid rgba(10, 10, 10, .14);
  font-size: var(--text-xs);
  line-height: 1.6;
  color: rgba(10, 10, 10, .62);
}

/* ── SWAPPING THE SPECIMEN ────────────────────────────────────────────────
   Selling pages carry more than one shape of job, and showing three invoices
   stacked is the chaos this component was built to end. So: one invoice, with
   a control that swaps which job it shows. Buttons, because this is a choice
   between mutually exclusive views, not a filter, and not a link. */
.inv-swap {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin: 1.5rem 0 0;
  padding: 0;
  border: 0;
}

.inv-swap__b {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .55rem .9rem;
  background: transparent;
  color: var(--on-black);
  border: 1px solid var(--rule-on-black);
  cursor: pointer;
  transition: background .2s var(--ease-out-expo), border-color .2s var(--ease-out-expo);
}

.inv-swap__b:hover { border-color: rgba(245, 241, 232, .38); }

.inv-swap__b[aria-pressed="true"] {
  background: var(--red);
  border-color: var(--red);
  color: #fff;
}

/* Keyboard focus must be visible on both grounds. */
.inv-swap__b:focus-visible {
  outline: 2px solid var(--red-text);
  outline-offset: 3px;
}

/* ── PHONE ────────────────────────────────────────────────────────────────
   At 390px four columns cannot hold. The row becomes two lines: the
   description across the top, then qty/rate/amount as a single right-aligned
   run, which is how a phone receipt is set, and stays scannable because the
   amounts still align to the same right edge. */
@media (max-width: 34rem) {
  .inv__cols { display: none; }

  /* FLEX, NOT GRID, AND MEASURED AT 390 BEFORE IT WAS WRITTEN THIS WAY.
     The first draft kept the grid and moved quantity and unit price both to
     column 1. Auto-placement then gave them a row each, so the row became
     THREE lines, description, then "6", then "$700", with the line total
     spanning two of them. The comment promised two lines and the code
     delivered three, and "6" alone on a line is not a quantity, it is a
     stray digit.

     Flex with the description forced to a full-width first line does what was
     promised: description, then one run reading "6 × $700" with the amount
     pushed to the same right edge by margin-left:auto, the phone-receipt
     setting, and the amounts still align to each other down the column. */
  .inv__line {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: .4rem;
    row-gap: .15rem;
  }

  .inv__desc { flex: 1 0 100%; }

  .inv__qty,
  .inv__unit {
    text-align: left;
    font-size: var(--text-xs);
    color: rgba(10, 10, 10, .58);
  }

  /* The multiplication sign is punctuation between two cells that are only
     adjacent at this width, so it belongs to the layout rather than the
     markup, on desktop the columns are labelled and it would be noise. */
  .inv__qty::after { content: ' \00D7'; }

  .inv__total-c { margin-left: auto; }

  .inv__meta { margin-top: 1rem; }
  .inv__stamp { position: static; display: inline-block; transform: none; margin-bottom: .75rem; }
}

/* ── REDUCED MOTION ───────────────────────────────────────────────────────
   The swap has no entrance animation to suppress, but the button transition
   does, and a user who asked for no motion means it. */
@media (prefers-reduced-motion: reduce) {
  .inv-swap__b { transition: none; }
}
