/* Ravito — Lane Board.
   Six ordered layers: (1) reset, tokens, [hidden]; (2) shell; (3) list;
   (4) pane, toast, key sheet; (5) document pages; (6) the deal pipeline.
   Nothing later re-states padding set earlier, and layers 5 and 6 are scoped
   inside .sheet / .deals so restyling the searches page or the pipeline can
   never reach into the list column. */

/* =====================================================================
   1. RESET, TOKENS, [hidden]
   ===================================================================== */

:root{
  /* Nine selects, a checkbox, a textarea and the scrollbars all render from
     this. Left as `dark` on a light page they all come back as dark slabs.
     One committed light theme — there is no dark path and no toggle, so the
     only lever is :root and no prefers-color-scheme block belongs anywhere. */
  color-scheme: light;

  /* Depth inverts on white. On graphite, nearer meant lighter; here nearer
     means darker, so a select, an input, a button and a thumbnail all sit one
     step BELOW the panel they are in, which reads as inset. The data plane is
     the cleanest: the list column is white and everything that is instrument
     rather than data is tinted, the further from the data the more tinted. */
  --bg:#FFFFFF;            /* the data plane: page ground, list scroller, rows */
  --surface:#F4F6F9;       /* chrome: topbar, rail, column header, status, pane */
  --surface-2:#E8ECF2;     /* the inset plane: selects, inputs, buttons, thumbs */
  --row-hover:#EDF1F7;
  --row-cursor:#DFE9F8;    /* cool, tied to --focus: "the machine is here" */
  --hair:#DDE3EA;          /* hairlines only — see --fill-hover for the fills */
  --hair-strong:#C4CDD8;
  --fill-hover:#D7DEE7;    /* the hover FILL on .btn and .act; a hairline value
                              is far too pale to read as a fill */

  --ink:#12171F;
  --ink-2:#3F4A57;
  --ink-3:#5C6775;
  /* Replaces opacity:.4 on disabled and waiting buttons. Darkened from #7C8693,
     which was 3.41:1 on a card and 3.11:1 on an inset control — under the floor,
     on the label that tells you what the button would do if you filled the field
     above it ("Run vehicle check", with no reg yet). The shape now carries the
     "you cannot press this" signal on its own, so the ink no longer has to say
     it by being hard to read. 4.93:1 on a card, 4.50:1 on a control. */
  --ink-disabled:#616C7A;
  --on-hue:#FFFFFF;        /* the label on any saturated fill */

  /* Three hues, three meanings, and nothing else gets one. Jade is money in
     your favour, rust is money against you, plate yellow is shortlisted.
     Every text token clears AA on every surface it can land on. */
  --jade:#056C4B;
  --jade-2:#0D8760;
  --jade-3:#3F9C7C;
  --rust:#AD3218;
  --rust-2:#CE4A31;
  /* Yellow loses the most contrast on white, so it is split by job: a fill
     that carries dark ink, a graphic mark, and a text colour. */
  --plate:#F2C230;         /* filled areas only — 1.68:1 against paper */
  --plate-mark:#B07E00;    /* plate yellow as a graphic: rails, dots, accents */
  --plate-text:#7D5800;    /* plate yellow as text */
  --plate-ink:#171207;     /* the dark ink ON plate yellow. NOT --bg: it was
                              byte-identical to it on the dark board and must
                              stay dark now that the page has gone white. */
  /* Mark only, never text: 3.06:1 on white. It draws the "maybe" rail and the
     "maybe" ring, where a graphic is judged on being seen rather than read. A
     rule that needs a grey for TEXT reaches for --ink-3, which is 4.85:1 on the
     worst surface it can land on. Same split by job as the three plate yellows. */
  --neutral:#8695A6;       /* maybe: the absence of a judgement, no hue */

  /* A fact about the advert (New, Price drop, Gone) is stated as a solid; a
     judgement about the van (the verdict) is stated as an outline. Without that
     split, "Price drop" and "Buy" were byte-identical chips sitting side by side
     on the same meta line, and so were "Gone" and "Avoid" — two green badges, or
     two rust ones, on one row, telling apart only if you stopped and read them.
     The tint below is the one exception in the other direction: it lifts the top
     verdict clear of the two beneath it without spending a fourth hue. */
  --jade-wash:#CDE9DC;     /* the fill under the strongest positive verdict */
  /* The wash under a row you have ticked for a bulk action. Plate yellow, the
     hue that already means "on your list", at the same strength as --jade-wash
     — light enough that the cursor's cool --row-cursor still wins on top of it,
     which is what keeps "where I am" and "what I have held" apart. */
  --pick:#FBF1D2;

  /* Focus is its own cold blue. "Where am I" is not a fact about the van. */
  --focus:#1B63D8;
  --sel:rgba(27,99,216,.16);

  --v-definitely_buy:var(--jade);
  --v-buy:var(--jade-2);
  --v-negotiate:var(--jade-3);
  --v-maybe:var(--neutral);
  --v-avoid:var(--rust-2);
  --v-ungraded:transparent;

  /* Two things sit on a photograph, where white-on-dark is the only treatment
     that works over an unknown image. They are the only dark chips left. */
  --scrim:rgba(12,17,23,.66);
  --scrim-strong:rgba(12,17,23,.72);
  /* Two shadows, and only two. Half-opaque black reads as lift on graphite and
     as a smudge on white. */
  --shadow-1:rgba(16,24,40,.06);
  --shadow-2:rgba(16,24,40,.14);
  --scroll-thumb:#C4CDD8;

  --r-sm:2px; --r-md:4px; --r-lg:6px;
  /* The pane gives up 28px at every width so the row can have a selection
     track. Measured, not guessed: the trailing slack track on the row is ZERO
     at 1280, 1440 and 1680, and the only give is the name track's distance
     above its own minimum — 27px, 21px and 53px. A 28px checkbox column taken
     from there would push the name below its minimum at 1440 and shear the bin
     button off the right-hand end with no scrollbar to reveal it, because
     content-visibility gives every row paint containment. The pane is 400px
     around a 4:3 photograph and a column of short facts; 372 costs it nothing. */
  --topbar-h:56px; --rail-w:248px; --pane-w:372px; --row-h:76px;

  /* The deal pipeline. Named by the job they do, and every one of them an alias
     of a value already on this page — the palette stays at three hues. A missed
     viewing is money going against you, today is "on your list", and a stall is
     plate yellow used as a graphic rather than as a fill. Nothing here is a new
     colour, which is why there is still only rust, jade and plate to learn. */
  --appt-over:var(--rust);
  --appt-now:var(--plate);
  --appt-now-ink:var(--plate-ink);
  --appt-soon:var(--surface-2);
  --stall:var(--plate-mark);
  --deal-col-w:296px;
  --deal-tray-w:208px;
  /* The one structural device the pipeline spends anything on: a right-aligned
     column of relative time, tabular figures, running through the card's
     last-event line, every row of the timeline and every coming-up chip. A
     pipeline's real axis is when, so the device encodes that instead of
     decorating something else. */
  --time-gutter:56px;

  /* One ordinary interface stack, no condensed display face and no monospace
     data face. Tabular figures do the column alignment instead — see below. */
  --f-ui:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI Variable Text","Segoe UI",Roboto,"Helvetica Neue",Arial,system-ui,sans-serif;
  /* Safety aliases. Every sizing rule in the sheet already names one of these
     three, so pointing all three at the same stack means a rule nobody
     remembers to visit still comes out in the normal interface font. Do not
     delete them. */
  --f-display:var(--f-ui);
  --f-text:var(--f-ui);
  --f-data:var(--f-ui);
  /* The one exception, and it is two rules: .banner-note code and .blank code
     print a shell command that exists to be retyped, where l/1/I and O/0 have
     to be unambiguous. Nothing on the board uses it. Set this to var(--f-ui)
     for literally zero monospace and nothing else in the sheet changes. */
  --f-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;

  scrollbar-width:thin;
  scrollbar-color:var(--scroll-thumb) transparent;
}

*{ box-sizing:border-box; }

/* An author display declaration beats the UA's [hidden] rule, and the rows,
   the column header and every panel here set display. Without this, hiding a
   filtered-out row does nothing at all and the count is the only thing that
   moves — with every test still green. */
[hidden]{ display:none !important; }

html, body{ height:100%; }
body{
  margin:0; overflow:hidden;
  background:var(--bg); color:var(--ink);
  font:500 15px/1.45 var(--f-ui);
  -webkit-font-smoothing:antialiased;
}
::selection{ background:var(--sel); }

/* Every field, rather than the two textareas that happened to get visited. Left
   to the UA a placeholder comes back as #757575, which is 3.89:1 on the inset
   --surface-2 field — under the floor, at body size, on the hint text of every
   search form and the reg box. That is exactly the grey-on-light problem this
   theme set out to remove, so the rule is stated once where nothing can miss it. */
input::placeholder, textarea::placeholder{ color:var(--ink-3); }

:where(a,button,select,input,textarea):focus-visible{
  outline:2px solid var(--focus); outline-offset:2px;
}

/* Every numeral on the board is tabular, so columns of money line up digit for
   digit down 1,600 rows. Tabular figures are a numeral setting rather than a
   typeface — SF Pro, Segoe UI and Roboto all ship them — which is what lets the
   board keep its alignment in an ordinary interface font.

   Granted here, and granted again on every rule below that sets a face with the
   `font:` shorthand — the shorthand resets all the font-variant longhands to
   normal, so inheriting it once is not enough. */
.v-price, .v-gap, .v-miles, .v-away, .v-when i, .v-upd i, .rc, .sub,
.pane-money, .pane-facts .fv, .kv .v, .deal-row b{
  font-family:var(--f-data);
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
    scroll-behavior:auto !important;
  }
}

/* =====================================================================
   2. SHELL — topbar, flashes, stage
   ===================================================================== */

/* Four rows, and the first two collapse to nothing when they are empty: the
   impersonation bar is only rendered while Kish is looking at a customer's
   board, and the flash strip only while there is something to say. An `auto`
   row with no item in it is zero high, so the board keeps the whole window on
   an ordinary page load. */
.wrap{
  height:100dvh;
  display:grid;
  grid-template-rows:auto var(--topbar-h) auto minmax(0,1fr);
  grid-template-areas:"viewbar" "topbar" "flash" "stage";
}
.topbar{ grid-area:topbar; }
.flashes{ grid-area:flash; }
.wrap > :last-child{ grid-area:stage; min-height:0; }

.topbar{
  display:flex; align-items:center; gap:14px; padding:0 16px;
  background:var(--surface); border-bottom:1px solid var(--hair);
  /* A grid item's automatic minimum is its content, so at 15px the bar's
     max-content pushed the whole stage 77px wider than the window between 720
     and 810px and the page grew a horizontal scrollbar. This lets it shrink to
     the track, which is what .sub's ellipsis was always there to absorb. */
  min-width:0;
  /* The bar scrolls inside itself; the page does not. This was written for
     375px and lived under a max-width:719px query for months, which turned out
     to be the wrong shape for it: the fault is not "the window is small", it is
     "the bar holds more than fits", and on Kish's ADMIN board it holds a great
     deal more — five nav links against a customer's three, plus the plate box,
     plus a status pill that can read "Updated 51 min ago · 1 source came back
     thin". That overflowed at 1280px (scrollWidth 1375) and reproduced the
     exact 375px symptom on a laptop: .brand crushed to a zero-width box with
     RAVITO painted straight across DEALS, and Accounts/Billing/Account
     running off the right-hand edge.
     Stated once, unconditionally, because overflow-x:auto costs nothing until
     something actually overflows — at 1512px and up this does not engage at
     all — whereas a breakpoint is a guess about content that keeps changing. */
  overflow-x:auto; scrollbar-width:none;
  /* Makes the bar the containing block for its own absolutely-positioned
     descendants, so overflow-x can actually clip them. Without it the plate
     box's .sr-only label — position:absolute, 1px wide, no positioned ancestor
     — resolved against the initial containing block instead, escaped the
     clipping, and sat at x=483 on a 375px screen. One invisible pixel gave the
     whole PAGE a horizontal scrollbar while every visible thing behaved. */
  position:relative;
}
.topbar::-webkit-scrollbar{ display:none; }
/* The brand must be able to shrink. `.nav` and `.you` are flex:none and the
   plate box is a fixed width, so the only two things in this bar that can give
   are the brand and — when it is holding a pill rather than a button — the
   status slot.

   `overflow:hidden` is the half that stops the overlap. min-width:0 lets the
   box shrink below its own h1, and the h1 is white-space:nowrap with nothing
   to clip it, so the text did not shrink with the box — it spilled straight
   over the nav and painted RAVITO across DEALS.

   flex:none was tried here first and was worse than the bug it fixed. It made
   the brand rigid, so at 1512px — a MacBook, not an edge case — the whole
   right-hand side left the viewport: Board, Deals, Lookups, Searches, Ask,
   Accounts, Billing, Account and Sign out, all sitting past x=1818 behind a
   scrollbar that .topbar deliberately hides. Unreachable navigation is a
   worse fault than an ugly masthead, and it hid on the board too, not just on
   /searches whose subtitle runs to 211 characters.

   So what gives way is, in order: .sub ellipsises (it already asks for that),
   then the brand name clips. Both are readouts. Nothing you click is ever
   pushed off the end. */
