/* ---------------------------------------------------------------------------
   The Holiday Edit: the components, in one file every page links.

   Everything the admin and the customer's quote page are built from is
   drawn here once: the buttons, the boxes she types in, the panels and the
   folds, the price tables, the item grid, the job card. The markup for each
   is a component in src/ui/, and this is its look. The site's quote form
   links this file too and draws the one control it shares, so a component
   written for one surface is available to every other without being copied.

   Served as a plain static asset at /ui.css, like /tokens.css, because a
   page the Worker renders cannot import a stylesheet Astro builds into a
   hashed name. It is public and holds nothing private: a class name is not
   a secret.

   What belongs here: how a component is drawn, at every width, including
   the way its own parts rearrange on a wide screen. What does not: where a
   page puts it, which is layout and stays with the page (the admin's board
   and rails in worker/admin/styles.ts, the customer page's column in
   worker/quotes/page.tsx, the site's own global.css). A surface tunes a
   component only through the custom properties below, never by restyling
   a part; a part that has to differ gets a property with a default here,
   so every other surface keeps working.

   Two names the marketing site owns are deliberately not used here: `.btn`
   and `.grid`. The site's own stylesheet loads after this one on every
   page, and a rule in both would leak into the other. The filled button is
   `.pill--fill` and the item grid is `.items`.
--------------------------------------------------------------------------- */

:root {
  /* The component tokens. The palette and the type stacks are /tokens.css;
     these are the colors and the one type setting that only a component
     uses. Their values are the admin's token sheet, design/admin-design-
     system.md, which is where they were decided. */
  --rule-strong: #D9CFBC; --grid-line: #EFE8DC;
  --field: #F7F2EA; --field-2: #FBF7F1; --double: #C6B8A3;
  --accent-wash: #F4E4D8; --deep-wash: #F1E1DE; --deep-wash-rule: #E7D2CE; --lost: #7A6A5C;
  --dark: #2E2620; --on-dark: #D9986B; --dark-soft: #BCAF9E; --dark-rule: #4A4036; --dark-line: #6B5C4E; --dark-ink: #FFFDF9;
  /* A flag's line on the dark band: the deep red lifted until it reads there. */
  --flag-on-dark: #E8B4A8;
  --caps: 500 11px/1 var(--sans);
  /* The film laid over a block target while a thumb is on it. */
  --press: rgba(46, 38, 32, .08);
  /* What a surface may tune. The corner every box shares; the type of the
     figure on a total line. */
  --ui-radius: 8px;
  --ui-total-size: 18px; --ui-total-font: var(--sans);
  /* The colours a surface may re-point. The customer's page and the form
     keep the deep red for the filled button, an outlined one's words and a
     link; the admin spends its colours on state instead (worker/admin/
     styles.ts), so there the button is ink and so is a link. */
  --pill-fill: var(--accent-deep); --pill-ink: var(--accent-deep); --ui-link: var(--accent-deep);
  --pill-accent-edge: var(--accent);
}
*, *::before, *::after { box-sizing: border-box; }
/* The browser's own rule for the hidden attribute is worth one class, so
   any display a rule here sets would beat it. The site's global.css carries
   the same guard for the same reason. */
[hidden] { display: none !important; }

/* --- words ------------------------------------------------------------- */
.caps { font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
.tab { font-variant-numeric: tabular-nums; }
.quiet { color: var(--ink-faint); font-size: 13px; margin: 8px 0 0; }
.help { margin: 0 0 12px; font-size: 13px; line-height: 1.45; color: var(--ink-faint); }
.empty { color: var(--ink-faint); margin: 12px 0; }
.only-wide { display: none; }

/* --- the pill: one button, in every strength ----------------------------
   Outlined by default: a second thing beside the first, or a way to reach
   them. Filled in the phase's color, it is the one thing a card asks her to
   do. The status tag is the same shape and not a button. */
.pill, .tag {
  display: inline-flex; align-items: center; justify-content: center;
  font: var(--caps); letter-spacing: .1em; text-transform: uppercase;
  border-radius: 999px; text-decoration: none; white-space: nowrap; cursor: pointer;
}
.pill { min-height: 44px; padding: 0 15px; border: 1px solid var(--edge); background: var(--card); color: var(--pill-ink); }
.pill--accent { border-color: var(--pill-accent-edge); }
.pill--small { padding: 0 14px; }
.pill--fill, .pill--now { padding: 0 18px; border-color: transparent; letter-spacing: .12em; }
.pill--fill { background: var(--pill-fill); color: var(--dark-ink); }
/* Today's step, and only today's: the one button the accent is spent on. */
.pill--now { background: var(--accent); color: var(--dark-ink); }
.pill--quiet { color: var(--lost); }
.pill--answer { background: none; border-color: var(--dark-line); color: var(--ground); }
.pill--wide { display: flex; width: 100%; min-height: 48px; letter-spacing: .14em; font-size: 12px; }
.tag { min-height: 32px; padding: 0 12px; font-size: 10.5px; letter-spacing: .12em; cursor: default; }
/* A status is said in its word, not its colour: every one is the same
   neutral tag. The accent one is the customer's page's. */
.tag { background: var(--field); color: var(--ink-soft); }
.tag--accent { background: var(--accent-wash); color: var(--accent-deep); }
/* Inline beside a word: "New" on a line of the customer's quote. */
.tag--mini { min-height: 0; padding: 4px 7px; font-size: 10px; vertical-align: middle; margin-left: 6px; }
/* A text button: Delete, Undo, Remove. No box, the caps, a colour. */
.textbtn { font: var(--caps); letter-spacing: .1em; text-transform: uppercase; min-height: 44px; padding: 0 4px; background: none; border: 0; color: var(--lost); cursor: pointer; }
.textbtn:hover { color: var(--ui-link); }
.danger { margin: 0; margin-left: auto; }
.undo { margin: 12px 0 8px; }
.undo .textbtn { padding: 0 18px; border: 1px solid var(--pill-accent-edge); border-radius: 999px; color: var(--pill-ink); letter-spacing: .12em; }

/* --- every box she types in, one size -----------------------------------
   44px tall like every button, 16px type so iOS does not zoom on focus,
   the same corner as a card. The component rules below say only what a
   box does differently; none of them sets a height or a radius. */
.line input, .line textarea, .line select, .q__next input[type=date], .mover select, .clock__for, .cust__find input, .pgw__row input {
  width: 100%; min-width: 0; font: inherit; font-size: 16px; line-height: 1.3; min-height: 44px; padding: 0 12px;
  border: 1px solid var(--edge); border-radius: var(--ui-radius); background: var(--card); color: var(--ink);
}
.line textarea { padding: 10px 12px; line-height: 1.45; }
.line input:disabled, .line select:disabled { color: var(--ink-faint); background: var(--field-2); }
.line { display: block; margin-bottom: 10px; }
.line > span { display: block; margin-bottom: 6px; font: 500 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
.line small { display: block; margin-top: 6px; font-size: 12px; line-height: 1.45; color: var(--ink-faint); }
/* A box that must be filled says so with a star after its label, read off
   the required attribute itself so a form cannot forget it and a box the
   script makes required (a from price on the quote) is marked the moment
   it is. The same star the public form draws with .req. On a list to pick
   from it sits on the legend, where the required is on the radios. */
.line:has(> :required) > span::after, .picks:has(:required) > legend::after { content: " *"; color: var(--accent-deep); letter-spacing: 0; }
/* Two fields side by side, each keeping its own space below. */
.pair { display: flex; gap: 8px; }
.pair .line { flex: 1 1 0; min-width: 0; }
/* Every dropdown: our own chevron, set in from the right edge the same
   distance the text sits in from the left. The native one on iOS is
   flush against the edge. */
.mover select, .line select, .clock__for {
  -webkit-appearance: none; appearance: none; padding-right: 38px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%238B7A66' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center;
}
/* Every pick from a list: src/ui/combo.tsx draws a native select and
   src/lib/combo.ts draws this over it, a box she types into. The select
   stays where it was, 1px and invisible, so it still posts and the browser
   can still point at it when it is required and empty. The box is a box
   like every other, with the same chevron, holding a text box with no
   chrome of its own; the list is a child of the body, placed in the page
   under the box (src/lib/place.ts), so an open section's `overflow:
   hidden` cannot cut it off and it scrolls with the box. Not `position:
   fixed`: on an iPhone with the keyboard up a fixed list landed over the
   label or the box itself. */
.combo__native { position: absolute; width: 1px !important; height: 1px; min-height: 0 !important; padding: 0 !important; margin: 0; border: 0 !important; opacity: 0; pointer-events: none; }
.combo { container-type: inline-size; min-width: 0; }
.combo__box {
  display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 4px 38px 4px 12px;
  border: 1px solid var(--edge); border-radius: var(--ui-radius); background-color: var(--card); color: var(--ink); cursor: text;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%238B7A66' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center;
}
.combo__box:focus-within { outline: 2px solid var(--accent); outline-offset: -1px; }
/* The text box inside carries none of a box's own size or edge: the box around it does. */
.combo .combo__input {
  display: block; width: 100%; min-width: 0; min-height: 0; height: auto; margin: 0; padding: 0; border: 0; border-radius: 0; background: none;
  font: 400 16px/1.3 var(--sans); color: var(--ink); text-overflow: ellipsis; outline: none; -webkit-appearance: none; appearance: none;
}
.combo .combo__input::placeholder { color: var(--ink-faint); opacity: 1; }
.combo .combo__input::-webkit-search-cancel-button { display: none; }
.combo--pictured .combo__box { min-height: 52px; }
.combo__box:has(.combo__thumb) { padding-left: 8px; }
.combo--disabled .combo__box { background-color: var(--field-2); cursor: default; }
.combo--disabled .combo__input, .combo--disabled .combo__sub { color: var(--ink-faint); background: none; }
.combo__pic { display: contents; }
.combo__sub:empty { display: none; }
/* The × on a clearable box, beside the chevron, 32px so it can be tapped without catching the box. */
.combo__clear {
  flex: none; display: grid; place-items: center; width: 32px; height: 32px; margin: -4px -8px -4px 0; padding: 0;
  border: 0; border-radius: 50%; background: none; color: var(--ink-faint); font: 400 22px/1 var(--sans); cursor: pointer;
}
.combo__clear:hover { background: var(--field); color: var(--ink); }
.combo__clear[hidden], .combo--typing .combo__clear { display: none; }
/* Typing, the box is the search: the name she is replacing loses its picture and its line. */
.combo--typing .combo__pic, .combo--typing .combo__box > .combo__text > .combo__sub { display: none; }
.combo--typing .combo__box { padding-left: 12px; }
.combo:has(~ .is-changed) .combo__box { background-color: var(--deep-wash); border-color: var(--accent-deep); }
.combo--invalid .combo__box { border-color: var(--accent-deep); }
.combo__text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.combo__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo__sub { font-size: 12px; line-height: 1.3; color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo__thumb { flex: none; display: flex; width: 48px; height: 36px; border-radius: 4px; background: var(--field); overflow: hidden; }
.combo__thumb img { display: block; width: 48px; height: 36px; object-fit: cover; }
.combo__thumb--colours { flex-direction: column; }
.combo__thumb--colours i { flex: 1; }
/* Half a phone's width (a package beside a palette) is too narrow for a
   picture and a name, so the box keeps the name. The list has room. */
@container (max-width: 220px) { .combo__box .combo__thumb { display: none; } .combo__box:has(.combo__thumb) { padding-left: 12px; } .combo--pictured .combo__box { min-height: 44px; } }
.combo__pop {
  position: absolute; z-index: 50; display: flex; flex-direction: column; padding: 6px;
  background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius); box-shadow: 0 10px 24px rgba(46, 38, 32, .14);
}
.combo__pop[hidden] { display: none; }
.combo__list { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; margin: 0; padding: 0; list-style: none; }
.combo__list:empty { display: none; }
.combo__list li { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 8px; border-radius: 6px; cursor: pointer; }
.combo__list li:hover, .combo__list li.is-active { background: var(--field); }
.combo__list li.is-current { box-shadow: inset 3px 0 0 var(--ink-soft); }
.combo__list li[aria-disabled=true] { cursor: default; background: none; }
.combo__list li[aria-disabled=true] .combo__name { color: var(--ink-faint); }
.combo__list .combo__name { font-size: 15px; color: var(--ink); }
.combo__list small { flex: none; font-size: 12px; color: var(--ink-faint); }
.combo__none { flex: none; margin: 8px 8px 4px; font-size: 13px; line-height: 1.4; color: var(--ink-faint); }
.combo__none[hidden] { display: none; }
/* On a touch screen the list opens as a sheet over the whole screen
   (src/lib/combo.ts pins its top to the top of what is on screen and pads
   its foot by what the keyboard covers): the field's name and Close, a box
   to type into at the top, where no keyboard can cover it, and the list
   under it, scrolling clear of the keyboard. Over the pinned bars, the
   phone's menu and the bar at the foot. Dragging its head moves nothing
   behind it. */
.combo__head, .combo__find { display: none; }
.combo__pop--sheet {
  position: fixed; z-index: 60; padding: calc(4px + env(safe-area-inset-top)) 16px calc(8px + env(safe-area-inset-bottom));
  border: 0; border-radius: 0; box-shadow: none; background: var(--card); overscroll-behavior: contain;
}
.combo__pop--sheet .combo__head { touch-action: none; flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 48px; }
.combo__head b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
.combo__close { flex: none; min-height: 44px; margin-right: -4px; padding: 0 4px 0 12px; border: 0; background: none; font: 500 12px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--ink); cursor: pointer; }
.combo__pop--sheet .combo__find {
  flex: none; display: block; width: 100%; min-height: 44px; margin: 0 0 8px; padding: 0 12px;
  border: 1px solid var(--edge); border-radius: var(--ui-radius); background: var(--card); color: var(--ink);
  font: 400 16px/1.3 var(--sans); -webkit-appearance: none; appearance: none;
}
.combo__pop--sheet .combo__find:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.combo__pop--sheet .combo__find::placeholder { color: var(--ink-faint); opacity: 1; }
.combo__pop--sheet .combo__list li { min-height: 48px; }
/* Suggestions under a street address box: src/lib/address.ts, on the quote
   form and the admin alike. A child of the body, placed in the page under
   the box, for the same reasons the combo's list is. */
.addr {
  position: absolute; z-index: 50; display: flex; flex-direction: column; padding: 4px;
  background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius); box-shadow: 0 10px 24px rgba(46, 38, 32, .14);
}
.addr[hidden] { display: none; }
.addr__list { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; margin: 0; padding: 0; list-style: none; }
.addr__list li { display: flex; flex-direction: column; justify-content: center; min-height: 48px; padding: 6px 10px; cursor: pointer; }
.addr__list li:hover, .addr__list li[aria-selected=true] { background: var(--field); }
.addr__main { font-size: 15px; line-height: 1.3; color: var(--ink); }
.addr__rest { font-size: 12.5px; line-height: 1.3; color: var(--ink-faint); }
.addr__credit { flex: none; margin: 4px 10px 2px; font-size: 11px; line-height: 1.3; color: var(--ink-faint); text-align: right; }
.addr__credit[hidden] { display: none; }
.addr__credit a { color: inherit; text-decoration: none; }
.addr__credit a:hover { text-decoration: underline; }

/* iOS draws a date box at its own intrinsic width and will not shrink it
   to fit, so it ran past the form's edge on a phone. And with the native
   look switched off, iOS draws the value through an inner element of its
   own that centres the text and ignores the box's padding, so "Sep 19,
   2026" sat centred and off the line every other box's text starts on.
   That inner element is styled below: left aligned, tall enough to sit
   on the box's line, no margin. The line-height is the box's inner
   height, which is what vertically centres single-line text in WebKit. */
.line input[type=date], .line input[type=datetime-local], .q__next input[type=date], .pgw__row input[type=date] {
  display: block; min-width: 0; max-width: 100%; -webkit-appearance: none; appearance: none; text-align: left; line-height: 42px;
}
.line input[type=date]::-webkit-date-and-time-value, .line input[type=datetime-local]::-webkit-date-and-time-value, .q__next input[type=date]::-webkit-date-and-time-value, .pgw__row input[type=date]::-webkit-date-and-time-value {
  text-align: left; min-height: 1.3em; margin: 0;
}
/* Two radios drawn as one switch: the choice posts with the form and
   reads without a script. */
.seg { display: inline-flex; align-items: stretch; min-height: 44px; padding: 3px; border: 1px solid var(--edge); border-radius: 999px; background: var(--card); }
.seg label { display: flex; cursor: pointer; }
.seg input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.seg span { display: inline-flex; align-items: center; padding: 0 14px; border-radius: 999px; font: var(--caps); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); white-space: nowrap; }
.seg input:checked + span { background: var(--dark); color: var(--dark-ink); }
.seg input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The halves of a switch that are whole phrases may wrap rather than run
   out of the pill on a narrow phone. */
.line--seg .seg { display: flex; }
.line--seg .seg label { flex: 1 1 auto; }
.line--seg .seg span { flex: 1; justify-content: center; padding: 4px 10px; text-align: center; white-space: normal; line-height: 1.25; }

/* Plus, box, minus: one control with three parts, a hairline between each.
   The behaviour is src/lib/stepper.ts. The buttons only show once html.js
   is set, because a button that does nothing is worse than none; the box
   works without them. */
.nudge {
  display: inline-flex; align-items: stretch; flex: 0 0 auto;
  border: 1px solid var(--edge); border-radius: var(--ui-radius);
  background: var(--card); overflow: hidden;
}
.nudge button {
  flex: 1 0 auto; width: 40px; min-height: 44px; padding: 0;
  border: 0; background: none; color: var(--ui-link); cursor: pointer;
  font: 400 20px/1 var(--sans);
}
.nudge button:active { background: var(--accent-wash); }
.nudge input {
  flex: 1 1 auto; width: 44px; min-width: 0;
  font: 400 16px/1 var(--sans);
  text-align: center; font-variant-numeric: tabular-nums;
  padding: 6px 0; border: 0;
  border-left: 1px solid var(--edge); border-right: 1px solid var(--edge); border-radius: 0;
  background: var(--card); color: var(--ink);
  -moz-appearance: textfield; appearance: textfield;
}
.nudge input::-webkit-outer-spin-button, .nudge input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.nudge input::placeholder { color: var(--ink-faint); }
.nudge input:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
html:not(.js) .nudge button { display: none; }
html:not(.js) .nudge input { border-left: 0; border-right: 0; }

/* --- what the last thing did ---------------------------------------------- */
/* News, in a wash, with a dot: it is news, not a warning. */
.flash {
  display: flex; align-items: center; gap: 9px; padding: 11px 13px; margin-bottom: 12px;
  background: var(--accent-wash); border-radius: var(--ui-radius); font-size: 14px; line-height: 1.4; color: var(--ink-soft);
}
.flash::before { content: ''; flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--accent-deep); }
/* A refused post, drawn inside the form it came from with every box still
   holding what she typed. The same wash as news, with the deep red dot:
   something she has to do, sitting where she has to do it. */
.flash--form { margin: 0 0 12px; align-items: flex-start; }
.flash--form::before { background: var(--lost); margin-top: 7px; }
/* A name in a sentence, something on the page to tap, pick or type in: bold
   (src/ui/said.tsx), so it never reads as a stray capital mid-line. */
.named { font-weight: 500; color: inherit; }
/* The undo, at the top, where she is looking the moment she realises. */
.banner {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  background: var(--dark); color: var(--ground); border-radius: var(--ui-radius);
  padding: 12px 14px; margin-bottom: 14px;
}
.banner p { margin: 0; flex: 1 1 auto; font-size: 14px; }
.banner strong { font-weight: 500; color: var(--dark-ink); }
.banner .textbtn { padding: 0 18px; border: 1px solid var(--dark-line); border-radius: 999px; color: var(--ground); letter-spacing: .12em; }
/* More than inventory holds, as she types: a box under the grid. */
.warn { margin: 10px 0 0; padding: 10px 12px; border: 1px solid var(--accent-deep); border-radius: var(--ui-radius); background: var(--deep-wash); font-size: 13px; line-height: 1.45; color: var(--ink); }
.warn b { font-weight: 500; }

/* --- panels, parts, folds, facts ------------------------------------------- */
.panel { background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius); padding: 14px; margin-bottom: 12px; }
.panel__h { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.panel__h h2 { font-size: 17px; line-height: 1.2; margin: 0; }
.panel__h span { font: 500 10px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); text-align: right; }
.panel__h span.plain { font: 400 11.5px/1 var(--sans); letter-spacing: 0; text-transform: none; }
.panel__foot { margin-top: 8px; padding-top: 11px; border-top: 1px solid var(--rule); font-size: 13px; line-height: 1.45; color: var(--ink-faint); }
.panel__foot b { font-weight: 400; color: var(--ink); font-variant-numeric: tabular-nums; }
.panel--owe { border-color: var(--accent-deep); }
.panel--owe .panel__h h2 { color: var(--accent-deep); }
/* As much room under the fold's line as over it when it is shut; the room
   under the last row of history comes with the fold when it is open. */