.brand{
  display:flex; align-items:center; gap:12px; min-width:0; overflow:hidden;
}
.brand h1{
  margin:0; font:700 17px/1 var(--f-display);
  text-transform:uppercase; letter-spacing:.10em; color:var(--ink); white-space:nowrap;
}
.brand-rule{ flex:none; width:1px; height:18px; background:var(--hair-strong); }
.sub{
  /* min-width:0 is what lets the ellipsis this rule already asks for actually
     fire. A flex item defaults to min-width:auto and refuses to shrink below its
     text, so at 15px the whole-board readout pushed the topbar wider than the
     window and the page grew a horizontal scrollbar between 720 and 810px. */
  margin:0; min-width:0; font-size:15px; line-height:1.2; color:var(--ink-3);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.sub:empty{ display:none; }
.sub:empty + *{ margin-left:0; }
.brand .sub:empty ~ .brand-rule{ display:none; }

.nav{ margin-left:auto; display:flex; gap:14px; flex:none; }
.nav a{
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  text-decoration:none; color:var(--ink-3);
  padding:8px 2px; border-bottom:2px solid transparent;
  transition:color 90ms linear;
}
.nav a:hover{ color:var(--ink-2); }
.nav a.on{ color:var(--ink); border-bottom-color:var(--jade); }

/* Shrinkable, and it shrinks BEFORE the brand does — flex-shrink:2 against the
   brand's implicit 1. The board's status pill runs to 390px ("Updated 12 min
   ago · 1 source came back thin"), and while it was flex:none that 390px came
   straight out of the navigation's budget: at 1280 the brand was crushed to
   nothing AND Sign out still sat off the right edge.

   A pill is a readout with its full text already on the `title`, so an
   ellipsis costs nothing. Ordering matters more than the individual rules:
   what gives way is the pill, then the subtitle, then the brand name. Every
   one of those is something you read. Nothing you CLICK is ever pushed off
   the end of the bar. */
/* Squeezable ONLY when what is in it is a readout. `.head-right` is a generic
   slot — base.html:91 is `{% block headright %}` — and five pages fill it with
   something you click: "Add a search", "Edit stages", "Back to the pipeline"
   twice, "All accounts". Just under this, the comment on .brand promises that
   nothing clickable is ever pushed off the end of the bar; a blanket
   `min-width:0; overflow:hidden` here broke that promise for all five, which
   is the same fault as the one it was added to fix, one element along.

   :has() states the actual rule rather than approximating it. Where it is
   unsupported the slot simply does not shrink, which is the old behaviour —
   a bar that can overflow, never a button sliced in half. */
.head-right{ flex:0 0 auto; }
/* flex-shrink:2, against the brand's implicit 1, so the readout gives way
   FIRST and twice as fast. On the slot itself it would also squeeze the five
   pages that fill it with a button — and .btn has no white-space of its own,
   so "Add a search" folded onto two lines inside a 56px bar rather than
   clipping visibly. The shrink belongs to the case that can afford it. */
.head-right:has(.pill){ flex-shrink:2; min-width:0; overflow:hidden; }
.pill{
  display:inline-block; font:700 13px/1 var(--f-display);
  text-transform:uppercase; letter-spacing:.06em;
  padding:7px 10px; border-radius:var(--r-sm); white-space:nowrap;
}
/* Truncation belongs to the pill in the TOPBAR and nowhere else. The same
   class is used by the banners inside the page, where there is room for the
   whole sentence and an ellipsis would hide the half that says what to do
   about it. Scoping it here also keeps `overflow` off the global rule, which
   changes how an inline-block baselines and would have nudged every pill in
   the app off the text it sits beside. */
.topbar .pill{ max-width:100%; overflow:hidden; text-overflow:ellipsis; }
.pill-ok{ color:var(--jade); box-shadow:inset 0 0 0 1px var(--jade-3); }
.pill-warn{ color:var(--rust); box-shadow:inset 0 0 0 1px var(--rust-2); }
.pill-neutral{ color:var(--ink-3); box-shadow:inset 0 0 0 1px var(--hair-strong); }

.flashes:empty{ display:none; }
.flashes{ padding:8px 16px 0; }

/* One banner shared four ways: flash, board staleness, the metered-source
   notes and the vehicle-check problems block. No amber anywhere — amber is
   spent on the shortlist. */
.banner{
  background:var(--surface-2); border-left:3px solid var(--rust-2);
  border-radius:var(--r-sm); padding:10px 14px; margin:0 0 8px;
  color:var(--ink-2); font-size:15px; line-height:1.5;
}
.banner strong{ color:var(--ink); }
.banner-ok{ border-left-color:var(--jade); }
.banner-err-box{ border-left-color:var(--rust-2); }
.banner-note{ border-left-color:var(--hair-strong); }
.banner-note code{
  background:var(--bg); border-radius:var(--r-sm);
  /* A white chip on a tinted banner needs an edge to be a chip at all. */
  box-shadow:inset 0 0 0 1px var(--hair);
  padding:2px 6px; font:500 15px var(--f-mono); color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.banner-err{ display:block; margin-top:6px; font:500 15px var(--f-ui); color:var(--rust);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* A source that came back thin, which is not the same as one that fell over
   and must not borrow rust to say so — the board is fine and the rest of it is
   current. Plate yellow as a graphic mark, which is the job it is already
   defined for; no new hue enters the palette to carry this. */
.banner-thin{ border-left-color:var(--plate-mark); }
ul.source-health{ margin:6px 0 0; padding:0 0 0 18px; }
ul.source-health li{ margin:2px 0; font:400 15px var(--f-ui);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
ul.source-health b{ color:var(--ink); font-weight:600; }
.src-when{ color:var(--ink-2); }

/* Document pages: one scroller, one measured column inside a full-bleed page. */
.sheet{ overflow-y:auto; padding:28px 32px 72px; }
.sheet-inner{ max-width:1080px; margin:0 auto; }

.label{
  margin:0 0 8px; font:700 13px/1 var(--f-display);
  text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3);
}
.quiet{ color:var(--ink-3); }
kbd{
  display:inline-block; min-width:18px; text-align:center;
  font:600 13px/1 var(--f-data); letter-spacing:.02em; color:var(--ink-2);
  background:var(--surface-2); border:1px solid var(--hair-strong);
  border-radius:var(--r-sm); padding:2px 5px;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}

/* The ring is on the base, not on one modifier. A --surface-2 fill is 1.10:1
   against the --surface card it sits on, so on white a filled button has no edge
   at all — "Pause", "Shortlist", "Bin" and "Save note" were formless grey areas
   standing beside three properly-edged ghost buttons. On graphite a lighter fill
   read as raised; here only a line does. The three saturated fills below clear it
   again, because a hairline ring on a jade or rust slab is just a seam. */
.btn{
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  border:0; border-radius:var(--r-sm); padding:10px 14px; cursor:pointer;
  /* A label on a control is one line. Without this the topbar's button folded
     in two inside a 56px bar the moment the slot could shrink at all. */
  white-space:nowrap;
  background:var(--surface-2); color:var(--ink);
  box-shadow:inset 0 0 0 1px var(--hair-strong);
  transition:background-color 90ms linear;
}
/* The hover fill, not --hair: a value tuned as a hairline is lighter than the
   button it would fill, so the button would brighten under the pointer. */
.btn:hover{ background:var(--fill-hover); }
/* A near-black label was correct on both a bright jade and a graphite page,
   which is exactly why it got overlooked. On a mid green it fails. */
.btn-primary{ background:var(--jade-2); color:var(--on-hue); box-shadow:none; }
.btn-primary:hover{ background:var(--jade); }
/* Identical to the base now that the base carries the ring, and kept as a name
   the templates already use rather than as a different look. */
.btn-secondary{ background:var(--surface-2); color:var(--ink); }
.btn-ghost{ background:transparent; color:var(--ink-2); box-shadow:inset 0 0 0 1px var(--hair-strong); }
.btn-ghost:hover{ background:var(--surface-2); color:var(--ink); }
.btn-danger{ color:var(--rust); }
.btn-danger:hover{ background:var(--surface-2); color:var(--rust); }
.btn-block{ display:block; width:100%; }
/* Dimmed rather than faded. On white, .4 opacity is not "recessed", it is gone.
   Hollow rather than filled, too: while an unavailable button shared the enabled
   secondary's exact --surface-2 slab, an active "Pause" read as unavailable and
   the only thing separating them was the label's ink. An empty outline is the
   one shape on the page that says "there is nothing to press here". */
.btn:disabled{
  opacity:1; background:transparent; color:var(--ink-disabled); cursor:not-allowed;
  box-shadow:inset 0 0 0 1px var(--hair);
}
a.btn{ text-decoration:none; display:inline-block; }

/* Board stage ---------------------------------------------------------- */

.board{
  display:grid;
  grid-template-columns:var(--rail-w) minmax(0,1fr) var(--pane-w);
  grid-template-areas:"rail list pane";
  height:100%; min-height:0;
}
.rail{ grid-area:rail; }
.board__list{ grid-area:list; }
.pane{ grid-area:pane; }

.rail{
  display:flex; flex-direction:column; min-height:0;
  background:var(--surface); border-right:1px solid var(--hair);
}
.rail-scroll{ flex:1; overflow-y:auto; overscroll-behavior:contain; padding:12px; }
.rail-foot{ flex:none; border-top:1px solid var(--hair); padding:10px 12px; }

.rail-nav{ display:flex; flex-direction:column; gap:1px; }
.rail-nav a{
  display:flex; align-items:center; justify-content:space-between; gap:8px;
  height:34px; padding:0 10px; border-radius:var(--r-sm);
  text-decoration:none; color:var(--ink-2); font-size:15px;
  transition:background-color 90ms linear, color 90ms linear;
}
.rail-nav a:hover{ background:var(--surface-2); color:var(--ink); }
.rail-nav a.on{ background:var(--surface-2); color:var(--ink); box-shadow:inset 2px 0 0 var(--jade); }
.rc{ font:600 13px/1.2 var(--f-data); letter-spacing:0; color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.rc-plate{ color:var(--plate-text); }
.rc-plate[data-zero]{ color:var(--ink-3); }

.rail .label{ margin:16px 0 8px; }

/* The nine controls live in a <details> so the narrow layout can collapse them
   without removing them. filters.js addresses all nine by id and null-guards
   only #grid, so one missing control throws on addEventListener and silently
   kills filtering, sorting and the results count. */
.refine > summary{ display:none; list-style:none; }
.refine > summary::-webkit-details-marker{ display:none; }

.f{ display:block; margin-bottom:8px; }
.fk{
  display:block; margin-bottom:4px;
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.08em;
  color:var(--ink-3);
}
.rail select{
  width:100%; height:34px; padding:0 8px;
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm);
  /* 15px is also what stops iOS zooming a select on focus. */
  font:400 15px/1 var(--f-data); cursor:pointer;
}
.rail select:hover{ border-color:var(--hair-strong); }
/* Not a fixed height: at 13px the longest label ("Price dropped only") wraps
   inside the ≤1023 refine grid's 150px cell, and a fixed box would spill it. */
.f-check{
  display:flex; align-items:center; gap:8px;
  height:auto; min-height:34px; margin-bottom:0; cursor:pointer;
}
.f-check .fk{ margin:0; }
.f-check input[type=checkbox]{ width:14px; height:14px; accent-color:var(--plate-mark); cursor:pointer; }

/* List column ---------------------------------------------------------- */

/* Four rows: header, list, bulk bar, status. The bulk bar's track is `auto`, so
   while it is [hidden] — display:none, from the reset at the top of the sheet —
   it measures nothing and the list has exactly the height it always had. It is
   a row of the grid rather than an overlay on purpose: a bar floating over the
   list would cover the last few rows it is offering to bin, which are the ones
   you would check before pressing it. */
.board__list{
  position:relative;
  display:grid; grid-template-rows:30px minmax(0,1fr) auto 26px;
  min-height:0; border-right:1px solid var(--hair);
}
.listscroll{
  overflow-y:auto; overscroll-behavior:contain; scroll-padding-block:8px;
  background:var(--bg);
}
/* Louder than the status line under it, because it is the only bar on the board
   that does something irreversible-looking to a lot of rows at once. The plate
   edge ties it to the wash on the rows it is holding. */
.bulkbar{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  flex-wrap:wrap; padding:8px 12px;
  background:var(--surface); border-top:2px solid var(--plate-mark);
}
.bulk-count{
  font:700 13px/1.3 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.bulk-acts{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
/* The bin is stated in its own hue here and nowhere else on the bar: it is the
   one button that can take 1,600 rows off the page in a single press. */
.bulkbar .btn-bin{ color:var(--rust); }
.bulkbar .btn-bin:hover{ background:var(--rust-2); color:var(--on-hue); box-shadow:none; }

.statusline{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:0 12px; background:var(--surface); border-top:1px solid var(--hair);
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink-3); overflow:hidden; white-space:nowrap;
}
.statusline kbd{ letter-spacing:0; }
.layout{ display:flex; gap:2px; flex:none; }
.lay{
  background:none; border:0; cursor:pointer; padding:4px 7px; border-radius:var(--r-sm);
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink-3); transition:color 90ms linear, background-color 90ms linear;
}
.lay:hover{ color:var(--ink-2); }
.lay.on{ color:var(--ink); background:var(--surface-2); }

/* =====================================================================
   3. THE LIST — column header, rows, chips, tags
   ===================================================================== */

/* One source of truth, two consumers: the header bar and every row. The MILES
   track is sized to the widest real value ("160,000 miles" at 15px is about
   98px), because the cell prints the unit and a clipped odometer reading is
   worse than a narrower name. Each band below drops a whole column rather than
   squeezing every one of them.

   The name track is capped rather than left as the only flexible one. As a
   lone 1.6fr it swallowed every pixel of a wide monitor — 1,101px at 2560 for
   titles that run to about 250 — so each row read as a short title, an ocean,
   and then the money exiled 800px away at the right edge. The trailing track
   takes the surplus instead, and the numeric columns stay a fixed, learnable
   distance from the title at every width.

   Every fixed track is sized from what its widest real value measures, plus its
   10px gutter: price and gap need 81, miles 112 ("120,000 miles"), away 34,
   the listing age 88, and the actions cell 62 for two 30px buttons and the gap
   between them. The sum has to clear the narrowest viewport in each band with
   the name at its minimum, or the row silently loses its right-hand end:
   content-visibility gives every row paint containment, so an overflowing track
   is clipped in place rather than producing a scrollbar that would show it. At
   1440 the bin button was disappearing exactly that way.

   Thirteen tracks now, and two of them are new. The SELECTION box takes 28px
   after the rail and is paid for out of the pane, not out of the name: the row's
   trailing slack is zero at 1280, 1440 and 1680, so the only place it could
   otherwise have come from is the 21–27px keeping a third of titles from
   truncating further.

   The UPDATED age gets a track of its own only above 1760px. At 1680 the name
   sits 53px above its minimum and a legible age column costs 64; below that the
   two ages collapse into the Listed cell instead — see .v-pair. Neither age is
   ever dropped, and the collapsed form costs no horizontal pixels at all,
   because .v-when is a vertical flex cell in a 76px row whose tallest content
   is 41px.

   FOURTEEN tracks now, and the newest of them is the PHONE. It was paid for by
   retiring the seller word's own column outright rather than by taking pixels
   the row has not got: the seller track only ever existed above 1680px, the
   word has had a chip on the meta line at every narrower width since the board
   was built, and a number that saves opening AutoTrader to ring somebody is
   worth more than a word that says "Trade" beside a row already sorted by it.
   That buys 76px at 1680 and the remaining 28 come out of the trailing slack,
   which measures 84px there once the seller column has gone.

   Below 1680 the phone track is zero and the number rides on the chip line
   instead, the same collapse the seller word and the two ages already use — the
   row has 24px of slack at 1440 and a legible number costs 104. Nothing is
   dropped at any width; it only moves. */
.board__list{
  --row-cols:4px 28px 96px minmax(180px,640px) 88px 88px 112px 48px 0 88px 0 0 64px minmax(0,1fr);
}
@media (min-width:1680px){
  .board__list{ --row-cols:4px 28px 104px minmax(200px,640px) 96px 96px 112px 56px 0 88px 0 120px 68px minmax(0,1fr); }
}
/* The one width with room for a twelfth visible column. */
@media (min-width:1760px){
  .board__list{ --row-cols:4px 28px 104px minmax(200px,640px) 96px 96px 112px 56px 0 88px 64px 120px 68px minmax(0,1fr); }
}
@media (max-width:1439px){
  .board__list{ --row-cols:4px 26px 96px minmax(170px,640px) 88px 88px 112px 48px 0 0 0 0 64px minmax(0,1fr); }
}

.colhead, .van{
  display:grid; grid-template-columns:var(--row-cols);
  align-items:center; column-gap:0;
}

/* The only place the uppercase micro register is dropped, and it is arithmetic
   rather than taste that forces it: at 13px/700/.06em, UNDER EXPECTED measures
   130px into 78px of track, AWAY 42px into 38px and ALL LISTINGS 99px into 88px.
   Sentence case at 13px/600 fits every one of them. The bar still reads as a
   header through its weight (600 against the rows' 500) and the rule beneath. */
.colhead{
  background:var(--surface); border-bottom:1px solid var(--hair);
  font:600 13px/1 var(--f-display); text-transform:none; letter-spacing:.01em;
  color:var(--ink-3); overflow:hidden;
  /* The header bar sits outside the scroller, so it is as wide as the column
     while the rows are a scrollbar narrower. Without matching that gutter every
     header reads 16px right of the figures underneath it. triage.js measures
     the real width, which is 0 where the scrollbars are overlays. */
  padding-right:var(--gutter,0px);
}
.colview{ padding-left:8px; font-weight:500; color:var(--ink-2); overflow:hidden; text-overflow:ellipsis; }
.colcount{
  padding-left:12px; font:400 15px/1 var(--f-text); letter-spacing:0;
  text-transform:none; color:var(--ink-2);
  overflow:hidden; white-space:nowrap; text-overflow:ellipsis;
}
/* The casing and tracking are restated because these are form controls: some UA
   sheets set text-transform and letter-spacing on every button, and
   `font: inherit` carries neither of them. Without this the five sortable
   headers could pick up a casing the rest of the bar has deliberately dropped,
   which reads as an unfinished header bar rather than a deliberate one. */
.colsort, .colseller{
  background:none; border:0; padding:0 10px 0 0; height:100%;
  font:inherit; color:inherit; text-align:right;
  text-transform:none; letter-spacing:.01em;
  border-bottom:2px solid transparent;
}
.colsort{ cursor:pointer; transition:color 90ms linear; }
.colsort:hover{ color:var(--ink-2); }
/* No arrow. It was the smallest type in the app, it did not indicate direction
   (there is one button per column), and the active column is already marked
   twice over — a 2px jade underline and a jump from --ink-3 to --ink. */
.colsort.on{ color:var(--ink); border-bottom-color:var(--jade); }
/* These sit over left-aligned values, so they read left as well. */
.colseller, .colwhen, .colupd{ text-align:left; padding:0 0 0 4px; }
/* Same 28px track as the row's box, so the tick in the header sits directly
   over the ticks it controls. */
.colcheck{ display:flex; align-items:center; justify-content:center; }
/* The same box as the rows', including the accent: left to the UA the header's
   indeterminate dash came back in the browser's own blue, which is the one
   colour on this board that means focus. */
.colcheck input{
  width:15px; height:15px; margin:0; cursor:pointer;
  accent-color:var(--plate-mark);
}

/* Rows ------------------------------------------------------------------ */

.grid{ display:block; }

.van{
  position:relative; height:var(--row-h);
  text-decoration:none; color:inherit;
  background:var(--bg);
  border-bottom:1px solid var(--hair);
  /* Fixed-height rows are the ideal case for this: 1,982 of them stay fluid
     because the ones off screen are never laid out. */
  content-visibility:auto;
  contain-intrinsic-size:auto var(--row-h);
  transition:background-color 60ms linear, opacity 120ms linear;
}
/* Every row is class="van card", and .card sets --surface further down the sheet
   at the same specificity, so on source order it would win and the rows would
   come out as tinted plates on a white scroller — the data plane dirtier than
   the instrument around it, which is backwards. Two classes settle it wherever
   the two rules happen to sit. .van:hover, .is-cursor and the gallery override
   it exactly as they did before. */
.van.card{ background:var(--bg); }

/* Held for a bulk action. Stated before hover and the cursor rather than after,
   so at equal specificity those two still win on source order: "where I am" has
   to stay readable on top of "what I have picked", and there is no third
   background level that could say both at once. The tick in the box is the
   mark that survives either — it is plate-marked and does not move. */
.van.is-picked{ background:var(--pick); }

/* Nothing per row that multiplies badly: no shadow, no transform, no filter.
   Depth is carried by hairlines and two background levels. */
.van:hover{ background:var(--row-hover); }
.van.is-cursor, .van:focus-visible{
  background:var(--row-cursor);
  outline:2px solid var(--focus);
  outline-offset:-2px;   /* inset, so it never clips against its neighbours */
}
/* Receding is done with ink and weight, not opacity. On graphite, fading pulled
   text toward the ground and read as recessed; on white the same arithmetic
   pulls it toward invisible. One ink step down plus one weight step down says
   the same thing and stays readable. */
.van.is-gone .v-t,
.van.is-gone .v-price b,
.van.is-gone .v-gap b{ color:var(--ink-3); font-weight:500; }
/* The rail and the AVOID chip already say it in colour, so the title only has
   to stop competing. */
.van.v-avoid .v-t{ color:var(--ink-2); font-weight:500; }
.van.is-binned .v-t,
.van.is-binned .v-price b,
.van.is-binned .v-gap b{ color:var(--ink-3); font-weight:500; }
.van.is-binned .v-shot img{ filter:grayscale(1); }
.van.is-binned:hover .v-t, .van.is-binned.is-cursor .v-t,
.van.is-binned:hover .v-price b, .van.is-binned.is-cursor .v-price b,
.van.is-binned:hover .v-gap b, .van.is-binned.is-cursor .v-gap b{
  color:var(--ink); font-weight:600;
}
.van.is-binned:hover .v-shot img, .van.is-binned.is-cursor .v-shot img{ filter:none; }
/* A shortlisted van that has sold is information, not clutter. `inherit` on the
   gap figure, so it picks its hue back up from .v-gap.under / .over rather than
   being pinned to a grey. */
.view-shortlist .van.is-gone .v-t,
.view-shortlist .van.is-gone .v-price b{ color:var(--ink); font-weight:600; }
.view-shortlist .van.is-gone .v-gap b{ color:inherit; font-weight:700; }
.van.is-leaving{ opacity:0; }

/* The whole 28px track is the target, not the 15px box: on a triage board the
   thing you aim at should be as big as the row is tall. */
.v-check{ display:flex; align-items:center; justify-content:center; height:100%; }
.v-box{
  width:15px; height:15px; margin:0; cursor:pointer;
  accent-color:var(--plate-mark);
}

.v-rail{ height:100%; background:var(--v-ungraded); }
.v-definitely_buy .v-rail{ background:var(--v-definitely_buy); }
.v-buy .v-rail{ background:var(--v-buy); }
.v-negotiate .v-rail{ background:var(--v-negotiate); }
.v-maybe .v-rail{ background:var(--v-maybe); }
.v-avoid .v-rail{ background:var(--v-avoid); }
/* Being on the list outranks the verdict. The graphic sibling of plate yellow:
   the fill value is 1.68:1 on white and would not be a mark at all. */
.van.is-saved .v-rail{ background:var(--plate-mark); }
/* Except on the shortlist itself, where every row is saved and a plate rail down
   the whole column says nothing at all — while the one view whose entire job is
   ranking your candidates against each other loses the only per-row signal it
   had for how they ranked. Here the verdict takes the rail back. */
.view-shortlist .van.is-saved.v-definitely_buy .v-rail{ background:var(--v-definitely_buy); }
.view-shortlist .van.is-saved.v-buy .v-rail{ background:var(--v-buy); }
.view-shortlist .van.is-saved.v-negotiate .v-rail{ background:var(--v-negotiate); }
.view-shortlist .van.is-saved.v-maybe .v-rail{ background:var(--v-maybe); }
.view-shortlist .van.is-saved.v-avoid .v-rail{ background:var(--v-avoid); }

.v-shot{
  position:relative; height:100%; overflow:hidden;
  background:var(--surface-2); display:block;
}
.v-shot img{ width:100%; height:100%; object-fit:cover; object-position:center 55%; display:block; }
.v-noshot{
  height:100%; display:grid; place-items:center;
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink-3);
}
/* Stays dark, and deliberately: it sits on a photograph, where white on a dark
   scrim is the only treatment that works over an unknown image. */
.v-shots{
  position:absolute; right:0; bottom:0; padding:1px 4px;
  background:var(--scrim); color:var(--on-hue);
  font:500 13px/1.4 var(--f-data); font-style:normal;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* Flattened, not faded. At .6 on white a photograph reads as a failed load. */
.is-gone .v-shot img{ filter:grayscale(1) contrast(.92) brightness(1.04); }

.v-name{
  min-width:0;              /* without it the grid refuses to shrink and the
                               ellipsis never fires */
  padding-left:12px; display:flex; flex-direction:column; gap:2px; overflow:hidden;
}
.v-t{
  font:600 17px/1.25 var(--f-text); letter-spacing:-.006em; color:var(--ink);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.v-s{
  font:400 15px/1.3 var(--f-text); color:var(--ink-2);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.v-meta{ display:flex; align-items:center; gap:4px; overflow:hidden; }

/* inline-BLOCK, not inline-flex, and that is the whole reason the rule below
   works: text-overflow has no effect on the anonymous run of text inside a flex
   container, so an inline-flex chip clips mid-letter however you style it. .tag
   has always been inline-block, which is why it has always ellipsised properly.
   A single centred line needs none of what flex was giving it — line-height does
   the same job — so the two chip forms now behave the same way. */
.chip{
  flex:none; height:20px; display:inline-block;
  padding:0 6px; border-radius:var(--r-sm);
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.05em;
  line-height:20px;   /* after the shorthand, which resets it to 1 */
  /* The same protection .tag has, and for the same reason: a chip sheared
     through the middle of a word with its own 1px ring sliced off reads as a
     rendering fault rather than as a narrow column. .v-meta clips from the
     right, so at 1440 with the pane closed the seller word was coming out as
     "PR" with no right-hand edge. Ellipsised inside its own border it still
     looks like a chip. */
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

/* Two families on one line, and they have to be told apart without reading them.
   A fact about the advert — New, Price drop, Gone — is a SOLID. A judgement
   about the van is an OUTLINE. Before the split, "Price drop" and "Buy" were
   byte-identical chips (jade text, jade-2 ring) sitting next to each other, as
   were "Gone" and "Avoid"; a row showed two identical green badges and nothing
   said which was the advert and which was the opinion. */

/* The verdict ladder inverts in lightness, not in meaning: more emphatic means
   darker and heavier on white. The three positive verdicts used to differ only
   in a 1px ring, 1.43:1 and 1.35:1 apart from each other, which on a 20px chip
   is no difference at all — so the ladder is carried by the weight of the mark
   instead, where it can actually be seen: washed and double-ringed, then ringed
   in the hue, then ringed in the palest hue with the words in plain ink. */
.verdict-definitely_buy{
  color:var(--jade); background:var(--jade-wash);
  box-shadow:inset 0 0 0 2px var(--v-definitely_buy);
}
.verdict-buy{ color:var(--jade); box-shadow:inset 0 0 0 1px var(--v-buy); }
.verdict-negotiate{ color:var(--ink-2); box-shadow:inset 0 0 0 1px var(--v-negotiate); }
.verdict-maybe{ color:var(--ink-3); box-shadow:inset 0 0 0 1px var(--v-maybe); }
.verdict-avoid{ color:var(--rust); box-shadow:inset 0 0 0 1px var(--v-avoid); }

/* On graphite three pale outlines in a row were all legible. On white an outline
   is the weakest mark there is, so the source chip is filled as well as outlined.
   The seller word drops its ring entirely: it was --ink-3 text inside a
   --hair-strong ring, which put it within 1.9:1 of the "maybe" verdict at the
   same size and casing, so "no judgement yet" looked like a fact about who was
   selling. Now every ringed chip on the line is a verdict and the unringed ones
   are not, which is the distinction worth being able to make at a glance. */
.chip-source{
  color:var(--ink-2); background:var(--surface-2);
  box-shadow:inset 0 0 0 1px var(--hair-strong);
}
/* Now shown at every width, not only below 1680: the seller's own column has
   been given to the phone, and this chip is where the word lives from here on.
   It was already the collapsed form on every screen narrower than a 27in
   monitor, so for almost every real session nothing about the row has changed. */
.chip-seller{
  display:inline-block; color:var(--ink-3); background:transparent;
  box-shadow:none; padding:0 2px; order:1;
}

/* The phone, on the chip line, for every width where its own column is zero.
   A button rather than a link — the row is one anchor and a nested one would be
   reparented — so the UA's button styling has to be undone before .chip's own
   rules can take. Plate ink, the hue that already means "on your list": it is
   the only thing on the line you can press, and it must not read as a verdict. */
.chip-phone{
  order:-1; flex:none; border:0; cursor:pointer; font-family:inherit;
  color:var(--plate-text); background:var(--surface-2);
  letter-spacing:.01em; font-variant-numeric:tabular-nums;
}
.chip-phone:hover{ background:var(--fill-hover); }
@media (min-width:1680px){
  /* Its own track up here, so the chip would be the same number twice. */
  .chip-phone{ display:none; }
}

/* A failed vehicle check. One of three adverts probed came back "Recorded as
   Category S", which is the single most decision-changing thing the enrichment
   returns, so it leads the line and never shrinks. */
.chip-warn{
  order:-3; flex:none;
  color:var(--rust); background:transparent;
  box-shadow:inset 0 0 0 1px var(--rust-2);
}

/* An auction, and its clock. Two tones and no more: about to close, and
   already closed. A nine-day auction is quiet, because it is not a decision to
   make today — which is the whole reason the clock is on the row at all. */
.chip-auction{
  order:-2; flex:none;
  /* Sentence case here too: "ENDS IN 2H 05M" measures 120px of a meta line
     that also has to hold a verdict, a source and a seller, and it was clipping
     the source chip to "EB…". "Ends in 2h 05m" measures 95. */
  text-transform:none; letter-spacing:.01em;
  color:var(--ink-2); background:var(--surface-2);
  box-shadow:inset 0 0 0 1px var(--hair-strong);
}
.chip-auction-urgent{
  color:var(--on-hue); background:var(--rust); box-shadow:none;
}
.chip-auction-ended{
  color:var(--ink-3); background:transparent;
  box-shadow:inset 0 0 0 1px var(--hair-strong);
}

/* The sacrifice order this line is designed around — state, note, verdict,
   source, seller, the last of which is the least missed — made real. Every chip
   was flex:none, so a meta line narrower than its contents did not sacrifice
   anything: it simply overflowed and .v-meta clipped whatever ran past the right
   edge. At 1280 that sheared "Buy but negotiate" in half on a row that had a
   seller word going spare, and at 1024 with the drawer out the name track is
   150px, which no verdict label fits.

   Shrink weights spend them in the intended order — the seller collapses first,
   then the source, and only then does the verdict give ground — and because
   .chip now ellipsises, each one shortens inside its own border rather than
   being cut through a letter. The state chips and the note dot keep flex:none:
   they are the whole reason the line carries anything at these widths. */
.chip-seller{ flex:0 1000 auto; min-width:0; }
.chip-source{ flex:0 100 auto; min-width:0; }
.verdict-definitely_buy, .verdict-buy, .verdict-negotiate,
.verdict-maybe, .verdict-avoid{ flex:0 1 auto; min-width:0; }
/* New, Price drop and Gone, for the widths where the LISTED column is gone.
   Without them a sold van on the shortlist looked exactly like a live one on
   any laptop screen, and nothing on the board said what had arrived since the
   last visit. First in the line, so if the chip row runs out of room it is the
   verdict and the source that get clipped, not the state. */
.chip-when{ display:none; order:-1; }
/* One family, three solids. New keeps the loudest of them — unread should be
   the mark you find first — but it comes off near-black: at #12171F it was the
   darkest thing on a light board by a distance, the only solid dark fill not
   sitting on a photograph, and it out-shouted the verdict beside it, which is
   the chip that should win the row. --ink-2 is still 9:1 white-on-fill and
   still the heaviest state mark, without reading as a piece of the old theme. */
.chip-new{ background:var(--ink-2); color:var(--on-hue); }
.chip-drop{ background:var(--jade); color:var(--on-hue); }
.chip-gone{ background:var(--rust); color:var(--on-hue); }
/* 8px, not 5. It is the only thing on the board that says you have already
   written something about this van — the exact fact that should stop you
   researching a listing you rang the seller about last week — and at 5px on a
   76px row, between two chips, it was the easiest mark here to scan past. */
.v-dot{
  flex:none; width:8px; height:8px; border-radius:50%; background:var(--plate-mark);
}

.v-price{
  display:flex; flex-direction:column; align-items:flex-end; justify-content:center;
  padding-right:10px; overflow:hidden;
}
.v-price b{ font:600 17px/1.15 var(--f-data); letter-spacing:-.005em; color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.v-price s{ font-size:13px; line-height:1.2; color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.v-price .vat{
  font:700 13px/1.2 var(--f-display); font-style:normal;
  text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3);
}
/* "+ VAT" means the number above it is a fifth light. Scanning past one is a
   real four-figure mistake, so it is the one label that gets a hue. */
.v-price .vat-plus{ color:var(--rust); }

/* "Current bid". The one label on this board that changes what the number above
   it MEANS, so it is set in the same weight as "+ VAT" and in plate ink rather
   than grey — a bid is a live thing, not a footnote. The row is deliberately
   not tinted: a whole coloured row would say "good" or "bad", and an auction is
   neither until it closes.

   Sentence case, which is the third and last place this sheet bends that rule
   and it bends for the same arithmetic as the other two: at 13px/700/.06em
   "CURRENT BID" measures 90px into an 88px cell and wrapped to two lines, one
   of which was the word "BID" on its own. Sentence case measures 72 and fits. */
.v-price .bidlabel{
  font:700 13px/1.2 var(--f-display); font-style:normal;
  text-transform:none; letter-spacing:.01em; color:var(--plate-text);
}

/* A mileage read out of the seller's prose rather than typed into the site's
   own box. Marked, not captioned: "120,000 miles" already measures 98px of a
   112px track, so a second word would push the odometer out of its own cell.
   A dotted rule costs no width at all and the sentence rides on the title —
   quiet on purpose, because a figure from a description is worth ranking on
   and worth knowing about, but it is not a warning. */
.v-miles b.said{
  text-decoration:underline dotted var(--hair-strong);
  text-underline-offset:3px;
}

/* The signature. Read down thirty rows it is a jagged edge you take in before
   you read a single digit. */
.v-gap{
  display:flex; flex-direction:column; align-items:flex-end; justify-content:center;
  /* 6, not 10: the widest real value on this board is "+£25,064" at 82px in an
     88px track. This is the only track-side adjustment in the re-theme, and it
     moves nothing — the figure stays right-aligned where it always was. */
  padding-right:6px; overflow:hidden;
}
/* nowrap, because a five-figure gap ("−£14,270") is a hair wider than the track
   and a broken minus sign sitting alone on the line above its own number is the
   one thing this column must never do. */
.v-gap b{ font:700 17px/1.1 var(--f-data); letter-spacing:-.005em; white-space:nowrap;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* Full strength. The hue is now text-grade on white, so a 3px bar at opacity 1
   is exactly as loud as .55 of a bright hue was on graphite — and it carries
   meaning, so it must not be the thing that fades. */
.v-gap .bar{
  display:block; height:3px; width:var(--bar,0%); max-width:88px;
  margin:2px 0; background:currentColor; opacity:1;
}
.v-gap i{ font:500 13px/1.2 var(--f-data); font-style:normal; letter-spacing:0; color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* currentColor on the bar means the figure and its bar can never disagree
   about which way the money runs. */
.v-gap.under{ color:var(--jade); }
.v-gap.over{ color:var(--rust); }
.v-gap.none{ color:var(--ink-3); }
.v-gap .nil{ font-weight:500; }

.v-miles, .v-away{
  display:flex; align-items:center; justify-content:flex-end;
  padding-right:10px; overflow:hidden;
}
.v-miles b, .v-away b{ font:500 15px/1.15 var(--f-data); color:var(--ink-2); white-space:nowrap;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.v-away.near b{ font-weight:600; color:var(--ink); }

.v-seller{
  font:700 13px/1.2 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink-3); padding-left:4px; overflow:hidden; white-space:nowrap;
}
.van[data-seller="private"] .v-seller{ color:var(--ink-2); }
/* Collapsed at every width now — the seller word gave its track to the phone.
   It kept a column only above 1680px and it has always had .chip-seller on the
   meta line below that, so the word is exactly where it has always been for
   anyone not on a 27in monitor. visibility, never display: a display:none grid
   item stops being an item and every cell after it slides up a track. */
.v-seller, .colseller{ visibility:hidden; padding:0; }

.v-when{
  display:flex; flex-direction:column; align-items:flex-start; justify-content:center;
  /* 1, not 2: three tags at once plus the age line needs 74.3px of a 76px row. */
  gap:1px; padding-right:8px; overflow:hidden;
}
.v-when i{ font:500 13px/1.2 var(--f-data); font-style:normal; color:var(--ink-3); white-space:nowrap;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* When the board last confirmed this one was still there — or, if it has gone,
   when it went. Its own track only above 1760px; below that the pair collapses
   into .v-pair and this cell is a zero-width, visibility:hidden grid item. */
.v-upd{
  display:flex; align-items:center; justify-content:flex-start;
  padding-right:8px; overflow:hidden;
}
.v-upd i{ font:500 13px/1.2 var(--f-data); font-style:normal; color:var(--ink-3); white-space:nowrap;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* The phone. Its own track above 1680px; below that the cell is a zero-width,
   visibility:hidden grid item and the number rides on the chip line — the same
   collapse .v-upd and .v-seller use, and for the same reason: a display:none
   grid item stops being an item and every cell after it slides up a track,
   which would land the action buttons on a zero. */
.v-tel{
  display:flex; align-items:center; justify-content:flex-start;
  /* 6, not 8: "0116 4848188" measures 100px at 15px, and a phone number
     ellipsised in the middle is not a phone number. */
  padding-right:6px; overflow:hidden;
}
/* A button, because the row is one anchor and a nested <a href="tel:"> would be
   reparented by the parser — which would tear the row out of #grid's flat child
   list and take sorting with it. It is still a link in every way that shows:
   triage.js sends the click to tel:, and the pane and the listing page both
   carry the real anchor. */
.v-tel .tel{
  border:0; padding:0; background:none; cursor:pointer; text-align:left;
  /* No letter-spacing. "0116 4848188" measures 104.2px at 15px/600 and the
     track has 114 to give once its gutter is paid; .01em put it at 112 into
     110 and ellipsised the last digit, which is the one failure this cell must
     never have — half a phone number is not a phone number. */
  font:600 15px/1.2 var(--f-data); color:var(--plate-text);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;
}
.v-tel .tel:hover{ text-decoration:underline; text-underline-offset:3px; }
/* Nothing to ring, and the cell says why on hover rather than sitting blank —
   three of the five sources never publish a number, and a bare gap on all of
   them reads as a broken feature rather than as the way those sites work. */
.v-tel .nil{ font:500 15px/1.2 var(--f-data); font-style:normal; color:var(--ink-3); }
/* The advert has gone. How long an AutoTrader proxy number stays alive after
   that is not known, so it is shown but not offered. */
.v-tel.is-stale .tel{ color:var(--ink-3); font-weight:500; }
.coltel{ text-align:left; padding:0 0 0 4px; }

/* The two ages in one cell, for every width without room for the second track.
   It wraps on purpose: "listed 3d · seen 2h" measures about 105px at the row's
   13px, and the Listed track is 88px with 8px of padding. The row has 35px of
   unused vertical space — the name stack is 41px in a 76px row — so the second
   line costs nothing, where a second column would have cost the name. */
.v-pair{ display:none; }
.v-pair span{ display:block; }
/* Sentence case, for the same reason the column header is: this is the second
   fixed track where the uppercase micro register does not fit. "PRICE DROP" at
   13px/700/.06em measures 100px into 80px of cell, so every price drop on the
   board would read "PRICE DRO…". Sentence case measures 80px and fits, and it
   is the more legible of the two — which is the whole point of the exercise.
   The chip form of these three labels stays uppercase: it sits on the flexible
   meta line, where the clipping order is already designed rather than forced.
   These are the only two places the rule bends. */
.tag{
  /* 1px of vertical padding, not 2: three tags and the age line together need
     72.3px of a 76px row, and at 2px they came to 78.3 and were clipped. */
  display:inline-block; max-width:100%; padding:1px 6px; border-radius:var(--r-sm);
  font:700 13px/1.25 var(--f-display); text-transform:none; letter-spacing:.01em;
  /* If a tag ever does outgrow its cell it ellipsises inside its own border,
     rather than being sheared through the middle of a word with the border cut
     off — which reads as a rendering fault rather than as a narrow column. */
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
/* The same three solids as the chip forms, so a fact about the advert states
   itself the same way whether it is sitting in the LISTED column or has moved
   onto the meta line — and so neither of them can be mistaken for the outlined
   verdict badge. White clears 9:1 on the first and 6.4:1 on the other two. */
.tag-new{ background:var(--ink-2); color:var(--on-hue); }
.tag-drop{ background:var(--jade); color:var(--on-hue); }
.tag-gone{ background:var(--rust); color:var(--on-hue); }

/* Always visible, never hover-only: a triage board needs a target you can
   aim at before the pointer arrives. */
.v-act{ display:flex; align-items:center; justify-content:center; gap:2px; }
/* A resting fill and a hairline border, because two bare glyphs at the end of a
   row read as typographic ornament on the 88% of rows that carry no verdict —
   and this is the control the whole board exists to press. One background and
   one border per row, no shadow and no transform. */
.act{
  width:30px; height:30px; padding:0; border-radius:var(--r-sm);
  background:var(--surface-2); border:1px solid var(--hair);
  color:var(--ink-3); cursor:pointer;
  display:grid; place-items:center;
  transition:background-color 90ms linear, color 90ms linear, border-color 90ms linear;
}
.van:hover .act, .van.is-cursor .act{ color:var(--ink-2); border-color:var(--hair-strong); }
.act svg{ width:17px; height:17px; fill:none; stroke:currentColor; stroke-width:1.6; }
.act-bin svg{ stroke-width:1.8; stroke-linecap:round; }
.act:hover{ background:var(--fill-hover); color:var(--ink); border-color:var(--hair-strong); }
.act[aria-pressed="true"]{ color:var(--plate-mark); border-color:currentColor; }
.act-star[aria-pressed="true"] svg{ fill:currentColor; }
.act-bin[aria-pressed="true"]{ color:var(--rust); border-color:currentColor; }
/* The single flourish in the design, on the one action that matters. */
.act.pop svg{ animation:vw-pop 180ms cubic-bezier(.34,1.56,.64,1); }
@keyframes vw-pop{ 0%{ transform:scale(1); } 55%{ transform:scale(1.18); } 100%{ transform:scale(1); } }
@keyframes vw-undone{
  0%{ width:4px; opacity:1; } 45%{ width:7px; opacity:.4; } 100%{ width:4px; opacity:1; }
}
.van.is-undone .v-rail{ animation:vw-undone 600ms ease-in-out; }

.empty{ text-align:center; color:var(--ink-3); padding:56px 0; font-size:15px; }
.blank{ margin:32px auto; max-width:520px; text-align:center; padding:26px; }
.blank p{ margin:0 0 8px; color:var(--ink-2); font-size:15px; line-height:1.55; }
.blank strong{ color:var(--ink); }
/* The one monospace left in the sheet, and it earns it: this is a command you
   retype, where l/1/I and O/0 have to be unambiguous. Full ink for the same
   reason. */
.blank code{
  display:inline-block; background:var(--surface-2); border:1px solid var(--hair);
  border-radius:var(--r-sm); padding:9px 12px; font:500 15px var(--f-mono); color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.card{ background:var(--surface); border:1px solid var(--hair); border-radius:var(--r-md); }
.listscroll .banner{ margin:10px 12px; }

/* Gallery — same DOM, one class.
   The card height is fixed rather than driven by its photo's aspect ratio. In a
   grid, every item feeds row sizing, so an auto height forces the browser to lay
   out all 1,982 of them and content-visibility can skip nothing — which locked
   the page solid. A known height and a fixed contain-intrinsic-size mean an
   off-screen card costs nothing, exactly as in the row list. */
#grid.view-gallery{
  display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr));
  grid-auto-rows:var(--tile-h); gap:1px; background:var(--hair);
}
.view-gallery .van{
  --tile-h:296px;
  grid-template-columns:minmax(0,1fr) auto; grid-template-rows:180px auto auto;
  height:var(--tile-h); background:var(--bg); border-bottom:0; padding-bottom:8px;
  contain-intrinsic-size:240px 296px;
}
.view-gallery .v-rail{ position:absolute; inset:0 auto 0 0; width:4px; z-index:1; }
.view-gallery .v-shot{ grid-column:1/-1; grid-row:1; width:100%; height:180px; aspect-ratio:auto; }
.view-gallery .v-shot img{ width:100%; height:100%; }
.view-gallery .v-name{ grid-column:1; grid-row:2; padding:10px 0 0 12px; align-self:start; }
.view-gallery .v-price{ grid-column:2; grid-row:2; align-self:start; padding-top:10px; }
.view-gallery .v-gap{ grid-column:2; grid-row:3; align-self:end; }
.view-gallery .v-act{ grid-column:1; grid-row:3; justify-content:flex-start; padding-left:8px; align-self:end; }
.view-gallery .v-miles,
.view-gallery .v-away,
.view-gallery .v-seller,
.view-gallery .v-when,
.view-gallery .v-upd,
.view-gallery .v-tel{ display:none; }
/* ...which means the chip has to come back, at every width. A card has no
   columns, so above 1680 the phone would otherwise be nowhere at all: the cell
   is display:none here and the chip is display:none up there. */
.view-gallery .chip-phone{ display:inline-block; }
/* A card has no columns to give the box, so it sits on the photograph, top
   left, where a photo grid has always put a selection tick. The z-index is not
   optional: .v-shot comes after it in the DOM and would paint straight over it. */
.view-gallery .v-check{
  grid-column:1; grid-row:1; z-index:2;
  align-self:start; justify-self:start; height:auto; padding:8px;
  /* Same treatment as the photo count and the drawer's close button: a scrim,
     because a bare tick on an unknown photograph can land on anything. */
  background:var(--scrim); border-bottom-right-radius:var(--r-md);
}
/* A card has none of the row's columns, so the header keeps only the two things
   that still mean something: what you are looking at, and how it is laid out.
   No sorting is lost — #f-sort is in the rail and is the accessible control. */
/* Three tracks, not two: "select all shown" still means something over a card
   grid, so it keeps its cell and the two surviving labels keep theirs. A
   fourth visible child in a two-track header would wrap onto a second row and
   double the bar's height. */
.board__list.layout-gallery .colhead{ grid-template-columns:auto auto minmax(0,1fr); }
.board__list.layout-gallery .colsort,
.board__list.layout-gallery .colseller,
.board__list.layout-gallery .colhead > span:empty{ display:none; }

/* =====================================================================
   4. PANE, TOAST, KEY SHEET
   ===================================================================== */

.pane{
  position:relative;
  background:var(--surface); border-left:1px solid var(--hair);
  overflow-y:auto; overscroll-behavior:contain; min-height:0;
}
.pane-close{ display:none; }

.pane-empty{
  padding:60px 24px; text-align:center; color:var(--ink-3); font-size:15px; line-height:1.55;
}
.pane-empty p{ margin:0 0 8px; }

.pane-shot{
  aspect-ratio:4/3; background:var(--surface-2); overflow:hidden;
  display:grid; place-items:center;
}
.pane-shot img{ width:100%; height:100%; object-fit:cover; object-position:center 55%; display:block; }

.pane-body{ animation:vw-fade 90ms linear; }
@keyframes vw-fade{ from{ opacity:.6; } to{ opacity:1; } }

.pane-money{ padding:18px 20px 14px; }
.pane-price{ display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
.pane-price b{ font:700 30px/1.05 var(--f-data); letter-spacing:-.02em; color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.pane-price .vat{
  font:700 13px/1 var(--f-display); font-style:normal;
  text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3);
}
.pane-price .vat-plus{ color:var(--rust); }
.pane-was{ margin:2px 0 0; font:400 13px var(--f-data); color:var(--ink-3); text-decoration:line-through;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}

.market{ position:relative; width:280px; max-width:100%; height:14px; margin:12px 0 8px; }
/* A faint full-width track, so the coloured bar reads as a distance from the
   middle rather than as a bar of its own. */
.market::before{
  /* The louder line, not the hairline: at 1.19:1 on the panel a 2px track is
     not a track, and there is nothing to measure the bar against. */
  content:""; position:absolute; left:0; right:0; top:6px; height:2px; background:var(--hair-strong);
}
/* The centre tick is what the bar is measured against, so it has to be legible;
   a 1px hairline in the structural grey disappears entirely — and on white a
   further .55 would have finished the job. Full strength, it carries meaning. */
.market .tick{ position:absolute; left:139px; top:0; bottom:0; width:1px; background:var(--ink-3); opacity:1; }
.market.nil .tick{ background:none; border-left:1px dashed var(--ink-3); }
.market .mbar{ position:absolute; top:2px; height:10px; }
.market .mbar.under{ right:141px; background:var(--jade); }
.market .mbar.over{ left:141px; background:var(--rust); }

.pane-line{ margin:0; font:400 15px/1.55 var(--f-text); color:var(--ink-2); }
.pane-line .under{ color:var(--jade); font-weight:600; }
.pane-line .over{ color:var(--rust); font-weight:600; }

.pane h3{
  margin:0 0 8px; font:600 17px/1.2 var(--f-text); letter-spacing:-.006em; color:var(--ink);
}
.pane-deal, .pane-verdict{ padding:14px 20px; border-top:1px solid var(--hair); }
.pane-deal .deal-row{ display:flex; justify-content:space-between; gap:12px; padding:3px 0; }
.pane-deal .deal-row span{
  font:700 13px/1.4 var(--f-display); text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3);
}
.pane-deal .deal-row b{ font:600 15px/1.2 var(--f-data); color:var(--ink-2);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.pane-deal .deal-margin b{ font:700 20px/1.1 var(--f-data); letter-spacing:-.01em; color:var(--jade);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.pane-deal .deal-speed{ margin:6px 0 0; font-size:15px; color:var(--ink-3); }

.pane-verdict .reason{ margin:8px 0 0; font:400 15px/1.55 var(--f-text); color:var(--ink-2); }
.pane-verdict ul{ margin:8px 0 0; padding:0; list-style:none; display:flex; flex-direction:column; gap:5px; }
.pane-verdict li{ position:relative; padding-left:14px; font:400 15px/1.55 var(--f-text); color:var(--ink-2); }
/* Structural ink, not jade. The sheet's own rule is that jade means money in
   your favour, so bulleting an AVOID van's reasons — Category N write-off, a
   fifth off the resale, a smaller buyer pool — in the "in your favour" green
   said the opposite of the list it was marking. The verdict is already stated
   in the chip above and in the rail; the bullet is a list marker. */
.pane-verdict li::before{
  content:""; position:absolute; left:0; top:8px;
  width:4px; height:4px; border-radius:50%; background:var(--ink-3);
}

.pane-facts{ border-top:1px solid var(--hair); }
.pane-facts .fr{
  display:flex; justify-content:space-between; align-items:center; gap:12px;
  padding:7px 20px; border-bottom:1px solid var(--hair);
}
.pane-facts .fr:nth-child(odd){ background:var(--surface-2); }
.pane-facts .fk2{
  font:700 13px/1.2 var(--f-display); text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3);
}
.pane-facts .fv{ font:500 15px/1.2 var(--f-data); color:var(--ink); text-align:right;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* A note about the value above it — why a mileage came out of the seller's
   prose, where a plate came from, whose number that actually is. Inside the
   value's own cell rather than in a row of its own, so .fr:nth-child(odd)
   keeps striping the facts it was striping before. */
.pane-facts .fnote{
  display:block; margin-top:2px;
  font:400 13px/1.4 var(--f-text); letter-spacing:0; color:var(--ink-3);
  text-align:right; white-space:normal;
}
/* The one thing in this panel you can press. Plate ink, the hue that already
   means "yours" — never jade, which on this board means money in your favour. */
.pane-facts .tel{
  color:var(--plate-text); font-weight:600; text-decoration:none;
  letter-spacing:.01em;
}
.pane-facts .tel:hover{ text-decoration:underline; text-underline-offset:3px; }

/* The auction clock, under the price where the decision is made. Two tones and
   no more, the same pair the row's chip uses. */
.pane-auction{ margin:6px 0 0; font:400 15px/1.5 var(--f-text); color:var(--ink-2); }
.pane-auction b{ font-weight:700; color:var(--ink); }
.pane-auction-urgent b{ color:var(--rust); }
.pane-auction-ended b{ color:var(--ink-3); font-weight:600; }
.pane-price .bidlabel{
  font:700 13px/1 var(--f-display); font-style:normal;
  text-transform:uppercase; letter-spacing:.06em; color:var(--plate-text);
}

/* The inset edge is what stops a yellow slab floating on white paper. */
.pane-facts .plate{
  display:inline-block; background:var(--plate); color:var(--plate-ink);
  box-shadow:inset 0 0 0 1px var(--plate-mark);
  border-radius:var(--r-md); padding:2px 8px;
  font:700 15px/1.4 var(--f-data); letter-spacing:.10em;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}

.pane-note{ padding:14px 20px 18px; border-top:1px solid var(--hair); }
.pane-note-head{ display:flex; align-items:baseline; justify-content:space-between; gap:8px; }
.note-saved{
  font:700 13px/1 var(--f-data); text-transform:uppercase; letter-spacing:.06em; color:var(--jade);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.pane-note textarea{
  width:100%; resize:vertical; margin-top:2px;
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:8px 10px;
  font:400 15px/1.55 var(--f-text);
}

/* Directly under the price now, not at the foot of the pane below the deal, the
   verdict, nine facts and the note box. Still sticky, but to the top: scroll
   down to read the seller's reasoning and the three things you can do about it
   come with you rather than being left 800px above. */
.pane-act{
  position:sticky; top:0; z-index:2;
  display:flex; flex-wrap:wrap; align-items:center; gap:6px;
  min-height:56px; padding:10px 20px;
  background:var(--surface); border-bottom:1px solid var(--hair);
}
.pane-act .btn{ display:inline-flex; align-items:center; gap:6px; }
.pane-act .btn.on{
  background:var(--plate); color:var(--plate-ink);
  box-shadow:inset 0 0 0 1px var(--plate-mark);
}
.pane-act .btn-bin.on{ background:var(--rust-2); color:var(--on-hue); box-shadow:none; }

/* One of only two things that lift off the page, and it does it with a shadow
   rather than a tint. */
.toast{
  position:absolute; left:14px; bottom:34px; z-index:5;
  display:flex; align-items:baseline; gap:10px;
  background:var(--surface-2); border:1px solid var(--hair-strong);
  border-radius:var(--r-sm); padding:8px 12px;
  box-shadow:0 2px 8px var(--shadow-1);
  animation:vw-toast 140ms cubic-bezier(.2,.7,.2,1);
}
@keyframes vw-toast{ from{ opacity:0; transform:translateY(8px); } to{ opacity:1; transform:none; } }
.toast .what{ font:400 15px/1.3 var(--f-text); color:var(--ink); }
.toast .undo{
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em; color:var(--plate-text);
}
.toast.is-err{ border-color:var(--rust-2); }
.toast.is-err .what{ color:var(--rust); }

.keysheet{
  position:fixed; z-index:20; top:50%; left:50%; transform:translate(-50%,-50%);
  width:min(420px,90vw); max-height:80vh; overflow-y:auto;
  background:var(--surface); border:1px solid var(--hair-strong);
  border-radius:var(--r-lg); padding:20px 22px;
  /* Contact plus ambient. Half-opaque black reads as lift on graphite and as a
     smudge on white. */
  box-shadow:0 1px 3px var(--shadow-1), 0 16px 40px var(--shadow-2);
}
.keysheet h2{
  margin:0 0 14px; font:700 13px/1 var(--f-display);
  text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3);
}
.keysheet dl{ margin:0; display:grid; grid-template-columns:auto minmax(0,1fr); gap:8px 14px; }
/* dt takes the body's 15px on purpose — it is the one place the scale is
   inherited rather than restated. */
.keysheet dt{ white-space:nowrap; }
.keysheet dd{ margin:0; font-size:15px; line-height:1.4; color:var(--ink-2); }
.keysheet .quiet{ margin:16px 0 0; font-size:15px; }

/* =====================================================================
   5. DOCUMENT PAGES — everything inside .sheet
   ===================================================================== */

/* 20, not 17, so a search name outranks a row title rather than matching it. */
.sheet h2{ font:600 20px/1.2 var(--f-text); letter-spacing:-.006em; margin:0; color:var(--ink); }
.sheet .card{ background:var(--surface); border:1px solid var(--hair); border-radius:var(--r-md); }
.sheet .hint{ font-size:15px; color:var(--ink-3); line-height:1.5; }
/* The board earns its 13px controls: eleven columns in a 76px row. A document
   page does not — the column is 740px wide, the prose beside the button is 15px,
   and "Save changes" was rendering smaller than the label of the field it saves.
   Same step of the same scale, one size up. */
.sheet .btn{ font-size:15px; }
/* A red flag about a specific vehicle should not be the smallest thing here. */
.sheet .warn{ font:600 15px/1.45 var(--f-text); color:var(--rust); margin:0 0 10px; }

.searches{ display:flex; flex-direction:column; gap:10px; }
.srow{
  display:flex; justify-content:space-between; align-items:center; gap:20px;
  padding:16px 18px;
}
/* One ink step and one weight step, not a fade. The Paused pill next to the
   name was always the real signal. */
.srow.off h2{ color:var(--ink-2); font-weight:500; }
.srow.off .srow-sum{ color:var(--ink-3); }
.srow-head{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.srow-sum{ margin:6px 0 0; font-size:15px; color:var(--ink-2); line-height:1.5; }
.srow-meta{
  margin:5px 0 0; font-size:15px; color:var(--ink-3);
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
}
.srow-actions{ display:flex; gap:8px; flex:none; align-items:center; }
.srow-actions form{ margin:0; }
.saved-chip{
  display:inline-flex; align-items:center; height:20px; padding:0 6px;
  border-radius:var(--r-sm); color:var(--plate-text);
  box-shadow:inset 0 0 0 1px var(--plate-mark);
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
}

.sform{ padding:20px 22px 22px; }
/* Top-aligned, not stretched. The multi-selects are five or six rows tall and
   everything sharing their line was being stretched to match, which left a 90px
   dead band under the short controls. */
.frow{ display:flex; align-items:flex-start; gap:14px; margin-bottom:20px; }
/* Named apart from the shell's .wrap on purpose. While it shared that name it
   also inherited height:100dvh, and align-content spread its three lines across
   a viewport — which is where the ~90px dead band under the filters came from,
   not from the height of the boxes. */
.frow-wrap{ flex-wrap:wrap; }
.fld{ display:flex; flex-direction:column; gap:6px; min-width:170px; }
.fld.wide{ flex:1; }
.fld input[type=text], .fld input[type=number], .fld select{
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:10px 12px;
  font:500 15px/1.2 var(--f-text);
}
/* No vertical padding, so the box is an exact whole number of option rows and
   clips between them. With padding the last entry was sliced horizontally
   through the middle of its glyphs, which reads as a broken render rather than
   as a list you can scroll. */
.fld select[multiple]{ padding:0; line-height:1.7; }
.fld select[multiple] option{ padding:0 9px; }
.sheet .f-check{ flex-direction:row; align-items:center; gap:9px; height:auto; }
.sheet .f-check input[type=checkbox]{ width:15px; height:15px; accent-color:var(--plate-mark); }
/* A row of ticks that share one .fk heading — the PistonHeads body types. It
   wraps rather than scrolls, because nine short labels on one line is a
   horizontal scrollbar on a phone and a set of options nobody knows is there. */
.fchecks{ display:flex; flex-wrap:wrap; gap:8px 18px; padding:2px 0; }
.fchecks .f-check{ display:flex; margin:0; min-width:0; }
.sform-actions{ display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:8px; }

/* Listing detail */
/* The way out, at the top. Sits above the two-column grid so it stays hard
   left under the topbar rather than riding the photograph's column. */
.detail-back{ margin:0 0 12px; }

.detail{ display:grid; grid-template-columns:minmax(0,1fr) 320px; gap:18px; align-items:start; }
.detail-side{ position:sticky; top:20px; }
.hero{ overflow:hidden; padding:0; margin-bottom:18px; }
/* Capped, because a portrait phone photo at full width pushes the price and
   the mileage below the fold on the one page you opened to read them. */
.hero img{ width:100%; max-height:460px; object-fit:cover; object-position:center 55%; display:block; }
.card.pad{ padding:16px 18px; margin-bottom:18px; }
.kv{ margin-bottom:18px; }
.kv .row{
  display:flex; justify-content:space-between; gap:14px;
  padding:11px 16px; border-bottom:1px solid var(--hair);
}
.kv .row:last-child{ border-bottom:0; }
.kv .row.muted .k, .kv .row.muted .v{ color:var(--ink-3); font-weight:500; }
.kv .k{ color:var(--ink-3); font-size:15px; line-height:1.45; }
.kv .v{ color:var(--ink); font-weight:600; font-size:15px; text-align:right; }
.kv .v s{ color:var(--ink-3); font-weight:400; margin-right:5px; }
.kv .v em{
  font-style:normal; font-size:13px; text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink-3); font-weight:700;
}
.kv .v.strong{ color:var(--jade); font-size:17px; }

.kv .row.cmp{ align-items:flex-start; }
.cmp-summary{ background:var(--surface-2); }
.cmp-summary .k{ font-weight:600; color:var(--ink); }
.cmp-where{ display:block; margin-top:3px; font-size:15px; color:var(--ink-3); font-weight:400; }
.cmp-gap{ display:block; margin-top:3px; font-size:15px; font-weight:500; color:var(--ink-2); }
/* A correctness warning about a price comparison, and it was the second
   smallest thing on the page. */
.cmp-note{ display:block; margin-top:2px; font-size:15px; font-weight:600; color:var(--rust); }
a.row.cmp{ text-decoration:none; color:inherit; transition:background-color 90ms linear; }
a.row.cmp:hover{ background:var(--row-hover); }

.rank-chip{
  display:inline-block; margin-right:7px; padding:2px 7px; border-radius:var(--r-sm);
  font:700 13px/1.3 var(--f-data); background:var(--surface-2); color:var(--ink-2);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.rank-good{ color:var(--jade); box-shadow:inset 0 0 0 1px var(--jade-3); }
.rank-mid{ color:var(--ink-2); }
.rank-poor{ color:var(--rust); box-shadow:inset 0 0 0 1px var(--rust-2); }
.rank-big{
  display:inline-block; padding:3px 10px; border-radius:var(--r-sm);
  font:700 20px/1.1 var(--f-data); letter-spacing:-.01em;
  background:var(--surface-2); color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.rank-of{ margin-left:8px; font-size:15px; color:var(--ink-3); font-weight:500; }
.rank-under{ color:var(--jade); }
.rank-over{ color:var(--rust); }

.desc{
  margin:0; font:400 15px/1.6 var(--f-text); color:var(--ink-2);
  white-space:pre-wrap; word-wrap:break-word;
}
.reason{ margin:0; font:400 17px/1.55 var(--f-text); color:var(--ink-2); }
.points{ margin:0; padding:0; list-style:none; display:flex; flex-direction:column; gap:6px; }
.points li{
  position:relative; padding-left:16px;
  font:400 15px/1.5 var(--f-text); color:var(--ink-2);
}
/* Structural ink, for the same reason as the pane's — see .pane-verdict above. */
.points li::before{
  content:""; position:absolute; left:2px; top:8px;
  width:5px; height:5px; border-radius:50%; background:var(--ink-3);
}
/* Two steps apart from .points rather than one, so the two bullet lists stay
   distinguishable at the new sizes. */
.points-big li{ font-size:17px; line-height:1.55; }

.detail .plate{
  margin:0 0 12px; text-align:center; border-radius:var(--r-md); padding:9px;
  background:var(--plate); color:var(--plate-ink);
  box-shadow:inset 0 0 0 1px var(--plate-mark);
  font:700 20px/1.2 var(--f-data); letter-spacing:.10em;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* A plate read off a photograph rather than published by the seller. Dashed,
   because that is what it is: a good guess. eBay hands the registration over in
   the advert's own field and that one gets the solid edge — the difference
   matters, since a billed check answers about whatever plate it is given and
   will never tell you it described a different vehicle. */
.detail .plate-guess{ box-shadow:inset 0 0 0 1px var(--plate-mark); border:1px dashed var(--plate-text); }
.regform{ display:flex; gap:8px; margin-bottom:12px; }
.regform input{
  flex:1; min-width:0; text-transform:uppercase; letter-spacing:.06em;
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:10px 12px;
  font:600 15px/1.2 var(--f-data);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.cost{ margin:9px 0 0; text-align:center; }

/* The way in to the four metered builders. A line of links rather than four
   more buttons in the masthead: an AutoTrader search is the ordinary act and
   these are the deliberate, costed one. */
.addlinks{ margin:-4px 0 12px; }
.addlinks a{ color:var(--ink-2); font-weight:600; text-underline-offset:3px; }

/* The verdict, at the top of the side column where the decisions are, rather
   than as a 50px chip in the nav line two thousand pixels away from everything
   it is about. It reads the same --v-* properties as the board chip, so the two
   can never drift apart. */
.side-verdict{ margin:0 0 14px; }
.side-verdict .pill{
  display:block; text-align:center;
  font-size:15px; letter-spacing:.10em; padding:11px 12px;
}

/* The price and the three things you can do about it, at the top of the page
   and on one line. They were a spec row near the top and a button block in the
   aside two thirds of the way down, so on a listing that runs to the seller's
   whole description and a dozen comparables, deciding meant scrolling back past
   everything you had just read. Still real forms — the star and the bin work
   with JavaScript off — and the buttons keep their keyboard hints. */
.headline{
  display:flex; align-items:center; justify-content:space-between;
  gap:16px; flex-wrap:wrap; padding:16px 18px; margin-bottom:18px;
}
.headline-price{ display:flex; align-items:baseline; gap:9px; flex-wrap:wrap; min-width:0; }
.headline-price b{
  font:700 30px/1.05 var(--f-data); letter-spacing:-.02em; color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.headline-price em{
  font:700 13px/1 var(--f-display); font-style:normal;
  text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3);
}
/* "+ VAT" means the figure beside it is a fifth light. Scanning past one is a
   real four-figure mistake, so it is the one label here that gets a hue. */
.headline-price .vat-plus{ color:var(--rust); }
/* "Current bid" says the same kind of thing about the same figure: that it is
   not the number you would pay. Plate ink rather than rust — an auction is not
   a warning, it is a different sort of price. */
.headline-price .bidlabel{ color:var(--plate-text); }
/* And on the "others like it" list, where it matters just as much: an auction
   among the comparables was printing "£2,300 · £4,200 cheaper" against a van
   nobody has agreed to pay £2,300 for. The gap is gone from those rows and
   this is what stands in its place. */
.cmp .bidlabel{ color:var(--plate-text); font-style:normal; font-weight:600; }

/* The auction clock, directly under the price and above the buttons — the only
   place on this page where it can be acted on. Two tones and no more: about to
   close, and already closed. */
.auctionline{
  margin:8px 0 0; display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
  font:400 15px/1.5 var(--f-text); color:var(--ink-3);
}
.auctionline b{ font:700 17px/1.3 var(--f-text); color:var(--ink); }
.auctionline-urgent b{ color:var(--rust); }
.auctionline-ended b{ color:var(--ink-3); }
/* The write-off category, in the same place and the same shape on a deal card
   and a deal page. It sits with the price deliberately: the price is low
   BECAUSE of it, and the pipeline is the surface Kish acts from. */
.writeoffline b{ color:var(--rust); }

/* A number you can ring, on the page you read before driving anywhere. */
.detail .tel{
  color:var(--plate-text); font-weight:600; text-decoration:none;
  letter-spacing:.01em;
}
.detail .tel:hover{ text-decoration:underline; text-underline-offset:3px; }
.headline-price s{
  font:400 15px/1.2 var(--f-data); color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.headline-act{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.headline-act form{ margin:0; }
.headline-act .btn{ display:inline-flex; align-items:center; gap:6px; }
.headline-act .btn.on{
  background:var(--plate); color:var(--plate-ink);
  box-shadow:inset 0 0 0 1px var(--plate-mark);
}
.headline-act .btn-bin.on{ background:var(--rust-2); color:var(--on-hue); box-shadow:none; }
/* On the primary button the hint sits on a jade slab, where the kbd chip's own
   --surface-2 fill would be the only pale thing on it. */
.headline-act .btn-primary kbd{
  background:transparent; color:var(--on-hue); border-color:var(--on-hue);
}
.noteform textarea{
  width:100%; resize:vertical;
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:10px 12px;
  font:400 15px/1.55 var(--f-text); margin-bottom:8px;
}

/* "Run now" feedback. The POST is synchronous and can take minutes on a
   metered source, so the page must not look dead while it waits. */
.btn.is-running{
  position:relative; padding-left:30px; cursor:progress;
  background:var(--surface-2); color:var(--jade);
  box-shadow:inset 0 0 0 1px var(--jade-3);
}
.btn.is-running::before{
  content:""; position:absolute; left:11px; top:50%; width:11px; height:11px;
  margin-top:-6px; border-radius:50%;
  border:2px solid var(--jade); border-top-color:transparent;
  animation:vw-spin .7s linear infinite;
}
@keyframes vw-spin{ to{ transform:rotate(360deg); } }
/* searches.js puts this on every other button while a search runs, so it has to
   stay readable rather than vanish. */
.btn.is-waiting{
  opacity:1; background:transparent; color:var(--ink-disabled); pointer-events:none;
  box-shadow:inset 0 0 0 1px var(--hair);
}
@media (prefers-reduced-motion:reduce){ .btn.is-running::before{ animation:none; } }

/* =====================================================================
   BREAKPOINTS — each one changes something structural
   ===================================================================== */

@media (min-width:1680px){
  :root{ --rail-w:264px; --pane-w:432px; }
}

/* The two ages, and where they live. Above 1760px each has a track. Below it
   there is not room for a second one — 53px of slack at 1680 against a 64px
   column — so the Updated cell and its header collapse to nothing and the pair
   is printed as two lines inside the Listed cell instead. visibility, never
   display: a display:none grid item stops being an item and every cell after it
   slides up a track, which would land the action buttons on a zero. */
@media (max-width:1759px){
  .v-upd, .colupd{ visibility:hidden; padding:0; }
  .v-age{ display:none; }
  .v-pair{ display:block; }
  /* The three state tags move onto the chip line to make room for the second
     age line, exactly as they already do below 1440. Nothing is lost — a Gone
     chip on something you shortlisted is still on the row, still solid, still
     first in the line. */
  .v-when .tag{ display:none; }
  .chip-when{ display:inline-block; }
}

/* The phone moves out of its own column and onto the row's chip line. Below
   1680 the row has 24px of slack and a legible number costs 104, so this is the
   width where it has to give the track back. visibility, never display: a
   collapsed cell has to stay a grid item, or every cell after it slides up a
   track and the actions column lands on the zero.

   (The seller word's version of this rule has moved up beside .v-seller — it
   now collapses at every width, having given its column to this one.) */
@media (max-width:1679px){
  .v-tel, .coltel{ visibility:hidden; padding:0; }
}

/* Below 1440 the listing age goes, and the three state tags move into the chip
   line with the seller word. They are the whole reason to glance at that column
   — a Gone chip on something you shortlisted is the difference between ringing
   a seller and not — so they move rather than disappear. */
@media (max-width:1439px){
  /* With up to three more chips on the line it can run out of room on a narrow
     name, and it clips from the right. So the order is worth what it costs: the
     state first, then your own note, then the verdict, the source and the
     seller — the last of which is also the least missed. */
  .v-dot{ order:-1; }
}

/* Next to go is the listing age: it is the least load-bearing number on the
   row, and the pane still carries it. */
@media (max-width:1439px){
  :root{ --rail-w:220px; --pane-w:336px; }
  .v-when, .colwhen{ visibility:hidden; padding:0; }
}

/* The pane becomes an overlay drawer, and the list takes the width back. */
@media (max-width:1279px){
  :root{ --rail-w:220px; }
  .board{ grid-template-columns:var(--rail-w) minmax(0,1fr); grid-template-areas:"rail list"; }
  /* 320, not 400. The drawer opens on every cursor move, so at 1024 a 400px
     panel sat over half an 804px list and hid the price, the gap and both
     action buttons on every row behind it — and comparing the focused van
     against the ones above and below is the only reason a pane sits beside a
     list rather than replacing it. */
  .pane{
    position:fixed; top:var(--topbar-h); right:0; bottom:0; width:320px; max-width:92vw;
    z-index:15; transform:translateX(100%);
    transition:transform 160ms cubic-bezier(.2,.7,.2,1);
    border-left:1px solid var(--hair-strong);
  }
  .board.pane-open .pane{ transform:none; }
  /* With the drawer out, the row gives up name width rather than sliding its
     money underneath it. The cap is the row's width less the drawer (320px) and
     the tracks that precede the gap (302px, the selection box included), which puts both money columns —
     the price and the signature under/over figure — clear of the panel. The
     mileage and the buttons still go behind it: the fixed tracks alone are
     500px against a 464px strip at 1024, so everything cannot fit, and these
     are the two worth keeping. The keyboard and the pane's own buttons cover
     the rest. */
  .board.pane-open .board__list{
    --row-cols:4px 26px 96px minmax(150px,calc(100% - 622px)) 88px 88px 112px 48px 0 0 0 0 64px minmax(0,1fr);
  }
  .pane-close{
    display:grid; place-items:center; position:absolute; top:8px; right:8px; z-index:3;
    width:30px; height:30px; padding:0; border:0; border-radius:var(--r-sm);
    /* Dark, and deliberately: it sits over a photograph, where white on a dark
       scrim is the only treatment that survives an unknown image. */
    background:var(--scrim); color:var(--on-hue); cursor:pointer;
  }
  .pane-close svg{ width:15px; height:15px; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; }
  /* The glyph is already white, so the scrim deepens instead. */
  .pane-close:hover{ background:var(--scrim-strong); }
}

/* One column. The rail becomes a strip; its nine controls collapse into a
   details panel but never leave the DOM — filters.js addresses all nine by id
   and one missing control kills filtering, sorting and the count. */
@media (max-width:1023px){
  .board{ grid-template-columns:minmax(0,1fr); grid-template-areas:"rail" "list"; grid-template-rows:auto minmax(0,1fr); }
  .rail{ border-right:0; border-bottom:1px solid var(--hair); }
  .rail-scroll{ padding:0; overflow:visible; }
  .rail-nav{ flex-direction:row; height:44px; align-items:center; gap:4px; padding:0 10px; overflow-x:auto; }
  .rail-nav a{ flex:none; }
  .rail .label{ display:none; }
  .rail-foot{ display:none; }
  .refine{ border-top:1px solid var(--hair); padding:0 10px 10px; }
  .refine > summary{
    display:block; cursor:pointer; padding:9px 0;
    font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3);
  }
  .refine-grid{ display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:8px; }
  .board__list{ --row-cols:3px 76px minmax(0,1fr) auto; border-right:0; }
  /* The column header is gone here, so the track list has to lose a row with
     it — otherwise the scroller inherits the header's 30px and the list all but
     disappears. */
  .colhead{ display:none; }
  .board__list{ grid-template-rows:minmax(0,1fr) auto 26px; }
  /* Two lines at a known height, so scrolling stays as cheap as it is wide.
     The first track has to hold the whole name stack — 8px of top padding, then
     21.25 title + 2 + 19.5 subtitle + 2 + a 20px chip = 72.75, so 74. At 66 the
     stack overflowed by 7px and .v-name clipped it, which took the bottom edge
     off every ring on the chip line: the verdict badge came out as two vertical
     strokes with no box around them, exactly the "reads as a rendering fault"
     failure the chips were given an ellipsis to avoid.
     The second track is sized by the action buttons, not by the gap figure —
     30px of button plus its 6px of padding is 36, so 38 holds with a little
     slack. 74 + 38 = 112, and contain-intrinsic-size has to stay in step with
     the height or the scroller's virtualisation estimate drifts. */
  .van{ height:112px; contain-intrinsic-size:auto 112px; grid-template-rows:74px 38px; align-items:stretch; }
  /* Columns stated, not left to auto-placement. These two used to be the only
     cells here with a row but no column, and that worked only because they
     happened to be the first two children: the selection box is now placed into
     column 2 row 1, which moves the auto-placement cursor past it, and the
     photo landed six tracks away with the whole row jumbled behind it. Every
     cell in this layout is explicit now, which is what the layout already
     assumed. */
  .van .v-rail{ grid-column:1; grid-row:1/span 2; }
  .van .v-shot{ grid-column:2; grid-row:1/span 2; height:100%; }
  .v-name{ grid-column:3; grid-row:1; padding:8px 8px 0 10px; }
  .v-price{ grid-column:4; grid-row:1; padding-top:8px; }
  .v-gap{ grid-column:3; grid-row:2; flex-direction:row; align-items:center;
          justify-content:flex-start; gap:8px; padding:0 10px 6px; }
  .v-gap .bar{ margin:0; max-width:60px; }
  .v-miles, .v-away, .v-seller, .v-when, .v-upd, .v-tel{ display:none; }
  .v-act{ grid-column:4; grid-row:2; justify-content:flex-end; padding:0 6px 6px 0; }
  /* Four tracks here, and a fifth for the box would shift every one of the
     explicit placements above. It goes on the photograph instead, which is
     where a narrow layout has room and where a tick is understood without a
     column header to name it. The z-index is load-bearing: .v-shot follows it
     in the DOM and spans both rows, so it would paint straight over the box. */
  .v-check{
    grid-column:2; grid-row:1; z-index:1;
    align-self:start; justify-self:start; height:auto; padding:6px;
    background:var(--scrim); border-bottom-right-radius:var(--r-md);
  }
  /* Every filter is in the folded panel at this width, so the bar is the only
     thing on screen that says how many are held. It wraps rather than shrinks. */
  .bulkbar{ padding:8px 10px; }
  .pane{ width:100%; max-width:100%; }
  .statusline .keys{ overflow:hidden; text-overflow:ellipsis; }
  .detail{ grid-template-columns:minmax(0,1fr); }
  .detail-side{ position:static; }
  .srow{ flex-direction:column; align-items:stretch; }
  .srow-actions{ justify-content:flex-start; flex-wrap:wrap; }
  .frow{ flex-wrap:wrap; }
}

@media (max-width:719px){
  .board__list{ --row-cols:3px 64px minmax(0,1fr) auto; }
  .sheet{ padding:20px 14px 56px; }
  .topbar{ gap:10px; padding:0 12px; }
  .brand .sub, .brand-rule{ display:none; }
}

/* =====================================================================
   6. THE DEAL PIPELINE — everything inside .deals, .dealcard,
      .dealpage, .timeline and .stages
   ===================================================================== */

/* Named .dealcard rather than .deal-card on purpose: listing.html has carried a
   .deal-card for the margin panel since long before this existed, and a rule
   written here would have reached across and restyled it. */

/* The nav badge: how many viewings are due today or already missed. Plate while
   they are only due, rust once one has been missed — the same two loud tones the
   cards use, and no third. */
.nav .due{
  display:inline-block; margin-left:6px; min-width:17px; padding:0 5px;
  border-radius:var(--r-sm); text-align:center;
  background:var(--plate); color:var(--plate-ink);
  font:700 13px/1 var(--f-data); letter-spacing:0; line-height:17px;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.nav .due-over{ background:var(--rust); color:var(--on-hue); }

/* --- the page ---------------------------------------------------------- */

.deals{
  position:relative;
  display:flex; flex-direction:column; min-height:0;
  /* min-width, not just min-height. A grid item's automatic minimum is its
     content, and eight 296px columns are 2,633px wide — so without this the
     stage grew past the window and the whole PAGE scrolled sideways instead of
     the strip inside it. The same fix, for the same reason, as the topbar. */
  min-width:0;
}
/* The container is tabindex="-1" and takes focus on load so its keydown listener
   has somewhere to fire from, and Chrome draws a ring round it for that — which
   put a blue box around the entire board on every page load. The ring belongs on
   the card the cursor is on, and j puts it there on the first keypress. */
.deals:focus, .deals:focus-visible{ outline:none; }
/* Where a card landed, said out loud. The keyboard path is not an afterthought,
   and a move you cannot see because the card scrolled is a move you would have
   to go and check. Off-screen rather than absent: an aria-live region with
   display:none announces nothing at all. */
.deals-live{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap;
}

/* What is due, across the full width and above the columns, because otherwise
   seeing what is coming depends on which column you happen to be looking at.
   Collapses to nothing when nothing is booked — the same :empty trick .flashes
   uses, which is why every tag inside it carries whitespace control. */
.deals-upnext{
  flex:none; display:flex; gap:8px; padding:10px 16px; overflow-x:auto;
  background:var(--surface); border-bottom:1px solid var(--hair);
}
.deals-upnext:empty{ display:none; }
.upchip{
  flex:none; max-width:340px; display:flex; flex-direction:column; gap:1px;
  padding:6px 10px; border-radius:var(--r-sm); text-decoration:none;
}
.up-when{
  font:700 13px/1.3 var(--f-display); text-transform:uppercase; letter-spacing:.05em;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.up-what{ font-size:13px; line-height:1.3; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.up-over{ background:var(--appt-over); color:var(--on-hue); }
.up-now{ background:var(--appt-now); color:var(--appt-now-ink); }
.up-soon, .up-later, .up-stale{
  background:var(--surface-2); color:var(--ink);
  box-shadow:inset 0 0 0 1px var(--hair-strong);
}
.up-soon .up-what, .up-later .up-what, .up-stale .up-what{ color:var(--ink-3); }

.deals-onecol{
  flex:none; margin:0; padding:9px 16px; font-size:15px; color:var(--ink-3);
  background:var(--surface); border-bottom:1px solid var(--hair);
}
.deals-onecol a{ color:var(--ink-2); }

/* The strip scrolls horizontally inside itself; the page body never does. The
   1px gap over a --hair ground is what draws the column separators, so nothing
   needs a border that a column could then be missing at one end. */
.deals-strip{
  flex:1; min-height:0; min-width:0; display:flex; gap:1px; overflow-x:auto;
  background:var(--hair);
}
.deals-col, .deals-tray{
  flex:0 0 var(--deal-col-w); display:flex; flex-direction:column; min-height:0;
  background:var(--bg);
}
.deals-tray{ flex-basis:var(--deal-tray-w); background:var(--surface); }
.deals-col__head{
  flex:none; display:flex; align-items:center; gap:8px;
  padding:11px 12px 9px; background:var(--surface);
  border-bottom:1px solid var(--hair);
}
.deals-col__head h2{
  margin:0; min-width:0; font:600 17px/1.2 var(--f-text); letter-spacing:-.004em;
  color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.col-n{
  flex:none; margin-left:auto; font:600 13px/1.2 var(--f-data); color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* The digit the m chord takes, revealed for 900ms when m is armed — the same
   window and the same teaching device as the board's g then i/s/b. */
.col-key{ display:none; }
.deals.is-chording .col-key{
  display:inline-block; flex:none; min-width:17px; padding:0 5px; text-align:center;
  border-radius:var(--r-sm); background:var(--plate); color:var(--plate-ink);
  font:700 13px/1 var(--f-data); line-height:17px;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* The only two coloured headers, because the outcome pair is different in kind
   from the six stages that lead to it. Every other stage takes its meaning from
   its position — that is what the colour is on a kanban. */
.col-won .deals-col__head{ box-shadow:inset 0 2px 0 var(--jade); }
.col-lost .deals-col__head{ box-shadow:inset 0 2px 0 var(--neutral); }

.deals-showing{
  flex:none; margin:0; padding:6px 12px; font-size:13px; color:var(--ink-3);
  background:var(--surface); border-bottom:1px solid var(--hair);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.deals-col__body{
  flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain; padding:10px;
}
.deals-col__foot{ flex:none; padding:0 10px 10px; }
.deals-window{
  display:inline-block; margin-top:6px; font-size:13px; color:var(--ink-3);
}
.deals-empty{
  margin:6px 2px; font-size:15px; line-height:1.5; color:var(--ink-3);
}
.deals-col.is-over{ background:var(--jade-wash); }

/* --- the card ---------------------------------------------------------- */

.dealcard{
  position:relative; margin-bottom:8px; overflow:hidden; cursor:grab;
  background:var(--bg); border:1px solid var(--hair); border-radius:var(--r-md);
  box-shadow:0 1px 2px var(--shadow-1);
  transition:box-shadow 120ms linear, transform 120ms cubic-bezier(.2,.7,.2,1);
}
.deals-tray .dealcard{ margin-bottom:6px; }
.dealcard:hover{ box-shadow:0 2px 6px var(--shadow-1); }
/* One ring for both, because the cursor IS the focus here: deals.js focuses the
   card it moves to, and a keyboard path with no visible landing place is not a
   keyboard path. */
.dealcard:focus-visible, .dealcard.is-cursor{
  outline:2px solid var(--focus); outline-offset:1px;
}
.dealcard.is-cursor{ background:var(--row-cursor); }
.dealcard.is-dragging{ opacity:.5; transform:scale(.98); }
.dealcard.is-shut{ background:var(--surface); }

.deal-rail{
  position:absolute; left:0; top:0; bottom:0; width:3px;
  background:var(--v-ungraded);
}
.v-definitely_buy > .deal-rail{ background:var(--v-definitely_buy); }
.v-buy > .deal-rail{ background:var(--v-buy); }
.v-negotiate > .deal-rail{ background:var(--v-negotiate); }
.v-maybe > .deal-rail{ background:var(--v-maybe); }
.v-avoid > .deal-rail{ background:var(--v-avoid); }

/* Across the top, above the photograph, so it is the first thing the eye lands
   on in a column. Solid for the two that are facts about the world — a viewing
   missed, a viewing today — and a hairline for one merely coming up. Never
   colour alone: each carries its own words, its own mark and its own weight. */
.deal-appt{
  position:relative; z-index:1;
  display:flex; align-items:center; gap:6px; padding:5px 10px;
  font:700 13px/1.3 var(--f-display); text-transform:uppercase; letter-spacing:.05em;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.appt-i{ flex:none; width:13px; height:13px; fill:currentColor; }
.appt-t{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.appt-over{ background:var(--appt-over); color:var(--on-hue); }
.appt-now{ background:var(--appt-now); color:var(--appt-now-ink); }
.appt-soon{
  background:var(--appt-soon); color:var(--ink);
  box-shadow:inset 0 1px 0 var(--hair-strong);
}

.deal-top{ display:flex; gap:9px; padding:9px 10px 0 13px; }
.deal-shot{
  flex:none; width:48px; height:36px; overflow:hidden;
  border-radius:var(--r-sm); background:var(--surface-2);
}
.tray-card .deal-shot{ width:40px; height:30px; }
.deal-shot img{ width:100%; height:100%; object-fit:cover; display:block; }
.deal-head{ flex:1; min-width:0; }
.deal-title{
  color:var(--ink); text-decoration:none;
  font:600 15px/1.3 var(--f-text); letter-spacing:-.002em;
  /* Two lines, then an ellipsis. A title cut mid-word with no mark is read as a
     rendering fault; a clamped one is read as a long title. */
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden;
}
.deal-title:hover{ text-decoration:underline; }
.deal-money{ display:flex; align-items:baseline; gap:7px; margin-top:3px; min-width:0; }
.deal-money b{
  font:600 15px/1.2 var(--f-data); color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.deal-money s{
  font-size:13px; line-height:1.2; color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* "Current bid" on a card, and on the deal page's hero. The pipeline is where
   Kish acts, so it is the last surface that can afford to print a bid as an
   asking price — the captured Transit sits at £2,300 with £6,500 in its own
   title. Same plate ink and same sentence case as the board row's, because it
   is the same statement about the same figure. */
.deal-money .bidlabel,
.deal-hero-money .bidlabel{
  font:700 13px/1.2 var(--f-display); font-style:normal;
  text-transform:none; letter-spacing:.01em; color:var(--plate-text);
  white-space:nowrap;
}
/* Wrap, rather than squeezing the price: a card is 13px of room and the label
   is a whole extra word. */
.deal-money{ flex-wrap:wrap; }
/* On a card the clock is a line of its own under the money, quieter than the
   listing page's — the decision has already been made by the time something is
   a deal, so this is a reminder rather than a headline. */
.dealcard .auctionline,
.deal-hero .auctionline{ margin-top:4px; font-size:13px; }
.dealcard .auctionline b,
.deal-hero .auctionline b{ font-size:13px; }

.deal-gap{
  font:600 13px/1.2 var(--f-data); font-style:normal; white-space:nowrap;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.deal-gap.under{ color:var(--jade); }
.deal-gap.over{ color:var(--rust); }
.deal-where{
  margin-top:2px; font-size:13px; line-height:1.35; color:var(--ink-3);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.deal-out{ color:var(--ink-3); text-decoration:none; padding:0 2px; }
.deal-out:hover{ color:var(--ink); }

.deal-flags{ display:flex; flex-wrap:wrap; gap:6px; padding:7px 10px 0 13px; }
.deal-flag{
  height:20px; padding:0 6px; border-radius:var(--r-sm);
  background:var(--surface-2); color:var(--ink-3);
  box-shadow:inset 0 0 0 1px var(--hair-strong);
  font:700 13px/1 var(--f-display); line-height:20px;
  text-transform:uppercase; letter-spacing:.05em; white-space:nowrap;
}

.deal-last{
  display:flex; gap:8px; align-items:flex-start;
  margin-top:8px; padding:7px 10px 0 13px; border-top:1px solid var(--hair);
}
.deal-when, .tl-when{
  flex:none; width:var(--time-gutter); text-align:right;
  font:500 13px/1.45 var(--f-data); color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.deal-said{
  min-width:0; font-size:15px; line-height:1.45; color:var(--ink-2);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden;
}
.deal-foot{
  display:flex; gap:8px; align-items:center; padding:5px 10px 9px 13px;
}
.deal-stall{
  flex:1; min-width:0; font-size:13px; line-height:1.4; color:var(--stall);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
/* The same slot, a different fact. On an open card it says how long this has sat
   where it is, and plate-as-a-graphic is right for that. On a finished one it
   says "Bought just now" — a statement about the past with nothing to chase, so
   it drops back to ordinary quiet ink rather than wearing a nudge's colour. */
.dealcard.is-shut .deal-stall{ color:var(--ink-3); }
.deal-notes{
  flex:none; display:inline-flex; align-items:center; gap:4px;
  font:500 13px/1 var(--f-data); color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}

/* The drag API fires no touch events at all, so without this the whole feature
   would be mouse-only. Always visible rather than revealed on hover, because a
   touchscreen has no hover to reveal it with. */
.deal-move{
  flex:none; height:20px; padding:0 7px; border:0; cursor:pointer;
  border-radius:var(--r-sm); background:var(--surface-2); color:var(--ink-2);
  box-shadow:inset 0 0 0 1px var(--hair-strong);
  font:700 13px/1 var(--f-display); line-height:20px;
  text-transform:uppercase; letter-spacing:.05em;
}
.deal-move:hover{ background:var(--fill-hover); color:var(--ink); }

/* One shared sheet rather than a picker baked into all 320 cards: the list of
   stages is the same list every time it opens. */
.movesheet{
  position:fixed; z-index:20; top:50%; left:50%; transform:translate(-50%,-50%);
  width:min(320px,90vw); max-height:80vh; overflow-y:auto;
  background:var(--surface); border:1px solid var(--hair-strong);
  border-radius:var(--r-lg); padding:18px 20px;
  box-shadow:0 1px 3px var(--shadow-1), 0 16px 40px var(--shadow-2);
}
.movesheet h2{
  margin:0 0 12px; font:700 13px/1 var(--f-display);
  text-transform:uppercase; letter-spacing:.08em; color:var(--ink-3);
}
.movesheet-list{ display:flex; flex-direction:column; gap:6px; }

/* --- the thread -------------------------------------------------------- */

/* Two registers, and this is the whole reason a mixed thread is readable: his
   words dominate and the app recedes. Scanning it you see what HE said, and the
   machine's lines are the connective tissue between them. */
/* The listing page's aside is 320px, and the 56px gutter eats a fifth of it.
   The rail earns its place there too — it is the same rail, and the thread on
   the listing is the same thread — but the rows are allowed to get tighter. */
.detail-side .timeline{ margin-bottom:0; }
.detail-side .tl{ gap:9px; }
.timeline{ margin-bottom:18px; }
.tl{ list-style:none; margin:0 0 16px; padding:0; display:flex; flex-direction:column; gap:11px; }
.tl-row{ display:flex; gap:10px; align-items:stretch; }
.tl-mark{ flex:none; width:2px; position:relative; }
.tl-note .tl-mark{ background:var(--hair-strong); border-radius:1px; }
.tl-sys .tl-mark::before{
  content:""; position:absolute; left:-1px; top:7px;
  width:4px; height:4px; border-radius:50%; background:var(--hair-strong);
}
.tl-body{ flex:1; min-width:0; }
.tl-text{ margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
.tl-note .tl-text{ font:400 15px/1.5 var(--f-text); color:var(--ink); }
.tl-sys .tl-text{ font:400 13px/1.55 var(--f-text); color:var(--ink-3); }
.tl-edited{ font-style:normal; font-size:13px; color:var(--ink-3); }
.tl-blank{ margin:0 0 16px; font-size:15px; color:var(--ink-3); }

/* Revealed on hover, but never taken out of the keyboard's reach — opacity, not
   visibility, because a hidden control is an unreachable one. */
.tl-acts{
  display:flex; gap:8px; align-items:flex-start; margin-top:5px;
  opacity:0; transition:opacity 90ms linear;
}
.tl-row:hover .tl-acts, .tl-acts:focus-within{ opacity:1; }
.tl-acts form{ margin:0; }
.tl-edit summary{
  cursor:pointer; list-style:none; display:inline-block;
  padding:4px 0; color:var(--ink-3);
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
}
.tl-edit summary::-webkit-details-marker{ display:none; }
.tl-edit summary:hover{ color:var(--ink); }
.tl-edit[open]{ display:block; width:100%; }
.tl-edit textarea{
  width:100%; resize:vertical; margin:6px 0 8px;
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:8px 10px;
  font:400 15px/1.55 var(--f-text);
}
.tl-acts .btn{ font-size:13px; padding:6px 9px; }

.tl-add textarea{
  width:100%; resize:vertical;
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:10px 12px;
  font:400 15px/1.55 var(--f-text);
}
.tl-add-foot{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  margin-top:8px;
}

/* --- the deal page ----------------------------------------------------- */

.dealpage{ display:grid; grid-template-columns:minmax(0,1fr) 320px; gap:18px; align-items:start; }
.dealpage-side{ position:sticky; top:20px; }
.deal-hero{ position:relative; overflow:hidden; margin-bottom:18px; }
.deal-hero-top{ display:flex; gap:14px; align-items:flex-start; }
.deal-hero-shot{ flex:none; width:150px; height:112px; overflow:hidden;
  border-radius:var(--r-sm); background:var(--surface-2); }
.deal-hero-shot img{ width:100%; height:100%; object-fit:cover; display:block; }
.deal-hero-head{ flex:1; min-width:0; }
.deal-hero-head h2{ margin:0 0 6px; }
.deal-hero-money{ display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
.deal-hero-money b{
  font:600 30px/1.1 var(--f-data); letter-spacing:-.012em; color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.deal-hero-money s{
  font-size:15px; color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.deal-hero-money .deal-gap{ font-size:15px; }
.deal-hero-facts{
  margin:8px 0 0; font-size:15px; line-height:1.5; color:var(--ink-3);
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
}
.deal-hero-note{ margin:14px 0 0; }
.deal-hero-act{ display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:14px; }

.appt-card{ margin-bottom:18px; }
.appt-now-line{ margin:0 0 12px; font:600 17px/1.35 var(--f-text); }
.appt-line-over{ color:var(--rust); }
.appt-line-now{ color:var(--plate-text); }
.appt-line-soon, .appt-line-later{ color:var(--ink-2); }
.appt-line-stale, .appt-line-none{ color:var(--ink-3); font-weight:500; }
/* The one field type layer 5 never had to style, and it lands on this page. */
.fld input[type=datetime-local]{
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:10px 12px;
  font:500 15px/1.2 var(--f-text);
}
.dealpage code{
  background:var(--surface-2); border-radius:var(--r-sm); padding:1px 5px;
  font:500 13px var(--f-mono); color:var(--ink-2);
}

/* --- the stage editor -------------------------------------------------- */

.stages-lede{ max-width:70ch; margin:0 0 18px; }
.stages{ display:flex; flex-direction:column; gap:8px; margin-bottom:18px; }
.stages-add-label{ margin-top:22px; }
/* A grid, not a flex row: with flex, a stage holding 137 deals said so in more
   characters than one holding 1, and every name field came out a different
   width down the page. Four stated tracks means the eight fields line up. */
.strow{
  display:grid; grid-template-columns:9px minmax(0,1fr) 190px auto auto;
  align-items:center; gap:12px; padding:12px 14px;
}
.strow.is-dragging{ opacity:.5; }
.strow-grip{
  flex:none; width:9px; height:15px; cursor:grab;
  /* Three hairlines rather than an icon font or a scraped glyph. */
  background:
    linear-gradient(var(--hair-strong) 1px, transparent 1px) 0 3px / 9px 5px repeat-y;
}
.strow-name{ flex:1; min-width:0; display:flex; gap:8px; margin:0; }
.strow-name input{
  flex:1; min-width:0;
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:9px 11px;
  font:500 15px/1.2 var(--f-text);
}
.strow-name-static{ font:500 15px/1.2 var(--f-text); color:var(--ink-2); }
.strow-count{ font-size:13px; line-height:1.4; color:var(--ink-3); text-align:right; }
.strow-acts{ display:flex; gap:8px; align-items:center; }
.strow-acts form{ margin:0; display:flex; gap:6px; align-items:center; }
.strow-nudge{ padding:8px 10px; }
/* Retiring a stage is a rare act, so its control folds away rather than
   shouting over the eight names it sits beside. */
.strow-arch summary{
  cursor:pointer; list-style:none; padding:9px 10px; border-radius:var(--r-sm);
  color:var(--ink-3); box-shadow:inset 0 0 0 1px var(--hair-strong);
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
}
.strow-arch summary::-webkit-details-marker{ display:none; }
.strow-arch summary:hover{ color:var(--ink); background:var(--surface-2); }
.strow-arch[open]{
  grid-column:1/-1; padding:12px 14px 4px; margin-top:4px;
  border-top:1px solid var(--hair);
}
.strow-arch[open] summary{ margin-bottom:10px; display:inline-block; }
.strow-arch form{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.strow-arch .hint{ flex-basis:100%; margin:0; }
.strow-arch select{
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:8px 10px;
  font:500 15px/1.2 var(--f-text);
}
.strow.is-archived{ background:var(--surface); }
.kind-won{ box-shadow:inset 0 2px 0 var(--jade); }
.kind-lost{ box-shadow:inset 0 2px 0 var(--neutral); }

/* --- the board's preview pane, now that notes append ------------------- */

.pane-notes{ list-style:none; margin:8px 0 6px; padding:0; display:flex; flex-direction:column; gap:7px; }
.pane-note-row{ display:flex; gap:8px; align-items:flex-start; }
.pane-note-said{
  flex:1; min-width:0; font-size:13px; line-height:1.45; color:var(--ink-2);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden;
}
.pane-notes-all{ display:inline-block; margin-bottom:8px; font-size:13px; color:var(--ink-3); }
.pane-note-foot{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  margin-top:8px;
}

/* --- narrower ---------------------------------------------------------- */

/* The strip stacks. Nothing scrolls sideways below this width except the
   coming-up chips, which are a single row of short phrases and are meant to. */
@media (max-width:1023px){
  .deals-strip{
    flex-direction:column; overflow-x:visible; overflow-y:auto;
    overscroll-behavior:contain;
  }
  .deals-col, .deals-tray{ flex:none; }
  /* The tray goes to the bottom once the strip stacks. Side by side it is a
     feeder you glance at; stacked first it is 146 shortlisted vans standing
     between him and the deals he actually came to look at. */
  .deals-col{ order:1; }
  .deals-tray{ order:2; border-right:0; border-top:1px solid var(--hair); }
  .deals-col__body{ overflow-y:visible; }
  .deals-col__head{ position:sticky; top:0; z-index:3; }
  .dealpage{ grid-template-columns:minmax(0,1fr); }
  .dealpage-side{ position:static; }
  .deal-hero-top{ flex-wrap:wrap; }
  /* The name gets the width, and the count and the two controls share the line
     under it. Five tracks in 375px would give the name about ninety pixels. */
  .strow{ grid-template-columns:9px minmax(0,1fr) auto; }
  .strow-name{ grid-column:2/-1; }
  .strow-count{ grid-column:2; text-align:left; }
  /* Always on below 1024: this is the width where there is no pointer to hover
     with, and an action revealed by hover is an action that does not exist. */
  .tl-acts{ opacity:1; }
}

@media (max-width:719px){
  /* The self-scrolling bar and .brand{flex:none} used to live here, written for
     375px. They are unconditional now — see .topbar in the shell — because the
     same crush turned up at 1280px on the admin board, where the bar carries
     five nav links, the plate box and a status pill. Only the tighter gap is
     genuinely about the width. */
  .nav{ gap:10px; }

  .deals-upnext{ padding:8px 12px; }
  .deals-col__head{ padding:10px; }
  .deals-col__body{ padding:8px; }
  .deal-hero-shot{ width:100%; height:auto; aspect-ratio:4/3; }
}

/* ── Registration lookup ───────────────────────────────────────────────────
   A plate box in the topbar and the dialog it opens. The input is styled as a
   number plate on purpose: it is the one field in the app you fill in from a
   photograph or a scrap of paper, and it should look like the thing you are
   copying. */
.regbox { display:flex; gap:6px; align-items:center; }
.regbox input {
  width:118px; font-size:15px; font-weight:700; letter-spacing:1.5px;
  text-align:center; text-transform:uppercase;
  background:var(--plate); color:var(--plate-text);
  border:1px solid var(--hair-strong); border-radius:4px; padding:5px 6px;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.regbox input::placeholder { color:var(--plate-text); opacity:.45; letter-spacing:1.5px; }
.regbox input:focus-visible { outline:2px solid var(--focus); outline-offset:1px; }

.regmodal {
  width:min(1040px, 94vw); max-height:88vh; padding:0;
  border:1px solid var(--hair-strong); border-radius:10px;
  background:var(--surface); color:var(--ink); box-shadow:var(--shadow-2);
}
.regmodal::backdrop { background:var(--scrim-strong); }
.regmodal-head {
  display:flex; justify-content:space-between; align-items:flex-start; gap:16px;
  padding:14px 18px; border-bottom:1px solid var(--hair);
  position:sticky; top:0; background:var(--surface); z-index:1;
}
.regmodal-head h2 {
  font-size:20px; font-weight:700; letter-spacing:1px; margin:0;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.regmodal-sub { font-size:13px; color:var(--ink-3); margin:2px 0 0; }
.regmodal-act { display:flex; gap:8px; align-items:center; flex-shrink:0; }
.regmodal-body { padding:16px 18px 22px; overflow-y:auto; max-height:calc(88vh - 62px); }
.regmodal-body:focus-visible { outline:none; }
.regmodal-wait, .regmodal-error { font-size:15px; color:var(--ink-3); margin:18px 0; }
.regmodal-error { color:var(--rust); }
.regmodal-warn {
  font-size:15px; font-weight:600; color:var(--rust);
  background:transparent; border-left:3px solid var(--rust);
  padding:9px 12px; border-radius:0 4px 4px 0; margin:0 0 14px;
}

/* Two columns of boxes on a wide screen, because the point of a fixed row set
   is holding two vehicles side by side and comparing them. */
.regmodal-body { column-gap:26px; }
@media (min-width:900px) { .regmodal-body { columns:2; } }
.doss { break-inside:avoid; margin:0 0 16px; }
.doss h3 {
  font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:1.2px;
  color:var(--ink-3); margin:0 0 6px; padding-bottom:4px;
  border-bottom:1px solid var(--hair);
}
.doss-rows { display:grid; grid-template-columns:auto 1fr; gap:3px 14px; margin:0; }
.doss-rows dt { font-size:13px; color:var(--ink-3); }
.doss-rows dd {
  font-size:13px; margin:0; text-align:right; font-weight:500;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}

/* The MOT table spans both columns: a run of odometer readings only means
   anything read straight down, and that is how a clocked vehicle shows up. */
.doss-mot { column-span:all; }
table.mot { width:100%; border-collapse:collapse; margin-top:2px; }
table.mot th {
  font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:1.1px;
  color:var(--ink-3); text-align:left; padding:5px 8px 5px 0;
  border-bottom:1px solid var(--hair);
}
table.mot td {
  font-size:13px; padding:5px 8px 5px 0; border-bottom:1px solid var(--hair);
  vertical-align:top;
}
table.mot .mot-odo { font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; }
tr.mot-fail td:nth-child(2) { color:var(--rust); font-weight:600; }
tr.mot-note td { color:var(--ink-3); border-bottom:none; padding-top:0; }

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

@media (max-width:900px) {
  .regbox input { width:96px; }
  .regmodal { width:96vw; }
}

/* The lookup history. A list of plates, each reopening its own stored record. */
.lookuplist .lookup-row { align-items:center; }
.lookup-row .k { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.lookup-row .v { display:flex; align-items:center; gap:12px; justify-content:flex-end; }
.lookup-van { font-size:15px; color:var(--ink-2); text-decoration:none; }
.lookup-van:hover { text-decoration:underline; }
.lookup-van.muted { color:var(--ink-3); }
.btn.btn-sm { font-size:13px; padding:3px 10px; }

/* =====================================================================
   7. THE DOOR AND THE ACCOUNT — login, billing, account, admin
   =====================================================================

   Scoped the same way layers 5 and 6 are: everything below lives inside
   .gate, .viewbar, .money or .acct-table, so restyling the way a customer
   pays can never reach into the list column.

   Nothing new in the palette. Jade is still money in your favour, rust money
   against you, and plate yellow still means "yours" — which is why it is the
   accent on the door, on the trial countdown and on the bar that says you are
   looking at somebody else's board. */

/* The door -------------------------------------------------------------- */

/* body is height:100% and overflow:hidden for the board, which has its own
   scroller inside. The gate has no such thing, so it takes the scroll back —
   otherwise a short window simply clips the button. */
.gate-body{ overflow-y:auto; background:var(--surface); }
.gate{
  min-height:100dvh; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:14px; padding:32px 20px 56px;
}
.gate-card{
  width:100%; max-width:380px;
  background:var(--bg); border:1px solid var(--hair); border-radius:var(--r-lg);
  box-shadow:0 1px 2px var(--shadow-1); padding:28px 28px 30px;
}
/* The wordmark, in the board's own masthead treatment, with the plate rule
   under it. It is the only ornament on the page and it is the one that says
   which product this is before anybody has typed anything. */
.gate-mark{
  margin:0 0 20px; padding-bottom:10px;
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.10em;
  color:var(--ink-3); border-bottom:2px solid var(--plate);
}
.gate h1{ margin:0 0 6px; font:600 20px/1.2 var(--f-text); letter-spacing:-.006em; }
.gate-lede{ margin:0 0 18px; font-size:15px; line-height:1.5; color:var(--ink-3); }
.gate h1 + .gate-form{ margin-top:18px; }
.gate-form{ display:flex; flex-direction:column; gap:14px; }
.gate-field{ display:flex; flex-direction:column; gap:6px; }
.gate-field > span{
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink-3);
}
.gate-field input{
  background:var(--surface-2); color:var(--ink);
  border:1px solid var(--hair); border-radius:var(--r-sm); padding:11px 12px;
  font:500 15px/1.2 var(--f-text); width:100%;
}
.gate-hint{ margin:0; font-size:13px; line-height:1.45; color:var(--ink-3); }
/* One sentence, whatever went wrong. Rust, because a refusal is not a warning
   about a van and the page has nothing else on it to confuse it with. */
.gate-err{
  margin:0 0 16px; padding:10px 13px; font-size:15px; line-height:1.45;
  background:var(--surface-2); border-left:3px solid var(--rust-2);
  border-radius:var(--r-sm); color:var(--ink-2);
}
.gate-foot{ margin:0; max-width:380px; font-size:13px; line-height:1.5; color:var(--ink-3); text-align:center; }
.gate-foot a{ color:var(--ink-2); }

/* Looking at somebody else's board -------------------------------------- */

/* Plate yellow, filled, across the whole width and above the topbar. It cannot
   be scrolled away and it cannot be dismissed, because the failure it exists to
   prevent is Kish forgetting whose shortlist he is reading. */
.viewbar{
  grid-area:viewbar;
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:8px 16px; background:var(--plate); color:var(--plate-ink);
}
.viewbar-what{
  font:700 13px/1.2 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
}
.viewbar form{ margin:0; }

/* The topbar's account end ---------------------------------------------- */

/* The account end of the masthead: who you are and what you owe, kept apart
   from .nav because .nav is the places on the board. The rule between them is
   what makes the two clusters read as two things rather than eight tabs. */
.you{
  flex:none; display:flex; align-items:center; gap:14px;
  margin-left:14px; padding-left:14px; border-left:1px solid var(--hair-strong);
}
.you a{
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  text-decoration:none; color:var(--ink-3);
  padding:8px 2px; border-bottom:2px solid transparent;
  transition:color 90ms linear;
}
.you a:hover{ color:var(--ink-2); }
.you a.on{ color:var(--ink); border-bottom-color:var(--jade); }
/* The trial countdown rides the Billing tab rather than taking a tab of its
   own. Three days is short enough that it wants seeing on every page, and
   plate yellow is already the hue that means "this one is yours". */
.you .trial{
  display:inline-block; margin-left:5px; padding:2px 5px; border-radius:var(--r-sm);
  background:var(--plate); color:var(--plate-ink);
  font:700 13px/1 var(--f-display); letter-spacing:.04em; text-transform:none;
}
.nav-out{ margin:0; display:flex; align-items:center; }
/* A button that reads as the nav link beside it. The POST underneath is what
   matters — a link that signs you out is a link an email can pull. */
.nav-out button{
  border:0; background:none; cursor:pointer; padding:8px 2px;
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
  color:var(--ink-3); border-bottom:2px solid transparent;
  transition:color 90ms linear;
}
.nav-out button:hover{ color:var(--ink-2); }

/* Under about 1200px the masthead runs out of room once the account end is on
   it. The whole-board readout is the thing worth losing: it is a summary of
   what the page below already shows. */
@media (max-width:1200px){
  .sub, .brand-rule{ display:none; }
}

/* The money pages ------------------------------------------------------- */

.sheet-narrow{ max-width:520px; }
/* One column of cards rather than a grid. Billing is read top to bottom —
   what you are on, what you have left, what happens when it runs out — and a
   two-column layout makes that order a matter of where the eye lands. */
.money{ display:flex; flex-direction:column; gap:18px; max-width:680px; margin-inline:auto; }
/* A label that follows a paragraph is starting a new subject, not captioning
   the one above it. Without this, "Top up" sat on the last line of the prose
   explaining what a check is. */
.money .hint + .label{ margin-top:20px; }
.money section{ margin:0; }
.money .card.pad{ margin:0; }
.money .label{ margin-bottom:10px; }
.money .hint{ margin:12px 0 0; }
.money .kv{ margin:14px 0 0; }
/* Inside a padded card the kv has already got its inset from the card, so it
   loses its own and keeps only the rules between rows. */
.kv-plain .row{ padding:10px 0; }
/* A flex item defaults to min-width:auto and refuses to shrink below its text,
   so the ellipsis .ua asks for never fires and a 120-character user agent
   simply pushes the right-hand column off the card. */
.money .kv .k{ min-width:0; }
.money .kv .v{ flex:none; }
/* Only the session rows. The ledger's key holds a run of inline text with a
   date block under it, and turning that into a flex row would put the date
   beside the description rather than beneath it. */
.kv-sessions .k{ display:flex; align-items:center; gap:8px; }
.kv-plain .row:first-child{ padding-top:2px; }

.plan-head{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.plan-price{
  margin:10px 0 0; font:600 30px/1 var(--f-display); letter-spacing:-.02em; color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.plan-price span{ font:500 15px/1 var(--f-ui); color:var(--ink-3); margin-left:6px; }
.money-acts{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin:16px 0 0; }
.money-acts form{ margin:0; }
/* Plate yellow as a pill: the trial, and "this browser". Text colour rather
   than the fill, because a filled chip in a row of outlined ones shouts. */
.pill-plate{ color:var(--plate-text); box-shadow:inset 0 0 0 1px var(--plate-mark); }

.big-money{
  margin:6px 0 0; font:600 30px/1 var(--f-display); letter-spacing:-.02em; color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.big-money-sub{ margin:6px 0 0; font-size:15px; color:var(--ink-3); }
.money-credit .label + .big-money{ margin-top:0; }

/* The three rungs, side by side, so the ladder is one glance rather than three
   sentences. The one you are already on carries the plate ring. */
.topups{ display:flex; gap:10px; flex-wrap:wrap; margin:0 0 14px; }
.topups form{ margin:0; flex:1 1 150px; }
.btn.topup{
  display:flex; flex-direction:column; align-items:flex-start; gap:4px;
  width:100%; text-align:left; text-transform:none; letter-spacing:0;
  padding:12px 14px; background:var(--bg);
}
.btn.topup b{
  font:600 17px/1 var(--f-display); letter-spacing:-.01em;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.btn.topup span{ font:500 13px/1.3 var(--f-ui); color:var(--ink-3); }
.btn.topup.on{ box-shadow:inset 0 0 0 2px var(--plate-mark); }

.topup-own{ display:flex; align-items:flex-end; gap:10px; flex-wrap:wrap; }
/* The pound sign belongs to the field, not to the label above it — a number
   box with no unit in it is a number box somebody types pence into. */
.pounds{
  display:inline-flex; align-self:flex-start; align-items:center; gap:6px;
  background:var(--surface-2); border:1px solid var(--hair);
  border-radius:var(--r-sm); padding:0 12px;
}
.pounds span{ color:var(--ink-3); font-size:15px; }
/* Typed for specificity, and only for that: .fld input[type=number] above is
   (0,2,1) and a bare .pounds input is (0,1,1), so the field kept the boxed look
   of the control it is sitting inside and rendered as a box within a box. */
.pounds input[type=number], .pounds input[type=text]{
  border:0; background:none; padding:10px 0; width:96px;
  font:500 15px/1.2 var(--f-text); color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.autoform{ display:flex; flex-direction:column; gap:14px; align-items:flex-start; }
.autoform .frow{ margin:0; }
.autoform .f-check{ display:flex; }

/* Every movement of money, never overwritten — so it is a list rather than a
   table of editable-looking rows. Jade in, rust out, the same two meanings
   they carry everywhere else. */
.money-ledger .kv{ margin:0; }
.led-when{ display:block; margin-top:3px; font-size:13px; }
.led-up{ color:var(--jade); }
.led-down{ color:var(--ink-2); }

/* Where you stand: three facts, equal weight, no chart. */
.standing{ display:flex; gap:26px; flex-wrap:wrap; }
.standing > div{ min-width:130px; }
/* Sentence case, deliberately. The card already carries an uppercase label,
   and a second one directly beneath it reads as the same heading said twice. */
.standing-k{ margin:0; font:500 13px/1 var(--f-ui); color:var(--ink-3); }
.standing-v{
  margin:6px 0 0; font:600 17px/1.2 var(--f-text); color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.standing-note{ margin:4px 0 0; font-size:13px; line-height:1.4; color:var(--ink-3); }

/* A user-agent string is 120 characters long and the useful part is the first
   forty. Clipped rather than parsed: whatever we recognised today, the next
   browser release would be the one we got wrong. */
.ua{ display:block; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

.feedlist{ margin:0; padding:0; list-style:none; display:flex; flex-direction:column; gap:8px; }
.feedlist li{ display:flex; align-items:center; gap:10px; }
.feed-name{ font-size:15px; color:var(--ink); }
.feedgrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:8px 16px; margin:0 0 16px; }
.feedgrid .f-check{ display:flex; align-items:center; gap:9px; font-size:15px; }
.feedgrid .f-check input[type=checkbox]{ width:15px; height:15px; accent-color:var(--plate-mark); }

.pwform{ display:flex; flex-direction:column; gap:14px; align-items:flex-start; max-width:340px; }
.pwform .gate-field{ width:100%; }
.pwform .gate-field > span{ font:500 13px/1 var(--f-ui); text-transform:none; letter-spacing:0; }
/* And every field label on these pages, for the same reason: inside a card
   that already carries an uppercase label, a second uppercase label directly
   under it reads as the heading said twice. The board's own forms keep the
   uppercase .fk — there the label IS the only heading. */
.money .fk{ font:500 13px/1 var(--f-ui); text-transform:none; letter-spacing:0; }

/* The paywall ----------------------------------------------------------- */

.paywall h2{ margin-bottom:8px; }
.paywall-lede{ margin:0; font-size:15px; line-height:1.55; color:var(--ink-2); }
.paywall .kv{ margin:16px 0 0; }
.paywall-plan{ margin-top:22px; padding-top:18px; border-top:1px solid var(--hair); }
.paywall-price{
  margin:2px 0 0; font:600 30px/1 var(--f-display); letter-spacing:-.02em; color:var(--ink);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.paywall-price span{ font:500 15px/1 var(--f-ui); color:var(--ink-3); margin-left:6px; }
.paywall form{ margin-top:18px; }
.paywall-foot{ text-align:center; }
.paywall-foot a{ color:var(--ink-2); }

/* Admin ----------------------------------------------------------------- */

.acct-table{ margin-bottom:26px; overflow:hidden; }
.acct-row{
  display:grid;
  grid-template-columns:minmax(180px,2fr) minmax(150px,1.4fr) 120px 90px 60px 130px auto;
  gap:14px; align-items:center;
  padding:12px 16px; border-bottom:1px solid var(--hair); font-size:15px;
}
.acct-row:last-child{ border-bottom:0; }
.acct-head{
  background:var(--surface-2); color:var(--ink-3); white-space:nowrap;
  font:700 13px/1 var(--f-display); text-transform:uppercase; letter-spacing:.06em;
}
.acct-row .num{
  text-align:right;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.acct-who{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.acct-who a{ color:var(--ink); text-decoration:none; font-weight:600; }
.acct-who a:hover{ text-decoration:underline; }
.acct-who .quiet{ font-size:13px; overflow:hidden; text-overflow:ellipsis; }
.acct-note{ display:block; margin-top:3px; font-size:13px; }
.acct-note-bad{ color:var(--rust); }
.acct-acts{ display:flex; gap:8px; justify-content:flex-end; }
.acct-acts form{ margin:0; }

/* A table of seven columns has nowhere left to go on a laptop screen, so it
   scrolls sideways inside its own card rather than shearing a button off. */
@media (max-width:1100px){
  .acct-table{ overflow-x:auto; }
  .acct-row{ min-width:900px; }
}