.panel--ledger { padding: 4px 14px; }
.panel--ledger > .fold[open] { padding-bottom: 4px; }
/* A part of a panel: a smaller head, a hairline above. */
.part { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }
.part:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.part__h { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.part__h h3 { font-size: 17px; line-height: 1.2; margin: 0; }
.part__h span { font-size: 11.5px; line-height: 1; color: var(--ink-faint); }
/* A caps head: a label, a rule running out from it, and the note at the
   right. A serif sub-head inside a section that already has one reads as
   its equal, and the hierarchy goes flat. */
.part--caps .part__h { align-items: center; gap: 8px; }
.part--caps .part__h h3 { display: flex; align-items: center; gap: 6px; flex: none; font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
.part__icon { flex: none; display: block; color: var(--ink-faint); }
.part__rule { flex: 1; height: 1px; background: var(--rule); }
.part--caps .part__h span:not(.part__rule) { font-size: 12px; }
/* A callout she wrote: her note on a version, above the lines. */
.callout { border-left: 2px solid var(--accent); padding: 2px 0 2px 14px; margin-top: 16px; color: var(--ink); }
.callout b { display: block; font: 500 11px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 6px; }
/* What they agreed to, under the lines. */
.agree { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--rule); font-size: 15px; }
.agree b { display: block; font: 500 11px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 8px; }

/* The fold: a native details, closed, remembered by nobody. One summary
   in the caps with a plus that turns to a dash, and a note at the right. */
.fold > summary {
  display: flex; align-items: center; gap: 8px; min-height: 44px; cursor: pointer; list-style: none;
  font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint);
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary::before { content: '+'; font-size: 14px; line-height: 1; }
.fold[open] > summary::before { content: '\2013'; }
.fold > summary em { margin-left: auto; font: 400 12px/1.3 var(--sans); letter-spacing: 0; text-transform: none; color: var(--ink-faint); font-style: normal; text-align: right; }
/* Inline, inside something else: the agreement, the address, Add a line. */
.fold--inline > summary { display: inline-flex; letter-spacing: .1em; }
.fold--inline > summary::before { content: '+ '; white-space: pre; font-size: inherit; }
.fold--inline[open] > summary::before { content: '\2013 '; }
.fold--inline > summary em { margin-left: 8px; text-align: left; }
.fold--inline[open] > summary { color: var(--ink-soft); }
/* A section's own fold: the summary reads as a heading. */
.fold--section > summary { color: var(--ink-soft); flex-wrap: wrap; min-height: 46px; }
/* The closed jobs, behind a fold at the foot rather than in the way. */
.fold--quiet > summary { color: var(--lost); letter-spacing: .14em; min-height: 46px; }
.fold--quiet > summary .group__note { margin-left: auto; }
/* A plain link of a summary, on the customer's page. */
.fold--plain > summary { display: block; min-height: 0; font: inherit; letter-spacing: 0; text-transform: none; color: var(--ui-link); }
.fold--plain > summary::before { content: none; }
/* Inside a card: a hairline between folds, and a body on the field ground
   that bleeds to the card's edges. */
.fold__body { margin: 0 -14px; padding: 10px 14px 14px; background: var(--field); border-top: 1px solid var(--rule); }
.fold__foot { display: flex; gap: 18px; align-items: center; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--rule); }
.fold__foot .fold { flex: 1; }

/* A fact: a label, the thing, and what to do with it. */
.fact { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-top: 1px solid var(--rule); }
.fact:first-child { border-top: 0; }
.fact > b { flex: none; width: 74px; font: 500 10.5px/1.3 var(--sans); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.fact > a:not(.pill) { flex: 1; min-width: 0; font-size: 15px; line-height: 1.3; color: var(--ui-link); text-decoration: none; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fact > span { flex: 1; min-width: 0; font-size: 15px; line-height: 1.3; color: var(--ink); overflow-wrap: anywhere; }
.fact--text { align-items: flex-start; padding: 9px 0 3px; }
.fact--text > span { font-size: 14px; line-height: 1.4; color: var(--ink-soft); }
.fact .no { color: var(--accent-deep); font-weight: 500; }
.fact--edit { padding: 2px 0 6px; }
.fact__form { margin: 0 0 6px; }
.fact__form .line { margin: 0 0 8px; }
.fact__choices { display: grid; gap: 10px; margin-top: 4px; }
/* Who the job is for, at the head of Request while nobody is linked: a
   wash, since it is the question the job is waiting on, then the people it
   looks like with a Link each, New customer, and a search. */
.cust { margin: 0 0 10px; padding: 10px 12px 12px; border-radius: var(--ui-radius); background: var(--accent-wash); }
.cust__head { margin: 0 0 6px; font: 500 10.5px/1.3 var(--sans); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.cust__head em { margin-left: 6px; font: 400 13px/1.3 var(--sans); letter-spacing: 0; text-transform: none; font-style: normal; color: var(--accent-deep); }
.cust__form { margin: 0; }
/* In the rail beside a wide job page there is no room for a button and its line side by side. */
.cust .does, .job__side .does { grid-template-columns: minmax(0, 1fr); }
.cust .does > .does__what, .job__side .does > .does__what { padding-top: 0; }
.cust__list { list-style: none; margin: 0 0 8px; padding: 0; }
.cust__list li { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-top: 1px solid var(--rule); }
.cust__list li:first-child { border-top: 0; }
.cust__list li > span { flex: 1; min-width: 0; }
.cust__list b { display: block; font-weight: 500; font-size: 15px; color: var(--ink); }
.cust__list em { display: block; font-style: normal; font-size: 13px; line-height: 1.35; color: var(--ink-faint); overflow-wrap: anywhere; }
.cust__why { font-weight: 500; color: var(--accent-deep); }
.cust__why--weak { color: var(--ink-soft); }
.cust__none { margin: 0 0 8px; font-size: 14px; }
.cust__payer { margin: 0 0 10px; font-size: 14px; color: var(--ink-soft); overflow-wrap: anywhere; }
.cust__payer em { font-style: normal; }
.cust__find { display: flex; gap: 8px; margin-top: 10px; }
.cust__find input { flex: 1; min-width: 0; }
/* The customers pages: the same list, with a total at the right of a job and the fold under a row. */
.custs { max-width: 760px; }
/* The promo codes page is the customers page's list, and a box holding a batch to copy.
   The make form shows only the boxes its switch picks, read off the checked radio
   rather than a script, so it holds with scripting off; a browser without :has shows
   every box, and the Worker reads only the ones the switch picks either way. */
.codes form:has([name=kind][value=one]:checked) .line:has([name=count]),
.codes form:has([name=kind][value=one]:checked) .line:has([name=prefix]),
.codes form:has([name=kind][value=batch]:checked) .line:has([name=code]),
.codes form:has([name=kind][value=batch]:checked) .line:has([name=uses]) { display: none; }
.codes__all { display: block; width: 100%; box-sizing: border-box; margin: 0 0 10px; padding: 10px 12px; font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .04em; color: var(--ink); background: var(--card); border: 1px solid var(--edge); border-radius: 8px; resize: vertical; }
/* The packages page is the same column: where the site stands and Publish, then the
   two lists as rows with an Edit fold, then the history. A save's message is drawn on
   the row it came from, under the fold, because that is where she is looking. */
.cat__state { margin: 0 0 10px; font-size: 15px; line-height: 1.45; color: var(--ink); }
.cat__changes { margin: 0 0 12px; padding-left: 18px; font-size: 14px; line-height: 1.45; color: var(--ink-soft); }
.cat__changes li { margin: 2px 0; }
.cat__saved { flex: 1 1 100%; margin: 6px 0 0; }
.cat .hand > b { font-size: 14px; }
.cat .pair .line:has(.cat__qty) { flex: 0 0 96px; }
.cat .visit { min-width: 0; }
.cat .flash { overflow-wrap: anywhere; }
/* A package's photos on /admin/packages: thumbnail, place, take it off. */
.cat__photo { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 6px 0; border-top: 1px solid var(--rule); }
.cat__photo img { flex: none; width: 96px; height: 72px; object-fit: cover; border-radius: 4px; background: var(--ground-warm); }
/* Edit photo needs the script; without it the photo shows from the middle, uncropped. */
.cat__photo [data-photo-edit] { display: none; }
.js .cat__photo [data-photo-edit] { display: inline-flex; }
/* Add photos: the pill is the button, and the file box inside it is out of
   sight. Without the script the box shows, so the files she picked are named. */
.cat__pick { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 10px; }
.cat__pick .help { margin: 0; }
.cat__add { position: relative; gap: 8px; }
.js .cat__add input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.js .cat__add:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
html:not(.js) .cat__add { white-space: normal; text-transform: none; letter-spacing: 0; }
.cat__photo.is-uploading img { opacity: .55; }
/* An upload's progress fills its button from the left. */
.cat__photo [data-photo-edit]:disabled { cursor: progress; min-width: 150px; font-variant-numeric: tabular-nums; color: var(--ink-soft); background: linear-gradient(to right, color-mix(in srgb, var(--accent) 30%, var(--card)) var(--p, 0%), var(--card) var(--p, 0%)); }
/* "Add from installs" under a package's photos: each install's photos in a
   grid of ticks. A photo already on the package is greyed and says so. */
.cat__from { margin-top: 10px; }
.cat__from > summary { cursor: pointer; min-height: 44px; display: flex; align-items: center; font: var(--caps); letter-spacing: .1em; text-transform: uppercase; color: var(--accent-deep); }
.cat__from-set { margin: 10px 0; }
.cat__from-set > b { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; }
.cat__from-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; }
.cat__from-photo { position: relative; display: block; cursor: pointer; }
.cat__from-photo img { display: block; width: 100%; aspect-ratio: 4 / 3; height: auto; object-fit: cover; border-radius: 4px; background: var(--ground-warm); }
.cat__from-photo input { position: absolute; top: 6px; left: 6px; width: 22px; height: 22px; margin: 0; accent-color: var(--accent-deep); }
.cat__from-photo input:disabled { cursor: default; }
.cat__from-photo input:disabled + img { opacity: .45; }
.cat__from-photo input:checked + img { outline: 3px solid var(--accent-deep); outline-offset: -3px; }
/* "Add from installs": the install picker is a combo (below), shown only
   with scripting; without it every install is listed with its photos. */
.cat__from-choose { display: none; margin: 8px 0 4px; }
.js .cat__from-choose { display: block; }
.cat__from-photo small { position: absolute; right: 6px; bottom: 6px; padding: 1px 6px; border-radius: 3px; background: var(--card); font-size: 12px; color: var(--ink-soft); }
/* A job's photos, in its install section: its install, and the customer's past ones (worker/admin/jobs/photos.tsx). */
.job-photos { margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--rule); }
.job-photos__h { margin: 0 0 6px; font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink); }
.job-photos__past { margin-top: 22px; }
/* On its own page under Installation, the pane is its frame. */
.pane .job-photos { margin-top: 0; padding-top: 0; border-top: 0; }
a.cat__from-photo { cursor: zoom-in; }
/* /admin/installs (worker/admin/installs/view.tsx). The installs as a grid
   of first photos, each with its state on the photo and its Edit fold under
   it; an open fold takes the whole row, so its form has room. */
.ins-filters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 12px; margin: 14px 0 6px; }
.ins-filters .line { flex: 1 1 150px; margin: 0; }
.js .ins-filters__go { display: none; }
.ins-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 16px; margin-top: 14px; }
.ins-card { display: flex; flex-direction: column; border: 1px solid var(--edge); border-radius: 6px; background: var(--card); overflow: hidden; }
.ins-card:has(.ins-card__edit[open]) { grid-column: 1 / -1; }
.ins-card--off .ins-card__img img { opacity: .6; }
.ins-card__img { position: relative; aspect-ratio: 4 / 3; background: var(--ground-warm); }
.ins-card__img img { display: block; width: 100%; height: 100%; object-fit: cover; }
.ins-card__state { position: absolute; left: 8px; top: 8px; padding: 2px 8px; border-radius: 3px; font: var(--caps); font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase; background: var(--card); color: var(--ink-soft); }
.ins-card__state--on { background: var(--accent-deep); color: var(--dark-ink); }
.ins-card__state--private { background: var(--ink); color: var(--card); }
.ins-card__body { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px 4px; font-size: 14px; }
.ins-card__body b { font-weight: 500; font-size: 15px; }
.ins-card__job, .ins-card__meta { color: var(--ink-soft); }
.ins-card__edit { padding: 0 12px 10px; }
.ins-card .cat__saved { margin: 0 12px 12px; }
/* The strip editor (worker/admin/pages/strip.tsx): the strip as it drifts
   on the site, each tile the shape of its place, 150px tall on a phone and
   180px on the desk, scrolling sideways. */
.st { padding-bottom: 0; }
.st__say { margin: 10px 0 12px; font-size: 14px; color: var(--ink-soft); }
.st__scroll { overflow-x: auto; margin: 0 -16px; padding: 4px 16px 10px; scrollbar-width: thin; }
.st__tiles { --th: 150px; display: flex; align-items: flex-start; gap: 10px; margin: 0; padding: 0; list-style: none; min-height: var(--th); }
.st-tile { position: relative; flex: none; width: calc(var(--th) * var(--rp, 1)); height: var(--th); }
.st-tile__photo { display: block; width: 100%; height: 100%; padding: 0; border: 0; border-radius: 6px; overflow: hidden; background: var(--dark); cursor: pointer; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.st-tile__photo img { display: block; width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
.st-tile__n { position: absolute; top: 6px; left: 6px; min-width: 22px; height: 22px; padding: 0 6px; box-sizing: border-box; border-radius: 11px; background: rgba(29, 26, 23, .78); color: #FFFDF9; font: 500 12px/22px var(--sans); text-align: center; }
.st-tile.is-new .st-tile__photo { box-shadow: 0 0 0 2px var(--accent-deep); }
.st-tile.is-new .st-tile__n { background: var(--accent-deep); }
.st-tile.is-picked .st-tile__photo { box-shadow: 0 0 0 3px var(--ink); }
.st-tile.is-dragging { opacity: .35; }
.st-tile__ctl { display: flex; gap: 4px 10px; flex-wrap: wrap; margin-top: 4px; font-size: 13px; }
.st--js .st-tile__ctl { display: none; }
.st-drop { flex: none; display: flex; align-items: center; justify-content: center; width: 150px; height: var(--th); padding: 0 12px; box-sizing: border-box; border: 1.5px dashed var(--edge); border-radius: 6px; font-size: 12.5px; line-height: 1.4; text-align: center; color: var(--ink-faint); }
.st__tiles:not(:has(.st-tile)) .st-drop { width: 100%; }
.st__tiles.is-over .st-drop { border-color: var(--accent-deep); color: var(--accent-deep); }
/* The strip, small, where a photo is put on the home page: this install's
   photos outlined, the rest there to place them among. */
.st--placer .st__tiles { --th: 84px; }
.st--placer .st-tile__n { top: 4px; left: 4px; min-width: 18px; height: 18px; border-radius: 9px; font-size: 10.5px; line-height: 18px; padding: 0 4px; }
.st--placer .st-tile.is-mine .st-tile__photo { box-shadow: 0 0 0 2px var(--accent); }
.st--placer .st-tile.is-mine.is-picked .st-tile__photo { box-shadow: 0 0 0 3px var(--ink); }
.st--placer .st-tile:not(.is-mine) .st-tile__photo img { opacity: .7; }
.st-ghost { position: fixed; z-index: 1000; margin: 0; pointer-events: none; opacity: .92; box-shadow: 0 12px 28px rgba(46, 38, 32, .3); border-radius: 6px; transform: rotate(-2deg); }
.st-sel { display: flex; align-items: center; justify-content: space-between; gap: 8px 12px; flex-wrap: wrap; margin-top: 6px; padding: 10px 12px; border: 1px solid var(--rule); border-radius: 8px; background: var(--card); }
.st-sel b { font-weight: 500; font-size: 14px; }
.st-sel__do { display: flex; gap: 6px; }
.st-note { margin: 10px 0 0; padding: 8px 12px; border-left: 2px solid var(--edge); font-size: 13px; color: var(--ink-soft); }
.st-add { margin-top: 22px; }
.st-add__find { display: block; width: 100%; box-sizing: border-box; min-height: 44px; margin: 10px 0 4px; padding: 0 14px; border: 1px solid var(--edge); border-radius: 8px; background: var(--card); font: 400 15px var(--sans); }
.st-add__list { margin: 6px 0 12px; padding: 0; list-style: none; }
.st-add__install { padding: 12px 0; border-bottom: 1px solid var(--rule); }
.st-add__who { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin: 0 0 8px; }
.st-add__who b { font-weight: 500; font-size: 15px; }
.st-add__who span { font-size: 12.5px; color: var(--ink-faint); }
.st-add__photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 6px; }
.st-add__photo { position: relative; display: block; aspect-ratio: 4 / 3; padding: 0; border: 0; border-radius: 4px; overflow: hidden; background: var(--dark); cursor: pointer; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.st-add__photo img { display: block; width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
.st-add__photo .tag { position: absolute; left: 4px; bottom: 4px; display: none; margin: 0; }
.st-add__photo.is-in img { opacity: .4; }
.st-add__photo.is-in .tag { display: inline-flex; }
/* A photo that did not upload: Retry on its button, the reason under it. */
.cat__err { flex: 1 1 100%; margin: 0; font-size: 14px; color: var(--accent-deep); }
.cat__photo.is-failed img { opacity: .55; }
.cat__said { min-height: 1em; margin: 6px 0 0; font-size: 14px; color: var(--ink-soft); }
/* The photo editor (worker/admin/photo-editor.ts). */
.pe { width: min(640px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); padding: 16px; border: 1px solid var(--edge); border-radius: 8px; background: var(--card); color: var(--ink); box-sizing: border-box; }
.pe::backdrop { background: rgba(20, 16, 12, .6); }
.pe__stage { position: relative; aspect-ratio: 4 / 3; overflow: hidden; border-radius: 4px; background: #1d1a17; touch-action: none; cursor: grab; user-select: none; }
.pe__stage canvas { display: block; width: 100%; height: 100%; }
.pe__frame { position: absolute; box-shadow: 0 0 0 999px rgba(0, 0, 0, .55); outline: 1px solid rgba(255, 255, 255, .85); pointer-events: none; }
.pe__focus { position: absolute; width: 22px; height: 22px; margin: -11px 0 0 -11px; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 0 1px rgba(0, 0, 0, .5); }
.pe__how { margin: 8px 0; font-size: 14px; color: var(--ink-soft); }
.pe__row { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; }
.pe { overflow: auto; overscroll-behavior: contain; }
/* Done stays in reach on a phone, where the sliders run past the screen. */
.pe__row--end { justify-content: flex-end; margin: 0; }
/* The status and the buttons stay in reach on a phone, where the sliders
   run past the screen: whatever Done is doing is always in sight. */
.pe__foot { position: sticky; bottom: -16px; margin-bottom: -16px; padding: 8px 0 16px; background: var(--card); border-top: 1px solid var(--rule); }
.pe__foot .pe__said:empty { display: none; }
.pe.is-busy .pe__stage, .pe.is-busy .pe__slider, .pe.is-busy .pe__row:not(.pe__row--end), .pe.is-busy [data-reset], .pe.is-busy [data-cancel] { opacity: .45; }
.pe.is-busy .pe__stage { cursor: progress; }
/* Done grows to say what it is doing; Start over, which cannot be used then, makes the room. */
.pe.is-busy [data-reset] { display: none; }
.pe [data-done]:disabled { cursor: progress; min-width: 130px; font-variant-numeric: tabular-nums; background: linear-gradient(to right, var(--accent-deep) var(--p, 0%), color-mix(in srgb, var(--accent-deep) 60%, var(--card)) var(--p, 0%)); }
.pe__stage:focus { outline: none; }
.pe__slider { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 0 8px; margin: 6px 0; font-size: 14px; }
.pe__slider input { grid-column: 1 / -1; width: 100%; min-height: 32px; accent-color: var(--accent-deep); }
.pe__slider output { font-variant-numeric: tabular-nums; color: var(--ink-soft); }
.pe__said { margin: 0 0 8px; font-size: 14px; color: var(--ink-soft); }
.cat__photo .line { flex: 0 0 110px; margin: 0; }
/* With the script, the order is dragged by the handle and the place menus
   are kept (renumbered as she drags) but out of sight. Without it, the menus. */
.cat__grip { display: none; appearance: none; border: 0; background: none; padding: 10px 4px; margin-right: -6px; cursor: grab; color: var(--ink-faint); touch-action: none; }
.cat__grip svg { display: block; fill: currentColor; }
.js .cat__grip { display: block; }
.js .cat__photo .line { display: none; }
/* With the menu gone, a row is the handle, the thumbnail, and its buttons
   stacked beside it, so a list of photos is short enough to drag through. */
.js .cat__photo { display: grid; grid-template-columns: auto 96px minmax(0, 1fr); column-gap: 12px; row-gap: 0; align-items: center; justify-items: start; }
.js .cat__photo > .cat__grip, .js .cat__photo > img { grid-row: 1 / span 4; align-self: center; }
.js .cat__photo > :not(.cat__grip):not(img) { grid-column: 3; }
.js .cat__photo .cat__remove { min-height: 36px; }
.js .cat__photo [data-photo-edit] { margin-bottom: 2px; }
.cat__photo.is-dragging { border: 1px dashed var(--accent); border-radius: 6px; }
.cat__photo.is-dragging > * { visibility: hidden; }
.cat__ghost { position: fixed; z-index: 1000; margin: 0; padding: 6px 8px; pointer-events: none; background: var(--card, #FFFDF9); border: 1px solid var(--accent); border-radius: 6px; box-shadow: 0 10px 28px rgba(46, 38, 32, .25); transform: scale(1.02); }
.cat__ghost .cat__grip { cursor: grabbing; }
.cat__remove { display: flex; align-items: center; gap: 8px; min-height: 44px; font-size: 14px; color: var(--ink-soft); }
.cat__remove input { flex: none; width: 20px; height: 20px; accent-color: var(--accent-deep); }
/* A palette's three colours where a price would be. */
.cat__swatch { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--edge); border-radius: 4px; background: var(--card); vertical-align: middle; }
.cat__swatch i { display: block; width: 16px; height: 22px; }
.cat__color { width: 100%; min-height: 44px; padding: 2px; }
.custs__foot { margin: 16px 0 8px; }
.custs__row > span { display: block; }
.custs__row .fold { margin-top: 2px; }
.custs__total { flex: none; font-variant-numeric: tabular-nums; font-size: 15px; color: var(--ink); }
.custs__acts { display: grid; gap: 10px; margin-top: 4px; }
.custs__find { margin: 0 0 10px; align-items: center; }
.custs__find a { flex: none; font-size: 13px; }
.cust__list b a { color: var(--ui-link); text-decoration: none; }
.contents { margin: 4px 0 0; padding-left: 18px; color: var(--ink-soft); font-size: 14px; }
.contents li { margin: 1px 0; }
.agreed { font-size: 13px; color: var(--ink-faint); }

/* --- the price table: names left, figures right, read down ------------------- */
.lines { width: 100%; border-collapse: collapse; }
.lines tr + tr th, .lines tr + tr td { border-top: 1px solid var(--rule); }
.lines th { text-align: left; font-weight: 400; color: var(--ink); padding: 10px 10px 10px 0; vertical-align: top; }
.lines th small, .lines th em { display: block; font-style: normal; font-size: 13px; color: var(--ink-faint); }
.lines td { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--ink); padding: 10px 0; vertical-align: top; }
.lines td small { color: var(--ink-faint); font-size: 12px; margin-right: 3px; }
.lines td s { color: var(--ink-faint); font-size: 12px; margin-right: 5px; }
/* Tight: inside a fact or a panel, where the lines are a record and not the page. */
.lines--tight { margin-top: 3px; }
.lines--tight tr + tr th, .lines--tight tr + tr td { border-top: 0; }
.lines--tight th { padding: 2px 10px 2px 0; font-size: 14px; color: var(--ink-soft); }
.lines--tight th em, .lines--tight th small { font-size: 12px; }
.lines--tight td { padding: 2px 0; font-size: 14px; color: var(--ink-soft); }
/* What changed since they last looked. */
.lines th .was { display: inline-block; margin-left: 6px; font-size: 12px; color: var(--accent-deep); text-decoration: line-through; }
.lines th .say { display: block; margin-top: 4px; font-size: 14px; color: var(--ink-soft); }
.lines tr.was-row th, .lines tr.was-row td { color: var(--ink-faint); text-decoration: line-through; }
.lines .changed td { color: var(--accent-deep); }
/* A line to tick: a suggestion, with a count box beside it when it is
   sold by the piece. With the plus and minus in the row there is no room
   on a phone for "$55 per bundle of ten" on one line, so the unit goes
   under the figure. */
.opt th label { display: flex; gap: 12px; align-items: flex-start; cursor: pointer; }
.opt th label > span { min-width: 0; }
.opt td { white-space: normal; }
.opt td small { display: block; margin: 2px 0 0; }
.opt input[type=checkbox] { width: 22px; height: 22px; margin: 2px 0 0; accent-color: var(--accent-deep); flex: none; }
/* A list to pick from on the admin: a package, a week, an add-on. One row
   per option, the control at the left, the price at the right in tabular
   figures, and a count box instead of a tick for a thing sold by the piece. */
.picks { margin: 0 0 14px; padding: 0; border: 0; min-width: 0; }
.picks legend { padding: 0; margin-bottom: 6px; font: 500 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
.picks__row { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 4px 0; border-top: 1px solid var(--rule); cursor: pointer; }
.picks > legend + .picks__row { border-top: 0; }
.picks__row input[type=radio], .picks__row input[type=checkbox] { width: 22px; height: 22px; margin: 0; accent-color: var(--accent-deep); flex: none; }
.picks__name { flex: 1; min-width: 0; font-size: 15px; line-height: 1.3; color: var(--ink); }
.picks__price { flex: none; font-size: 14px; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }
.picks__price small { font-size: 12px; color: var(--ink-faint); }
.picks__row--count { cursor: default; }
.picks__row--count .nudge { flex: none; }
.picks--choice { padding-left: 34px; }
.newjob .picks__row .nudge input { width: 64px; }
/* The questions that belong to one package are collapsed until it is
   picked. Under html.js only, and until the script marks the chosen
   package's own with data-on (NEWJOB_SCRIPT in worker/admin/shell.tsx):
   keyed off the class the head sets rather than added by the script, so
   a long page does not paint every group and then fold them. With
   scripting off nothing here matches and every group is drawn. */
html.js .newjob [data-choice-for]:not([data-on]), html.js .newjob [data-drawn-to]:not([data-on]) { display: none; }
/* Her prices, the quote editor's rows: one per line the picks make and
   one line of her own at a time, collapsed the same way and shown by the
   same script. Every row has its rule above it, the first included,
   because the first one drawn is seldom the first one showing. */
html.js .newjob [data-line-for]:not([data-on]), html.js .newjob [data-own]:not([data-on]) { display: none; }
.newjob__prices { margin: 0 0 14px; border-top: 1px solid var(--rule); }
.newjob__lines .edit__row:first-child { border-top: 1px solid var(--rule); padding-top: 14px; }
.newjob__prices > .line { margin-top: 14px; }
.newjob__note { margin-top: 12px; }
.views__new { margin-left: auto; }
/* On a phone a row with a count box has no room for the name and the
   price on one line, so the name takes the line and the price sits with
   the box under it. */
@media (max-width: 719px) {
  .picks__row--count { flex-wrap: wrap; row-gap: 4px; }
  .picks__row--count .picks__name { flex: 1 1 100%; }
  .picks__row--count .picks__price { margin-left: auto; }
}
/* The sum under the lines. */
.total { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; border-top: 2px solid var(--ink); margin-top: 6px; padding: 12px 0 14px; font-size: 13px; color: var(--ink-soft); }
.total b { font: 500 var(--ui-total-size)/1 var(--ui-total-font); color: var(--ink); font-variant-numeric: tabular-nums; }
.total b small { font-size: 13px; color: var(--ink-faint); margin-right: 4px; font-family: var(--sans); }
/* Every version, one to a line. */
.history { list-style: none; margin: 8px 0 0; padding: 0; font-size: 13px; color: var(--ink-soft); }
.history li { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 10px; padding: 6px 0; border-top: 1px solid var(--rule); }
.history li b { color: var(--ink); font-weight: 500; margin-right: 6px; }
.history li em { flex: 1 1 100%; font-style: normal; color: var(--ink-faint); }

/* --- the jobs board ---------------------------------------------------- */
/* Today: the day, as a dark band, above everything and in both groupings. */
.today { background: var(--dark); border-radius: var(--ui-radius); padding: 14px; color: var(--ground); }
.today__head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px; }
.today__head b { font: var(--caps); letter-spacing: .14em; text-transform: uppercase; color: var(--on-dark); }
.today__head span { font-size: 12px; line-height: 1; color: var(--dark-soft); }
.today__item { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-top: 1px solid var(--dark-rule); }
.today__list > .today__item:first-child { border-top: 0; padding-top: 0; }
/* The list scrolls only where the jobs board's rail caps it (styles.ts);
   its scrollbar is drawn for the dark ground rather than the page's. */
.today__list { scrollbar-width: thin; scrollbar-color: var(--dark-line) transparent; }
/* The name and its line are a link to the job, the same page the button
   beside them leads into. Drawn as the text it always was; the name
   underlines on hover so a pointer is promised a click it takes. */
.today__who { display: block; flex: 1; min-width: 0; color: inherit; text-decoration: none; }
.today__who b { display: block; font: 600 18px/1.2 var(--serif); color: var(--dark-ink); overflow-wrap: anywhere; }
.today__who span { display: block; font-size: 13px; line-height: 1.35; color: var(--dark-soft); }
.today__who span.today__flag { color: var(--flag-on-dark); }
.today__who:hover b { text-decoration: underline; text-underline-offset: 3px; }
.today__item form { flex: none; }
.today__foot { padding-top: 12px; border-top: 1px solid var(--dark-rule); font-size: 13px; line-height: 1.35; color: var(--dark-soft); }
.today__none { padding: 2px 0 10px; font-size: 14px; color: var(--dark-soft); }

/* The filters: chips on a phone, a rail on a wide screen. Links, so it is
   a url she can leave open and the page still needs no script. */
.filters { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.filters::-webkit-scrollbar { display: none; }
.filters a {
  display: inline-flex; align-items: center; gap: 6px; flex: none; min-height: 38px; padding: 0 13px;
  font: var(--caps); letter-spacing: .1em; text-transform: uppercase; text-decoration: none;
  color: var(--ink-soft); background: var(--card); border: 1px solid var(--edge); border-radius: 999px;
}
.filters a b { font-weight: 400; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.filters a.at { background: var(--dark); border-color: var(--dark); color: var(--dark-ink); }
.filters a.at b { color: var(--dark-ink); }
.filters a.faint { color: var(--ink-faint); }
/* Needs you, with something in it: the count is a flag, a dot and the deep red. */
.filters a.flagged b { color: var(--accent-deep); font-weight: 500; }
.filters a.flagged::before { content: ''; flex: none; width: 7px; height: 7px; margin-right: 1px; border-radius: 50%; background: var(--accent-deep); }
.filters a.flagged.at b { color: var(--flag-on-dark); }

/* A row of links picking one thing: By phase / By week, Bought / Written off / Counted. */
.views { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.views__switch { display: flex; background: var(--card); border: 1px solid var(--edge); border-radius: 999px; padding: 3px; }
.views__switch a {
  display: inline-flex; align-items: center; min-height: 38px; padding: 0 15px; border-radius: 999px;
  font: var(--caps); letter-spacing: .1em; text-transform: uppercase; text-decoration: none; color: var(--ink-soft);
}
.views__switch a[aria-current] { background: var(--dark); color: var(--dark-ink); }
.views__note { font-size: 12px; line-height: 1.3; color: var(--ink-faint); }
.modes { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 11px; }
/* Four tabs since the case form: two to a row on a phone rather than one wrapping mid-word. */
.modes a { flex: 1 1 calc(50% - 5px); display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--edge); background: none; color: var(--ink-soft); font: var(--caps); letter-spacing: .1em; text-transform: uppercase; text-decoration: none; white-space: nowrap; }
@media (min-width: 520px) { .modes a { flex-basis: 0; } }
.modes a.at { background: var(--dark); border-color: var(--dark); color: var(--dark-ink); }

/* A group's head: the name, a hairline, and what is in it. */
.group { margin-bottom: 18px; }
.group__head { display: flex; align-items: center; gap: 8px; margin: 0 2px 8px; }
.group__head b { font: var(--caps); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-soft); }
.group__head span:not(.group__note) { flex: 1; height: 1px; background: var(--rule-strong); }
.group__note { font-size: 12px; line-height: 1; color: var(--ink-faint); font-variant-numeric: tabular-nums; }

/* --- one job ----------------------------------------------------------- */
.q {
  background: var(--card); border: 1px solid var(--edge); border-left: 3px solid var(--rule-strong);
  border-radius: var(--ui-radius); padding: 13px 14px 4px; margin-bottom: 8px; scroll-margin-top: 12px;
}
/* The edge says whether the job needs her, never its phase: the rule at
   rest, the accent when its trip is today or running, the deep red when
   something is waiting on her. `stateOf` in worker/admin/jobs/plan.ts. */
.q--now { border-left-color: var(--accent); }
.q--flag { border-left-color: var(--accent-deep); }
.q--closed { background: none; }
.q--closed h2, .q--closed .q__total { color: var(--ink-soft); }
/* Back from the job page lands on the card she opened, and the tint says which. */
.q:target { background: var(--field); border-color: var(--rule-strong); }
.q__head { display: flex; gap: 13px; align-items: stretch; }
/* The day, as a chip at the left edge, so the days line up into a column
   she can run her eye down. A job with no day shows an empty one. */
.q__day {
  flex: none; width: 50px; display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 6px 0; border-radius: 6px; background: var(--field); text-align: center; color: var(--ink-soft);
}
.q__day b { font: 500 10px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; }
.q__day span { margin-top: 3px; font: 500 21px/1 var(--sans); font-variant-numeric: tabular-nums; }
.q--now .q__day { background: var(--accent-wash); color: var(--accent-deep); }
.q__day--none, .q--now .q__day--none { background: none; border: 1px dashed var(--rule-strong); color: var(--ink-faint); }
a.q__day { text-decoration: none; }
.q__main { flex: 1; min-width: 0; }
/* On a list card the head is a link to the job, the same one Open is: the
   name, the total and the two lines under them take the tap. It is drawn as
   the text it always was, in the colours each line sets for itself, and the
   name takes the deep red on hover so a pointer is promised a click it
   takes. On the job's own page the head is a div and none of this applies. */
a.q__main { display: block; color: inherit; text-decoration: none; }
a.q__main:hover h2 { text-decoration: underline; text-decoration-color: var(--rule-strong); text-underline-offset: 3px; }
.q__top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.q__top h2 { font-size: 20px; line-height: 1.15; overflow-wrap: anywhere; margin: 0; }
.q__total { flex: none; font: 500 17px/1 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A new price not agreed yet, under the agreed one the money counts. */
.q__newprice { display: block; margin-top: 3px; text-align: right; font: 500 12px/1 var(--sans); color: var(--accent-deep); }
.q__date { margin-top: 2px; font: 500 14px/1.35 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
.q__date--needs { color: var(--accent-deep); }
/* A flag's line carries a dot, so the red is not the only thing saying it. */
.q__date--needs::before { content: ''; display: inline-block; width: 7px; height: 7px; margin: 0 7px 1px 0; border-radius: 50%; background: var(--accent-deep); vertical-align: middle; }
.q__date--quiet { color: var(--ink-faint); font-weight: 400; }
.q__meta { font-size: 13px; line-height: 1.35; color: var(--ink-faint); }
/* The action row: the status, one contextual pill, and a hairline running
   from where it is into the one button that moves it. */
.q__act { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 12px; }
.q__line { flex: 1; height: 1px; background: var(--rule); }
.q__arrow { flex: none; width: 0; height: 0; border-left: 5px solid var(--rule); border-top: 4px solid transparent; border-bottom: 4px solid transparent; }
.q__next { display: flex; align-items: center; gap: 6px; flex: none; margin: 0; }
.q__next input[type=date] { width: 150px; }
/* Schedule wants a day typed beside it, and the box says so. */
.q__when { display: flex; align-items: center; gap: 8px; margin: 0; font: 400 13px/1.35 var(--sans); color: var(--ink-faint); white-space: nowrap; }
/* On a phone the caption, the box and the button do not fit on the action
   row, so they take a row of their own under it, left to right like a
   question: the caption, the box growing to fill, the button at the end.
   The hairline and its arrowhead are not drawn on that card, because a
   line pointing at the right edge with the button on the next row pointed
   at nothing. Until 2026-09-21 the pair wrapped whole and floated right. */
.q__next--day { flex: 1 1 100%; min-width: 0; }
/* min-width: 0 on the label as well as the box: a flex item will not shrink
   below its content's natural width unless told, and a date box's natural
   width plus the caption plus the button is wider than a phone's card. */
.q__next--day .q__when { flex: 1 1 0; min-width: 0; }
.q__next--day input[type=date] { flex: 1 1 0; width: auto; min-width: 0; }
.q__act:has(.q__next--day) > .q__line, .q__act:has(.q__next--day) > .q__arrow { display: none; }
.q--closed .q__act { display: none; }
/* Where it stands: three chips, one per axis, and the sentence, under the
   action row on the job's page so it is on the first screen of a phone. The
   sentence links into the panel its step lives in. A contradiction between
   her status and the rows is its own line, in the deep red. */
.stand { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--rule); }
.stand__chips { display: flex; flex-wrap: wrap; gap: 6px; }
.stand__chip {
  display: inline-flex; align-items: baseline; gap: 5px; min-height: 26px; padding: 0 10px; border-radius: 999px;
  font-size: 12px; line-height: 26px; color: var(--ink); background: var(--rule);
}
.stand__chip b { font: var(--caps); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.stand__chip--wait { background: var(--deep-wash); color: var(--accent-deep); }
.stand__chip--wait b { color: var(--accent-deep); }
.stand__chip--do { background: var(--accent-wash); color: var(--accent-deep); }
.stand__chip--settled { background: var(--accent-wash); color: var(--ink); }
.stand__chip--warn { background: none; border: 1px solid var(--accent-deep); color: var(--accent-deep); line-height: 24px; }
.stand__chip--warn b { color: var(--accent-deep); }
.stand__text { margin: 8px 0 0; font-size: 15px; line-height: 1.4; color: var(--ink); }
.stand__text a { color: inherit; text-decoration: none; }
.stand__text a:hover { color: var(--ui-link); }
.stand__hint { color: var(--ink-faint); }
.stand--wait .stand__text a, .stand--warn .stand__text a { color: var(--accent-deep); }
.stand--quiet .stand__text { color: var(--ink-faint); }
.stand__warn { margin: 6px 0 0; font-size: 14px; line-height: 1.4; font-weight: 500; color: var(--accent-deep); }
.q__meta--warn { color: var(--accent-deep); }
/* The folds under a card. There is no Open: the name and the two lines
   under it are the link into the job (since October 2026). */
.q__folds { margin-top: 8px; border-top: 1px solid var(--rule); }
.q__folds > .fold { border-top: 1px solid var(--rule); }
.q__folds > .fold:first-child { border-top: 0; }
/* The head of the job's own page: the card, with the day of the next trip
   and the line that moves it as two folds across its foot. Side by side,
   because each is one control and neither is worth a band of its own; an
   open one takes the whole width, since a date box beside a dropdown at
   390px is two half-boxes. */
.q__folds--page { display: flex; flex-wrap: wrap; gap: 16px; }
.q__folds--page > .fold { flex: 0 1 auto; border-top: 0; }
.q__folds--page > .fold[open] { flex: 1 1 100%; }
.q__folds--page .day-form { margin-top: 6px; padding-top: 0; border-top: 0; }
.q__folds--page .mover { margin-top: 8px; }
.q--page { padding-bottom: 14px; margin-bottom: 12px; }
.q--page .q__top h2 { font-size: 24px; }
.back {
  display: inline-flex; align-items: center; min-height: 44px; margin-bottom: 2px;
  font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); text-decoration: none;
}
.back::before { content: '\2190'; margin-right: 6px; }
.back:hover { color: var(--ui-link); }

/* The invoice: one line and one button. */
.invoice { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--rule); }
.invoice__row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; font-size: 13px; color: var(--ink-soft); }
.invoice__line { flex: 1 1 auto; }
.invoice__line strong { font-weight: 500; color: var(--ink); }
.invoice__row > a { color: var(--ui-link); text-decoration: none; border-bottom: 1px solid var(--rule-strong); }
.invoice form { margin: 0; }
.invoice__trail { margin: 6px 0 0; font-size: 12px; }
.invoice__who { margin: 6px 0 0; }
.invoice__act { margin-top: 8px; }
.invoice__blocked { margin: 0; }

/* Move it anywhere: one line in its own panel on the job page. */
.mover { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0; }
.mover__does { flex: 1 0 100%; }
/* Closing a job that still owes money: the two answers, above the select. See Mover in worker/admin/jobs/view.tsx. */
.mover__close { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; margin: 0 0 12px; padding-bottom: 12px; border-bottom: 1px solid var(--rule); }
.mover__close .help, .mover__close .flash { flex: 1 0 100%; margin: 0; }
.mover label { flex: 1; display: flex; align-items: center; gap: 8px; min-width: 0; }
.mover select, .mover label > .combo { flex: 1; }
html.js .mover button { display: none; }

/* --- the arc: how far a job has got ---------------------------------------
   Two things at once. A segment is filled by what the ROWS prove, in its
   own phase's colour, so the bar runs brick through ochre the way the rest
   of the admin does; and the segment her STATUS sits on stands proud. A
   filled segment past the tall one is the record running ahead of what she
   has marked, which is ordinary. A hollow red one under the tall one is a
   claim nothing backs, and it is the only colour here that asks for
   anything. Markup: src/ui/arc.tsx. The reckoning: worker/admin/jobs/arc.ts. */
/* Runs to the edges of the card, and each segment takes the width its own
   NAME needs rather than an equal share. Equal shares meant "New" and
   "Scheduled" both got 39px on an eight step job, so the longest word set
   the type size for every word, and it came out at 7px. Sized to their
   names they fit at 9px, which is what the six step arcs were already
   using, so one size now reads on every job. The cost is that the filled
   run is no longer a fraction of the whole: this says which steps are
   done, not what percentage, which is the question it was built for. */
.arc {
  display: flex; align-items: flex-start; gap: 3px; margin: 14px -14px 0;
  padding: 0; list-style: none; container-type: inline-size;
}
.arc__step { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.arc__bar { display: block; height: 8px; border-radius: 3px; background: var(--rule); }
/* Every segment is named, rather than the bar dropping steps or breaking
   words. "Scheduled" needs 59px in the small caps this app uses for labels
   everywhere else, and 48px in sentence case, which is what pays for the
   type staying legible instead of going to 6px; the names are names rather
   than labels here, so sentence case is right on its own terms too. Sized
   by a query on the ROW's width, because the row is what every name has to
   share: the query container is .arc, not the segment, or each segment
   would answer its own 40px and read every phone as the narrowest one. */
.arc__name {
  font: 500 9px/1.25 var(--sans); color: var(--ink-faint); text-align: center;
  white-space: nowrap;
}
/* The longest arc a job can have is nine names, which measure about 330px
   of type at 9px with the gaps counted (eight, before Invoiced, were 290).
   The row is 358px on the phone this is drawn for and 288px on the
   narrowest one still in use, so the names step down a point rather than
   wrapping or being cut. The rungs sit a little above where the type
   actually stops fitting, because a font that loads slightly wider than
   the one measured here should not clip a name. */
@container (max-width: 355px) { .arc__name { font-size: 8px; } }
@container (max-width: 319px) { .arc__name { font-size: 7px; } }
/* And up. 9px is what a phone can afford, not what the names want: the
   middle column on a laptop gives the row 606px for 330px of type, so for
   a while the arc was set at the phone's size on a screen with twice the
   room for it. It stops at 11px, which is the caption size the rest of the
   admin uses, because the bar under the names is 8px tall and type taller
   than that reads as the heading of something rather than the label of a
   segment. The rungs are on the row, like the ones above. */
@container (min-width: 430px) { .arc__name { font-size: 11px; } }
@container (min-width: 395px) and (max-width: 429px) { .arc__name { font-size: 10px; } }
.arc__say { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.arc__step--at .arc__name { color: var(--ink); }
.arc__step--done .arc__bar { background: var(--ink-soft); }
/* Never happened, and the job went past it. Quiet: a quote she chose not
   to send is not a problem and must not nag like one. */
.arc__step--skipped .arc__bar {
  background: repeating-linear-gradient(115deg, var(--rule-strong) 0 2px, transparent 2px 5px);
}
/* Claimed right now, and nothing backs it. */
.arc__step--unbacked .arc__bar { background: none; box-shadow: inset 0 0 0 1.5px var(--accent-deep); }
.arc__step--unbacked .arc__name { color: var(--accent-deep); }
/* Where she says it is. Taller rather than another colour, so it reads
   against every fill state without inventing a fifth one. It grows by 3px
   in BOTH directions out of the 8px it is given, so it still takes 8px of
   the row and every name keeps the same baseline; growing downwards alone
   pushed this one name 6px below the others, which read as a fault rather
   than as emphasis. */
.arc__step--at .arc__bar { height: 14px; margin: -3px 0; }
/* An exit, not a position. The arc keeps what it earned and goes quiet. */
.arc--lost .arc__step--done .arc__bar { background: var(--rule-strong); }

/* --- a section: open panel or one line, one component ---------------------
   The job's page is four of these in a fixed order and the phase decides
   which are open. It is one <details> in both states, so a section she
   opened is visibly the same thing as the section beside it that she did
   not: the summary is the strip when it is closed and the panel's header
   band when it is open. Its colour is its STATE, the same three every
   page uses: grey at rest, the accent for the one section today's step
   happens in, the deep red for a flag. It holds in both states.
   Markup: src/ui/section.tsx. Which are open: worker/admin/jobs/sections.ts. */
.sect { margin: 0; --tone: var(--ink-faint); --band: var(--field); }
.sect--now { --tone: var(--accent); --band: var(--accent-wash); }
.sect--flag { --tone: var(--accent-deep); }
.sect > summary {
  display: flex; align-items: center; gap: 12px; min-height: 58px; padding: 0 4px;
  border-top: 1px solid var(--rule-strong); cursor: pointer; list-style: none;
}
.sect > summary::-webkit-details-marker { display: none; }
.sect > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sect > summary h2 { flex: none; font-size: 17px; line-height: 1; }
.sect__icon { flex: none; display: block; color: var(--tone); }
.sect__fact {
  flex: 1; min-width: 0; font-size: 13px; line-height: 1.3; color: var(--ink-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sect--flag > summary .sect__fact { font-weight: 500; color: var(--accent-deep); }
.sect__note { flex: 1; min-width: 0; font-size: 12px; line-height: 1.3; color: var(--ink-faint); }
.sect__glyph { flex: none; font: 500 14px/1 var(--sans); color: var(--ink-faint); }
.sect__glyph::before { content: '+'; }
/* One pixel each, and they are not a fudge.
   `align-items: center` centres BOXES, and the ink inside a box does not
   sit at the box's centre: it sits (ascent - descent) / 2 away from it,
   which is a property of the font at that size. Cormorant at 17px and Jost
   at 13px land in different places, so the heading and the fact were 1.5px
   apart and the icon 2px from the fact, measured off the rendered pixels
   with descenders taken out of the text. That reads as a row that is not
   centred, and it is.
   Line-height cannot fix it. A centred box puts the baseline at
   (ascent - descent) / 2 from the box centre whatever the line-height is,
   which is why setting 1.3 on the heading changed nothing. The only fixes
   are to restructure the row around a shared baseline, which the icon
   cannot join, or to nudge. Measured: 2.00px of spread across the four
   things becomes 0.63px. If the type or the icon size changes, measure
   again rather than trusting these numbers. */
.sect > summary .sect__fact, .sect > summary .sect__note { position: relative; top: 1px; }
.sect__icon { margin-top: -1px; }
/* Open, it is a card with its own coloured band, and the fact gives way to
   the note: the fact is a summary of what is inside, and what is inside is
   now on the screen. */
.sect__note, .sect[open] > summary .sect__fact { display: none; }
.sect[open] > summary .sect__note { display: block; }
.sect[open] > summary .sect__glyph::before { content: '\2013'; }
.sect[open] {
  background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius);
  overflow: hidden; margin-bottom: 12px;
}
.sect[open] > summary { min-height: 0; padding: 12px 14px; border-top: 0; background: var(--band); }
.sect[open] > summary h2 { font-size: 19px; }
/* A CLOSED strip after an open card, and the foot after one, stand off it
   by 2px so the strip's own border-top does not butt against the card's
   edge. An OPEN section must not take it: `overflow: hidden` makes the
   card a block formatting context, so the margin cannot escape it and
   lands inside, painting 2px of card ground above the coloured band. That
   was a white line across the top of every open section that followed
   another one. Two open cards are already apart by the 12px margin-bottom
   above. */
.sect[open] + .sect:not([open]) > summary, .sect[open] + .foot { margin-top: 2px; }
.sect__body { padding: 12px 14px 14px; }
.sect[open] .part:first-child { margin-top: 0; }
/* Status is an ordinary section: it folds like the three under it, and it
   is simply open by default. It takes the same state colours as the rest;
   the phase is in its words. */
/* Inside the Status panel the card is the panel: one border, one ground,
   one radius. The left edge goes with them, because the band above it now
   says the phase in the same colour and says it louder. */
.sect--status .q--page {
  background: none; border: 0; border-radius: 0; padding: 0; margin: 0;
}
/* A panel's bottom padding is for content, and the last thing in Status is
   a CLOSED fold: a 44px tap row with its one label centred in it, so it
   already carries 22px of its own under the words. The padding doubled it
   and left a band of nothing under "Move to". The row's own half is the
   panel's bottom padding here, and the padding comes back the moment the
   fold opens onto a form that needs it. */
.sect--status .sect__body { padding-bottom: 0; }
.sect--status:has(.q__folds--page > .fold[open]) .sect__body { padding-bottom: 14px; }

/* --- the nav over the sections: a row on a phone ------------------------------
   Plain anchors, pinned under the admin's bar as a row of icons, each in
   its section's state colour, the one she is at underlined in ink. Not drawn on a wide screen, where the
   sidebar is the nav. Where it SITS is the page's business and is in
   worker/admin/styles.ts. */
.secnav__a { --tone: var(--ink-faint); }
.secnav__a--now { --tone: var(--accent); }
.secnav__a--flag { --tone: var(--accent-deep); }
/* However many sections there are. It was `repeat(4, ...)` until Status
   joined them on 2026-09-22, and a hard count is a row that silently
   overflows the next time one is added. */
.secnav { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
/* The two big block targets on this page, a nav row and a section's own
   head, say they were pressed themselves. Left to itself iOS paints a
   translucent grey rectangle over a tapped link or summary, and over
   something 54px tall and a quarter of the screen wide that is not
   feedback, it is a flash across the design. Suppressed, and replaced with
   `--press`, a film of ink laid over whatever ground is already there: the
   page under a nav row, and any of the four tonal bands under a section's
   head once it is open. A layer rather than a colour, so one rule covers
   all five and none of them has to name a darker version of itself. Not
   `filter: brightness()`, which would put the head on a compositing layer
   of its own for the length of a press, which is the family of thing this
   was fixing. Never suppress the highlight without putting something in
   its place: a control that does not answer a tap reads as broken. */
.secnav__a, .sect > summary { -webkit-tap-highlight-color: transparent; }
.secnav__a:active, .sect > summary:active { background-image: linear-gradient(var(--press), var(--press)); }
.secnav__a {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 5px;
  min-height: 54px; padding-top: 8px; text-decoration: none; border-bottom: 2px solid transparent;
}
.secnav__icon { flex: none; display: block; width: 22px; height: 22px; color: var(--tone); }
/* The full name is the link's name for a screen reader; the short word is
   what fits under the icon. Clipped rather than `display: none`, which
   would leave the link with no name at all. */
.secnav__name { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.secnav__short { font: 500 9.5px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
/* Where she is, which is what an underline in a row of tabs means. Set by
   the tab script in the shell as `aria-current`, so the attribute a screen
   reader reads and the mark an eye sees cannot disagree. Nothing is drawn
   here for a section being open: the page says that by opening it. */
.secnav__a[aria-current] { border-bottom-color: var(--ink); }
.secnav__a[aria-current] .secnav__short { color: var(--ink); }
.secnav__dot {
  position: absolute; top: 6px; left: 50%; margin-left: 7px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent-deep);
  border: 2px solid var(--ground);
}

/* --- saving: where a save starts and ends ----------------------------------
   One form, one Save named for what it saves, and a line beside it saying
   what is unsaved. The line and the marks are the admin's unsaved-change
   script (worker/admin/unsaved.ts), so with scripting off the line is only
   drawn after a save, and the button is the ordinary filled one. Markup:
   src/ui/savebar.tsx. The rules: design/admin-design-system.md, "Saving". */
.savebar {
  display: flex; align-items: center; gap: 10px; min-height: 66px; margin-top: 14px;
  padding: 10px 0 0; border-top: 1px solid var(--rule-strong);
}
.savebar__say { flex: 1; min-width: 0; }
.savebar__state { display: block; font: 500 13.5px/1.3 var(--sans); color: var(--ink-faint); }
.savebar__list { display: block; font-size: 12px; line-height: 1.35; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.savebar__list:empty { display: none; }
.savebar__save { flex: none; }
html:not(.js) .savebar:not(.savebar--saved) .savebar__say { visibility: hidden; }
/* Clean, the Save is quiet and still posts; changed, it is the deep red,
   the colour of something waiting on her. */
.js form[data-save]:not(.is-dirty) .savebar__save { background: var(--card); border-color: var(--rule); color: var(--ink-faint); }
/* Save tapped with nothing changed: the bar says so, with a small nudge. */
.savebar__state--nudge { color: var(--ink); }
@media (prefers-reduced-motion: no-preference) {
  .savebar__state--nudge { animation: savebar-nudge .3s ease-out; }
  @keyframes savebar-nudge { 0%, 100% { transform: none; } 30% { transform: translateX(-3px); } 60% { transform: translateX(3px); } }
}
form.is-dirty .savebar { border-top-color: var(--accent-deep); }
form.is-dirty .savebar__state { color: var(--accent-deep); }
form.is-dirty .savebar__save { background: var(--accent-deep); border-color: var(--accent-deep); color: var(--dark-ink); }
/* Pinned to the foot of the screen, or of the pane it sits in. */
.savebar--pinned {
  position: sticky; bottom: 0; z-index: 3; margin: 14px calc(-1 * var(--bleed-r, 14px)) 0 calc(-1 * var(--bleed-l, 14px));
  padding: 10px var(--bleed-r, 14px) calc(10px + env(safe-area-inset-bottom)) var(--bleed-l, 14px);
  background: var(--card); box-shadow: 0 -6px 18px rgba(46, 38, 32, .08);
}
/* A changed box: the wash of a flag and its edge. A stepper is marked as a
   whole, and its row says what it was. */
.is-changed, .line .is-changed, .nudge.is-changed { background: var(--deep-wash); border-color: var(--accent-deep); }
.nudge.is-changed input { background: transparent; }
label.is-changed { border-radius: var(--ui-radius); }
.row__name .was { font-size: 12px; font-weight: 400; color: var(--accent-deep); }
/* Leaving with a change unsaved: under the bar on a phone, at the top of
   the pane on a wide screen. Only the script draws it. */
.leave {
  position: sticky; top: calc(var(--bar, 0px) + env(safe-area-inset-top)); z-index: 3;
  margin: 0 calc(-1 * var(--bleed-r, 0px)) 10px calc(-1 * var(--bleed-l, 0px));
  padding: 12px var(--bleed-r, 14px) 4px var(--bleed-l, 14px);
  background: var(--deep-wash); border-bottom: 1px solid var(--accent-deep);
}
.leave--pane { top: 0; margin: 0 0 10px; padding: 12px 14px 4px; }
.leave__say { margin: 0; font: 500 14px/1.4 var(--sans); color: var(--ink); }
.leave__say b { font-weight: 500; }
.leave__list { display: block; font-weight: 400; font-size: 12px; color: var(--ink-soft); }
.leave__list:empty { display: none; }
.leave__do { display: flex; gap: 8px; max-width: 420px; margin-top: 10px; }
.leave__do .pill { flex: 1; }
.leave__save { background: var(--accent-deep); border-color: var(--accent-deep); color: var(--dark-ink); }
.leave__stay {
  min-height: 40px; margin-top: 2px; padding: 0; border: 0; background: none; cursor: pointer;
  font: var(--caps); letter-spacing: .1em; text-transform: uppercase; color: var(--accent-deep);
}

/* --- the bar pinned to the foot of a phone --------------------------------
   The clock is what she taps standing at the truck, and before this it was
   the last thing on the page. Pinned, it is always a thumb away whatever
   she has scrolled to, and it carries the one next step beside it. Fixed
   rather than sticky, so it does not lift off the foot at the end of the
   scroll; the body's own bottom padding is the runway that keeps the last
   line of the page clear of it. Not drawn on a wide screen: there the
   clock is in view in the install panel and the step is in the head. */
.jobbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 3;
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px calc(12px + env(safe-area-inset-bottom));
  background: var(--dark); color: var(--ground);
  box-shadow: 0 -6px 18px rgba(46, 38, 32, .12);
}
.jobbar__what { flex: 1; min-width: 0; }
.jobbar__what b { display: block; font: 500 10px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--on-dark); }
.jobbar__what span { display: block; margin-top: 4px; font-size: 13px; line-height: 1.3; color: var(--dark-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jobbar form { display: flex; gap: 8px; margin: 0; }
.jobbar .pill { flex: none; }

/* --- the install ---------------------------------------------------------
   The Installation section (worker/admin/jobs/install.tsx): Trips, then
   rows that each open a page of their own. Where those pages sit, a pane
   beside the list on a wide screen or the whole page on a phone, is the
   admin's layout, in worker/admin/styles.ts. */
/* Trips: one card, in one of three states, and only the next step. */
.trips__head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.trips__head h3 { flex: none; margin: 0; font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink); }
.trips__icon { flex: none; display: block; color: var(--tone, var(--ink-faint)); }
.trips__rule { flex: 1; height: 1px; background: var(--rule); }
.trips__head .tag { flex: none; margin: 0; padding: 0 9px; min-height: 22px; font-size: 9.5px; letter-spacing: .12em; }
.trips__card { display: block; margin: 0; padding: 12px 13px; border-radius: var(--ui-radius); background: var(--field); border: 1px solid var(--rule); }
.trips__card--live { background: var(--dark); border-color: var(--dark); }
.trips__card--none { background: var(--field-2); border: 1px dashed var(--rule-strong); }
.trips__caps { display: block; font: 500 10px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
.trips__card--live .trips__caps { color: var(--on-dark); }
.trips__line { margin: 7px 0 0; font: 500 16px/1.3 var(--sans); color: var(--ink); }
.trips__line span { font-weight: 400; color: var(--ink-soft); }
.trips__card--live .trips__line { color: var(--ground); }
.trips__card--live .pill { margin-top: 12px; }
/* A trip left running: the flag's line, lifted for the dark card. */
.trips__stale { margin: 4px 0 0; font-size: 12.5px; line-height: 1.4; color: var(--flag-on-dark); }
.trips__stale a { color: inherit; text-decoration-color: currentColor; }
.trips__none { display: block; font: 500 15px/1.3 var(--sans); color: var(--ink); }
.trips__card--none .line { margin: 10px 0 0; }
.trips__do { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; margin-top: 12px; }
.trips__do form { margin: 0; }
.trips__do--stack { grid-template-columns: minmax(0, 1fr); margin-top: 10px; }
.trips > .row-link { margin-top: 8px; }

/* A row that opens a page of its own. Rows in a list are split by a
   hairline, never a heavier rule; the chosen one, the page open beside
   the list on a wide screen, is on --field. Markup: src/ui/row.tsx. */
.row-link {
  position: relative; display: flex; align-items: center; gap: 12px; min-height: 60px;
  margin: 0 -10px; padding: 6px 10px; border-radius: var(--ui-radius); color: inherit; text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.row-link:active { background-image: linear-gradient(var(--press), var(--press)); }
.row-link__text { flex: 1; min-width: 0; }
.row-link__title { display: block; font: 500 15px/1.3 var(--sans); color: var(--ink); }
.row-link__title span { font-weight: 400; color: var(--ink-soft); }
.row-link__fact { display: block; font-size: 13px; line-height: 1.35; color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-link__caret { flex: none; display: block; color: var(--ink-faint); }
.row-link--chosen { background: var(--field); }
.inst__rows > .row-link::before, .pane__box--rows > .row-link + .row-link::before {
  content: ''; position: absolute; left: 10px; right: 10px; top: 0; height: 1px; background: var(--grid-line);
}
.pane__box--rows > .row-link { margin: 0 -14px; padding: 6px 14px; border-radius: 0; }
.pane__box--rows > .row-link::before { left: 14px; right: 14px; }

/* A part: its title and note, the form in a box, the Save bar at its
   foot. On a phone the part is the page; wide, it is the pane. */
.pane__up { display: none; }
.pane__head { padding: 18px 0 12px; }
.pane__title { margin: 0; font: 600 26px/1.1 var(--serif); color: var(--ink); }
.pane__note { margin: 5px 0 0; font-size: 13px; line-height: 1.4; color: var(--ink-faint); }
.pane__caps { display: block; margin: 4px 2px 6px; font: 500 10px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
.pane__box { background: var(--card); border: 1px solid var(--rule); border-radius: 10px; padding: 14px; }
.pane__box + .pane__caps { margin-top: 16px; }
.pane__box .line:first-child { margin-top: 0; }
.pane__box--grid { padding: 12px 12px 14px; }
.pane__box--rows { padding: 0 14px; overflow: hidden; }
.pane .flash { margin-bottom: 12px; }
.notes-box {
  display: block; width: 100%; min-height: 160px; padding: 12px; border: 1px solid var(--rule); border-radius: 10px;
  background: var(--card); font: 400 16px/1.45 var(--sans); color: var(--ink); resize: vertical;
}
.notes-box.is-changed { background: var(--deep-wash); border-color: var(--accent-deep); }
/* Already there? on the schedule: a way round scheduling, in the field wash. */
.already { margin: 14px 0 0; padding: 14px; border-radius: 10px; background: var(--field); }
.already b { display: block; font: 500 14px/1.35 var(--sans); color: var(--ink); }
.already p { margin: 3px 0 10px; font-size: 13px; line-height: 1.45; color: var(--ink-soft); }
/* Remove, at the foot of its page and under a rule: never beside Save. */
.foot-act { margin-top: 28px; padding-top: 12px; border-top: 1px solid var(--rule-strong); }
.foot-act__note { margin: 8px 2px 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-faint); }
/* The phone's bar on a part, in place of the admin's: Back, and whose job. */
.part-bar { display: none; }
.cost { margin: 10px 0; font-size: 14px; color: var(--ink-soft); }
.cost strong { font-weight: 500; color: var(--ink); font-variant-numeric: tabular-nums; }
.cost em { display: block; font-style: normal; color: var(--ink-faint); font-size: 12px; }

/* --- the item grid: three folds, a row per item, a box with plus and minus --- */
.items { margin-bottom: 4px; }
/* A kind with two dimensions, drawn as the shelf she took it off: sizes
   down, colours across, one box a cell. Ten pumpkin rows become four
   lines. The one place a number box has no plus and minus beside it:
   three columns at 390px leaves about 96px a cell, and a stepper in that
   is a 30px target, which is the thing this design keeps being corrected
   for. The bare box is full size and centred instead. */
/* The box the matrix sits in, and the answer to a kind with more colours
   than fit. A cell is never drawn under its minimum, so a wide kind makes
   the grid wider than this box and this box scrolls, with the label column
   pinned to its left the way a spreadsheet pins a header. Nothing is
   dropped and nothing is squeezed to a 20px plus, and the column half in
   view at the right edge is its own invitation to push it. When everything
   fits, which is the ordinary case, nothing scrolls and this is one more
   box. */
.items__scroll {
  overflow-x: auto; overscroll-behavior-x: contain;
  margin-bottom: 9px; padding: 3px 0;
  background: var(--grid-line); border-radius: var(--ui-radius);
}
.items__matrix {
  display: grid;
  /* The label column takes what its longest label needs and no more, up to
     a ceiling: "Medium" gets 52px and gives the rest to the cells, and a
     value she names "Extra large" is held at 68px and wraps onto a second
     line rather than pushing a stepper out of its track. A fixed width
     could only be right for the labels that happen to be there today, and
     they are hers to rename. The cells share what is left, down to the
     width a stepper needs and no further. */
  grid-template-columns: fit-content(68px) repeat(var(--cols, 3), minmax(var(--cell-min, 92px), 1fr));
  gap: 3px;
  /* The side padding belongs to the GRID, not to the box that scrolls: a
     scroll container's padding-right is not part of what can be scrolled
     to in most browsers, so the last column would end flush against the
     edge. This scrolls with the cells and the last one gets its inset. */
  padding-inline: 3px;
}
/* On a phone it runs to the edges of the panel. Every pixel it gives back
   to the padding comes off the cells, and the cells are what hold the
   stepper: bleeding buys about 19px a cell, which is the difference
   between a plus she can hit with a glove on and one she cannot. */
.sect__body > form > .items > .items__scroll, .sect__body > form > .items--plain > .items__scroll { margin-inline: -14px; border-radius: 0; }
/* Pinned, so the size is still there once the colours have scrolled past
   it. Sticky cannot leave its own grid, so it comes to rest against the
   grid's 3px padding and a sliver of the scrolling row shows beside it;
   the two shadows paint that sliver and the gap on the other side in the
   grid's own ground, so a cell passes under the label rather than around
   it. */
.items__kind, .items__side { position: sticky; left: 0; z-index: 1; box-shadow: -6px 0 0 var(--grid-line), 3px 0 0 var(--grid-line); }
.items__kind { background: var(--grid-line); }
/* The corner cell sits in the label column, so its own width is part of
   what the cells have left. Set smaller and tighter than a column head for
   that reason: at 10px with .1em it was the widest thing in the column and
   was taking 10px off every stepper to say a word the panel above already
   implies. */
.items__kind { display: flex; align-items: center; min-height: 30px; padding: 0 4px; font: 500 9.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); }
.items__col { display: flex; align-items: center; justify-content: center; min-width: 0; font: 500 10px/1 var(--sans); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.items__side {
  display: flex; align-items: center; min-height: 44px; padding: 4px;
  background: var(--card); border-radius: 6px;
  font-size: 13px; line-height: 1.15; color: var(--ink);
  overflow-wrap: anywhere;
}
.items__none { display: flex; align-items: center; justify-content: center; min-height: 44px; background: var(--card); border-radius: 6px; color: var(--ink-faint); }
/* The swap: a fold under the grid, because it is a few pieces a season. */
.swap { margin-top: 14px; padding-top: 2px; border-top: 1px solid var(--rule); }
.cat { border: 1px solid var(--edge); border-radius: var(--ui-radius); overflow: hidden; margin-bottom: 9px; background: var(--card); }
.cat__head { display: flex; align-items: center; gap: 10px; min-height: 50px; padding: 0 13px; background: var(--field); cursor: pointer; list-style: none; }
.cat__head::-webkit-details-marker { display: none; }
.cat__name { font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); }
.cat__line { flex: 1; height: 1px; background: var(--rule); }
.cat__meta { font-size: 12px; line-height: 1; color: var(--accent-deep); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cat__glyph::before { content: '+'; font-size: 17px; line-height: 1; color: var(--ink-faint); }
.cat[open] .cat__glyph::before { content: '\2013'; }
.cat__cols { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 6px 13px; border-top: 1px solid var(--rule); background: var(--field-2); font: 500 9.5px/1.2 var(--sans); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-faint); }
.cat__col--i { flex: 1; min-width: 0; }
.cat__col--c { flex: none; width: 108px; text-align: center; }
.cat__col--p { flex: none; width: 122px; text-align: center; }
.cat__col--a { flex: none; width: 60px; text-align: right; }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 8px; padding: 8px 13px; border-top: 1px solid var(--rule); }
/* The name and its caption on one line, the caption giving way first;
   on an entry form the boxes take the next line so there is room. */
.row__id { flex: 1 1 120px; min-width: 0; display: flex; align-items: baseline; gap: 7px; }
.items--bought .row__id, .items--out .row__id, .items--count .row__id { flex-basis: 100%; }
.row__name { flex: none; max-width: 62%; font-size: 15px; line-height: 1.2; color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row--has .row__name { color: var(--ink); }
.items--plain .row__name { flex: 1; max-width: none; }
.row__cap { flex: 1; min-width: 0; font-size: 11.5px; line-height: 1.2; color: var(--ink-faint); font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* On the install grid the caption is only ever the over-inventory warning,
   so it goes under the name rather than squeezing it into an ellipsis. */
.items--plain .row__id { flex-wrap: wrap; }
.items--plain .row__cap { flex: 1 1 100%; }
.items--plain .row__cap:empty { display: none; }
/* On a case the caption goes under the name as well, so a name like
   "Medium orange" is never cut to make room for "works out to $3.48
   each" beside it, and it wraps rather than trailing off: two lines of it
   under the name still sit inside the height the price box gives the row. */
.items--case .row__id { flex-direction: column; align-items: flex-start; gap: 2px; }
.items--case .row__name, .items--case .row__cap { flex: none; max-width: 100%; }
.items--case .row__cap { white-space: normal; }
.items--case .row__cap:empty { display: none; }
.row--over .row__cap { color: var(--accent-deep); }
.row--over input[type=number] { border-color: var(--accent); }
.row__cells { flex: none; display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-left: auto; }
.row .nudge { width: 108px; }
.row__amt { flex: none; width: 60px; text-align: right; font: 500 13.5px/1.2 var(--sans); color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.row--has .row__amt { color: var(--ink); }
.items--count .row__amt { color: var(--accent-deep); }
/* A cell of the matrix is a `.row`, so the over-inventory warning reads it
   the way it reads a row, and these rules have to come AFTER `.row`'s own:
   same specificity, so source order is what decides. Before they did, every
   cell carried the row's padding and lost 26px of its 79px to it. */
.row--cell { display: block; min-width: 0; padding: 0; border-top: 0; }
.row--cell .row__name { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
/* The shared stepper, sized to the cell. Squeezed by default: the number
   keeps a fixed 26px, two digits at 16px tabular, and the two buttons
   share what is left, about 33px on a phone against the 40px they get in a
   fold's row, at the full 44px height.
   The squeeze is only ever needed when three of these have to share a
   phone. Given the room, the cell asks for the ordinary stepper back with
   a query on its own width, which is true wherever the room comes from: a
   kind with two colours, a wide screen, or a kind with so many colours
   that the grid scrolls and a cell may as well be full size when she gets
   to it. Without container query support nothing upgrades and every cell
   is the squeezed one, which is the control that already worked. */
.row--cell { container-type: inline-size; }
.row--cell .nudge { display: flex; width: 100%; border-radius: 6px; }
.row--cell .nudge button { flex: 1 1 0; width: auto; min-width: 0; font-size: 18px; }
.row--cell .nudge input { flex: 0 0 26px; width: 26px; padding: 0; color: var(--ink-faint); }
@container (min-width: 126px) {
  .row--cell .nudge button { flex: 1 0 auto; width: 40px; font-size: 20px; }
  .row--cell .nudge input { flex: 1 1 auto; width: 44px; padding: 6px 0; }
}
.row--cell.row--has .nudge input { font-weight: 500; color: var(--ink); }
.row--cell.row--over .nudge { border-color: var(--accent); }
/* The price of a bought row: one figure, and which figure it is. */
.price { flex: none; display: flex; align-items: stretch; width: 122px; height: 56px; border: 1px solid var(--edge); border-radius: var(--ui-radius); background: var(--card); overflow: hidden; }
.price__box { flex: 1; min-width: 0; display: flex; align-items: center; padding: 0 4px 0 8px; }
.price__dollar { color: var(--ink-faint); font-size: 13px; }
.price__in { width: 100%; min-width: 0; border: 0; padding: 0 2px; font: 500 16px/1 var(--sans); text-align: right; color: var(--ink); background: none; font-variant-numeric: tabular-nums; }
.price__in::placeholder { color: var(--ink-faint); font-weight: 400; }
/* The price box on its own, no Each / For all: a price that is always the price of one. */
.price--one { width: 96px; }
.basis { flex: none; width: 52px; display: flex; flex-direction: column; border-left: 1px solid var(--grid-line); }
.basis label { flex: 1; display: flex; cursor: pointer; }
.basis label + label { border-top: 1px solid var(--grid-line); }
.basis input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.basis span { flex: 1; display: flex; align-items: center; justify-content: center; font: 500 9px/1 var(--sans); letter-spacing: .04em; text-transform: uppercase; color: var(--ink-faint); background: var(--field-2); }
.basis input:checked + span { background: var(--dark); color: var(--dark-ink); }
.basis input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: -2px; }
.cat__sub { display: flex; align-items: baseline; gap: 8px; padding: 8px 13px; border-top: 1px solid var(--rule); }
.cat__sub-label { flex: 1; min-width: 0; text-align: right; font: 500 10.5px/1.2 var(--sans); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.cat__sub-pieces { flex: none; font-size: 11px; line-height: 1.2; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.cat__sub-money { flex: none; width: 60px; text-align: right; font: 500 14px/1.2 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
/* The bar at the foot of an entry form: the whole trip, or what is leaving, or what the count found. */
.bar { display: flex; align-items: center; gap: 10px; margin-top: 13px; padding: 13px; border-radius: var(--ui-radius); background: var(--dark); }
.bar__label { flex: 1; min-width: 0; font: 500 10.5px/1.2 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--on-dark); }
.bar__pieces { flex: none; font-size: 12px; line-height: 1.2; color: var(--dark-soft); font-variant-numeric: tabular-nums; }
.bar__money { flex: none; text-align: right; font: 500 18px/1.1 var(--sans); color: var(--dark-ink); font-variant-numeric: tabular-nums; }
.bar--out { background: var(--deep-wash); border: 1px solid var(--deep-wash-rule); }
.bar--out .bar__label, .bar--out .bar__money { color: var(--accent-deep); }
.bar--out .bar__pieces { color: #6E4C43; }
.bar--count { background: var(--card); border: 1px solid var(--rule-strong); }
.bar--count .bar__label { color: var(--ink); }
.bar__rows { display: block; margin-top: 3px; font: 400 12px/1.2 var(--sans); letter-spacing: 0; text-transform: none; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
/* The case's fraction of list, under the dark bar's label. */
.bar__ratio { display: block; margin-top: 3px; font: 400 12px/1.2 var(--sans); letter-spacing: 0; text-transform: none; color: var(--dark-soft); font-variant-numeric: tabular-nums; }
.bar__pair { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.bar__pair b { font: 500 9px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint); }
.bar__pair span { font: 500 16px/1.1 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
.bar__pair--short span { color: var(--accent-deep); }
.entry .line { margin-top: 12px; }

/* --- the money panel ------------------------------------------------------ */
.money__bal { margin: 0; font-size: 15px; color: var(--ink-soft); }
.money__bal strong { font-weight: 500; color: var(--ink); }
.money__tips { margin: 4px 0 0; font-size: 14px; color: var(--ink-soft); }
.money__ask { margin: 10px 0 0; padding: 10px 12px; border-left: 2px solid var(--accent-deep); display: grid; gap: 8px; }
.money__ask-q { margin: 0; font-size: 14px; color: var(--ink); }
.money__ask-q b { font-weight: 500; }
.money__ask .help { margin: 0; }
/* A button and what tapping it does: who is emailed, what moves. Under the
   button on a phone; beside it once there is room, with the button at the
   width of its own words so the sentences line up on one edge. */
.does { display: grid; gap: 4px; }
.does__what { margin: 0; font-size: 13px; line-height: 1.4; color: var(--ink-faint); }
.does__tick { display: flex; align-items: center; gap: 8px; min-height: 44px; margin-top: 2px; font-size: 14px; color: var(--ink); cursor: pointer; }
.does__tick input { width: 18px; height: 18px; margin: 0; accent-color: var(--accent-deep); }
@media (min-width: 720px) {
  .does { grid-template-columns: 170px 1fr; align-items: start; gap: 4px 14px; }
  .does > .does__what { padding-top: 4px; }
}
/* A price not agreed yet: the reason the figure above is not the one she just typed. */
.money__pending { margin: 6px 0 0; padding-left: 10px; border-left: 2px solid var(--accent-deep); font-size: 14px; line-height: 1.4; color: var(--ink); }
.panel--money .invoice { margin-top: 10px; }
.money__debt { margin-top: 10px; padding: 10px 12px; background: var(--accent-wash); border-radius: var(--ui-radius); font-size: 13px; color: var(--ink); }
.money__debt p { margin: 0; }
/* In place of a button that needs the job's customer: see LinkFirst in worker/admin/jobs/customer.tsx. */
.linkfirst { margin: 10px 0 0; padding: 10px 12px; background: var(--accent-wash); border-radius: var(--ui-radius); font-size: 14px; line-height: 1.4; color: var(--ink); }
.linkfirst a { color: var(--accent-deep); font-weight: 500; white-space: nowrap; }
.money__issue { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 0; }
/* Every movement of money, one to a line, with the figure at the right
   edge in tabular numerals so a column of them reads down. It was a
   two-column table until her own rows got a fold to correct them in: a
   form cannot live in half a table row, and this is the shape the trips
   list already uses for the same interaction, which is the point. */
.money__rows { margin: 10px 0 0; padding: 0; list-style: none; font-size: 14px; color: var(--ink-soft); }
/* Three columns, not a wrapping row: what it was, what it came to, and
   the fold. A flex row wrapped the fold onto a line of its own as soon as
   a note made the description long, which put Edit under the wrong thing.
   The grid keeps the figure and the fold where they are however long the
   line gets, and the fold spans the whole width when it opens. */
.money__row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; column-gap: 10px; align-items: baseline; padding: 4px 0; }
.money__what { grid-column: 1; min-width: 0; }
/* The note and the no-invoice mark go under the line rather than along
   it, the way the table this replaced already had them. */
.money__what em { display: block; font-style: normal; color: var(--ink-faint); font-size: 12px; }
.money__sum { grid-column: 2; font-weight: 400; font-variant-numeric: tabular-nums; }
/* Stripe's own rows say whose they are, in the place the fold would be,
   so a line without an Edit is answered rather than just quiet. */
.money__from { grid-column: 3; font: var(--caps); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
/* The fold is .fix rather than .move: .move is the stock ledger's own row
   further down this sheet, and borrowing it put that component's
   border-top across the top of every money row. Two pages, two words. */
.fix { grid-column: 3; }
/* Open, the fold takes the whole width, so its form is not the sliver
   left beside the figure; Edit stays at the right where it was. */
.fix[open] { grid-column: 1 / -1; }
.fix[open] > summary { display: flex; justify-content: flex-end; }
.fix > summary { display: inline-flex; align-items: center; min-height: 32px; cursor: pointer; list-style: none; font: var(--caps); letter-spacing: .1em; text-transform: uppercase; color: var(--accent-deep); }
.fix > summary::-webkit-details-marker { display: none; }
.fix__form { margin: 6px 0 4px; padding: 10px 12px; background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius); }
.fix__actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 4px; }
.money__form { margin-bottom: 0; }
.money__fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 12px; }
.money__fields .line { margin: 0; min-width: 0; }
.money__way { grid-column: 1 / -1; }

/* The quote as a record: what was agreed, read down, with the editor
   folded under it. This is what the section opens on, because "what did we
   agree" is the question a settled quote is asked and a page of boxes is
   not an answer to it. */
.quote__record { margin-top: 10px; }
.quote__record em { display: block; font-style: normal; font-size: 12px; color: var(--ink-faint); }
.quote__edit { border-top: 1px solid var(--rule); }
/* The editor and the history are two rows under the total, a hairline
   between each, at the ordinary 44px of a tap. The history was an inline
   fold, whose summary sits in a line box and carried that line's leading
   under it, and the editor had 14px of margin over the total's own 14px
   of padding and a 52px row: about 60px of air between two labels. */
.quote__history { margin-top: 0; border-top: 1px solid var(--rule); }
.quote__history > summary { display: flex; }
.quote__history .history { margin-top: 0; }
.quote__edit .edit { margin-top: 4px; }

/* --- the quote's editor -------------------------------------------------
   One row per line: the name and her note to them, then the numbers, then
   the line's total with its switch and Remove. Plain divs, not a table: the
   cells stack on a phone and sit in a row wide, and a table cell forced to
   display: block used to beat the flex rules on the cells inside it. */
.edit__row { padding: 14px 0; border-top: 1px solid var(--rule); }
.edit__row:first-child { border-top: 0; padding-top: 0; }
.edit__row--opt { background: linear-gradient(90deg, var(--accent-wash), transparent 60%); margin: 0 -14px; padding-inline: 14px; }
.edit__row--add { border-top: 1px solid var(--rule); }
.edit__main .line + .line { margin-top: 8px; }
.edit__main .line:last-child input { color: var(--ink-soft); }
.edit__nums { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
/* Two boxes to a row, so "Price of one" has room for its label at every
   width; four across left the labels running into each other. */
.edit__nums .line { flex: 1 1 calc(50% - 4px); min-width: 0; margin: 0; }
.edit__nums .line > span { white-space: nowrap; }
.edit__nums input, .edit__nums select { padding-inline: 10px; }
.edit__foot { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 12px; margin-top: 10px; }
/* On a phone the switch takes the whole row and the total and Remove
   sit under it; squeezed beside them its halves wrapped to three lines. */
.edit__foot .line { flex: 1 1 100%; margin: 0; }
.edit__pkg { flex: 1 1 auto; margin: 0; min-height: 44px; display: inline-flex; align-items: center; }
.edit__sum { flex: none; margin-left: auto; min-height: 44px; display: inline-flex; align-items: center; font: 500 17px/1 var(--sans); color: var(--ink); }
.edit__addit { flex: 1 1 auto; margin-left: auto; }
/* The form's default button, for Enter. Off screen rather than hidden,
   because a display: none button is not what every browser submits. */
.edit__default { position: absolute; left: -9999px; width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; }
.edit__actions { display: grid; gap: 12px; margin-top: 12px; }
.edit__actions .pill { width: 100%; }
.edit__add { margin: 6px 0 0; padding: 0; border-top: 1px dashed var(--rule-strong); }
.edit__add > summary { width: 100%; }
/* The custom line's name box: only for "Custom line" in the dropdown.
   Hidden under html.js and shown by the class the script sets, so the
   page is the right shape on the first frame; with scripting off it is
   simply there, and a blank name on a custom line is refused. */
.js .edit__custom:not(.edit__custom--on) { display: none; }
.edit__row--discount .line:has([name=add_off]), .edit__row--discount .line:has([name=add_qty]), .edit__row--discount .line:has([name=add_included]), .edit__row--discount .line--seg { display: none; }
/* A package picked on the Add row (a request that asked her to choose one) is one, nothing covers it and it is always agreed. */
.edit__row--package .line:has([name=add_qty]), .edit__row--package .line:has([name=add_included]), .edit__row--package .line--seg { display: none; }
/* A promo code's figure is worked out from what it covers, so its Add row has no price either, and the name box takes the code. */
.edit__row--code .line:has([name=add_price]), .edit__row--code .line:has([name=add_note]) { display: none; }
.edit__code { margin: 0; display: flex; flex-direction: column; gap: 2px; }
.edit__code b { font-weight: 500; font-size: 15px; color: var(--ink); }
.edit__code em { font-style: normal; font-size: 13px; color: var(--ink-faint); }

/* --- the stock page ---------------------------------------------------- */
/* The inventory matrix, read only. Same shape as the install's and the same
   answer to a kind with more colours than fit: it scrolls under its own
   pinned labels. A cell can be narrower here than one holding a stepper,
   because what is in it is a count over a price and not a thing to tap. */
.matrix__scroll { overflow-x: auto; overscroll-behavior-x: contain; border: 1px solid var(--edge); border-radius: 6px; }
.matrix__scroll + .matrix__scroll { margin-top: 10px; }
.matrix { display: grid; grid-template-columns: 58px repeat(var(--cols, 3), minmax(64px, 1fr)); gap: 1px; background: var(--grid-line); }
.matrix__kind, .matrix__row { position: sticky; left: 0; z-index: 1; box-shadow: -1px 0 0 var(--grid-line); }
.matrix > * { background: var(--card); padding: 10px 8px; text-align: right; }
.matrix__kind { text-align: left; padding: 8px; font: 600 12px/1.2 var(--serif, serif); color: var(--ink); }
.matrix__col { padding: 8px; font: 500 10px/1.2 var(--sans); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-faint); }
.matrix__row { text-align: left; font-size: 13px; line-height: 1.2; color: var(--ink-soft); }
.matrix__none { font-size: 13px; line-height: 1; color: var(--ink-faint); }
.matrix__n { display: block; font: 500 17px/1 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
.matrix__c { display: block; margin-top: 2px; font-size: 11px; line-height: 1; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.matrix__short { background: var(--deep-wash); }
.matrix__short .matrix__n, .matrix__short .matrix__c { color: var(--accent-deep); }
.rest { margin-top: 8px; border-top: 1px solid var(--rule); }
.hand { display: flex; align-items: baseline; gap: 10px; padding: 9px 0; border-top: 1px solid var(--rule); }
.hand > span:first-child { flex: 1; font-size: 14px; line-height: 1.3; color: var(--ink-soft); }
.hand > b { font: 500 15px/1 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
.hand > em { width: 54px; text-align: right; font: 400 12px/1 var(--sans); font-style: normal; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.hand--none > span:first-child, .hand--none > b { color: var(--ink-faint); font-weight: 400; }
/* A row with an Edit fold at its end: open, the fold's form takes the whole width under the row. */
.hand--edit { flex-wrap: wrap; }
/* A level on a supplier's list: the name and one price box. A row sits in under its kind's levels. */
.hand--price { align-items: center; }
.hand--price .price--one { height: 44px; }
.hand--sub > span:first-child { padding-left: 16px; }
.hand--edit > span:first-child em { font-style: normal; margin-left: 4px; }
/* A row in a preview, with a tick that says whether it starts active. */
.hand .tick { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.hand .tick input { width: 20px; height: 20px; margin: 0; accent-color: var(--accent-deep); }
.hand .nudge { flex: none; width: 108px; }
/* Needs reconciling: one form per item, the installs it went to under the
   count, then a price box and the two answers on one row. */
.panel--owe .help { color: var(--ink-soft); margin-bottom: 6px; }
.owe { margin: 0; padding: 12px 0 4px; border-top: 1px solid var(--rule); }
.owe__h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin-bottom: 8px; }
.owe__h b { font-weight: 500; color: var(--ink); font-size: 15px; }
.owe__h span { flex: 1 1 100%; font-size: 12.5px; line-height: 1.4; color: var(--ink-faint); }
.owe__row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; }
.owe__price { flex: 1 1 120px; margin: 0; }
.owe__row .pill { flex: 1 1 auto; }
.flash--buy { margin: 11px 0 0; }
/* The ledger: every movement, one to a line, with the balance after it. */
.ledger__cols { display: none; }
.move { display: flex; gap: 12px; padding: 11px 0; border-top: 1px solid var(--rule); }
.move__when { flex: none; width: 52px; font-size: 12px; line-height: 1.35; color: var(--ink-faint); }
.move__what { flex: 1; min-width: 0; }
.move__what b { display: block; font-weight: 400; font-size: 14px; line-height: 1.35; color: var(--ink); }
.move__what span { display: block; font-size: 12px; line-height: 1.35; color: var(--ink-faint); overflow-wrap: anywhere; }
.move__change { flex: none; text-align: right; }
.move__change b { display: block; font: 500 15px/1.2 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
.move__change b.down { color: var(--lost); }
.move__change span { display: block; margin-top: 1px; font-size: 11px; line-height: 1.2; color: var(--ink-faint); font-variant-numeric: tabular-nums; }

/* --- the season, and the reports ------------------------------------------- */
.season { background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius); padding: 15px; }
.season > b { display: block; font: var(--caps); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 12px; }
.season__row { display: flex; justify-content: space-between; align-items: baseline; padding: 9px 0; border-top: 1px solid var(--rule); }
.season__row:first-of-type { border-top: 0; padding-top: 0; }
.season__row span { font-size: 13px; line-height: 1.3; }
.season__row b { font: 500 15px/1 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
.season__row b.big { font-size: 19px; }
.season__row--warn b { color: var(--accent-deep); }
/* Low inventory: the same fact the stock page names, beside the list. */
.low { background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius); padding: 15px; }
.low--short { background: var(--accent-wash); }
.low > b { display: block; font: var(--caps); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 8px; }
.low--short > b { color: var(--accent-deep); }
.low p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--ink-soft); }
.low a { display: inline-block; margin-top: 10px; font: var(--caps); letter-spacing: .12em; text-transform: uppercase; text-decoration: none; color: var(--accent-deep); }
.kpis { display: grid; grid-template-columns: 1fr; gap: 12px; margin-bottom: 12px; }
.kpi { background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius); padding: 15px; }
.kpi--dark { background: var(--dark); border-color: var(--dark); }
.kpi__h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 9px; }
.kpi__h b { font: 500 10px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
.kpi--dark .kpi__h b { color: var(--on-dark); }
.kpi__h span { font-size: 11.5px; line-height: 1; color: var(--ink-faint); }
.kpi--dark .kpi__h span { color: var(--dark-soft); }
.kpi__n { display: flex; align-items: baseline; gap: 8px; }
.kpi__n b { font: 500 26px/1 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
.kpi--dark .kpi__n b { color: var(--dark-ink); font-size: 30px; }
.kpi__n span { font: 500 12px/1 var(--sans); color: var(--on-dark); font-variant-numeric: tabular-nums; }
.kpi__sub { margin-top: 5px; font-size: 12px; line-height: 1.4; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.kpi--dark .kpi__sub { color: var(--dark-soft); }
.kpi--wide { display: none; }
/* A report table: prices on one edge, read down, a season row at the foot. */
.rt { width: 100%; border-collapse: collapse; }
.rt th, .rt td { padding: 7px 0; border-top: 1px solid var(--rule); vertical-align: baseline; }
.rt thead th, .rt thead td { border-top: 0; padding: 0 0 5px; font: 500 9.5px/1.2 var(--sans); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-faint); }
.rt thead td { padding-left: 8px; }
/* The label takes what the figures leave, and clips rather than wraps:
   width 100% with max-width 0 is the table idiom for a flex: 1 column. */
.rt th { width: 100%; max-width: 0; text-align: left; font-weight: 400; font-size: 14px; line-height: 1.3; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rt td { text-align: right; padding-left: 8px; font-size: 13.5px; line-height: 1.3; color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
.rt tr.faint th, .rt tr.faint td { color: var(--ink-faint); }
.rt tfoot th, .rt tfoot td { border-top: 3px double var(--double); padding-top: 10px; padding-bottom: 2px; font-weight: 500; color: var(--ink); }
.rt .w { display: none; }
.rt--short td.short { font-weight: 500; color: var(--accent-deep); }
.report { margin-bottom: 12px; }

/* --- how the components rearrange on a wider screen ------------------------ */
@media (min-width: 720px) {
  .only-wide { display: inline; }
  .only-narrow { display: none; }
  .rt .w { display: table-cell; }
  .kpi--wide { display: block; }
  .kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .ledger__cols { display: flex; align-items: baseline; gap: 20px; padding: 6px 0; border-top: 1px solid var(--rule); font: 500 10px/1.2 var(--sans); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
  .ledger__cols span:nth-child(1), .move__when { width: 112px; }
  .ledger__cols span:nth-child(2) { flex: 1; }
  .ledger__cols span:nth-child(3), .move__where { flex: none; width: 36%; }
  .ledger__cols span:nth-child(4) { width: 72px; text-align: right; }
  .ledger__cols span:nth-child(5) { width: 78px; text-align: right; }
  .move { gap: 20px; align-items: baseline; }
  .move__when { font-size: 13px; }
  .move__when br { display: none; }
  .move__what span { display: none; }
  .move__where { font-size: 13px; line-height: 1.35; color: var(--ink-faint); }
  .move__change { display: flex; gap: 20px; }
  .move__change b { width: 72px; }
  .move__change span { width: 78px; margin: 0; font-size: 13px; line-height: 1.2; }
}
@media (max-width: 719px) {
  .move__where { display: none; }
}
@media (min-width: 960px) {
  .today { padding: 15px; }
  .today__item { flex-wrap: wrap; }
  .today__item form { flex: 1 1 100%; }
  .today__item .pill { width: 100%; }
  .filters { flex-direction: column; gap: 0; overflow: visible; background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius); padding: 6px 4px; }
  .filters a { min-height: 0; padding: 12px; border: 0; border-radius: 6px; background: none; }
  /* The count to the right; the dot, when there is one, stays by the name. */
  .filters a b { margin-left: auto; }
  .filters a.at { background: var(--dark); }
  .filters a.faint { border-top: 1px solid var(--rule); border-radius: 0; margin-top: 4px; }
  .q { padding: 16px 20px 2px; margin-bottom: 9px; }
  .q__day { display: none; }
  .q__top h2 { font-size: 22px; }
  .q__total { font-size: 19px; }
  .q__date { margin-top: 5px; font-size: 15px; }
  .q__act { gap: 8px; margin-top: 14px; }
  .q__next--day { flex: none; }
  .q__next--day .q__when { flex: none; }
  .q__next--day input[type=date] { flex: none; width: 150px; }
  .q__act:has(.q__next--day) > .q__line { display: block; }
  .q__act:has(.q__next--day) > .q__arrow { display: block; }
  .q__folds { display: flex; gap: 0 26px; flex-wrap: wrap; margin-top: 10px; }
  .q__folds > .fold { border-top: 0; flex: 1 1 100%; }
  .q__folds > .fold > summary em { margin-left: 0; }
  .q__folds > .fold--request { flex: 0 1 auto; }
  /* The head card's two folds stay side by side here, as they already are
     on a phone. Full width came from the rule above, which is the LIST
     card's, where a fold opens onto a panel; on this card they are two
     one-line labels, and stacking them put 26px of nothing between "Next
     trip" and "Move to" and another 22px under the last one. They are
     short, the card is 650px, and a closed fold is a label, not a row.
     Either one still takes the full width the moment it opens. */
  .q__folds--page > .fold { flex: 0 1 auto; }
  .q__folds > .fold[open] { flex: 1 1 100%; }
  .fold__body { margin: 0 -20px; padding: 12px 20px 16px; }
  .q--page { padding-bottom: 18px; margin-bottom: 16px; }
  .q--page .q__day { display: flex; }
  .q--page .q__top h2 { font-size: 26px; }
  .mover label { flex: 0 1 360px; }
  .group { margin-bottom: 22px; }
  .edit__row { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.5fr); gap: 10px 14px; align-items: start; }
  .edit__row--opt { margin: 0 -20px; padding-inline: 20px; }
  .edit__nums { margin-top: 0; }
  .edit__foot { grid-column: 1 / -1; margin-top: 0; }
  .edit__foot .line { flex: 0 1 360px; }
  .edit__addit { flex: 0 1 auto; }
  .day-form .line { flex: 0 1 280px; }
  .money__fields { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .panel { padding: 18px 20px; }
  .panel__h h2 { font-size: 19px; }
  .sect[open] > summary { padding: 14px 20px; }
  .sect[open] > summary h2 { font-size: 20px; }
  .sect > summary { gap: 14px; padding: 0 6px; }
  .sect > summary h2 { width: 120px; }
  .sect__fact { font-size: 14px; }
  /* In the rail, their request is NOT a fold. It sits beside the sections
     rather than under them and stays in view the whole time, so there is
     nothing for folding it to buy: it is drawn as the open panel it would
     become, whatever the `open` attribute says, and the summary stops
     being a control.

     `::details-content` is what opens it. CSS cannot set the open
     attribute, but it can stop the browser hiding the content, and the
     attribute keeps deciding the phone, where a fold is still worth
     having. A browser without `::details-content` draws the fold this page
     always drew, which is the state this replaces rather than a broken
     one, and the keyboard can still toggle the attribute under it with
     nothing on screen changing. So the glyph goes: a + that does not fold
     anything is the one thing here that would be a lie. */
  .job__side .sect::details-content { content-visibility: visible; }
  .job__side .sect {
    background: var(--card); border: 1px solid var(--edge); border-radius: var(--ui-radius);
    overflow: hidden; margin-bottom: 0;
  }
  .job__side .sect > summary {
    min-height: 0; padding: 14px 20px; border-top: 0; background: var(--band); cursor: default;
    flex-wrap: wrap; row-gap: 2px;
  }
  .job__side .sect > summary:active { background-image: none; }
  .job__side .sect > summary h2 { width: auto; font-size: 20px; }
  .job__side .sect__fact, .job__side .sect__glyph { display: none; }
  /* A 340px column is not a row: "Their request" and its line share about
     140px each beside the icon, which wraps a four-word note onto two
     lines against the heading. On its own line it has the whole column and
     takes one. This is what the fact did here while this was a fold. */
  .job__side .sect__note { display: block; flex: 1 1 100%; }
  /* A request longer than the rail scrolls its BODY, under a head that
     holds still: the head says what the card is, and scrolled away it
     leaves a list of phone numbers and addresses with no name over it.
     The rail is a flex column capped at the window (styles.ts), so the
     card shrinks to fit it, the head keeps its height, and the room left
     goes to the body, which scrolls. `::details-content` sits between the
     two and passes the room down. Without it a details element's content
     cannot be laid out, so the card is left its full height and the rail
     scrolls whole, head and all, which is what it did before. */
  @supports selector(::details-content) {
    .job__side .sect { display: flex; flex-direction: column; min-height: 0; }
    .job__side .sect > summary { flex: none; }
    .job__side .sect::details-content { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
    .job__side .sect__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  }
  .sect__body { padding: 14px 20px 18px; }
  .sect--status:has(.q__folds--page > .fold[open]) .sect__body { padding-bottom: 18px; }
  .jobbar { display: none; }
  .items__scroll { padding: 4px 0; }
  /* The bleed is a phone's bargain: every pixel the matrix gives back to
     the panel's padding comes off the cells, and on a phone that is the
     difference between a plus she can hit and one she cannot. On a wide
     screen there is nothing to buy, three columns have about 157px each
     either way, and a grid running past the padding that everything above
     and below it respects reads as a piece that did not get the message.
     So it sits inside the panel here and takes its corners back. */
  .sect__body > form > .items > .items__scroll, .sect__body > form > .items--plain > .items__scroll {
    margin-inline: 0; border-radius: var(--ui-radius);
  }
  .items__matrix { grid-template-columns: fit-content(110px) repeat(var(--cols, 3), minmax(var(--cell-min, 92px), 1fr)); gap: 4px; padding-inline: 4px; }
  .items__kind, .items__side { box-shadow: -8px 0 0 var(--grid-line), 4px 0 0 var(--grid-line); }
  .items__side { font-size: 14px; padding: 4px 8px; }
  .matrix { grid-template-columns: 72px repeat(var(--cols, 3), minmax(64px, 1fr)); }
  .matrix > * { padding: 11px 9px; }
  .matrix__n { font-size: 18px; }
  .panel--ledger { padding: 4px 20px; }
  .panel--ledger > .fold[open] { padding-bottom: 6px; }
}
/* A list on /admin/packages dragged into order by the handle beside each name. */
.js .cat__grip--row { display: inline-block; vertical-align: -2px; padding: 8px 8px 8px 2px; margin: -8px 2px -8px -2px; }
.hand.is-dragging { outline: 1px dashed var(--accent); outline-offset: -1px; border-top-color: transparent; }
.hand.is-dragging > * { visibility: hidden; }
.cat__ghost--row { padding: 9px 12px; list-style: none; }
.cat__order { margin: 10px 0 4px; }
/* A palette's colours: each with Remove, and the spare one at the foot with Add. */
.cat__colour { display: flex; align-items: center; gap: 12px; padding: 4px 0; }
.cat__colour.is-dragging { opacity: .35; }
/* The spare row has no handle; it lines up with the rows that do. */
.js .cat__colour:not([data-colour]) { padding-left: 30px; }
.cat__colour .cat__color { flex: none; width: 120px; }
.cat__colour .cat__remove { min-height: 44px; }
/* Delivery as one figure: the most it costs is not asked. */
.visit__form:has(input[name="kind"][value="fixed"]:checked) .cat__most { display: none; }

/* ---------------------------------------------------------------------------
   The Catalog and Website pages (design/catalogue-admin-handoff.md, and
   "Shared by the Catalog and Website pages" in design/admin-design-
   system.md): the Publish strip and its page, the palette chip, and the
   Pages hub's sketch of the site.
--------------------------------------------------------------------------- */

/* The Publish strip. Waiting changes are --field and neutral; rebuilding
   is the dark band; a publish the site never showed is the deep wash. */
.pub {
  display: flex; align-items: center; gap: 10px; min-height: 44px; margin: 0 0 12px; padding: 0 14px;
  border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--ground);
  color: var(--ink-faint); text-decoration: none; font-size: 13.5px; line-height: 1.3;
}
.pub__caps { flex: none; font: 500 10px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
.pub__line { flex: 1; min-width: 0; }
.pub__go {
  flex: none; display: inline-flex; align-items: center; min-height: 32px; padding: 0 14px; border: 0; border-radius: 999px;
  background: var(--dark); color: var(--dark-ink); font: 500 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; cursor: pointer;
}
.pub--pending { background: var(--field); color: var(--ink); }
.pub--pending:active { background-image: linear-gradient(var(--press), var(--press)); }
.pub--building { background: var(--dark); border-color: var(--dark); color: var(--dark-ink); }
.pub--building .pub__caps { color: var(--on-dark); }
.pub--stuck { background: var(--deep-wash); border-color: var(--accent-deep); color: var(--accent-deep); }
.pub--stuck .pub__go { background: var(--accent-deep); }
/* Codes have no Publish: a neutral line in the strip's place. */
.pub--none { background: none; border-style: dashed; }

/* The Publish page. */
.publish { display: flex; flex-direction: column; gap: 12px; max-width: 640px; }
.publish__sub { margin: 0; font-size: 13px; color: var(--ink-faint); }
.publish__group { margin: 0; }
.publish__lines { margin: 0; padding: 0; list-style: none; }
.publish__lines li { padding: 9px 0; border-top: 1px solid var(--grid-line); font-size: 14.5px; line-height: 1.4; color: var(--ink); }
.publish__lines li:first-child { border-top: 0; padding-top: 2px; }
.publish__go .help { margin-bottom: 10px; }
.publish__state { padding: 14px; border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--field); font-size: 14px; line-height: 1.45; color: var(--ink-soft); }
.publish__state p { margin: 0; }
.publish__state b { display: block; margin-bottom: 6px; font: var(--caps); letter-spacing: .14em; text-transform: uppercase; }
.publish__state--building { background: var(--dark); border-color: var(--dark); color: var(--dark-ink); }
.publish__state--building b { color: var(--on-dark); }
.publish__state--stuck { background: var(--deep-wash); border-color: var(--accent-deep); color: var(--ink); }
.publish__state--stuck b { color: var(--accent-deep); }
.publish__state--stuck .pill { margin-top: 12px; }
/* What stops a publish, said before she taps it: a flag, so the deep red. */
.warnbox { padding: 12px 14px; border: 1px solid var(--accent-deep); border-radius: var(--ui-radius); background: var(--deep-wash); font-size: 13.5px; line-height: 1.45; color: var(--ink); }
.warnbox b { display: block; font-weight: 500; color: var(--accent-deep); }
.warnbox p { margin: 6px 0 0; }

/* A palette as the site's paint chip: a framed rectangle, its colours as bands top to bottom. */
.chip {
  display: inline-flex; flex-direction: column; gap: 2px; flex: none; width: 24px; height: 31px; padding: 3px;
  background: var(--card); border: 1px solid var(--edge); border-radius: 4px; vertical-align: middle;
}
.chip__band { flex: 1; box-shadow: inset 0 0 0 1px rgba(46, 38, 32, .06); }
.chip--big { width: 64px; height: 84px; padding: 5px; gap: 3px; border-radius: 6px; }

/* --- Pages: the site drawn page by page ------------------------------------ */
.pg__list, .pg__pane { display: none; }
.pg--part .pg__map { display: none; }
.pg--part .pg__pane { display: block; }
.pg__chips { display: flex; gap: 6px; margin: 0 calc(-1 * var(--bleed-r, 14px)) 0 calc(-1 * var(--bleed-l, 14px)); padding: 0 var(--bleed-r, 14px) 0 var(--bleed-l, 14px); overflow-x: auto; scrollbar-width: none; }
.pg__chips::-webkit-scrollbar { display: none; }
.pg__chip {
  flex: none; display: inline-flex; align-items: center; min-height: 36px; padding: 0 14px;
  border: 1px solid var(--rule-strong); border-radius: 999px; background: var(--card); color: var(--ink);
  font: 500 10.5px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; text-decoration: none;
}
.pg__chip[aria-current] { background: var(--ink); border-color: var(--ink); color: var(--dark-ink); }
.pg__legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 12px; font-size: 12px; line-height: 1.3; color: var(--ink-soft); }
.pg__legend span { display: inline-flex; align-items: center; gap: 6px; }
.pg__key { width: 16px; height: 10px; border-radius: 2px; }
.pg__key--here { border: 1.5px solid var(--ink); }
.pg__key--from { border: 1.5px dotted var(--ink-faint); background: var(--field); }
.pg__browser { margin-top: 12px; padding: 10px 10px 12px; border: 1px solid var(--rule); border-radius: 12px; background: var(--card); box-shadow: 0 1px 3px rgba(46, 38, 32, .06); }
.pg__bar { display: flex; align-items: center; gap: 6px; padding: 0 2px 8px; border-bottom: 1px solid var(--grid-line); }
.pg__bar i { width: 7px; height: 7px; border-radius: 50%; background: var(--rule-strong); }
.pg__bar span { flex: 1; min-width: 0; margin-left: 6px; font-size: 11.5px; line-height: 1; color: var(--ink-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* A block in the sketch, bordered by where it is edited: solid ink here,
   dotted on --field-2 from Catalog. The chosen one,
   on a wide screen, is outlined in ink with a soft ring. */
.pgb { display: block; margin-top: 8px; padding: 9px 10px 10px; border-radius: 8px; background: var(--card); color: var(--ink-soft); text-decoration: none; }
.pgb--here { border: 1.5px solid var(--ink); }
.pgb--from { border: 1.5px dotted var(--ink-faint); background: var(--field-2); }
.pgb:active { background-image: linear-gradient(var(--press), var(--press)); }
.pgb__head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.pgb__label { flex: 1; min-width: 0; font: 500 9.5px/1.2 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); }
.pgb__tag {
  flex: none; display: inline-flex; align-items: center; min-height: 20px; padding: 0 8px; border-radius: 999px;
  font: 500 9px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; color: var(--ink-soft);
}
.pgb__tag--here { border: 1px solid var(--ink); background: var(--card); color: var(--ink); }
.pgb__tag--from { border: 1px dotted var(--ink-faint); background: var(--field); }
.sk-tab { display: flex; align-items: center; gap: 6px; height: 24px; max-width: 88%; padding: 0 9px; border-radius: 7px 7px 0 0; background: var(--grid-line); font-size: 11.5px; }
.sk-tab i { flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--accent); }
.sk-tab span, .sk-cards b, .sk-cards em { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sk-title { display: block; font: 600 19px/1.1 var(--serif); color: var(--ink); }
.sk-sub { display: -webkit-box; margin-top: 4px; font-size: 11.5px; line-height: 1.4; color: var(--ink-faint); -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.sk-strip { display: flex; gap: 3px; overflow: hidden; }
.sk-strip img { flex: none; height: 56px; border-radius: 2px; object-fit: cover; background: var(--grid-line); }
.sk-empty { display: block; padding: 14px 0; border: 1px dashed var(--double); border-radius: 4px; text-align: center; font-size: 12px; color: var(--ink-faint); }
.sk-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.sk-cards > span { min-width: 0; }
.sk-cards img, .sk-cards i { display: block; width: 100%; aspect-ratio: 4 / 3; border-radius: 3px; object-fit: cover; background: var(--grid-line); }
.sk-cards > span > span { display: flex; gap: 4px; margin-top: 3px; }
.sk-cards b { flex: 1; min-width: 0; font: 600 12.5px/1.15 var(--serif); color: var(--ink); }
.sk-cards em { font: 500 10.5px/1.3 var(--sans); font-style: normal; font-variant-numeric: tabular-nums; }
.sk-fan { display: flex; flex-wrap: wrap; gap: 10px; }
.sk-steps { display: flex; flex-wrap: wrap; gap: 6px; }
.sk-steps > span { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; line-height: 1; }
.sk-steps i { width: 18px; height: 18px; border: 1px solid var(--double); border-radius: 50%; font: 500 9.5px/16px var(--sans); font-style: normal; text-align: center; }
.sk-pills { display: flex; flex-wrap: wrap; gap: 4px; }
.sk-pills span { display: inline-flex; align-items: center; min-height: 20px; padding: 0 8px; border-radius: 999px; background: var(--field); font-size: 11px; line-height: 1; }
.sk-form, .sk-lines { display: flex; flex-direction: column; gap: 5px; }
.sk-form i { display: block; height: 15px; border: 1px solid var(--rule-strong); border-radius: 4px; background: var(--card); }
.sk-lines i { display: block; height: 5px; border-radius: 3px; background: var(--rule-strong); }
.sk-head { display: block; margin-bottom: 6px; font: 600 14px/1.15 var(--serif); color: var(--ink); }
.sk-season { display: flex; align-items: center; gap: 8px; }
.sk-season b { display: inline-flex; align-items: center; min-height: 26px; padding: 0 12px; border-radius: 999px; background: var(--dark); color: var(--dark-ink); font: 500 10px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; }
.sk-season span { font-size: 11.5px; color: var(--ink-faint); }

/* The chosen block's pane. */
.pg__caps { display: block; font: 500 10px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
.pg__title { margin: 6px 0 0; font: 600 26px/1.1 var(--serif); color: var(--ink); }
.pg__when { display: flex; align-items: center; gap: 8px; margin-top: 8px; font-size: 12.5px; line-height: 1.35; color: var(--ink-faint); }
.pg__sub { display: flex; align-items: center; gap: 8px; margin: 18px 2px 8px; }
.pg__sub > span { white-space: nowrap; font: var(--caps); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-soft); }
.pg__sub > i { flex: 1; height: 1px; background: var(--rule-strong); }
.pg__sub .tag--mini { margin: 0; }
.pg__say { margin: 12px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); }
.pg__pane > .pill--wide { margin-top: 14px; }
.pg__pane .help { margin: 8px 2px 0; }
.pg__also { padding: 0 14px; border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--card); }
.pg__also a { display: flex; align-items: center; gap: 10px; min-height: 58px; padding: 8px 0; border-top: 1px solid var(--grid-line); text-decoration: none; }
.pg__also a:first-child { border-top: 0; }
.pg__also a > span:first-child { flex: 1; min-width: 0; }
.pg__also b { display: block; font-weight: 500; font-size: 14.5px; line-height: 1.3; color: var(--ink); }
.pg__also a > span > span { display: -webkit-box; font-size: 12.5px; line-height: 1.35; color: var(--ink-faint); -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.pg__out { flex: none; font: 500 9.5px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); }
.pg__strip { display: flex; gap: 4px; margin-top: 16px; overflow-x: auto; scrollbar-width: none; }
.pg__strip img { flex: none; height: 120px; border-radius: 3px; object-fit: cover; background: var(--grid-line); }
.pg__from { margin: 10px 0 0; padding: 0 14px; list-style: none; border: 1px dotted var(--ink-faint); border-radius: var(--ui-radius); background: var(--field); }
.pg__from li { display: flex; align-items: center; gap: 10px; min-height: 46px; border-top: 1px solid var(--grid-line); font-size: 14.5px; line-height: 1.3; color: var(--ink); }
.pg__from li:first-child { border-top: 0; }
.pg__from li > span:not(.chip) { flex: 1; min-width: 0; }
.pg__from b { font-weight: 500; font-size: 14px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

/* The package cards' order: a draft with one Save. A row out of its saved
   place shows its number in the deep red, and the list is outlined in it. */
.pg-order { margin-top: 10px; padding-top: 4px; }
/* The rows sit in the form's own card (.cl-form), like every form here. */
.pg-order__rows { margin: 0; padding: 0; list-style: none; }
.pg-order__row { display: flex; align-items: center; gap: 10px; min-height: 60px; border-top: 1px solid var(--grid-line); touch-action: none; user-select: none; -webkit-user-select: none; }
.pg-order__row:first-child { border-top: 0; }
.pg-order__row.is-dragging { opacity: .5; }
.pg-order__n { width: 16px; font: 500 12px/1 var(--sans); color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.pg-order__row.is-moved .pg-order__n { color: var(--accent-deep); }
.pg-order__row img, .pg-order__none { flex: none; width: 48px; aspect-ratio: 4 / 3; border-radius: 3px; object-fit: cover; background: var(--grid-line); }
.pg-order__row--off img { opacity: .4; }
.pg-order__name { flex: 1; min-width: 0; }
.pg-order__name b { display: block; font-weight: 500; font-size: 14.5px; line-height: 1.25; color: var(--ink); }
.pg-order__row--off .pg-order__name b { color: var(--ink-faint); }
.pg-order__name span { display: block; font-size: 12px; line-height: 1.3; color: var(--ink-faint); }
.pg-order__mv {
  flex: none; display: flex; align-items: center; justify-content: center; width: 40px; height: 40px;
  border: 1px solid var(--rule); border-radius: 50%; background: var(--card); color: var(--ink); cursor: pointer;
}
.pg-order__mv svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pg-order__mv:disabled { opacity: .3; cursor: default; }

/* A tap that saves on its own: a form of its own, never inside one with a Save. */
.pg-tap { margin-top: 6px; }
.pg-tap--card { margin-top: 16px; padding: var(--card-pad); border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--card); }
.pg-tap__head { display: flex; align-items: center; gap: 8px; }
.pg-tap__head b { flex: 1; font-weight: 500; font-size: 15px; line-height: 1.3; color: var(--ink); }
.pg-tap__head .tag--mini { margin: 0; }
.pg-tap--card .help { margin: 4px 0 10px; }
.pg-tap__two { display: flex; gap: 6px; }
.pg-tap__two .pill { flex: 1; min-height: 44px; }
.pg-tap .pill[aria-pressed="true"], .pg-season__opt[aria-pressed="true"] i::after { background: var(--ink); }
.pg-tap .pill[aria-pressed="true"] { border-color: var(--ink); color: var(--dark-ink); }
.pg-season { display: flex; flex-direction: column; gap: 6px; }
.pg-season__opt {
  display: flex; align-items: center; gap: 12px; min-height: 62px; padding: 10px 12px; width: 100%;
  border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--field-2); text-align: left; cursor: pointer; font: inherit;
}
.pg-season__opt[aria-pressed="true"] { border-color: var(--ink); background: var(--card); }
.pg-season__opt i { flex: none; display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border: 1.5px solid var(--double); border-radius: 50%; }
.pg-season__opt[aria-pressed="true"] i { border: 2px solid var(--ink); }
.pg-season__opt i::after { content: ''; width: 10px; height: 10px; border-radius: 50%; }
.pg-season__opt b { display: block; font-weight: 500; font-size: 15px; line-height: 1.3; color: var(--ink); }
.pg-season__opt span span { display: block; font-size: 12.5px; line-height: 1.4; color: var(--ink-faint); }

/* The words: one form a block (worker/admin/pages/words.tsx). Her labels
   are sentences, "Close button (in the gallery)", so they are set as
   sentences rather than in the small caps a short label gets. A group is a
   fieldset with its name on the rule; a list's entries are cards with
   their moves at the top, and the empty one to add another is a fold. */
.line--words > span { text-transform: none; letter-spacing: 0; font: 500 13.5px/1.35 var(--sans); color: var(--ink); }
.pgw > input[name="intent"] + .pgw__top > .pg__sub { margin-top: 0; }
/* A group's name can be long ("When and where step: the address and city limits"), so it wraps here. */
.pgw .pg__sub > span { white-space: normal; line-height: 1.3; }
.pgw .pg__sub > i { min-width: 16px; }
.pgw__top { margin-bottom: 4px; }
.pgw__help { margin: -2px 2px 12px; }
.line--words textarea { field-sizing: content; min-height: 70px; }
.pgw__set { min-width: 0; margin: 14px 0 6px; padding: 12px 0 0; border: 0; border-top: 1px solid var(--grid-line); }
.pgw__item { min-width: 0; margin: 4px 0 12px; padding: 10px 12px 2px; border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--field-2); }
.pgw__set > legend, .pgw__item > legend { padding: 0 6px; margin-left: -6px; font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); }
.pgw__set > legend { padding: 0 8px 0 0; margin-left: 0; }
.pgw__set > .pgw__help { margin: 0 0 10px; }
.pgw__top > .pgw__set:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.pgw__acts { display: flex; align-items: center; justify-content: flex-end; gap: 6px; margin: -2px 0 8px; }
.pgw__acts .pg-order__mv { width: 36px; height: 36px; }
.pgw__acts .textbtn { margin-left: 6px; }
.pgw__add { margin: 0 0 12px; padding: 0 12px; border: 1px dashed var(--double); border-radius: var(--ui-radius); }
.pgw__add > summary { display: flex; align-items: center; min-height: 44px; list-style: none; cursor: pointer; font-weight: 500; font-size: 14.5px; color: var(--ink); }
.pgw__add > summary::-webkit-details-marker { display: none; }
.pgw__add > summary::before { content: '+'; width: 14px; margin-right: 6px; color: var(--ink-faint); }
.pgw__add[open] > summary::before { content: '−'; }
.pgw__rows { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.pgw__row { display: flex; align-items: center; gap: 6px; }
.pgw__row > input { flex: 1; }
.pgw__row .pgw__acts { margin: 0; }
.pgw__row--add > input { border-style: dashed; }
.pgw__new { font-size: 12px; color: var(--ink-faint); }
.pgw__new:empty { display: none; }
/* A span, because a label holds only phrasing content, so it says again what `.line > span` (the label's words) would otherwise give it. */
.line > span.pgw__hex { display: flex; align-items: center; gap: 8px; margin: 0; font: inherit; letter-spacing: normal; text-transform: none; color: inherit; }
.pgw__hex > i { flex: none; width: 32px; height: 32px; border: 1px solid var(--rule); border-radius: 50%; background: var(--field-2); }

/* --- Catalog: a list, and one item at a time (worker/admin/catalog/) ------- */
.cl__pane { display: none; }
.cl--part .cl__pane, .cl--one .cl__pane { display: block; }
.cl--part .cl__list { display: none; }
.cl__head h2 { display: none; }
.cl__head p { margin: 0 0 10px; font-size: 12.5px; line-height: 1.4; color: var(--ink-faint); }
.cl__title { margin: 0; font: 600 26px/1.1 var(--serif); color: var(--ink); }
.cl__sub { margin: 5px 0 0; font-size: 13px; line-height: 1.4; color: var(--ink-faint); }
.cl__pane > .flash { margin-top: 12px; }
.cl__add { margin-top: 12px; }
.cl__add[aria-current], .in__head .pill[aria-current] { background: var(--field); }

.cl-rows, .cl-order__rows { border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--card); overflow: hidden; }
.cl-row {
  display: flex; align-items: center; gap: 10px; min-height: 58px; padding: 8px 12px; flex: 1; min-width: 0;
  border-top: 1px solid var(--grid-line); color: var(--ink); text-decoration: none;
}
.cl-rows > .cl-row:first-child, .cl-order__row:first-child { border-top: 0; }
.cl-order__row:first-child .cl-row { border-top: 0; }
.cl-row:active { background-image: linear-gradient(var(--press), var(--press)); }
.cl-row[aria-current] { background: var(--field); }
.cl-row__text { flex: 1; min-width: 0; }
.cl-row__text b { display: block; font-weight: 500; font-size: 15px; line-height: 1.3; }
.cl-row__text span { display: block; font-size: 12.5px; line-height: 1.35; color: var(--ink-faint); }
.cl-row__text .cl-row__flag { color: var(--accent-deep); }
.cl-row--off .cl-row__text b, .cl-row--off .cl-row__right { color: var(--ink-faint); }
.cl-row--off .chip { opacity: .5; }
.cl-row__right { flex: none; font-weight: 500; font-size: 14.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cl-row__caret { flex: none; color: var(--ink-faint); }
.cl-row .chip { width: 31px; height: 40px; }

/* An ordered list: each row the link and two arrows. The Save shows once a row has moved. */
.cl-order__row { display: flex; align-items: center; gap: 4px; padding-right: 8px; border-top: 1px solid var(--grid-line); touch-action: pan-y; }
.cl-order__row .cl-row { border-top: 0; }
/* Dragging a row: it stays where it will drop, lifted, and nothing selects. */
.cl-order__row { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
.cl-order__row.is-dragging { position: relative; z-index: 1; background: var(--card); box-shadow: 0 8px 22px rgba(46, 38, 32, .18); cursor: grabbing; }
.is-ordering, .is-ordering * { cursor: grabbing !important; }
.cl-order__row .cl-row__caret { display: none; }
.cl-order__row.is-moved .cl-row__text b { color: var(--accent-deep); }
.cl-order__row.is-dragging { opacity: .5; }
.cl-order__mv {
  flex: none; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px;
  border: 1px solid var(--rule); border-radius: 50%; background: var(--card); color: var(--ink); cursor: pointer;
}
.cl-order__mv svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.cl-order__mv:disabled { opacity: .3; cursor: default; }
.cl-order.is-dirty .cl-order__rows { border-color: var(--accent-deep); }
.cl-order .savebar { display: none; margin-top: 10px; }
.js .cl-order.is-dirty .savebar { display: flex; }
.cl-tail { padding: 10px 12px; font-size: 12.5px; line-height: 1.35; color: var(--ink-faint); }
.cl-tail b { display: block; font-weight: 500; font-size: 15px; color: var(--ink-soft); }

/* An item: its taps, its one form, and Delete at the foot. */
.flagline {
  display: flex; gap: 8px; margin: 12px 0 0; padding: 10px 12px; border-radius: var(--ui-radius);
  background: var(--deep-wash); border-left: 3px solid var(--accent-deep); font-size: 13.5px; line-height: 1.45; color: var(--ink);
}
.flagline::before { content: ''; flex: none; width: 7px; height: 7px; margin-top: 7px; border-radius: 50%; background: var(--accent-deep); }
/* Every card in a pane has the same room inside, all round: --card-pad. A
   Save bar ending one sizes to its button rather than a fixed height, so
   the button sits that same distance from the card's foot. */
:root { --card-pad: 16px; }
.cl-tap { margin-top: 14px; padding: var(--card-pad); border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--card); }
.cl-tap__head { display: flex; align-items: center; gap: 8px; }
.cl-tap__head b { flex: 1; font-weight: 500; font-size: 14.5px; line-height: 1.3; color: var(--ink); }
.cl-tap__head .tag--mini { margin: 0; }
.cl-tap .help { margin: 4px 0 8px; }
.cl-tap__two { display: flex; gap: 6px; max-width: 320px; }
.cl-tap__two .pill { flex: 1; min-height: 40px; }
.cl-tap .pill[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--dark-ink); }
.cl-tap__repl { display: flex; align-items: flex-end; gap: 8px; max-width: 420px; margin-top: 12px; padding: 0; background: none; }
.cl-tap__repl .line { flex: 1; margin: 0; }
.cl-tap__repl.is-changed .combo__box, .cl-tap__repl.is-changed select { border-color: var(--accent-deep); background: var(--deep-wash); }
.js .cl-tap__go { display: none; }
.cl-form { margin-top: 12px; padding: var(--card-pad) var(--card-pad) 0; border: 1px solid var(--rule); border-radius: 10px; background: var(--card); }
.cl-form.is-dirty { border-color: var(--accent-deep); }
.cl-form .savebar--pinned { min-height: 0; margin: var(--card-pad) calc(-1 * var(--card-pad)) 0; padding: 12px var(--card-pad) var(--card-pad); border-radius: 0 0 10px 10px; }
/* A Save bar that is not pinned ends the card itself: room under it, as above. */
.cl-form > .pg__sub:first-of-type { margin-top: 0; }
.cl-form > .savebar:not(.savebar--pinned) { min-height: 0; margin-top: var(--card-pad); padding: 12px 0 var(--card-pad); }
.cl-form:has(input[name="by_quantity"][value="0"]:checked) .cl-unit { display: none; }
/* An add-on priced by package: the one price, or a row per package, whichever the Price switch says. */
.cl-form:has(input[name="by_package"][value="1"]:checked) .cl-oneprice,
.cl-form:has(input[name="by_package"][value="0"]:checked) .cl-bypackage { display: none; }
.cl-bypackage { margin: 4px 0 12px; }
.cl-pp { display: grid; grid-template-columns: minmax(0, 1fr) 96px auto; align-items: center; gap: 10px; min-height: 44px; border-top: 1px solid var(--rule); }
.cl-pp:last-child { border-bottom: 1px solid var(--rule); }
.cl-pp__name { font-size: 15px; }
.cl-pp input[type="text"] { width: 100%; text-align: right; font-variant-numeric: tabular-nums; }
.cl-pp input[type="text"]:disabled { opacity: 0.4; }
.cl-pp__quote { font-size: 13px; color: var(--ink-faint); text-align: right; }
.cl-pp__off { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; white-space: nowrap; min-height: 44px; }
.cl-pp:has(.cl-pp__off input:checked) .cl-pp__name { color: var(--ink-faint); }
.cl-form:has(input[name="kind"][value="fixed"]:checked) .cat__most { display: none; }
/* Delivery, by area on a map or inside or outside the city limits, whichever Priced says. */
.cl-form:has(input[name="mode"][value="areas"]:checked) .cat__city,
.cl-form:has(input[name="mode"][value="city"]:checked) .cat__areas { display: none; }
.cat__areas .amap { margin: 8px 0 12px; }
.cl-make:has(input[name="kind"][value="one"]:checked) .cl-make__batch,
.cl-make:has(input[name="kind"][value="batch"]:checked) .cl-make__one { display: none; }
.cl-note { margin: 12px 0 0; padding: 10px 12px; border-radius: var(--ui-radius); background: var(--field); font-size: 13px; line-height: 1.45; color: var(--ink-soft); }
.cl-preview { margin-top: 14px; padding: 10px 12px; border: 1px dashed var(--double); border-radius: var(--ui-radius); }
.cl-preview__label { display: block; margin-bottom: 6px; font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
.cl-preview__line { display: flex; gap: 10px; padding: 4px 0; font-size: 14px; line-height: 1.35; color: var(--ink); }
.cl-preview__line span { flex: 1; min-width: 0; }
.cl-preview__line b { font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cl-delete { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--rule-strong); }
.cl-delete .textbtn { color: var(--ink-soft); }
.cl-delete .textbtn:disabled { color: var(--ink-faint); cursor: default; }
.cl-delete .help { margin: 4px 0 0; }
.cl-delete .line { max-width: 420px; }

/* A palette's colours: a picker, its hex code and ×, then the big chip. */
.cl-palette { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: start; }
.cl-colours { margin: 12px 0 0; }
.cl-colour { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.cl-colour .cat__color { flex: none; width: 44px; height: 40px; padding: 2px; border-radius: 8px; }
.cl-hex { flex: 1; min-width: 0; max-width: 120px; min-height: 40px; padding: 0 10px; border: 1px solid var(--rule); border-radius: 8px; font: 400 15px/1 var(--sans); font-variant-numeric: tabular-nums; text-transform: uppercase; }
html:not(.js) .cl-hex { display: none; }
.cl-colour__x { flex: none; position: relative; display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; color: var(--ink-soft); font-size: 20px; cursor: pointer; }
.cl-colour__x input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.cl-colour:has(.cl-colour__x input:checked) { opacity: .45; }
.cl-colour:has(.cl-colour__x input:checked) .cl-colour__x { background: var(--deep-wash); color: var(--accent-deep); }
.cl-colour__add { display: inline-flex; align-items: center; gap: 6px; font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink); cursor: pointer; }
.cl-colour--spare:not(:has(input[name$=":add"]:checked)) .cat__color { opacity: .45; }
.cl-colour__more { display: block; margin: 10px 0 0; }
.cl-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.cl-palette__chip { padding-top: 14px; text-align: center; }

/* Codes: the batch, its buttons, and each code that something has happened to. */
.cl-codes { margin-top: 18px; }
.cl-codes .pg__sub { margin-top: 6px; }
.cl-codes__n { font-size: 12px; color: var(--ink-faint); }
.cl-codes .codes__all { width: 100%; font: 400 14px/1.5 ui-monospace, Menlo, monospace; }
.cl-codes__do { display: flex; gap: 8px; margin: 8px 0 12px; }
.cl-codes__list { margin: 0; padding: 0 12px; list-style: none; border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--card); }
.cl-codes__list li { display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 6px 0; border-top: 1px solid var(--grid-line); }
.cl-codes__list li:first-child { border-top: 0; }
.cl-codes__list li > span { flex: 1; min-width: 0; }
.cl-codes__list b { display: block; font-weight: 500; color: var(--ink); }
.cl-codes__list em { display: block; font-style: normal; font-size: 12.5px; color: var(--ink-faint); }
.cl-codes .cust__find { margin-top: 10px; }

/* --- Packages: the list, a package's parts, and one part (worker/admin/packages/page.tsx) */
.pk__package, .pk__pane { display: none; }
.pk--package .pk__list, .pk--part .pk__list, .pk--part .pk__package { display: none; }
.pk--package .pk__package, .pk--part .pk__pane { display: block; }
.pk-row__cover { flex: none; display: block; width: 64px; aspect-ratio: 4 / 3; border-radius: 4px; object-fit: cover; background: var(--grid-line); }
.cl-row--off .pk-row__cover { opacity: .45; }
.pk__elsewhere { margin: 12px 2px 0; }
.pk-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.pk-head > img, .pk-head > i { flex: none; width: 72px; aspect-ratio: 4 / 3; border-radius: 4px; object-fit: cover; background: var(--grid-line); }
.pk-head .cl__sub { display: block; margin-top: 3px; }
.pk-head__badge { margin: 6px 0 0; }
.pk-parts { border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--card); overflow: hidden; }
.pk-parts .row-link { margin: 0; padding: 8px 16px; border-radius: 0; }
.pk-parts .row-link + .row-link { border-top: 1px solid var(--grid-line); }
/* Whether an order of the package goes straight to an invoice, and what
   would make one a quote (WhenOrdered in worker/admin/packages/page.tsx). */
.pk-order { margin-top: 14px; padding: var(--card-pad); border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--card); }
.pk-order__title { display: block; font-weight: 500; font-size: 14.5px; line-height: 1.3; color: var(--ink); }
.pk-order .help { margin: 4px 0 0; }
.pk-order__ifs { margin: 6px 0 0; padding-left: 18px; font-size: 13px; line-height: 1.45; color: var(--ink); }
.pk-order__ifs li + li { margin-top: 2px; }

/* The photos: up to twelve tiles, the first the cover. A new one, not saved
   yet, is numbered and outlined in the deep red; the picked one in ink. One
   taken off keeps its tile until Save with its number struck through, and a
   tile after it carries where it is now, faded, and where it will be. */
.pk-photos { margin: 12px 0 0; border: 0; padding: 0; }
.pk-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.pk-tile { position: relative; display: block; margin: 0; padding: 0; border: 0; background: none; }
.pk-tile img { display: block; width: 100%; aspect-ratio: 4 / 3; height: auto; border-radius: 6px; object-fit: cover; background: var(--grid-line); }
.pk-tile__grip { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; padding: 0; border: 0; border-radius: 6px; background: none; cursor: grab; touch-action: none; }
.pk-tile__grip svg { display: none; }
.pk-tile__n {
  position: absolute; top: 6px; left: 6px; z-index: 2; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 11px;
  background: var(--card); color: var(--ink); font: 500 11px/22px var(--sans); text-align: center; pointer-events: none;
}
.pk-tile__was { opacity: .6; }
.pk-tile.is-cover .pk-tile__n { background: var(--ink); color: var(--dark-ink); }
.pk-tile__cover {
  position: absolute; bottom: 6px; left: 6px; z-index: 2; padding: 3px 7px; border-radius: 999px; background: var(--ink); color: var(--dark-ink);
  font: 500 9px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; pointer-events: none;
}
.pk-tile.is-new img { outline: 2px solid var(--accent-deep); outline-offset: -2px; }
.pk-tile.is-new .pk-tile__n { background: var(--accent-deep); color: var(--dark-ink); }
.pk-tile.is-off .pk-tile__n { background: var(--card); color: var(--ink-soft); text-decoration: line-through; }
.pk-tile.is-picked img { outline: 3px solid var(--ink); outline-offset: -3px; }
.pk-tile.is-drop::before { content: ''; position: absolute; top: 0; bottom: 0; left: -5px; width: 3px; border-radius: 2px; background: var(--ink); }
.pk-tile:has(input[name$=":remove"]:checked) img { opacity: .35; }
.pk-tile.is-dragging { opacity: .4; }
.pk-tile.is-uploading img, .pk-tile.is-failed img { opacity: .55; }
.js .pk-tile__at, .js .pk-tile__rm { display: none; }
/* With scripting each tile is its own 4:3 box, so a photo still loading (or
   one the browser can't draw) never collapses it; the uploader's buttons sit
   over its foot. */
.js .pk-tile.cat__photo { display: block; aspect-ratio: 4 / 3; gap: 0; border-radius: 6px; background: var(--grid-line); }
.js .pk-tile.cat__photo.is-dragging { border: 1px dashed var(--accent); }
.js .pk-tile.cat__photo > img { position: absolute; inset: 0; width: 100%; height: 100%; }
.js .pk-tile__edit { position: absolute; left: 6px; right: 6px; bottom: 6px; margin: 0; justify-content: center; min-width: 0 !important; }
.js .pk-tile__edit:not([data-retry]):not(:disabled) { display: none; }
.pk-tile__edit { position: relative; z-index: 3; margin-top: 4px; }
.pk-tile .cat__err { position: relative; z-index: 3; margin: 4px 0 0; font-size: 12px; color: var(--accent-deep); }
.pk-upload {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; margin-top: 8px; min-height: 64px;
  border: 1.5px dashed var(--double); border-radius: 6px; color: var(--ink-soft); cursor: pointer;
  font: var(--caps); letter-spacing: .12em; text-transform: uppercase;
}
.pk-upload > span:first-of-type { font-size: 20px; letter-spacing: 0; }
.pk-upload input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.pk-upload.is-drop { border-color: var(--ink); background: var(--field); }
.pk-acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.pk-hint { margin: 8px 0 0; }
.pk-ghost { position: fixed; z-index: 50; width: 80px; height: 60px; border-radius: 6px; object-fit: cover; box-shadow: 0 8px 20px rgba(46, 38, 32, .3); pointer-events: none; }

/* From your installs: the box to narrow them, then each install's photos on the site. */
.pk-sugs { margin-top: 18px; }
.pk-filter { display: flex; align-items: flex-end; gap: 8px; }
.pk-filter .line { flex: 1; margin: 0; }
.pk-sug { margin-top: 12px; }
.pk-sug > b { display: block; font-weight: 500; font-size: 14px; color: var(--ink); }
.pk-sug > b span { font-weight: 400; color: var(--ink-faint); }
.pk-sug__photos { display: flex; gap: 6px; margin-top: 6px; overflow-x: auto; scrollbar-width: none; }
.pk-sug__photo { position: relative; flex: none; width: 96px; cursor: pointer; touch-action: pan-x; user-select: none; -webkit-user-select: none; }
.pk-sug__photo img { display: block; width: 96px; height: 72px; border-radius: 4px; object-fit: cover; -webkit-user-drag: none; }
.pk-sug__photo input { position: absolute; top: 4px; right: 4px; z-index: 1; }
.js .pk-sug__photo input { opacity: 0; pointer-events: none; }
.pk-sug__photo.is-on { cursor: default; }
.pk-sug__photo.is-on img { opacity: .5; }
.pk-sug__on, .pk-sug__photo.is-on::after {
  position: absolute; left: 4px; bottom: 4px; padding: 2px 6px; border-radius: 999px; background: var(--card); color: var(--ink);
  font: 500 9px/1.4 var(--sans); letter-spacing: .1em; text-transform: uppercase;
}
.pk-sug__photo.is-on:not(:has(.pk-sug__on))::after { content: 'On it'; }

/* --- Installs (worker/admin/installs/page.tsx) ------------------------------ */
.in__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.in-search input { width: 100%; min-height: 44px; padding: 0 12px; border: 1px solid var(--rule); border-radius: var(--ui-radius); background: var(--card); font: 400 15px/1 var(--sans); }
.in-chips { margin-top: 10px; margin-bottom: 4px; }
.in-season { margin-top: 6px; }
.in-season__n { font-size: 12px; color: var(--ink-faint); }
/* An install, photo first: its photos as a strip at their shapes, the kept
   ones faint, then who it was and the three counts. */
/* Padded and pulled out by the same, like every list's rows: the photos
   and words stay where they were, and the chosen one's wash has room round them. */
.in-row { display: block; margin: 0 -8px 2px; padding: 8px 8px 10px; border-radius: var(--ui-radius); color: var(--ink); text-decoration: none; }
.in-row[aria-current] { background: var(--field); }
.in-row__strip { display: flex; gap: 3px; overflow: hidden; border-radius: 4px; }
.in-row__strip img { flex: none; height: 96px; aspect-ratio: 4 / 3; object-fit: cover; background: var(--grid-line); }
.in-row__strip img.is-kept { opacity: .5; }
.in-row__none { display: flex; align-items: center; justify-content: center; height: 96px; border: 1.5px dashed var(--double); border-radius: 4px; font-size: 13px; color: var(--ink-faint); }
.in-row__text { display: block; margin-top: 6px; }
.in-row__text b { display: block; font: 600 19px/1.15 var(--serif); }
.in-row__text span { display: block; font-size: 12.5px; color: var(--ink-faint); }
.in-counts { display: flex; gap: 12px; margin-top: 4px; }
.in-count { display: inline-flex; align-items: center; gap: 4px; font: 500 12.5px/1 var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
.in-count--none { color: var(--double); }
.in__who { font-size: 28px; }

/* Its photos: tap to pick, the mark at the foot says where each shows. */
.in-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin-top: 14px; }
.in-tile { position: relative; display: block; aspect-ratio: 4 / 3; border-radius: 6px; overflow: hidden; background: var(--grid-line); cursor: pointer; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.in-tile img { display: block; width: 100%; height: 100%; object-fit: cover; -webkit-user-drag: none; }
.in-tile--kept img { opacity: .85; }
.in-tile input { position: absolute; top: 6px; right: 6px; z-index: 2; }
.js .in-tile input { opacity: 0; pointer-events: none; }
.in-mark { position: absolute; left: 6px; bottom: 6px; z-index: 1; display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; background: var(--dark); color: var(--dark-ink); font-style: normal; }
.in-legend .in-mark { position: static; width: 20px; height: 20px; }
.in-tile__check { display: none; }
.js .in-tile__check { display: block; position: absolute; top: 6px; right: 6px; z-index: 1; width: 24px; height: 24px; border: 2px solid var(--dark-ink); border-radius: 50%; background: rgba(46, 38, 32, .25); }
.in-tile:has(input:checked) { outline: 3px solid var(--ink); outline-offset: -3px; }
.in-tile:has(input:checked) .in-tile__check { background: var(--ink); border-color: var(--ink); }
/* The tick from the circle's middle, a little high, since a tick's weight sits low. */
.in-tile:has(input:checked) .in-tile__check::after { content: ''; position: absolute; left: 50%; top: 45%; width: 5px; height: 10px; border: solid var(--dark-ink); border-width: 0 2px 2px 0; transform: translate(-50%, -50%) rotate(45deg); }
.in-tile__edit {
  position: absolute; top: 6px; left: 6px; z-index: 3; display: none; padding: 5px 10px; border-radius: 999px;
  background: var(--card); color: var(--ink); font: 500 10px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; text-decoration: none;
}
html:not(.js) .in-tile__edit { display: inline-flex; }
@media (hover: hover) { .in-tile:hover .in-tile__edit { display: inline-flex; } }
.in-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 10px 0 0; font-size: 12px; color: var(--ink-soft); }
.in-legend span { display: inline-flex; align-items: center; gap: 6px; }
/* The choice bar: pinned to the foot while something is picked on a phone,
   always at the foot of the pane on a wide screen. */
.in-bar { position: sticky; bottom: 0; z-index: 3; margin: 12px calc(-1 * var(--bleed-r, 14px)) 0 calc(-1 * var(--bleed-l, 14px)); padding: 10px var(--bleed-r, 14px) calc(10px + env(safe-area-inset-bottom)) var(--bleed-l, 14px); background: var(--card); box-shadow: 0 -6px 18px rgba(46, 38, 32, .08); border-top: 1px solid var(--rule); }
.js .in-choose:not(.is-picking) .in-bar { display: none; }
.in-bar__say { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.in-bar__say b { font-weight: 500; font-size: 14px; color: var(--ink); }
.in-bar__say .tag--mini { margin: 0 0 0 auto; }
.in-bar__do { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin-top: 8px; }
.in-bar__do .pill { min-height: 44px; padding: 0 6px; font-size: 10px; }
.in-bar__do .pill[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--dark-ink); }
.in-bar__do .pill:disabled { opacity: .35; }

/* Adding photos: a box to tap, or, on a wide screen, to drop them on. */
.in-add { margin-top: 14px; }
.in-add .cat__photos { margin: 0; padding: 0; border: 0; }
.in-drop { position: relative; display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 52px; padding: 10px; border: 1.5px dashed var(--double); border-radius: var(--ui-radius); color: var(--ink-soft); cursor: pointer; text-align: center; }
.in-drop input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.in-drop b { font: var(--caps); letter-spacing: .12em; text-transform: uppercase; color: var(--ink); }
.in-add--empty .in-drop { flex-direction: column; min-height: 220px; }
.in-add--empty .in-drop b { font: 600 22px/1.1 var(--serif); letter-spacing: 0; text-transform: none; }
.in-add--empty .in-drop span { max-width: 280px; font-size: 13px; line-height: 1.45; color: var(--ink-faint); }
.in-drop.is-drop { border-color: var(--ink); background: var(--field); }
.in-add__go { margin-top: 8px; }
.in-add .help { margin-top: 6px; }
.in-job { width: 100%; border: 0; background: none; text-align: left; font: inherit; cursor: pointer; }

/* One photo. */
.in-photo { position: relative; margin-top: 8px; }
.in-photo img { display: block; width: 100%; max-height: 240px; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; background: var(--dark); }
.in-photo__nav { display: flex; align-items: center; justify-content: space-between; margin-top: 6px; font-size: 12.5px; color: var(--ink-faint); }
.in-photo__nav a { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border: 1px solid var(--rule); border-radius: 50%; color: var(--ink); text-decoration: none; font-size: 18px; }
/* One photo's editor (worker/admin/installs/editor.tsx). On a phone the
   preview sticks under the bar, the width of the screen, so a slider moved
   below is seen as it moves; the column around it is `display: contents`
   so the page itself is what it sticks in. */
.pe2 { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; margin-top: 8px; }
.pe2__view { display: contents; }
.pe2__side { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-content: start; min-width: 0; }
.pe2__stage {
  position: sticky; top: calc(var(--bar, 0px) + env(safe-area-inset-top)); z-index: 2; align-self: start;
  height: 240px; margin: 0 calc(-1 * var(--bleed-r, 14px)) 0 calc(-1 * var(--bleed-l, 14px));
  background: var(--dark); overflow: hidden; touch-action: none; user-select: none;
}
.pe2__stage img { display: block; width: 100%; height: 100%; object-fit: contain; }
.pe2__stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; cursor: grab; }
.pe2__stage canvas:active { cursor: grabbing; }
.pe2__loading { position: absolute; left: 12px; right: 12px; bottom: 10px; margin: 0; text-align: center; font-size: 12.5px; color: var(--dark-ink); }
.pe2__frames-head { margin: 2px 0 8px; font: var(--caps); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.pe2__frames-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; align-items: end; }
.pe2__frame { margin: 0; min-width: 0; }
.pe2__frame div { overflow: hidden; border-radius: 4px; background: var(--dark); }
.pe2__frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pe2__frame figcaption { margin-top: 4px; font-size: 11.5px; color: var(--ink-faint); }
.pe2__shapes legend .was { margin-left: 8px; font: 400 12px/1 var(--sans); letter-spacing: 0; text-transform: none; color: var(--accent-deep); }
.pe2__shape-pills { display: flex; flex-wrap: wrap; gap: 5px; }
.pe2__shape { flex: 1 1 auto; display: flex; }
.pe2__shape input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.pe2__shape span {
  flex: 1; display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 12px;
  border: 1px solid var(--edge); border-radius: 999px; background: var(--card); color: var(--ink);
  font: 500 10px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; cursor: pointer;
}
.pe2__shape input:checked + span { background: var(--ink); border-color: var(--ink); color: var(--dark-ink); }
.pe2__shape input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Changed: the "was" beside the heading says so; the pill itself stays a pill. */
label.pe2__shape.is-changed { background: none; border: 0; }
.pe2__slider { display: block; margin-top: 14px; }
.pe2__slider-head { display: flex; align-items: baseline; gap: 8px; font-size: 14px; }
.pe2__slider-head output { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
.pe2__slider-head .was { font-size: 12px; color: var(--accent-deep); }
.pe2__slider input[type=range] { display: block; width: 100%; margin: 6px 0 0; accent-color: var(--ink); }
.pe2__slider input[type=range].is-changed { accent-color: var(--accent-deep); background: none; }
.pe2__slider:has(input.is-changed) output { color: var(--accent-deep); }
.pe2__turns { display: flex; gap: 8px; margin-top: 14px; }
.pe2__reset { display: block; margin-top: 16px; }
.in-where .pg-season { margin-top: 8px; }
.in-edit { margin: 8px 0 0; padding: 0; border: 0; }

/* ---------------------------------------------------------------------------
   The delivery map (src/ui/area-map.tsx): her areas, shaded where she
   delivers and grey where she does not, drawn in miles. Its sizes are in
   the map's own units, so the type is a fraction of a mile and scales with
   the map. A tap on a shaded area picks it in the select beside it.
--------------------------------------------------------------------------- */
.amap { display: block; width: 100%; height: auto; max-width: 560px; aspect-ratio: 1; background: var(--field-2); border: 1px solid var(--rule); border-radius: var(--ui-radius); }
.amap__area { fill: var(--grid-line); stroke: var(--card); stroke-width: 0.25; }
.amap__area--on { fill: var(--accent-wash); cursor: pointer; }
.amap__area--on:hover { fill: color-mix(in srgb, var(--accent) 30%, var(--accent-wash)); }
/* The packages page's map once the address has settled the area: a key, not a picker. */
.amap.is-decided .amap__area--on { cursor: default; }
.amap.is-decided .amap__area--on:not(.is-picked):hover { fill: var(--accent-wash); }
.amap__area.is-picked { fill: color-mix(in srgb, var(--accent) 55%, var(--accent-wash)); stroke: var(--accent-deep); stroke-width: 0.35; }
.amap__counties { fill: none; stroke: var(--double); stroke-width: 0.12; stroke-dasharray: 0.6 0.4; pointer-events: none; }
.amap__states { fill: none; stroke: var(--ink-faint); stroke-width: 0.25; pointer-events: none; }
.amap__towns, .amap__pin { pointer-events: none; }
.amap__town circle { fill: var(--ink-soft); }
.amap__town text { font: 400 1.25px/1 var(--sans); fill: var(--ink-faint); }
.amap__town--big text { font-size: 1.6px; font-weight: 500; fill: var(--ink-soft); }
.amap__pin circle:first-child { fill: var(--accent-deep); opacity: 0.25; }
.amap__pin circle:last-child { fill: var(--accent-deep); stroke: var(--card); stroke-width: 0.15; }
