/* Viola Engineering — Project Hub
   Palette and metrics carried over from the design, lightened on 29 September
   2026: warm grey rules on white blocks over a faint ground, #FFBE00 as the
   only accent, the serif kept for page titles and a sans for everything that
   is scanned. */

:root {
  /* Rules on white. They were black, as the design and the firm's printed
     reports do it, and on a screen of tables that made every screen heavy and
     flat at once: every line said the same thing as loudly as the last. They
     are now warm greys — a grey with a little of the amber in it, not a flat
     mid-grey — and black is kept for ink and for the one primary button.
     The amber is the only other colour and is used as fill, never as text: at
     #ffbe00 on white it carries 1.5:1 and cannot hold a letterform. Links take
     the blue instead.

     Contrast on white: ink-2 11.4:1, ink-3 5.3:1 (4.8:1 on the ground), so the
     quiet text is still text. */
  color-scheme: light;

  --ink: #000;
  --ink-2: #3b3a37;
  --ink-3: #6e6b64;
  --rule: #bdb9b0;
  --rule-soft: #dedbd4;
  --rule-faint: #ebe9e4;
  --fill: #fff;
  --fill-2: #efede8;
  /* The page behind the blocks. Depth from the surface, not from more lines. */
  --ground: #f5f4f1;
  /* Bars and trend lines. Not ink: a chart of black bars outweighs every word
     on the screen, and the amber month has to stand out from its neighbours,
     not merely differ from them. 7.1:1 on white. */
  --mark: #5c5850;
  --accent: #ffbe00;
  /* 5.9:1 on white and 5.1:1 on the grey of a hovered or open row. The old
     #0062ff was 4.3:1 there. */
  --link: #0058e6;

  /* Service colours. Identity, not magnitude: each code keeps its hue wherever it
     appears, and a filter that hides some never repaints the rest.

     Checked with the dataviz validator against this white ground, not by eye —
     all five clear the lightness band, the chroma floor and 3:1 contrast, and the
     worst adjacent pair holds ΔE 12.3 under deuteranopia, comfortably past the 8
     the check demands. Re-run it before changing any of them.

     The amber is deliberately NOT among them. It has one job on this page — the
     thing that wants attention now — and a service that wore it would blunt that. */
  /* Geotech is the green one. It was the brown, and the green belonged to the
     lab; the two swapped, and the declaration order below changed with them,
     which is the part that is not cosmetic.

     Re-validated rather than eyeballed, as the note above says to. The checks
     are on ADJACENT pairs, so the order these are declared in is the order
     that gets checked — and three pairs in this set are weak: green against
     teal (ΔE 12.2 normal), green against brown (3.7 under deuteranopia, where
     both collapse to a yellow), and blue against purple (2.9 under
     protanopia). Sequenced so none of those three is ever a neighbour, the set
     passes with exactly the numbers the old one had: worst adjacent ΔE 12.3
     deutan, 22.1 normal, all five over 3:1 on white.

     Worth knowing: no five-colour set passes on ALL pairs rather than adjacent
     ones — the validator's own reference palette does not either, and it says
     so, pointing at secondary encoding instead of a better palette. The hub
     has that already: every place a service wears a colour it also carries its
     code as text, so identity is never colour alone. */
  --svc-cmt: #2e5eaa;
  --svc-geo: #2f7a33;
  --svc-con: #7a4a9e;
  --svc-geophys: #00918c;
  --svc-lab: #a0521b;
  --svc-other: #6b6660;
  /* GEOPHYS as TEXT. The teal above is right for a mark but 3.9:1 as a label
     on white, under the 4.5:1 text needs; darkened enough for that it drops
     under the validator's chroma floor and reads as grey, so it cannot replace
     the mark colour. Labels get this one; stripes and charts keep the teal. */
  --svc-geophys-text: #00736f;

  /* Reserved for state, never for identity. Amber is "still owed"; this is
     "it failed" — a concrete set that missed its design strength, a break that
     never got entered. Nothing decorative wears it. */
  --critical: #b3261e;
  /* The serif heads a page, the way the firm's printed reports do. Everything
     that is scanned — tables, labels, figures — is the sans, which has aligned
     figures and holds up at 12px where the serif did not. */
  --serif: "Times New Roman", Tinos, serif;
  --sans: "IBM Plex Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  --mono: ui-monospace, Menlo, Consolas, monospace;
}

/* ------------------------------------------------------------- dark -- */

/* The same palette at night, not the light one inverted. The ground is the
   darkest thing, the blocks sit a step lighter on it, and the rules are a
   step lighter again, so depth reads the same way round as it does by day.
   Amber stays #ffbe00: every place it is used it is fill, mixed into the
   block behind it, so it darkens with the block and keeps its one job.

   Measured, not chosen by eye. On the block colour: ink 15:1, ink-2 10.5:1,
   ink-3 6.1:1, link 7.1:1, critical 5.9:1, chart marks 6.8:1.

   The service colours are lighter and re-validated for this surface with the
   dataviz validator, in their declaration order, as the note on the light set
   asks: all five inside the dark lightness band, over the chroma floor and
   3:1 on both the blocks and the ground, worst adjacent pair ΔE 8.4 under
   deuteranopia (teal against purple) and 20.2 in normal vision.

   Two blocks, because a viewer sets the theme two ways: the system setting,
   which only the media query sees, and an explicit choice, which sets
   data-theme on the root. Each is guarded so an explicit choice wins — grey
   included, or a dark system setting would paint over it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="grey"]) {
    --ink: #f2f0eb;
    --ink-2: #cfcbc3;
    --ink-3: #9f9a90;
    --rule: #57534b;
    --rule-soft: #3a3833;
    --rule-faint: #2e2c28;
    --fill: #1d1c1a;
    --fill-2: #2a2825;
    --ground: #151412;
    --mark: #a8a398;
    --link: #79a6ff;
    --critical: #f07167;
    --svc-cmt: #6a8fd8;
    --svc-geo: #58a85b;
    --svc-con: #a47bcf;
    --svc-geophys: #119a93;
    --svc-geophys-text: #119a93;
    --svc-lab: #c9743c;
    --svc-other: #a39e96;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --ink: #f2f0eb;
  --ink-2: #cfcbc3;
  --ink-3: #9f9a90;
  --rule: #57534b;
  --rule-soft: #3a3833;
  --rule-faint: #2e2c28;
  --fill: #1d1c1a;
  --fill-2: #2a2825;
  --ground: #151412;
  --mark: #a8a398;
  --link: #79a6ff;
  --critical: #f07167;
  --svc-cmt: #6a8fd8;
  --svc-geo: #58a85b;
  --svc-con: #a47bcf;
  --svc-geophys: #119a93;
  --svc-geophys-text: #119a93;
  --svc-lab: #c9743c;
  --svc-other: #a39e96;
  color-scheme: dark;
}

/* ------------------------------------------------------------- grey -- */

/* Between the two: light type on a warm mid-dark grey, for a room too dim for
   white and too light for black. Only ever an explicit choice — no system
   setting asks for it — so it has one block.

   Measured like the others. On the block colour: ink 9.7:1, ink-2 7.5:1,
   ink-3 5.8:1, link 5.7:1, critical 5.6:1, chart marks 5.8:1; on the grey of a
   hovered or open row every text colour still clears 4.5:1.

   The service colours are the dark set's, re-run through the dataviz validator
   on this block: inside the dark lightness band, over the chroma floor and 3:1,
   worst adjacent pair ΔE 8.4 under deuteranopia and 20.2 in normal vision. On
   the hovered-row grey they dip to 2.6–2.8:1, which is allowed because every
   service colour on the page travels with its code as text. The GEOPHYS label
   colour is lightened to 5.5:1 for the same reason the light one is darkened. */
:root[data-theme="grey"] {
  --ink: #f6f4ef;
  --ink-2: #dcd8d0;
  --ink-3: #c4beb3;
  --rule: #76726a;
  --rule-soft: #55524c;
  --rule-faint: #4a4843;
  --fill: #403e3a;
  --fill-2: #4b4944;
  --ground: #34332f;
  --mark: #c4beb2;
  --link: #9dbdff;
  --critical: #ffa39a;
  --svc-cmt: #6a8fd8;
  --svc-geo: #58a85b;
  --svc-con: #a47bcf;
  --svc-geophys: #119a93;
  --svc-geophys-text: #5ccbc3;
  --svc-lab: #c9743c;
  --svc-other: #b9b4ab;
  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
}

body { padding: 24px; }

a { color: var(--link); }

input, select, textarea, button { font-family: inherit; color: var(--ink); }

/* Focus is otherwise invisible against the hairline borders. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.u-spacer { flex: 1; }

/* ---------------------------------------------------------------- shell -- */

.app {
  width: 100%;
  max-width: 1680px;
  margin: 0 auto;
  background: var(--fill);
  border: 1px solid var(--rule);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
}

.masthead {
  display: flex;
  align-items: stretch;
  border-bottom: 2px solid var(--rule);
}

/* The brand column keeps the sidebar's width so the two line up down the page;
   what came down is the mark inside it, which was setting the height of the
   whole masthead. */
.masthead__brand {
  width: 220px;
  flex: none;
  padding: 11px 18px;
  border-right: 1px solid var(--rule);
  display: flex;
  align-items: center;
}

.masthead__brand img { width: 88px; height: auto; display: block; }

.tabs {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 22px;
  font-size: 15px;
}

.tab {
  padding: 13px 12px;
  cursor: pointer;
  /* Was the muted ink, which on the dark ground read as switched off. */
  color: var(--ink-2);
  border: 0;
  border-bottom: 3px solid transparent;
  background: none;
  font-size: inherit;
  white-space: nowrap;
}

.tab:hover { color: var(--ink); }

.tab[aria-selected="true"] {
  font-weight: 700;
  color: var(--accent);
  border-bottom-color: var(--accent);
}


/* Whose view this is. Sits next to the search box because that is where the eye
   already goes, and it is a button: the way out of the filter is the thing that
   tells you the filter is on. */
.whoami {
  border: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-radius: 999px;
  padding: 4px 11px;
  font: inherit;
  font-size: 11px;
  color: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.whoami:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); }

.search {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 9px 13px;
  font-size: 14px;
  width: 230px;
  /* Raised off the masthead so the one input up here reads as a control. */
  background: var(--fill-2);
  color: var(--ink);
}

.search::placeholder { color: var(--ink-3); }
.search:focus { border-color: var(--accent); background: var(--fill); }

/* The light / dark switch, in the circle that used to be an empty avatar. */
.theme {
  width: 30px;
  height: 30px;
  flex: none;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: var(--fill);
  color: var(--ink-2);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}

.theme:hover { border-color: var(--ink-3); color: var(--ink); }

/* ------------------------------------------------------------ connection -- */

/* One short line since the technical detail moved to a tooltip, so it no longer
   needs the height of two. Between them the masthead and this bar were 134px of
   the page before a word of content, on a window where the nav stacks above it
   as well — content began 284px down a 768px screen. */
.conn {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 6px 22px;
  border-bottom: 1px solid var(--rule);
  background: var(--fill-2);
  font-size: 11.5px;
}

.conn__dot {
  width: 9px;
  height: 9px;
  flex: none;
  border: 1px solid var(--rule);
  border-radius: 50%;
}

.conn__dot--empty { background: var(--fill); }
.conn__dot--pending { background: var(--rule-soft); }
.conn__dot--live { background: var(--ink); }

.conn__state { font-weight: 700; }

/* The figures are from an earlier day. Amber, the hub's one colour for "look
   at this now", and as fill rather than text, which is the only way it holds a
   letterform. The whole bar rather than a badge in it: this is the one fact
   that changes how everything below it should be read, and it sits under the
   masthead on every screen so that nobody has to go to Settings to learn it. */
.conn--behind {
  background: color-mix(in srgb, var(--accent) 38%, var(--fill));
  border-bottom: 2px solid var(--accent);
}

.conn--behind .conn__state { letter-spacing: 0.02em; }
.conn--behind .conn__detail { color: var(--ink); }

/* The Aldoa pull is current, but something else the daily run writes is not.
   Quieter: a rule down the left edge, the same spine the heavy loose ends
   wear, because the page is mostly right and one part of it is not. */
.conn--lagging { box-shadow: inset 3px 0 0 var(--accent); }
.conn__endpoint { color: var(--ink-2); font-family: var(--mono); font-size: 10.5px; }
.conn__detail { color: var(--ink-3); }

/* ----------------------------------------------------------------- body -- */

.body { display: flex; align-items: stretch; }

.sidebar {
  width: 220px;
  flex: none;
  border-right: 1px solid var(--rule);
  padding: 18px 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
  font-size: 12.5px;
}

.sidebar__group { display: flex; flex-direction: column; gap: 4px; }

.sidebar__label {
  padding: 0 18px 3px;
  font-size: 9.5px;
  letter-spacing: 0.12em;
  font-weight: 700;
  color: var(--ink-3);
}

.sidebar__item {
  margin: 0 10px;
  padding: 5px 8px;
  border: 0;
  border-left: 3px solid transparent;
  border-radius: 6px;
  background: none;
  color: var(--ink-2);
  font-size: inherit;
  text-align: left;
  cursor: pointer;
}

.sidebar__item[aria-current="true"] {
  background: var(--fill-2);
  font-weight: 700;
  color: var(--ink);
  border-left-color: var(--accent);
}

.main {
  flex: 1;
  min-width: 0;
  padding: 22px 24px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  border-right: 1px solid var(--rule);
}

.main__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 12px;
}

.main__title { font-size: 19px; font-weight: 700; letter-spacing: 0.04em; }
.main__subtitle { font-size: 11.5px; color: var(--ink-2); margin-top: 4px; }

/* --------------------------------------------------------------- button -- */

.btn {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 6px 11px;
  background: var(--fill);
  font-size: 11.5px;
  cursor: pointer;
}

.btn--primary { background: var(--ink); color: var(--fill); font-weight: 700; }
.btn--wide { padding: 8px 12px; }
.btn:disabled { color: var(--rule-soft); cursor: default; }

.btn-row { display: flex; gap: 8px; font-size: 11.5px; }

/* ---------------------------------------------------------------- stats -- */

.stats {
  display: flex;
  border: 1px solid var(--rule);
  border-radius: 6px;
  overflow: hidden;
}

.stat {
  flex: 1;
  padding: 12px 14px;
  border-right: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 5px;
  /* A card is a button when it leads somewhere, and a button brings its own
     type, alignment and background — reset here so a linked card and a plain
     one are the same object with one of them clickable. */
  font: inherit;
  text-align: left;
  background: none;
  border-top: 0;
  border-left: 0;
  border-bottom: 0;
  color: inherit;
}

/* The affordance is the number turning into a link colour under the cursor,
   not a border: four cards side by side with hover boxes would read as four
   buttons on a toolbar rather than as the count they are. */
.stat--link { cursor: pointer; }
.stat--link:hover { background: var(--fill-2); }
.stat--link:hover .stat__value { color: var(--link); }
.stat--link:hover .stat__label { text-decoration: underline; }

.stat:last-child { border-right: 0; }
.stat__label { font-size: 9.5px; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }
.stat__value { font-size: 23px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.stat__note { font-size: 10.5px; color: var(--ink-2); }

/* -------------------------------------------------------------- filters -- */

/* A size up, alongside the filter bar on the other screens. These are the
   controls that decide what the table underneath is, so they read at the size
   of the rows rather than as a caption above them. */
.filters { display: flex; align-items: center; gap: 10px; font-size: 12.5px; flex-wrap: wrap; }

.chip {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 6px 11px;
  background: var(--fill);
  font-size: inherit;
  cursor: pointer;
}

.chip[aria-pressed="true"] { background: var(--fill-2); font-weight: 700; }

/* The chip that opens the minor service types. Dashed, because it is a way in
   rather than a filter: pressing it changes what the row offers, not what the
   table shows. */
.chip--more { border-style: dashed; color: var(--ink-3); }
.chip--more:hover { border-style: solid; color: var(--ink); }

.sort-toggle {
  border: 0;
  background: none;
  color: var(--ink-3);
  text-decoration: underline;
  cursor: pointer;
  font-size: inherit;
}

/* ---------------------------------------------------------------- table -- */

.table { display: flex; flex-direction: column; }

.table__head,
.table__row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px;
}

.table__head {
  padding: 8px 10px;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  font-size: 9.5px;
  letter-spacing: 0.08em;
  font-weight: 700;
}

.table__head--roomy { gap: 12px; font-size: 9.5px; letter-spacing: 0.1em; }

/* The service a job belongs to, on every row that is a job. The hue was
   scoped to the project table, and only on hover or when a row was open, so
   the same five services were invisible in Field Data, the work order queue
   and Site Locations — grey lists of rows whose one distinguishing fact was
   already known and already had a colour.

   A 2px spine, not a wash: it reads down a column at a glance and leaves the
   row itself white for the status pills to work against. An open row is told
   apart by its background wash rather than by the spine, so the spine can be
   strong enough to actually read here. These rules sit
   above the open-row ones on purpose — equal specificity, later wins. */
[data-svc="CMT"] { --row-hue: var(--svc-cmt); }
[data-svc="GEO"] { --row-hue: var(--svc-geo); }
[data-svc="GEOPHYS"] { --row-hue: var(--svc-geophys); }
[data-svc="CON"] { --row-hue: var(--svc-con); }
[data-svc="LAB"] { --row-hue: var(--svc-lab); }

.table__row[data-svc] {
  box-shadow: inset 3px 0 0 color-mix(in srgb, var(--row-hue, transparent) 72%, transparent);
}

/* A header that sorts. It has to keep looking like a header — the table reads
   as a table because the top row is quiet — so the button is reset to the
   heading's own type and the affordance is carried by the cursor and the hover
   rather than by a border. */
.table__head--sorted { padding-top: 4px; padding-bottom: 4px; }

.sorth {
  font: inherit;
  letter-spacing: inherit;
  color: var(--ink-2);
  background: none;
  border: 0;
  /* No horizontal padding and no negative margin to cancel it: either one
     moves the header text off the column its rows are drawn in, and a heading
     three pixels out of line with its own column is worse than a hover
     highlight that hugs the words. */
  padding: 3px 0;
  text-align: left;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-width: 0;
}

/* Underline rather than a filled box. With no horizontal padding to give it,
   the box gripped the words tight enough to read as a defect, and eight of
   them across the top of a table is a row of grey blocks. The underline is
   what the rest of the hub uses to say a thing is clickable. */
.sorth:hover { color: var(--link); text-decoration: underline; }
.sorth.is-sorted { color: var(--ink); }

/* The arrow runs larger than the heading it sits beside. The heading is set in
   9.5px small caps, and a glyph drawn to that size disappears into the letters
   next to it; the arrow is the one part of the row that has to be readable at
   a glance, because it is the only thing saying which column the table is in
   order by. Sized in em off its own font-size, so the reserved room grows with
   it and reordering still cannot shift the row sideways. */
.sorth__mark { font-size: 1.5em; width: 0.9em; flex: none; line-height: 1; }

.table__row {
  border-bottom: 1px solid var(--rule-soft);
  border-radius: 6px;
  font-size: 11.5px;
  background: var(--fill);
  text-align: left;
  border-left: 0;
  border-right: 0;
  border-top: 0;
  width: 100%;
  cursor: pointer;
  font-family: inherit;
}

.table__row--roomy { gap: 12px; font-size: 12.5px; cursor: default; }
.table__row[aria-selected="true"] { background: var(--fill-2); }

.table__sort {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  letter-spacing: inherit;
  font-weight: 700;
  text-align: left;
  text-decoration: underline;
  cursor: pointer;
}

.cell { flex: none; color: var(--ink-2); }
.cell--grow { flex: 1; color: inherit; }
.cell--center { text-align: center; }
.cell--client { width: 180px; }
.cell--office { width: 74px; }
.cell--status { width: 100px; }
.cell--who { width: 38px; }
.cell--wos { width: 36px; text-align: center; }
.cell--review { width: 44px; text-align: center; }
.cell--wo-no { width: 96px; font-weight: 700; color: inherit; }
.cell--date { width: 104px; }
/* Money reads right-aligned and in tabular figures, so a column of it can be
   compared down rather than read across. */
.cell--money { width: 92px; text-align: right; font-variant-numeric: tabular-nums; }
.cell--tech { width: 132px; }
.cell--wo-status { width: 150px; }
.cell--form-status { width: 168px; }
.cell--name { width: 150px; color: inherit; }
.cell--role { width: 120px; }
.cell--org { width: 180px; color: var(--ink-2); }
.cell--phone { width: 126px; }
.cell--transmittal { width: 96px; text-align: center; }

.project { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* In a list the site name leads and the number sits under it, because that is
   the order people say them in. The number stays the key everywhere it really
   is one: exports, folder paths, report headers. Flipped with flex order so
   the markup is untouched and every list changes together. */
.project__name {
  order: 1;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
}

.project__head { order: 2; }
.project__number { font-weight: 400; font-size: 10.5px; color: var(--ink-3); }

.pill {
  display: inline-block;
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 2px 6px;
  font-size: 10.5px;
  background: var(--fill);
}

.pill--roomy { padding: 2px 7px; font-size: 10.5px; }
.pill--muted { background: var(--fill-2); }
.pill--inverse { background: var(--ink); color: var(--fill); }
.pill--quiet { color: var(--ink-3); }

.strong { font-weight: 700; }
.email { flex: 1; min-width: 0; color: var(--link); text-decoration: underline; }

.empty { padding: 22px 10px; font-size: 12.5px; color: var(--ink-3); font-style: italic; }
.note { font-size: 11.5px; color: var(--ink-3); font-style: italic; }

/* Figures line up under each other.
   
   Proportional digits are right for prose and wrong for a column of counts: a
   1 is half the width of a 7, so a column of them wanders and the eye cannot
   compare down it. This was set on the money cells and nowhere else, so every
   other count in the hub — work orders, reviews, specimens, page numbers —
   drifted. Times has tabular figures; they just have to be asked for. */
.cell--wos,
.cell--review,
.cell--transmittal,
.loose__count,
.brk__design,
.brk__num,
.pager,
.chip,
.today__value,
.sch__cell { font-variant-numeric: tabular-nums; }

/* ----------------------------------------------------------- pagination -- */

.pager {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-top: 2px;
  font-size: 11.5px;
  flex-wrap: wrap;
}

.pager__range { color: var(--ink-3); }
.pager__rule { width: 1px; height: 20px; background: var(--rule-soft); margin: 0 4px; }

.page-btn {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 5px 11px;
  background: var(--fill);
  font-size: inherit;
  cursor: pointer;
}

.page-btn[aria-current="page"] { background: var(--ink); color: var(--fill); font-weight: 700; }
.page-btn--size { padding: 5px 9px; }
.page-btn--size[aria-pressed="true"] { background: var(--fill-2); }
.page-btn--step { padding: 5px 10px; }

/* ----------------------------------------------------------- basic info -- */

.panel {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 12.5px;
}

.panel__row {
  display: flex;
  gap: 16px;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--rule-faint);
}

.panel__row:last-child { border-bottom: 0; padding-bottom: 0; }
.panel__key { width: 170px; flex: none; color: var(--ink-3); }
.panel__val { flex: 1; min-width: 0; }

.stub {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 22px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.stub__title { font-size: 12.5px; font-weight: 700; }
.stub__body { font-size: 12.5px; color: var(--ink-2); line-height: 1.5; max-width: 560px; }

/* ------------------------------------------------------------------ rail -- */

.rail {
  width: 320px;
  flex: none;
  padding: 22px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.rail__head {
  display: flex;
  flex-direction: column;
  gap: 5px;
  border-bottom: 2px solid var(--rule);
  padding-bottom: 10px;
}

.rail__eyebrow { font-size: 9.5px; letter-spacing: 0.12em; font-weight: 700; color: var(--ink-3); }
.rail__number { font-size: 15px; font-weight: 700; }
.rail__name { font-size: 12.5px; color: var(--ink-2); }

.rail__section { display: flex; flex-direction: column; gap: 7px; }
.rail__title { font-size: 9.5px; letter-spacing: 0.12em; font-weight: 700; }

/* Label and value are one pair, so they sit next to each other. `space-between`
   pushed them to opposite ends of whatever the row happened to be, which at rail
   width put 180px of nothing between "Last activity" and its em dash, and looked
   worse still with the rail stacked. A fixed label column keeps the gap constant
   and the pair legible at every width. */
.fact {
  display: grid;
  grid-template-columns: 112px minmax(0, 1fr);
  column-gap: 10px;
  align-items: baseline;
  font-size: 11.5px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--rule-faint);
}

.fact__key { color: var(--ink-3); }
.fact__val { text-align: left; overflow-wrap: anywhere; }

.attention {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.attention__item {
  display: flex;
  gap: 9px;
  align-items: flex-start;
  font-size: 11.5px;
  line-height: 1.35;
  border: 0;
  background: none;
  padding: 0;
  text-align: left;
  cursor: pointer;
  font-family: inherit;
}

.attention__mark {
  width: 9px;
  height: 9px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  margin-top: 3px;
  flex: none;
  background: var(--fill);
}

.attention__mark--solid { background: var(--ink); }
.attention__mark--soft { background: var(--rule-soft); }

.siteplan {
  border: 1px solid var(--rule);
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.siteplan__head {
  padding: 9px 12px;
  border-bottom: 1px solid var(--rule);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  font-weight: 700;
}

.siteplan__body {
  height: 126px;
  background:
    linear-gradient(to top right, transparent 49.6%, var(--ink) 49.6%, var(--ink) 50.4%, transparent 50.4%),
    var(--fill-2);
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.siteplan__caption {
  font-size: 10.5px;
  color: var(--ink-2);
  background: var(--fill);
  padding: 2px 6px;
  border-radius: 4px;
  margin-bottom: 8px;
}

.log { display: flex; gap: 9px; font-size: 11.5px; line-height: 1.4; }
.log__date { width: 52px; flex: none; color: var(--ink-3); }
.log__text { flex: 1; color: var(--ink-2); }

.rail__actions { display: flex; gap: 8px; font-size: 11.5px; margin-top: auto; }
.rail__actions .btn { flex: 1; text-align: center; padding: 8px 10px; }

/* --------------------------------------------------------------- footer -- */

.colophon {
  border-top: 2px solid var(--rule);
  padding: 9px 22px;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 9.5px;
  color: var(--ink-3);
  letter-spacing: 0.05em;
}


/* ------------------------------------------------------ the day in full -- */

/* Wider than the Add project dialog and taller: it is holding four tables, and
   a day that needs scrolling to read is a day you have to remember rather than
   see. */
.modal__panel--day { width: 1040px; }

/* The collapsible sections sit flush against each other rather than each
   carrying its own card border. */
.modal__panel--day .loose { border-radius: 6px; }
.loose__body--flush { padding: 0 0 4px; }
.loose__body--flush .table__row { border-bottom-color: var(--rule-faint); }

/* The heading is a row of its own parts, so the count sits at the end. */
.modal__panel--day .loose__head { gap: 8px; }
.modal__panel--day .loose__label { flex: none; }

.wxline { display: inline-flex; align-items: baseline; gap: 6px; margin-right: 14px; }

@media (max-width: 1100px) {
  .modal__panel--day { width: calc(100% - 32px); }
}

/* ---------------------------------------------------------------- toast -- */

.toast {
  position: absolute;
  left: 24px;
  bottom: 52px;
  background: var(--ink);
  color: var(--fill);
  border-radius: 6px;
  padding: 10px 14px;
  font-size: 11.5px;
  max-width: 520px;
}

/* ---------------------------------------------------------------- modal -- */

/* Fixed, not absolute. It was positioned inside .app, which on a long page is
   thousands of pixels tall — so the dialog was pinned to the top of the
   document and scrolled away with it, and its max-height of 100% meant the
   height of the whole app rather than of the screen, which is why it never
   scrolled inside itself either. Both faults were the same line. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgba(0, 0, 0, 0.66);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 56px 0;
  border: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
}

.modal::backdrop { background: transparent; }

.modal__panel {
  width: 880px;
  max-width: calc(100% - 32px);
  /* Of the screen now, so the panel can never be taller than what is on it. */
  max-height: calc(100vh - 112px);
  /* The body scrolls rather than the whole panel, so the title and the row of
     destinations stay put while the day is read. */
  overflow: hidden;
  background: var(--fill);
  border: 1px solid var(--rule);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
}



/* Expanded: as much of the screen as there is. The point is the reports — a
   strength report or a daily field report is a page of tables, and reading one
   through a 420px window is reading it through a letterbox. At full size the
   contents get the room instead of their own scrollbar. */
.modal__panel--big {
  width: calc(100% - 32px);
  max-width: 1720px;
  max-height: calc(100vh - 32px);
}

/* Doubled class so order cannot decide this. The 420px cap below is written with
   one class and this with another, and at equal specificity the later rule wins —
   which meant the cap stayed on at full size and the expanded panel gave the
   report no more room than the small one did. Same trap as the Today rows. */
.modal__panel--day.modal__panel--big .detail { max-height: none; }

/* The dialog sits closer to the top when it is this tall, so the padding above
   it does not eat a quarter of the screen. */
.modal:has(.modal__panel--big) { padding: 16px 0; align-items: stretch; }


/* The title bar is the handle. */
.modal__panel--day .modal__head { cursor: move; user-select: none; }
.modal__panel--day .modal__head button { cursor: pointer; }

/* Nothing animates while it is being dragged, and the pointer keeps the panel
   even when it outruns it. */
.modal__panel--day.is-dragging { transition: none; }
.modal__panel--day.is-dragging * { pointer-events: none; }


/* How many photographs a report carries, and the way to them. The count is the
   useful half: the pictures themselves cannot live in the hub — the field set is
   about 2,600 in Aldoa and 25,000 more on the library, against a 16 MB budget
   for the whole page — but knowing a report has nine of them behind it is what
   decides whether to go and look. */
.cell--pics { width: 96px; }

.pics {
  font-size: 11px;
  color: var(--link);
  text-decoration: underline;
  white-space: nowrap;
}

.pics--none { color: var(--ink-3); text-decoration: none; font-style: italic; }

/* The head and foot hold still; only the middle moves. */
.modal__head,
.modal__foot { flex: none; }

.modal__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  min-height: 0;
}

/* The body is a flex column, so its sections were flex items and shrank to fit
   it rather than keeping their height. A section that shrank still had
   overflow: hidden on it, so an open report was quietly cut off at the fold —
   and the body never scrolled, because as far as it was concerned its contents
   fitted. They only fitted because it had squashed them. */
.modal__body > * { flex: none; }

/* A report opened inside the panel is reading its own contents, which can run
   long; it gets its own ceiling rather than pushing the dialog past the screen. */
.modal__panel--day .detail { max-height: 420px; overflow-y: auto; }

.modal__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  padding: 18px 24px;
  border-bottom: 2px solid var(--rule);
}

.modal__title { font-size: 17px; font-weight: 700; letter-spacing: 0.04em; }
.modal__sub { font-size: 11.5px; color: var(--ink-2); margin-top: 4px; }

.steps {
  display: flex;
  border-bottom: 1px solid var(--rule-soft);
  padding: 0 24px;
  font-size: 11.5px;
  flex-wrap: wrap;
}

.step {
  padding: 10px 14px;
  border: 0;
  border-bottom: 3px solid transparent;
  background: none;
  color: var(--ink-3);
  font-size: inherit;
  cursor: pointer;
}

.step[aria-selected="true"] { border-bottom-color: var(--accent); font-weight: 700; color: var(--ink); }

.modal__body { padding: 20px 24px; display: flex; flex-direction: column; gap: 22px; }

.fieldset { display: flex; flex-direction: column; gap: 12px; border: 0; padding: 0; margin: 0; }

.fieldset__legend {
  font-size: 10.5px;
  letter-spacing: 0.12em;
  font-weight: 700;
  border-bottom: 1px solid var(--rule);
  padding: 0 0 5px;
  width: 100%;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}

.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 18px; }

.field { display: flex; flex-direction: column; gap: 5px; }
.field--full { grid-column: span 2; }
.field__label { font-size: 11.5px; font-weight: 700; }
.field__flag { color: var(--ink-3); font-weight: 400; }
.field__hint { font-size: 10.5px; color: var(--ink-3); }

.field input,
.field select {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 12.5px;
  background: var(--fill);
}

.field input[readonly] { background: var(--fill-2); }

.linkish {
  border: 0;
  background: none;
  padding: 0;
  font-size: 11.5px;
  font-family: inherit;
  color: var(--link);
  text-decoration: underline;
  cursor: pointer;
}

.contacts-table { border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }

.contacts-table__head {
  display: flex;
  gap: 12px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--rule);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  font-weight: 700;
  background: var(--fill-2);
}

.contacts-table__row {
  display: flex;
  gap: 12px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--rule-faint);
  font-size: 11.5px;
  color: var(--ink-3);
}

.contacts-table__row:last-child { border-bottom: 0; }
.col-name { flex: 1.2; min-width: 0; }
.col-role { flex: 1; min-width: 0; }
.col-email { flex: 1.2; min-width: 0; }
.col-phone { width: 110px; flex: none; }
.col-transmittal { width: 96px; flex: none; text-align: center; }

.types { display: flex; flex-wrap: wrap; gap: 8px; }

.type {
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 6px 12px;
  font-size: 11.5px;
  background: var(--fill);
  cursor: pointer;
}

.type[aria-pressed="true"] { background: var(--ink); color: var(--fill); }

.preview {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 11.5px;
}

.preview__row { display: flex; gap: 12px; }
.preview__key { width: 96px; flex: none; color: var(--ink-3); }
.preview__val { flex: 1; min-width: 0; color: var(--ink-2); }
.preview__val--strong { color: var(--ink); font-weight: 700; }
.preview__val--path { font-family: var(--mono); font-size: 11.5px; color: var(--ink); }

.check {
  display: flex;
  gap: 12px;
  align-items: center;
  border-top: 1px solid var(--rule-faint);
  padding: 9px 0 0;
  margin: 0;
  cursor: pointer;
  background: none;
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
  font: inherit;
  text-align: left;
  width: 100%;
}

.check__box {
  width: 14px;
  height: 14px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  flex: none;
  background: var(--fill);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--fill);
  font-size: 10.5px;
}

.check__box--on { background: var(--ink); }
.check__text { flex: 1; }

.modal__foot {
  border-top: 2px solid var(--rule);
  padding: 14px 24px;
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 11.5px;
}

.modal__readiness { color: var(--ink-3); }

/* ---------------------------------------------------------- narrow view -- */

/* Below the design's 1680px the fixed columns squeeze the project name. */
@media (max-width: 1520px) {
  .cell--client { width: 150px; }
}

@media (max-width: 1180px) {
  body { padding: 12px; }
  .body { flex-wrap: wrap; }
  .sidebar { width: 100%; border-right: 0; border-bottom: 1px solid var(--rule); flex-direction: row; flex-wrap: wrap; }
  .main { border-right: 0; }
  .rail { width: 100%; border-top: 1px solid var(--rule); }
  .cell--client, .cell--office, .cell--org { display: none; }
}

@media (max-width: 900px) {
  /* Keep every tab reachable rather than clipping the strip. Scrolls without
     a bar, the way the phone strip does: between 721 and 900px it drew a grey
     scrollbar under the tabs, and the fade says there is more instead. */
  .tabs {
    overflow-x: auto;
    padding: 0 12px;
    scrollbar-width: none;
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  }
  .tabs::-webkit-scrollbar { display: none; }
  .masthead__brand { width: 130px; padding: 10px 12px; }
  .masthead__brand img { width: 74px; }
  .search { width: 120px; }
  .conn { flex-wrap: wrap; }
}

@media (max-width: 720px) {
  .grid { grid-template-columns: 1fr; }
  .field--full { grid-column: span 1; }
  .cell--who { display: none; }
  .stats { flex-wrap: wrap; }
  .stat { flex: 1 1 50%; }
}

/* ------------------------------------------------------- expanded form -- */


/* ----------------------------------------------------------- row affordance -- */

/* Rows came in four kinds — open a menu, navigate, expand in place, or do
   nothing — and looked the same until clicked. The rule is now structural:
   a row that is a <button> is clickable and shows it; a row that is a <div> is
   a container and stays inert. Expanding rows keep their own disclosure mark,
   and project rows keep the menu caret, so the three live kinds stay apart. */
button.table__row { cursor: pointer; }
button.table__row:hover { background: var(--fill-2); }
button.table__row:hover .project__name { color: var(--link); text-decoration: underline; }

/* A container row must not look live. */
div.table__row:not(.table__row--drop):not(.table__row--expandable) { cursor: default; }


/* --------------------------------------------------------- table sizing -- */

/* One sizing rule for every table in the hub. They had been tuned separately
   and none of them agreed with another.

   Each column keeps one width across every row — a table whose columns do not
   line up is worse than a cramped one — and the widths below are sized to the
   content that actually appears in them. Where the honest sum is wider than the
   window the table scrolls sideways; it does not squeeze whichever column
   happens to be flexible. That column was the scope on Work Orders, left with
   80px to print "Drill & Epoxy, Rebar, Glulam, Daily Field Report" while the
   status column held 168 for the word "Draft". */
.table { overflow-x: auto; }

/* Rows stay the width of the table. A row sized to its own content is a row
   whose columns start somewhere different from every other row, which is the
   one thing a table may not do — long text wraps inside its column instead. */

/* A status pill is the width of its word. */
.cell--form-status { width: 116px; }
.cell--wo-status { width: 116px; }

/* The project block and the prose column are what the row is for, so they get
   room to be read rather than the leftovers. */
.cell--fd-project { width: 248px; }
.cell--grow {
  flex: 1 1 232px;
  /* Flex items default to min-width:auto, which lets a long word push its own
     column wider and shove every column after it along — the drift that made
     Field Data rows disagree about where TECHNICIAN began. */
  min-width: 0;
}

/* Nothing is cut short: a row that will not fit scrolls rather than lying about
   what is on it. */
.table .cell--grow {
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
}

/* Aldoa's own column names are short and its values shorter, so the settings
   tables do not need the prose floor. */
.sets__table .cell--grow { flex: 1 1 96px; min-width: 0; }

/* The project name is the line people read; let it use the width it now has. */
.project__name { white-space: normal; }

.table__row--expandable { cursor: pointer; }
.table__row--expandable[aria-expanded="true"] { background: var(--fill-2); border-bottom-color: var(--rule); }

/* The one mark saying a row opens. At 10.5px in the lightest ink it read as a
   speck of dust beside 12.5px text, so on a list of a hundred and twenty-six
   forms nothing said any of them could be expanded. Set at the size of the
   text it sits next to. */
.disclosure {
  width: 14px;
  flex: none;
  color: var(--ink-2);
  font-size: 12.5px;
  line-height: 1;
}

/* An expanded form is the one thing on these screens that is READ rather
   than scanned: it is a field report, a page of sentences and readings that
   somebody wrote on site and somebody else has to check. It was set at the
   size of a table row, in rows three pixels apart, which is right for a
   list of 660 work orders and wrong for a document. */
.detail {
  border-bottom: 1px solid var(--rule-soft);
  padding: 18px 24px 24px 32px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  font-size: 12.5px;
}

/* A section of the form. It earns a rule across the panel because the sections
   are what turn a flat list of fields back into the document it was filled in
   as. */
.detail__heading {
  font-size: 10.5px;
  letter-spacing: 0.12em;
  font-weight: 700;
  color: var(--ink-2);
  border-bottom: 1px solid var(--rule-soft);
  padding-bottom: 7px;
  margin-top: 22px;
}

.detail__heading:first-child { margin-top: 0; }

/* A form reads as a record, not as a list of eleven equal lines. The label
   column is fixed so every value starts in the same place down the page, and
   the rows are tight enough that a section reads as a block. */
.detail__row {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr);
  gap: 4px 20px;
  align-items: baseline;
  padding: 6px 0;
}

.detail__key {
  color: var(--ink-3);
  font-size: 11px;
  letter-spacing: 0.02em;
}

/* Capped at a reading measure. A value on a 1400px screen was running the
   full width of the panel, which is fine for a date and unreadable for the
   sentence somebody typed into a one-line field. */
.detail__val { min-width: 0; overflow-wrap: anywhere; max-width: 88ch; }

/* Under about a tablet the label cannot hold a column of its own. */
@media (max-width: 860px) {
  .detail__row { grid-template-columns: 1fr; gap: 0; padding: 5px 0; }
  .detail__key { font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; }
}
.detail__val--quiet { color: var(--ink-3); font-style: italic; }

.detail__block { display: flex; flex-direction: column; gap: 8px; padding: 2px 0; }

.detail__prose {
  white-space: pre-wrap;
  line-height: 1.6;
  max-width: 74ch;
  border-left: 2px solid var(--rule-faint);
  padding-left: 14px;
  padding-top: 2px;
  padding-bottom: 2px;
}

/* Test tables are wide; let them scroll rather than pushing the page out. */
.detail__tablewrap { overflow-x: auto; max-width: 100%; }

.detail__table {
  border-collapse: collapse;
  font-size: 11.5px;
  min-width: 100%;
}

.detail__table th,
.detail__table td {
  border: 1px solid var(--rule-faint);
  padding: 6px 11px;
  text-align: left;
  white-space: nowrap;
}

.detail__table th {
  background: var(--fill-2);
  font-size: 9.5px;
  letter-spacing: 0.06em;
}

.detail__omitted { font-size: 10.5px; color: var(--ink-3); font-style: italic; max-width: 70ch; }

/* Values recomputed here from raw inputs, because Aldoa does not store them. */
.detail__table th.is-computed,
.detail__table td.is-computed { background: rgba(255, 190, 0, 0.16); }
.detail__val.is-computed {
  background: rgba(255, 190, 0, 0.16);
  padding: 1px 5px;
  border-radius: 4px;
}

/* A work order with no report yet falls back to the scheduled date. */
.cell--date.is-scheduled { font-style: italic; color: var(--ink-3); }

/* ------------------------------------------------------------ loose ends -- */

.loose { border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }

.loose__head {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  background: var(--fill);
  border: 0;
  cursor: pointer;
  font: inherit;
  text-align: left;
}

.loose__head[aria-expanded="true"] { background: var(--fill-2); border-bottom: 1px solid var(--rule); }

/* Unread messages, in the sidebar beside the board. Amber, because it is the
   hub's one colour for something wanting attention now, and small: it is a
   nudge rather than an alarm. */
.ping {
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--ink);
  font-size: 9.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The categories worth interrupting somebody for: money already spent, work
   finished and not delivered, a result somebody is waiting on. A spine rather
   than a fill, so the screen still reads as one list. */
.loose__head--heavy { box-shadow: inset 3px 0 0 var(--accent); }

/* A stat card that leads somewhere says so at rest, not only under the
   cursor: four identical cards where three are clickable is a guessing game. */
.stat__go { margin-left: 5px; color: var(--link); font-weight: 700; }

/* Zero is not news. Full size so the row keeps its rhythm, quieter so the eye
   goes to the number that is not zero. */
.stat__value--nil { color: var(--rule-soft); }
.loose__label { flex: 1; font-size: 12.5px; font-weight: 700; }

.loose__count {
  flex: none;
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 1px 9px;
  font-size: 11.5px;
  background: var(--fill);
}

.loose__body { padding: 10px 12px 12px 32px; display: flex; flex-direction: column; gap: 6px; }

.loose__why {
  font-size: 11.5px;
  color: var(--ink-3);
  font-style: italic;
  max-width: 78ch;
  padding-bottom: 4px;
}

.loose__item {
  display: flex;
  gap: 12px;
  align-items: baseline;
  font-size: 11.5px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--rule-faint);
}

.loose__item:last-child { border-bottom: 0; }
.loose__num { width: 140px; flex: none; font-weight: 700; white-space: nowrap; }

/* The number opens the project in the hub; the arrow beside it is the way out
   to Aldoa, kept small because it is the second choice now rather than the
   only one. */
.loose__num .linkish { font-weight: 700; font-size: inherit; }

.loose__away {
  margin-left: 5px;
  font-size: 10px;
  color: var(--ink-3);
  text-decoration: none;
}

.loose__away:hover { color: var(--link); }

.loose__name { width: 240px; flex: none; color: var(--ink-2); }
.loose__detail { flex: 1; min-width: 0; }
.loose__meta { width: 150px; flex: none; color: var(--ink-3); text-align: right; }

@media (max-width: 1180px) {
  .loose__name { width: 180px; }
  .loose__meta { display: none; }
}

/* ---------------------------------------------------------- row menu -- */

/* The row is a container now: a select button plus its own menu, because a
   button cannot contain another button. */
.table__row { padding: 0; gap: 0; align-items: stretch; }

.table__row__select {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 11.5px;
  text-align: left;
  cursor: pointer;
  color: inherit;
}

.cell--menu { width: 34px; flex: none; }

/* The floating ⋯ row menu was replaced by the in-row dropdown below
   (.table__row--drop / .rowdrop); its rules are gone with it. */

/* The row menu must not be clipped by the table's own stacking. */
.table { position: relative; }

/* ---------------------------------------------------------------- plans -- */

.siteplan__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }

.siteplan__all {
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--fill);
  padding: 1px 7px;
  font: inherit;
  font-size: 9.5px;
  letter-spacing: 0;
  font-weight: 400;
  cursor: pointer;
}

.siteplan__list { display: flex; flex-direction: column; }

.planlink {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 7px 12px;
  border-bottom: 1px solid var(--rule-faint);
  font-size: 11.5px;
  color: var(--ink);
  text-decoration: none;
}

.planlink:last-child { border-bottom: 0; }
.planlink:hover { background: var(--fill-2); }

.planlink__kind {
  flex: none;
  width: 30px;
  font-size: 9.5px;
  letter-spacing: 0.06em;
  font-weight: 700;
  color: var(--ink-3);
}

.planlink__name {
  flex: 1;
  min-width: 0;
  color: var(--link);
  text-decoration: underline;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.siteplan__body--empty { height: auto; padding: 14px 12px; background: none; display: block; }

.plans__group {
  font-size: 9.5px;
  letter-spacing: 0.1em;
  font-weight: 700;
  color: var(--ink-3);
  margin-top: 4px;
}

.cell--plan-kind { width: 46px; font-size: 9.5px; letter-spacing: 0.06em; font-weight: 700; }
.cell--plan-size { width: 74px; text-align: right; }

/* Jump from a hub record to the same record in Aldoa. */
.detail__open { padding-bottom: 10px; font-size: 12px; }
.loose__num a { color: var(--link); }
.cell--wo-no a { color: var(--link); }

/* Loose-end triage */
.loose__hidden { font-size: 10.5px; color: var(--ink-3); font-style: italic; }
.loose__acts { width: 88px; flex: none; display: flex; gap: 4px; justify-content: flex-end; }

.loose__act {
  border: 1px solid var(--rule-soft);
  border-radius: 4px;
  background: var(--fill);
  padding: 1px 7px;
  font: inherit;
  font-size: 10.5px;
  cursor: pointer;
  color: var(--ink-3);
}

.loose__act:hover { border-color: var(--rule); color: var(--ink); background: var(--fill-2); }
.loose__item--hidden { color: var(--ink-3); font-style: italic; }
.loose__clear { font-size: 11.5px; color: var(--ink-3); font-style: italic; padding: 4px 0; }

/* Inactive: on the register but not in the tracked set. Distinct from the
   Aldoa statuses — outlined in amber rather than filled, so a long list of
   history reads as background against the live rows. */
/* Active, said as a word. The pill is reserved for a status that is not the
   normal one, so a column of them reads as exceptions rather than as furniture. */
.status--plain { color: var(--ink-3); }
.status--none { color: var(--rule-soft); }

.pill--inactive {
  background: rgba(255, 190, 0, 0.18);
  border-color: var(--accent);
  color: var(--ink-3);
}

/* -------------------------------------------------------- break schedule -- */

/* The day rows reuse .loose; only the bits specific to breaks live here. */

.brk__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
  background: var(--fill-2);
  font-size: 11.5px;
}

.brk__barlabel { font-weight: 700; letter-spacing: 0.04em; }
.brk__dash { color: var(--link); text-decoration: none; border-bottom: 1px solid currentColor; }
.brk__dash:hover { background: rgba(255, 190, 0, 0.2); }

.brk__rel { font-weight: 400; font-style: italic; color: var(--ink-3); margin-left: 8px; }

/* Amber, matching the Inactive pill: the hub's one colour for "look at this". */
.brk__flag {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 1px 6px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: rgba(255, 190, 0, 0.2);
}

.brk--overdue { border-color: var(--accent); }
.brk--overdue > .loose__head { background: rgba(255, 190, 0, 0.14); }

.brk__set {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 11.5px;
  padding: 4px 0;
  border-bottom: 1px solid var(--rule-faint);
}

.brk__set:last-of-type { border-bottom: 0; }
.brk__set--done { color: var(--ink-3); }

.brk__num { width: 140px; flex: none; font-weight: 700; white-space: nowrap; }
.brk__num a { color: var(--link); }
.brk__setid { width: 64px; flex: none; }
.brk__name { width: 220px; flex: none; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.brk__spec { flex: 1; min-width: 0; }
.brk__design { width: 108px; flex: none; text-align: right; color: var(--ink-3); }
.brk__state { width: 84px; flex: none; text-align: right; }
.brk__owed { font-weight: 700; }
.brk__done { font-style: italic; color: var(--ink-3); }

/* ---------------------------------------------------- strength results -- */

/* One set per row: who it belongs to, what it averaged, and what it was meant
   to make. The margin is the column people read — "fail" alone does not say
   whether it missed by ten psi or by two thousand. */
.str__row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 11.5px;
  padding: 5px 0;
  border-bottom: 1px solid var(--rule-faint);
}

.str__row:last-child { border-bottom: 0; }
.str__job { width: 230px; flex: none; display: flex; gap: 6px; align-items: baseline; min-width: 0; }
.str__name { color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.str__set { width: 62px; flex: none; }
.str__cast { width: 96px; flex: none; color: var(--ink-3); }
.str__spec { width: 92px; flex: none; color: var(--ink-3); }
.str__avg { width: 82px; flex: none; text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
.str__design { width: 96px; flex: none; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.str__margin { width: 62px; flex: none; text-align: right; font-variant-numeric: tabular-nums; }
.str__note { flex: 1; min-width: 0; color: var(--ink-3); font-style: italic; }

/* Under design is the one state that matters, so it takes the reserved colour
   — the same one a break that never got entered wears. Over is not a
   celebration and stays quiet. */
.str__under { color: var(--critical); font-weight: 700; }
.str__over { color: var(--ink-3); }
.str__row--fail .str__avg { color: var(--critical); }

/* ------------------------------------------------------------- field data -- */

/* The cross-project form browser. Rows, pills and the pager are the table styles
   already in use; only the filter bar and the project column are new. */

.fd__bar {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  flex-wrap: wrap;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--rule-soft);
}

.fd__group { display: flex; flex-direction: column; gap: 5px; }

/* The filter bar reads a size up from the rest of the furniture. It is the one
   strip on these screens that is worked rather than read: the labels say what
   a control does, and the control says what it is currently set to, which is
   the difference between a table of 660 rows and a table of 15. Set at the
   size of the rows underneath it rather than the size of a caption. */
.fd__label {
  font-size: 10.5px;
  letter-spacing: 0.1em;
  font-weight: 700;
  color: var(--ink-3);
}

.fd__control {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 6px 9px;
  font-size: 12.5px;
  font-family: inherit;
  background: var(--fill);
}

/* Scoped to the bar: .chip and .linkish are the hub's everywhere-shapes, and
   growing them here must not grow the type chips over the project table. */
.fd__bar .chip { font-size: 12.5px; padding: 6px 11px; }
.fd__bar .linkish { font-size: 12.5px; }

.fd__clear { padding-bottom: 7px; }

/* The project table turned .table__row into a zero-padding container so it could
   carry a row menu. A row that is itself the button needs its own padding back. */
.table__row--padded { padding: 10px; gap: 12px; }

.cell--fd-project { width: 210px; }

/* --------------------------------------------------------- work orders -- */

.wo { display: flex; flex-direction: column; gap: 16px; }

/* Age is the column the view exists for, so it reads as a number, not prose. */
.cell--wo-age { width: 58px; text-align: right; font-variant-numeric: tabular-nums; }

@media (max-width: 1180px) {
  .cell--wo-no { width: 84px; }
}

/* Seven columns do not fit a phone. They are dropped in the order they can be
   spared, and only inside .wo — Field Data shares these class names and its own
   narrow layout is already settled. The scope line is what identifies the visit,
   so it keeps the space the others give up. */
@media (max-width: 900px) {
  .wo .cell--tech { display: none; }
}

@media (max-width: 720px) {
  /* Seven columns cannot share one phone-width line. Hiding enough of them to
     fit left the scope cell at zero width, printing over its neighbours, so the
     row wraps instead: identity, age and status on the first line, the scope —
     the part that says what the visit was — on its own line underneath. */
  .wo .table__head,
  .wo .table__row { flex-wrap: wrap; }
  .wo .cell--grow { flex: 1 0 100%; }
  /* Age says the same thing as the date, in less room. */
  .wo .cell--date { display: none; }
  .wo .cell--pm { display: none; }
  .wo .cell--form-status { width: 96px; }
  .wo .cell--wo-no { width: 70px; }
}


@media (max-width: 1180px) {
  .cell--fd-project { width: 160px; }
}

/* Narrower, not gone. This hid the column outright, which was aimed at Field
   Data and landed on every table that uses the class — Work Orders, Geotech
   and both settings screens lost the one cell that says which job a row is,
   on every phone and tablet. The phone layout at the end of the file stacks
   it onto a line of its own. */
@media (max-width: 900px) {
  .cell--fd-project { width: 140px; }
}

/* A Daily Field Report row carries the technician's scope line under the template
   name. One line only — the full text is the cell's tooltip, and the contents are
   a click away. */

.formline { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.formline__summary {
  color: var(--ink-3);
  font-size: 10.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Indented under the form name and tied to it by a hairline, so the two
     lines read as one row. Flush left they read as two, and a hundred and
     twenty-six forms of that is a wall: the eye has nothing to tell it where
     one report stops and the next starts. */
  margin-left: 2px;
  padding-left: 9px;
  border-left: 1px solid var(--rule-faint);
}

/* A form row is two lines where every other table row is one, so it needs more
   between rows than the hairline the one-line tables use — otherwise the gap
   inside a row and the gap between rows are the same, which is what made this
   list hard to read down. */
.table__row--expandable.table__row--roomy { border-bottom-color: var(--rule-soft); }
.table__row--expandable.table__row--roomy.table__row--padded { padding-top: 9px; padding-bottom: 9px; }

/* Work type on a Daily Field Report row. Inferred, not quoted, so it is set in
   the accent rather than dressed up as one of the Aldoa status pills. */

.worktype {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 7px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: rgba(255, 190, 0, 0.16);
  font-size: 9.5px;
  letter-spacing: 0.04em;
  color: var(--ink-2);
  white-space: nowrap;
  vertical-align: 1px;
}

/* ----------------------------------------------------------- directory -- */

/* The company-wide contact list. Its own organization column, because the
   project table's .cell--org is dropped on narrow screens and here the company
   is half the point of the row. */

.cell--cnt-org { width: 220px; }
.cell--cnt-projects { width: 78px; text-align: center; }

.fd__control--wide { min-width: 280px; }

.cnt__projects { display: flex; flex-wrap: wrap; gap: 6px; }

.cnt__project {
  display: flex;
  flex-direction: column;
  gap: 1px;
  align-items: flex-start;
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
  background: var(--fill);
  padding: 5px 9px;
  font: inherit;
  font-size: 11.5px;
  text-align: left;
  cursor: pointer;
  max-width: 260px;
}

.cnt__project:hover { border-color: var(--rule); background: var(--fill-2); }
.cnt__project-no { font-weight: 700; white-space: nowrap; }

.cnt__project-name {
  color: var(--ink-3);
  font-size: 10.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 240px;
}

@media (max-width: 1180px) {
  .cell--cnt-org { width: 160px; }
}

@media (max-width: 900px) {
  .cell--cnt-org { display: none; }
  .fd__control--wide { min-width: 0; }
}

/* Stands in for the Reload / Pull buttons in the artifact, which has no server
   to reach and no network to reach it over. */
.conn__snap { color: var(--ink-3); font-style: italic; }

/* A work type with no scope behind it: inferred from the report text, so it is
   drawn as a dotted outline rather than presented as something Aldoa states. */
.worktype--guess {
  border-style: dotted;
  background: none;
  color: var(--ink-3);
}

/* One or two scopes list four trades; the tag truncates and the title carries it. */
.worktype {
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: bottom;
}

/* ---------------------------------------------------------------- schedule -- */

/* Technician down the side, day across the top. One week at a time: the median
   week is 17 work orders across 6 technicians, which fits; a month would not. */

.sch__bar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--rule-soft);
}

.sch__range { font-size: 12.5px; font-weight: 700; margin-left: 6px; }

/* Rows rather than a grid: a project is one outlined band, which a CSS grid
   cannot express — it can only border each cell. The day columns keep their
   widths because every row lays out the same way. */
/* No min-width: the grid divides the width it is given. It used to be sized to
   its own content, which is what put every week under a scrollbar no matter how
   few columns it had. */
.sch { display: flex; flex-direction: column; gap: 6px; }

.sch__row {
  display: flex;
  gap: 4px;
}


/* The job column and the corner above it are one column: if they disagree,
   every day heading sits over the wrong day. */
.sch__corner { width: 156px; flex: none; }


.sch__day {
  flex: 1 1 0;
  min-width: 0;
  background: var(--fill);
  border: 1px solid var(--rule-soft);
  border-radius: 8px;
  padding: 7px 9px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  position: relative;
}

.sch__dayname { font-size: 9.5px; letter-spacing: 0.1em; font-weight: 700; }
.sch__daynum { font-size: 10.5px; color: var(--ink-3); }

.sch__daycount {
  position: absolute;
  top: 7px;
  right: 9px;
  font-size: 10.5px;
  color: var(--ink-3);
}

/* Today is the one thing worth finding without reading the dates. */
.sch__day--today { background: rgba(255, 190, 0, 0.28); }
.sch__day--today .sch__daynum,
.sch__day--today .sch__daycount { color: var(--ink-2); }
.sch__cell--today { background: rgba(255, 190, 0, 0.09); }

.sch__job {
  width: 156px;
  flex: none;
  background: var(--fill);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 8px 9px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  align-items: flex-start;
  font: inherit;
  font-size: 11.5px;
  text-align: left;
  cursor: pointer;
  min-width: 0;
}

.sch__job:hover { background: var(--fill-2); border-color: var(--accent); }
.sch__jobno { font-weight: 700; white-space: nowrap; }

.sch__jobname {
  color: var(--ink-3);
  font-size: 10.5px;
  max-width: 100%;
  /* Wraps rather than ellipsising: the name is why the row is findable. */
  white-space: normal;
  overflow-wrap: anywhere;
}

.sch__cell {
  flex: 1 1 0;
  min-width: 0;
  background: var(--fill);
  border: 1px solid var(--rule-faint);
  border-radius: 8px;
  padding: 5px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 40px;
}

.sch__card {
  display: flex;
  flex-direction: column;
  gap: 1px;
  align-items: flex-start;
  width: 100%;
  border: 1px solid var(--rule-soft);
  border-left: 3px solid var(--rule-soft);
  border-radius: 4px;
  background: var(--fill);
  padding: 4px 7px;
  font: inherit;
  font-size: 10.5px;
  text-align: left;
  cursor: pointer;
}

.sch__card:hover { border-color: var(--rule); background: var(--fill-2); }

/* Still open: the only status distinction the grid has room to make. */
.sch__card--open { border-left-color: var(--accent); }

/* A booking with no work order behind it. Dotted edge rather than a colour: it
   is a different kind of thing, not a different status. */
/* A booking off the schedule sheet is drawn exactly like a work order. It is the
   same thing — somebody going somewhere to do something — and the dotted outline
   it used to carry made half the week look provisional when it is not. Where it
   came from is said by the tag on the card, which is information; the border was
   only decoration pretending to be information.

   Behaviour already follows the project rather than the source: a card opens its
   project whichever list it came from, and only a booking with no project number
   at all — a lab day, a consult — has nothing to open. */
.sch__card:disabled { cursor: default; }
.sch__card:disabled:hover { background: var(--fill); border-color: var(--rule-soft); }

.sch__cardno { font-weight: 700; }

/* The card is a small block of prose now, not a single line. */
.sch__card { line-height: 1.35; }

.sch__cardscope {
  color: var(--ink-3);
  max-width: 100%;
  /* All of it, wrapped. A card that shows half a sentence is a card you have
     to click to read, which is the opposite of what a week grid is for. */
  white-space: normal;
  overflow-wrap: anywhere;
}

/* The columns divide the width that is there rather than insisting on a floor
   and pushing the rest under a scrollbar. flex-basis is 0 so every day is the
   same width whatever is in it — a week whose columns drift with their content
   is not a grid — and the scope lines wrap into whatever that width turns out
   to be.

   Five columns rather than seven is what makes that readable: the weekend is
   dropped when nothing is on it, which is almost every week.

   The scroll is kept as a floor for the case the stacked layout below does not
   cover, not as the normal state. */
.sch__scroll { overflow-x: auto; max-width: 100%; }

/* Under about a laptop's width, five columns of prose are slivers no matter how
   the space is divided. The grid stops being a grid and becomes one day after
   another — each with its own heading, full width for its cards, and no
   sideways scrolling at any size. */
@media (max-width: 1080px) {
  .sch { min-width: 0; }
  .sch__scroll { overflow-x: visible; }

  /* The header row of day names has nothing to head any more. */
  .sch__row:first-child { display: none; }

  .sch__row { flex-wrap: wrap; gap: 6px; }
  .sch__job { width: 100%; }

  .sch__cell {
    flex: 1 1 100%;
    min-height: 0;
    border-style: dashed;
  }

  /* An empty day would otherwise be a dashed box and a heading under every
     job. :empty does not match these — the template leaves whitespace inside the
     cell, which counts as content — so the question is whether it holds a card. */
  .sch__cell:not(:has(.sch__card)) { display: none; }

  /* Each cell says which day it is, since the heading row is gone. */
  .sch__cell::before {
    content: attr(data-day);
    font-size: 9.5px;
    letter-spacing: 0.1em;
    font-weight: 700;
    color: var(--ink-3);
  }
}


/* The technician is what the cell is for now; the scope is the second line. */
.sch__cardwho { font-weight: 700; }

/* What to do with the job or person just clicked. It sits above the grid rather
   than dropping out of the cell, because the grid scrolls sideways and a popup
   inside it would be clipped. */

.sch__pick {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 9px 12px;
  border: 1px solid var(--rule);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
  background: var(--fill-2);
  font-size: 11.5px;
}

.sch__picklabel { margin-right: 2px; }
.sch__pickname { color: var(--ink-3); margin-left: 6px; }
.sch__pick .chip { background: var(--fill); }

/* Whatever the bar is about, marked in the grid so the two stay connected. */
.sch__job--picked { background: rgba(255, 190, 0, 0.22); border-color: var(--accent); }
.sch__card--picked { border-color: var(--ink); background: var(--fill-2); }

/* A tracked job with nobody on it this week. Still listed — an empty row is the
   point — but it should not compete with the jobs that have work. */
.sch__job--idle { background: none; border-color: var(--rule-soft); }
.sch__job--idle .sch__jobno { font-weight: 400; color: var(--ink-3); }
.sch__job--idle .sch__jobname { color: var(--rule-soft); }

/* ------------------------------------------------------- service type row -- */

/* A second, quieter filter axis under the status chips. Lighter than the status
   row on purpose: status is the question people ask first, service narrows it. */

.filters--type { gap: 6px; margin-top: -8px; }

.filters__label {
  font-size: 10.5px;
  letter-spacing: 0.1em;
  font-weight: 700;
  color: var(--ink-3);
  margin-right: 2px;
}

.chip--type {
  padding: 4px 9px;
  font-size: 11.5px;
  border-color: var(--rule-soft);
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
}

.chip--type[aria-pressed="true"] { border-color: var(--rule); }

/* A row is a button now; keep its text left-aligned like the div it was. */
.pipe__row { text-align: left; width: 100%; }
.pipe__detail .detail__heading:first-of-type { margin-top: 0; }
.chip__count { color: var(--ink-3); font-variant-numeric: tabular-nums; }
.chip--type[aria-pressed="true"] .chip__count { color: var(--ink-2); }

/* A service with nothing under the current status stays in the row so it does not
   reflow, but reads as unavailable rather than as a button that does nothing. */
.chip--empty {
  border-color: var(--rule-faint);
  color: var(--ink-3);
  opacity: 0.55;
  cursor: default;
}

/* ------------------------------------------------------ service type tag -- */

/* The service code, beside the project number. Quieter than the status pill on
   purpose: status is what you scan the column for, service is what you already
   half-know from the number you are looking at. */

.project__head { display: flex; align-items: baseline; min-width: 0; }

/* The margin rather than a flex gap, because the tag also follows plain inline
   text in Loose Ends, the break schedule, the rail and the schedule grid. */
.project__type {
  flex: none;
  margin-left: 6px;
  white-space: nowrap;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.07em;
  color: var(--ink-3);
  border: 1px solid var(--rule-faint);
  border-radius: 4px;
  padding: 0 4px;
  line-height: 1.5;
}

/* --------------------------------------------------- project row dropdown -- */

/* The row is the menu. The destinations drop open inside it rather than floating
   out of a separate button on the right, so the panel belongs to the project
   visibly as well as logically. */

.table__row--drop { flex-wrap: wrap; }

.table__row--drop .table__row__select { flex: 1 1 100%; }

/* An open row is tinted with its own service colour rather than a flat grey. The
   hue comes through a data attribute, not a class: .svc-CMT sets `color`, and on
   a row that would repaint every word in it.

   The wash is 6% and the spine is the full hue — enough to say "this one is open,
   and it is a materials job" at a glance, without a second saturated block
   competing with the status pills inside the row. */
.table__row--drop[data-svc="CMT"] { --row-hue: var(--svc-cmt); }
.table__row--drop[data-svc="GEO"] { --row-hue: var(--svc-geo); }
.table__row--drop[data-svc="GEOPHYS"] { --row-hue: var(--svc-geophys); }
.table__row--drop[data-svc="CON"] { --row-hue: var(--svc-con); }
.table__row--drop[data-svc="LAB"] { --row-hue: var(--svc-lab); }

.table__row--drop.table__row--drop:hover:not(.table__row--open) {
  background: color-mix(in srgb, var(--row-hue, var(--ink)) 4%, var(--fill));
}

/* Open rows read as one block with their panel: the fill runs through both and
   the divider under the row is a hairline rather than a full rule. */
/* Two classes, because .table__row[aria-selected="true"] sets the flat grey and
   is more specific than a single class — opening a row also selects it, so the
   grey was winning and the tint never showed. */
.table__row--drop.table__row--open {
  background: color-mix(in srgb, var(--row-hue, var(--ink)) 7%, var(--fill));
  border-bottom-color: var(--rule-soft);
  box-shadow: inset 3px 0 0 var(--row-hue, var(--ink-3));
}

.table__row--open .table__row__select {
  border-bottom: 1px solid color-mix(in srgb, var(--row-hue, var(--ink)) 22%, transparent);
}

/* The caret sits where the ⋯ button used to, so the column keeps its alignment
   with the header row. At the shared .disclosure size it was almost invisible
   against the row, and it is the only affordance saying a row opens. */
.table__row--drop .cell--menu {
  display: flex;
  align-items: center;
  justify-content: center;
}

.table__row--drop .cell--menu .disclosure {
  width: auto;
  font-size: 12.5px;
  color: var(--ink-2);
}

.table__row--drop:hover .cell--menu .disclosure,
.table__row--open .cell--menu .disclosure { color: var(--ink); }

.rowdrop {
  flex: 1 1 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px 10px 10px 10px;
}

/* The destinations sit on the tinted ground as plain white buttons, so they read
   as the actionable thing in the row; the hue only arrives on hover. */
.rowdrop__item {
  border: 1px solid color-mix(in srgb, var(--row-hue, var(--ink)) 28%, var(--rule-faint));
  border-radius: 6px;
  padding: 4px 10px;
  background: var(--fill);
  color: inherit;
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
}

.rowdrop__item:hover {
  border-color: var(--row-hue, var(--rule));
  color: var(--row-hue, var(--ink));
  background: color-mix(in srgb, var(--row-hue, var(--ink)) 7%, var(--fill));
}

/* ------------------------------------------------------------- reporting -- */

/* Charts take the ink and the hairline rules the rest of the page uses; the amber
   is spent on one thing at a time — the month still filling up, a count that is
   still owed — so it keeps meaning "look here". */

.report__block {
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
  padding: 12px 14px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The minimum is capped at the width there is. A bare minmax(520px, 1fr) on a
   351px phone does not wrap to one column — it makes one 520px column, and the
   frame's overflow: hidden cuts the right third off without a scrollbar to say
   so. That was Today's On Site list and Reporting's rankings on every phone. */
.report__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(330px, 100%), 1fr));
  gap: 14px;
}

.report__cols--wide { grid-template-columns: repeat(auto-fit, minmax(min(520px, 100%), 1fr)); }

.report__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.report__title { font-size: 10.5px; letter-spacing: 0.1em; font-weight: 700; }
.report__note { font-size: 10.5px; color: var(--ink-3); font-style: italic; }

/* Sized to include the axis band, so the card never grows a nested scrollbar. */
.chart { width: 100%; height: auto; overflow: visible; }
.chart__tick { font-size: 9.5px; fill: var(--ink-3); font-variant-numeric: tabular-nums; }
.chart__value { font-size: 10.5px; font-weight: 700; fill: var(--ink); }

/* Bars and numbers in one object: the chart is its own table view, so no value is
   reachable only by hovering. */
.rank { display: flex; flex-direction: column; gap: 5px; font-size: 11.5px; }

.rank__row { display: flex; align-items: center; gap: 8px; }

.rank__label {
  width: 168px;
  flex: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-2);
}

.rank__track { flex: 1; min-width: 40px; display: flex; }

/* A Today row that carries a project name needs two lines, so the single-line
   ellipsis the ranked bars use has to come off for this one variant. */
.rank__label--project {
  width: 210px;
  overflow: visible;
  white-space: normal;
  display: flex;
  color: inherit;
}

.rank__bar { height: 9px; background: var(--mark); border-radius: 0 3px 3px 0; min-width: 2px; }
.rank__bar--flag { background: var(--accent); }

.rank__count { width: 42px; flex: none; text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
.rank__share { width: 34px; flex: none; text-align: right; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.rank__more { font-size: 10.5px; color: var(--ink-3); font-style: italic; padding-top: 2px; }

.report__verdicts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
  align-items: baseline;
  gap: 12px 24px;
}
.verdict { display: flex; align-items: baseline; gap: 7px; }
.verdict__count { font-size: 20px; font-weight: 700; }
.verdict__label { font-size: 11.5px; color: var(--ink-2); }
.verdict--flag .verdict__count { background: rgba(255, 190, 0, 0.35); padding: 0 6px; border-radius: 4px; }

/* Client names run long, so that block gets a wider label track and a trailing
   column for the job count. */
.rank--wide .rank__label { width: 250px; }

.rank__meta {
  /* Sized to its note rather than fixed: "5 jobs" fits anything, but "1 draft
     · 21 to review · of 212 filed" in 58px wrapped to three lines and made
     every row on Unfinished, by technician three rows tall. */
  min-width: 58px;
  flex: none;
  white-space: nowrap;
  text-align: right;
  color: var(--ink-3);
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------- settings -- */

.srcs { display: flex; flex-direction: column; font-size: 11.5px; }

.srcs__row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 5px 0;
  border-bottom: 1px solid var(--rule-faint);
}

.srcs__row:last-child { border-bottom: 0; }

.srcs__row--head {
  font-size: 9.5px;
  letter-spacing: 0.08em;
  font-weight: 700;
  color: var(--ink-3);
  border-bottom-color: var(--rule);
}

.srcs__row--missing { color: var(--ink-3); font-style: italic; }

.srcs__file { width: 188px; flex: none; font-weight: 700; white-space: nowrap; }
.srcs__file a { color: var(--link); }
.srcs__what { flex: 1; min-width: 0; color: var(--ink-2); }
/* Where a source came from when it is not the file: read live, or why not. */
.srcs__via { color: var(--ink-3); font-style: italic; }
.srcs__rows { width: 52px; flex: none; text-align: right; font-variant-numeric: tabular-nums; }
/* A view standing on a source whose contents are behind its fetch time. Amber,
   because it is the hub's one colour for "look at this", and because the fact is
   not an error — the schedule sheet simply has not been filled in for this week
   yet, which is normal on a Monday and worth knowing on a Thursday. */
.stale {
  font-size: 10.5px;
  font-style: italic;
  padding: 0 6px;
  border-radius: 4px;
  background: rgba(255, 190, 0, 0.3);
  color: var(--ink-2);
}

/* Two date columns now: when the file was written, and the newest thing in it. */
.srcs__age { width: 150px; flex: none; text-align: right; color: var(--ink-3); }

/* The one thing on this page worth catching the eye: a file that did not load, or
   one old enough that the page may be lying about the state of the work. */
.srcs__age--stale { color: var(--ink); background: rgba(255, 190, 0, 0.35); padding: 0 5px; border-radius: 4px; }

.setting {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 7px 0;
  border-bottom: 1px solid var(--rule-faint);
  flex-wrap: wrap;
}

.setting:last-of-type { border-bottom: 0; }
.setting__label { width: 210px; flex: none; font-size: 11.5px; display: flex; flex-direction: column; gap: 2px; }
.setting__note { font-size: 10.5px; color: var(--ink-3); font-style: italic; }
.setting__control { display: flex; gap: 5px; flex-wrap: wrap; }

.setting__prose { font-size: 11.5px; color: var(--ink-2); display: flex; flex-direction: column; gap: 6px; }
.setting__prose p { margin: 0; max-width: 78ch; }
.setting__prose b { color: var(--ink); }

/* ------------------------------------------------------------- service hue -- */

/* The code beside a project number carries its service colour, so the table is
   scannable by discipline without reading a word. Tinted ground, full-strength
   text: the hue has to hold a two- or three-letter word at 9.5px. */

.project__type {
  color: var(--svc-other);
  border-color: currentColor;
  background: color-mix(in srgb, currentColor 8%, transparent);
}

.svc-CMT { color: var(--svc-cmt); }
.svc-GEO { color: var(--svc-geo); }
.svc-GEOPHYS { color: var(--svc-geophys-text); }
.svc-CON { color: var(--svc-con); }
.svc-LAB { color: var(--svc-lab); }

/* The service chips echo the same hues, so the filter row and the table agree. */
.chip--type.svc-CMT,
.chip--type.svc-GEO,
.chip--type.svc-GEOPHYS,
.chip--type.svc-CON,
.chip--type.svc-LAB { border-color: color-mix(in srgb, currentColor 45%, var(--rule-faint)); }

.chip--type[aria-pressed="true"].svc-CMT,
.chip--type[aria-pressed="true"].svc-GEO,
.chip--type[aria-pressed="true"].svc-GEOPHYS,
.chip--type[aria-pressed="true"].svc-CON,
.chip--type[aria-pressed="true"].svc-LAB {
  border-color: currentColor;
  background: color-mix(in srgb, currentColor 10%, transparent);
}

.chip--type .chip__count { color: inherit; opacity: 0.7; }

/* Failure, as distinct from work still owed. */
.is-critical { color: var(--critical); }

/* Not yet a breach. The accent is the hub's one colour for the thing that wants
   attention now, and a calibration you can still book is exactly that. It is a
   background rather than text: #ffbe00 cannot hold a letterform on white. */
.is-soon {
  background: rgba(255, 190, 0, 0.22);
  border-radius: 4px;
  padding: 1px 5px;
}
.rank__bar--critical { background: var(--critical); }

/* A standing figure that leads to the reports behind it. Reset from the
   button defaults so it still reads as the number it is. */
.verdict--link {
  font: inherit;
  text-align: left;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: inherit;
}

.verdict--link:hover .verdict__count { color: var(--link); }
.verdict--link:hover .verdict__label { text-decoration: underline; }
.verdict--critical .verdict__count {
  background: color-mix(in srgb, var(--critical) 14%, transparent);
  color: var(--critical);
}

/* ------------------------------------------------------------ panel rhythm -- */

/* Every block on every screen is the same object: one hairline, one radius, the
   same inner padding. Before this the table sat bare on the page while Reporting
   and Settings were in cards, so the app read as two different applications. */

.table,
.loose,
.report__block,
.stats {
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
}

.stats { overflow: hidden; }

/* Rows get a little more air. At 11.5px Times the old 10px padding put three
   lines of a 592-row table inside a thumb's width. */
.table__row--drop .table__row__select { padding: 11px 12px; }
.table__head { padding: 9px 12px; }

/* The heading rule is the one place a full-strength black rule still earns its
   weight — it separates the head from the body rather than boxing anything in. */
.table__head { border-bottom: 1px solid var(--rule); }

/* ------------------------------------------------------------- rail facts -- */

/* Below 1180px the rail stops being a 320px sidebar and becomes a full-width
   block under the main column. The fact rows are `space-between`, so in that
   state the key and its value were flung to opposite ends of a 900px row —
   "Status" on the far left, "Active" on the far right, with a hand's width of
   nothing between them.
   
   Columns rather than a breakpoint: at sidebar width this is one column and
   nothing changes; stacked, it fills the width as a spec sheet and every value
   stays beside the label it belongs to. */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(236px, 1fr));
  column-gap: 24px;
  row-gap: 7px;
}

/* Same fault, same fix: the log line's date and text had the full width between
   them once the rail stacked. */
.rail .log { max-width: 560px; }

/* ------------------------------------------------------------- site visits -- */

/* Field Data bundles by visit — one project on one day — because that is how the
   work arrives: a technician goes out once and files a daily report, a sampling
   record and a strength test per set. 1,113 forms become 648 visits. */

.visit { border-bottom: 1px solid var(--rule-faint); }
.visit:last-child { border-bottom: 0; }
.visit--open { background: var(--fill-2); }
.visit--open > .table__row { border-bottom: 1px solid var(--rule-soft); }

.visit__date { font-weight: 700; white-space: nowrap; }
.visit__ago { display: block; font-size: 9.5px; color: var(--ink-3); font-style: italic; }

.visit__kinds { display: flex; flex-wrap: wrap; gap: 4px; }

.visit__kind {
  font-size: 10.5px;
  border: 1px solid var(--rule-faint);
  border-radius: 4px;
  padding: 1px 6px;
  white-space: nowrap;
  background: var(--fill);
}

.visit__kind b { font-weight: 700; margin-left: 3px; }
.visit__clear { font-size: 10.5px; color: var(--ink-3); }

/* Work still owed out of a visit, in the page's one "attention" colour. */
.pill--owed { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 22%, transparent); }

/* Reserved, and this is what it was reserved for: a set that did not make
   its design strength. Amber is work still owed; this is work that came back
   wrong. Nothing decorative wears it, so one red pill on a screen is one
   thing to go and look at. */
.pill--fail {
  border-color: var(--critical);
  background: color-mix(in srgb, var(--critical) 12%, transparent);
  color: var(--critical);
  font-weight: 700;
}

/* The forms inside a visit are indented under it and lose the date and project
   columns, which the visit header already carries. */
.visit__forms { padding-left: 26px; background: var(--fill); }
.visit__forms .table__row { border-bottom: 1px solid var(--rule-faint); }
.visit__forms .table__row:last-of-type { border-bottom: 0; }

/* ------------------------------------------------------------------ trend -- */

/* One line per measure, no axis: the value sits beside it and the change column
   carries the direction, so the line only has to show the shape. */
.spark { width: 100%; height: 18px; display: block; overflow: visible; }
.spark--empty { font-size: 10.5px; color: var(--ink-3); font-style: italic; }

/* Better or worse depends on the measure — a falling backlog is good, a falling
   project count is not — so the tone is decided per row, never by the sign. */
/* A change for the worse reads darker and bold; better or flat stays quiet.
   They wore the LAB service's brown and the failed red, which belong to other
   things: a count that went up is not a failed test. */
.trend--good { color: var(--ink-3); }
.trend--bad { color: var(--ink); font-weight: 700; }

/* A ranked row that goes somewhere. It is a button, so it resets like one, but it
   has to keep the row's geometry or the columns stop lining up with the rows
   above and below that do not link. */
.rank__row--link {
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  padding: 0;
  cursor: pointer;
  width: 100%;
}

.rank__row--link:hover .rank__label { color: var(--link); text-decoration: underline; }
.rank__row--link:hover .rank__bar { background: var(--link); }
.rank__row--link:hover .rank__bar--flag { background: var(--accent); }
.rank__row--link:hover .rank__bar--critical { background: var(--critical); }

/* Offered, not imposed: the page never reloads itself out from under a reader,
   it just says newer data exists. Amber because it is the page's one "look here". */
.btn--new {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 22%, var(--fill));
  font-weight: 700;
}

/* ------------------------------------------------------------------ today -- */

/* The morning board. Cards are buttons — each one is a question with a view that
   answers it — and a zero is stated rather than hidden, so an empty lane means
   something and the board reads the same shape every day. */
.today__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px;
}

.today {
  display: flex;
  flex-direction: column;
  gap: 3px;
  align-items: flex-start;
  text-align: left;
  padding: 12px 14px;
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
  background: var(--fill);
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.today:hover { border-color: var(--rule); }
.today__label { font-size: 9.5px; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; }
.today__value { font-size: 23px; font-weight: 700; line-height: 1; }
.today__note { font-size: 10.5px; color: var(--ink-2); }

/* Amber is still "owed", red still "failed" — and a lane with nothing in it drops
   back to plain, so colour never appears just because a card exists. */
.today--attention { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--fill)); }
.today--critical { border-color: var(--critical); background: color-mix(in srgb, var(--critical) 7%, var(--fill)); }
.today--critical .today__value { color: var(--critical); }
/* Zero is not news. Matched to .stat__value--nil, which the stat cards have
   always used: --ink-3 is the colour of a quiet note, not of a number being
   set aside, so two of the three cards on Today read as loudly as the one
   that mattered. Full size either way, so the row keeps its rhythm. */
.today--clear .today__value { color: var(--rule-soft); font-weight: 400; }

.today__who { font-size: 11px; color: var(--ink-2); }

/* ------------------------------------------------------- the lab strip -- */

/* One line under the cards naming the projects today's breaks sit in. A count
   tells the lab how much; only the names tell them where. */
.lab {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.lab__label {
  font-size: 10.5px;
  letter-spacing: 0.1em;
  font-weight: 700;
  color: var(--ink-3);
}

.lab__job {
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  padding: 5px 10px;
  border: 1px solid var(--rule-soft);
  border-radius: 999px;
  background: none;
  font: inherit;
  font-size: 11.5px;
  color: inherit;
  cursor: pointer;
  max-width: 100%;
}

.lab__job:hover { border-color: var(--ink); background: var(--fill-2); }

/* An overdue specimen is a different problem to one due today, so it is not
   left to blend into the same chip. */
.lab__job--late {
  border-color: var(--critical);
  background: color-mix(in srgb, var(--critical) 8%, transparent);
}

.lab__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lab__count { font-weight: 700; font-variant-numeric: tabular-nums; }
.lab__job--late .lab__count { color: var(--critical); }

/* ------------------------------------------------------------- weather -- */

/* The week as recorded on the schedule sheet. Temperatures only take colour
   when they cross a threshold that changes what happens on site, so a normal
   week reads as plain numbers. */
/* A grid rather than wrapped flex: flex grows whatever is on the last row to
   fill it, so a week the sheet has only filled in one day of turned that day
   into a tile the width of the page. Every day is the same size here whether
   there is one of them or seven. */
/* The message board. A thread, not a chat window: the hub sits open on a desk
   all day and what people want is "did anybody say anything", read top to
   bottom, rather than bubbles and presence dots. */
/* What was done about a loose end, beside the buttons that do it. */
.acted { display: inline-flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.acted__said { font-size: 10.5px; color: var(--ink-3); }

/* The hub and Aldoa disagreeing, said out loud. Amber rather than red: it is
   a thing to finish, not a thing that has gone wrong. */
.acted__stale {
  font-size: 10.5px;
  padding: 0 6px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}

/* A note against a loose end. It sits under its row and spans the width, so
   it reads as a margin note on that line rather than as another column. */
.note-line, .note-edit { flex: 1 1 100%; display: flex; align-items: baseline; gap: 8px; }

.note-line {
  font: inherit;
  font-size: 11px;
  text-align: left;
  background: none;
  border: 0;
  border-left: 2px solid var(--accent);
  padding: 2px 0 2px 8px;
  margin-top: 4px;
  cursor: pointer;
  color: var(--ink-2);
}

.note-line:hover .note-line__text { text-decoration: underline; }
.note-line__text { flex: 1; min-width: 0; }
.note-line__by { flex: none; color: var(--ink-3); font-size: 10.5px; }

.note-edit { margin-top: 5px; }

.note-edit__box {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 11.5px;
  padding: 4px 8px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--fill);
}

/* The folder worker's answer, under the plan it is about. */
.preview__act { display: flex; align-items: center; gap: 10px; padding-top: 8px; }

/* The row exactly as it will land, so it can be read before it is pasted —
   and selected by hand if the clipboard is refused. */
/* The six cells, each against the cell it goes in. A block of tab-separated
   text would be quicker to paste and would not survive the first person who
   pasted it one column to the left. */
.cells { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; }

.cells__row {
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr) minmax(0, 1.2fr);
  gap: 10px;
  align-items: baseline;
  padding: 3px 0;
  border-bottom: 1px solid var(--rule-faint);
}

.cells__ref {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  color: var(--ink);
}

.cells__val { font-family: var(--mono); font-size: 11px; color: var(--ink); }
.cells__why { font-size: 10.5px; color: var(--ink-3); }

/* A job that only Aldoa knows about. Marked rather than announced: it is the
   first thing on the page because it is the thing to do, and it goes away for
   good the moment the row is on the sheet. */
.push {
  border-left: 3px solid var(--accent);
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 14px 16px;
}

.push__head { display: flex; flex-direction: column; gap: 3px; }

.push .rowout { margin-top: 0; }

.rowout {
  margin-top: 8px;
  padding: 8px 10px;
  border: 1px solid var(--rule-soft);
  border-radius: 4px;
  background: var(--fill-2);
  font-family: var(--mono);
  font-size: 11px;
  overflow-x: auto;
  white-space: nowrap;
  user-select: all;
}
.preview__hint { font-size: 11px; color: var(--ink-3); }

.preview__ready {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 8px;
  padding: 10px 12px;
  font-size: 12px;
  border: 1px solid var(--accent);
  border-radius: 6px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* A refusal is the worker doing its job, so it is stated plainly rather than
   styled as breakage. */
.preview__refused {
  margin-top: 8px;
  padding: 10px 12px;
  font-size: 12px;
  border: 1px solid var(--critical);
  border-radius: 6px;
  background: color-mix(in srgb, var(--critical) 8%, transparent);
  color: var(--critical);
}

.preview__done {
  margin-top: 8px;
  padding: 10px 12px;
  font-size: 12px;
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
}

.preview__note { padding-top: 8px; font-size: 11px; color: var(--ink-3); font-style: italic; }

.chat { display: flex; flex-direction: column; gap: 14px; }

.chat__thread {
  display: flex;
  flex-direction: column;
  gap: 18px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 16px 18px;
  /* Tall enough to hold a conversation and capped so the composer stays in
     view: a box you have to scroll the page to reach is a box nobody posts
     from. */
  max-height: 52vh;
  min-height: 160px;
  overflow-y: auto;
}

.chat__day { display: flex; flex-direction: column; gap: 10px; }

/* The date as a rule across the thread, the way a day breaks in a log. */
.chat__daymark {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 9.5px;
  letter-spacing: 0.1em;
  font-weight: 700;
  color: var(--ink-3);
  text-transform: uppercase;
}

.chat__daymark::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule-soft);
}

.chat__msg {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding-left: 10px;
  border-left: 2px solid var(--rule-faint);
  max-width: 78ch;
}

/* Your own messages carry the accent the hub uses for "this one is yours",
   rather than being flipped to the other side of the page: a thread that
   zigzags is harder to read back than one that runs straight down. */
.chat__msg--mine { border-left-color: var(--accent); }

.chat__meta { display: flex; align-items: baseline; gap: 8px; }
.chat__who { font-weight: 700; font-size: 11.5px; }
.chat__at { font-size: 10.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* Said quietly and permanently: a message that has been rewritten should not
   pretend it is what was originally posted. */
.chat__edited { font-size: 10px; color: var(--ink-3); font-style: italic; }

.chat__edit {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 10.5px;
  color: var(--ink-3);
  cursor: pointer;
  text-decoration: underline;
}

.chat__edit:hover { color: var(--link); }

/* pre-wrap so a message typed with line breaks keeps them. */
.chat__text { font-size: 12.5px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }

/* A job mentioned in a message. Bold and in the link colour so it reads as
   the reference it is, at the size of the sentence around it. */
.chat__job { font-size: inherit; font-weight: 700; }

/* The channel a message went to, beside who said it. Quiet: it is a filing
   detail, not part of the sentence. */
.chat__topic {
  border: 1px solid var(--rule-soft);
  border-radius: 999px;
  background: none;
  font: inherit;
  font-size: 9.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 0 7px;
  cursor: pointer;
}

.chat__topic:hover { border-color: var(--ink); color: var(--link); }

.chat__pick { flex: none; }

.chat__compose { display: flex; flex-direction: column; gap: 8px; }

.chat__box {
  width: 100%;
  font: inherit;
  font-size: 12.5px;
  line-height: 1.5;
  padding: 10px 12px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--fill);
  resize: vertical;
}

.chat__box:disabled { background: var(--fill-2); color: var(--ink-3); cursor: not-allowed; }

.chat__actions { display: flex; align-items: center; gap: 12px; }
.chat__hint { flex: 1; min-width: 0; font-size: 11px; color: var(--ink-3); }

/* Site locations. One collapsible per town, the same shape Loose Ends and the
   day panel use, so the screen reads as an index of where the firm is working
   and opens to the jobs in one place. */
.sites { display: flex; flex-direction: column; gap: 8px; }

/* The address itself is the point of the row, so it reads at body size rather
   than as a supporting detail. */
.site__where { font-size: 12px; }

/* A town with no street is a different kind of answer and has to look like
   one: it tells you which office the job belongs to and will not get anybody
   to the gate. */
.site__where--vague { color: var(--ink-3); font-style: italic; }

.cell--site-go { width: 84px; }
.cell--site-open { width: 76px; }

.site__go { font-size: 11px; font-weight: 600; }

.wx { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }

/* One block per week, because that is the unit the sheet keeps and the crew
   works to. Without the break, a fortnight of tiles reads as one long run and
   the Sunday boundary — the point where the schedule was rewritten — vanishes. */
.wx__week + .wx__week { margin-top: 14px; }

.wx__label {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 6px;
}

/* Whose job it is, beside who went to it. Narrow: these are initials more often
   than names. */
.cell--pm { width: 116px; }

/* A weather day opens what was filed that day, so it is a button and says so.
   It is a record of a day on site, and the first question about a cold morning
   is what got poured on it. */
.wx__day {
  font: inherit;
  text-align: left;
  cursor: pointer;
  /* Its own ground and ink. Left to the browser, a button takes the system's
     button colours, which in dark mode are a mid-grey the temperatures sat on
     at 1.9:1. */
  background: var(--fill);
  color: var(--ink);
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 11.5px;
}

.wx__day:hover { border-color: var(--ink); background: var(--fill-2); }
.wx__day:hover .wx__date { color: var(--link); text-decoration: underline; }

.wx__date { font-weight: 700; font-size: 10.5px; letter-spacing: 0.04em; }
/* Town, high/low, rain: three columns that line up down the card, so the
   town can never run into the temperature. */
.wx__office {
  display: grid; grid-template-columns: minmax(0, 1fr) auto 2.8em;
  align-items: baseline; column-gap: 6px;
}
.wx__where { min-width: 0; color: var(--ink-3); font-size: 10.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wx__temps { white-space: nowrap; font-variant-numeric: tabular-nums; }
.wx__temp { font-variant-numeric: tabular-nums; }
.wx__sep { color: var(--ink-3); margin: 0 2px; }
.wx__rain { color: var(--ink-3); font-size: 10.5px; text-align: right; font-variant-numeric: tabular-nums; }
.wx__office .wx__none { grid-column: 2 / 4; }
.wx__none { color: var(--ink-3); font-style: italic; font-size: 10.5px; }

/* Cold is the critical one: below 40F a placement needs protection or it does
   not happen. Hot is the accent — it changes how you work, not whether. */
.wx__temp.is-cold { color: var(--critical); font-weight: 700; }
.wx__temp.is-hot { background: rgba(255, 190, 0, 0.3); border-radius: 3px; padding: 0 3px; font-weight: 700; }

.wx__flag { font-size: 10px; font-style: italic; padding-left: 2px; white-space: nowrap; }
.wx__flag--cold { color: var(--critical); }
.wx__flag--hot { color: var(--ink-2); }

/* A booking that lives only on the schedule sheet, with no Aldoa work order
   behind it. Outlined rather than filled: it is a provenance note, not a
   status. */
/* Matches the service tag beside a project number: the hub already has a shape
   for "a fact about this row", and this is one. The dotted version read as
   provisional, which is the one thing these bookings are not. */

/* The start time, which only the schedule sheet knows. Set in the accent's
   ground rather than its ink — #ffbe00 cannot hold a letterform — because it is
   the first thing anybody reads off a row in the morning. */
.attime {
  display: inline-block;
  margin-right: 7px;
  padding: 0 5px;
  border-radius: 4px;
  background: rgba(255, 190, 0, 0.3);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* On a card it follows the technician, so it separates rather than leads. */
.attime--card { margin-right: 0; margin-left: 6px; font-size: 9.5px; }


/* Which part of a job. PTL-263492 is six rows on the schedule sheet — Misc, 100
   Receiving Tunnel, 200 Grain Silos and the rest — and without this they are
   the same row printed six times. Quieter than the service tag beside it: the
   number says which job, this says which corner of it. */
.area {
  /* Shrinks, with its ellipsis, rather than holding its width: flex: none let
     "100 Receiving Tunnel" run out of a phone-width cell and under the name of
     whoever was on site. */
  flex: 0 1 auto;
  min-width: 0;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 4px;
  background: var(--fill-2);
  font-size: 9.5px;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 190px;
}

/* Worked out rather than recorded. The hub already draws an inference with a
   dotted edge — the work type on a field report does it — so this does too, and
   the tooltip says what the inference was made from. */
.sheetmark--guess { border-style: dotted; font-style: italic; }

.sheetmark {
  display: inline-block;
  margin-left: 7px;
  padding: 0 5px;
  border: 1px solid var(--rule-faint);
  border-radius: 999px;
  font-size: 9px;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  vertical-align: 1px;
}


/* --------------------------------------------------- field data settings -- */

.sets { display: flex; flex-direction: column; gap: 16px; }

/* These tables print Aldoa's own columns, and a concrete mix design has ten of
   them. Left to flex they shrink to nothing and the text prints over the next
   cell, so each column keeps a floor and the table scrolls sideways instead. */
.sets__table { overflow-x: auto; }

/* Aldoa's own column names are short and its values shorter; these tables do
   not need the 200px floor the prose columns take. */
.sets__table .cell--grow { min-width: 92px; }


/* A Today row that opens its destinations is the project table's row menu in a
   narrower space: the row becomes the container and the button inside it keeps
   the grid. */
/* Doubled class on purpose: these rules were inserted above .tlist__row, and at
   equal specificity the later rule wins — the row stayed display:grid and the
   menu became a grid item sitting beside the row instead of under it. */
.tlist__row.tlist__row--drop {
  display: block;
  padding: 0;
  cursor: default;
}

.tlist__row.tlist__row--drop:hover { background: none; }

.tlist__select {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1.3fr) 16px;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 10px 6px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.tlist__select:hover { background: var(--fill-2); }
.tlist__select:hover .project__number { color: var(--link); text-decoration: underline; }

/* Open rows carry the service tint, the same signal the project table gives. */
.tlist__row.tlist__row--open { background: var(--fill-2); }
.tlist__row--open .tlist__select { background: none; }

@media (max-width: 1240px) {
  .tlist__select { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) 16px; }
  /* Placed rather than flowed: .tlist__what spans the row, so anything after it
     in source order lands on a third row of its own. */
  .tlist__select .disclosure { grid-column: 3; grid-row: 1; }
  .tlist__select .tlist__what { grid-column: 1 / -1; grid-row: 2; padding-left: 2px; }
}



/* ------------------------------------------------------- today's lists -- */

/* On Site Today and Filed Today used the ranked-bar row, which is built for a
   bar chart: a fixed narrow label, a track in the middle and a number at the
   end. A work order is not a bar. The scope wrapped to four lines in a column
   sized for a percentage, and the whole block read as squeezed. */

.tlist { display: flex; flex-direction: column; }

.tlist__row {
  display: grid;
  /* Project wide enough for a real name, then two readable columns. */
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1.3fr);
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 10px 6px;
  border: 0;
  border-bottom: 1px solid var(--rule-soft);
  background: none;
  font: inherit;
  font-size: 12px;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.tlist__row:last-child { border-bottom: 0; }
.tlist__row:hover { background: var(--fill-2); }
.tlist__row:hover .project__number { color: var(--link); text-decoration: underline; }

.tlist__project { gap: 3px; }
.tlist__project .project__name { font-size: 12.5px; }
.tlist__project .project__number { font-size: 10.5px; }

.tlist__who { color: var(--ink-2); min-width: 0; }

/* The scope line is the answer to "what is happening there", so it gets room
   and two lines before it gives up. The full text is the row's tooltip. */
.tlist__what {
  color: var(--ink-2);
  min-width: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Below this the three columns stop being three columns. */
@media (max-width: 1240px) {
  .tlist__row { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
  .tlist__what { grid-column: 1 / -1; padding-left: 2px; }
}


/* -------------------------------------------------------------- shortcuts -- */

.keys {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 40;
  width: 250px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 14px;
  background: var(--fill);
  border: 1px solid var(--rule);
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  font-size: 11px;
}

.keys__head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; }
.keys__head .report__title { flex: 1; }
.keys__row { display: flex; align-items: baseline; gap: 10px; }

.keys kbd {
  flex: none;
  min-width: 36px;
  text-align: center;
  font-family: var(--mono);
  font-size: 10px;
  border: 1px solid var(--rule-soft);
  border-radius: 4px;
  padding: 1px 5px;
  background: var(--fill-2);
}

/* --------------------------------------------------------------- step nav -- */

/* Stepping between projects without going back to the table. Sits with the
   header buttons because it is navigation, not a control on the view. */
.step-nav { display: inline-flex; align-items: center; gap: 4px; margin-right: 6px; }
.step-nav__count {
  font-size: 10.5px;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  min-width: 62px;
  text-align: center;
}

/* ------------------------------------------------------------- geotech -- */

/* The closeout checklist: four marks rather than four words. A geotech job is
   not finished when the invoice is paid, it is finished when all four are
   ticked, and that reads at a glance only if it is a shape rather than a
   sentence. Filled is done, hollow is not. */
.cell--closeout { width: 104px; display: flex; align-items: center; gap: 3px; }

.tick { font-size: 11px; color: var(--rule-soft); line-height: 1; }
.tick--on { color: var(--svc-geo); }

.tick__count {
  margin-left: 5px;
  font-size: 10px;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* What a job is waiting on, and whose problem that is.
   Ours is the one worth reading first, so it carries the weight; a job out with
   a client is a fact rather than a task and sits back. Never colour alone —
   the label says which it is. */
.cell--pending { width: 190px; display: flex; flex-direction: column; gap: 1px; }

.pending { font-size: 12px; }
.pending--us { font-weight: 700; color: var(--ink); }
.pending--nobody { color: var(--ink-3); font-style: italic; }

.pending__at { font-size: 10.5px; color: var(--ink-3); }

@media (max-width: 720px) {
  .cell--pending { width: 128px; }
  .pending { font-size: 11.5px; }
}

/* What has been set aside, and the way back to it. Never a filter that hides
   quietly: the count is the control, so the number of things not on screen is
   always on screen. */
.aside {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  margin-top: 10px;
  padding: 10px 12px;
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
  background: var(--fill-2);
  font: inherit;
  font-size: 11.5px;
  text-align: left;
  cursor: pointer;
}

.aside:hover { border-color: var(--rule); }

.aside__count {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  min-width: 18px;
}

.aside__text { flex: 1; color: var(--ink-2); }
.aside__do { color: var(--link); text-decoration: underline; }

@media (max-width: 720px) {
  .aside { min-height: 48px; }
}

/* The closeout written out, for when the four marks are not enough and you want
   to know which of them is missing. */
.closeout { display: flex; flex-direction: column; gap: 4px; }

.closeout__step { display: flex; align-items: baseline; gap: 8px; }
.closeout__label { font-size: 12px; }
.closeout__label--todo { color: var(--ink-3); }
.colophon__build { font-variant-numeric: tabular-nums; }

/* What's new: one short block per release, the date first. */
.news { display: flex; flex-direction: column; gap: 10px; }
.news__head { display: flex; gap: 8px; align-items: baseline; }
.news__date { font-weight: 700; font-size: 12px; }
.news__tag { font-size: 11px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.news__list { margin: 3px 0 0; padding-left: 18px; font-size: 12.5px; color: var(--ink-2); display: flex; flex-direction: column; gap: 2px; }
/* A budget estimate's comments keep the line breaks they were typed with. */
.be__comments { white-space: pre-line; }
.sheetmark--linked { border-style: dashed; }
.sheetmark--split { border-color: var(--accent); }

/* The work order queue, one line a row: number and name side by side, the
   scope cut to its line with the whole of it on hover. */
.table__row--tight.table__row--padded { padding-top: 5px; padding-bottom: 5px; align-items: center; }
.table__row--tight .cell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.project--line { display: flex; gap: 8px; align-items: baseline; min-width: 0; flex-direction: row; }
.project--line .project__number { flex: none; }
.project--line .project__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.table__row--tight .pill { padding-top: 1px; padding-bottom: 1px; }
.wo__extra {
  margin-left: 5px; font-size: 10.5px; font-weight: 600; color: var(--ink-2);
  border: 1px solid var(--accent); border-radius: 3px; padding: 0 3px;
}
@media (max-width: 900px) {
  .table__row--tight .cell { white-space: normal; }
}
.closeout__when { font-size: 11.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
/* Tentatives already on the geo tracker: a plain list under the queue. */
.geo__tentatives { margin: 4px 0 0; display: flex; flex-direction: column; }
.geo__tentative {
  display: flex; align-items: baseline; gap: 10px; width: 100%;
  padding: 7px 12px; border: 0; border-bottom: 1px solid var(--rule-faint);
  background: none; color: inherit; font: inherit; font-size: 13px; text-align: left; cursor: pointer;
}
.geo__tentative:hover { background: var(--fill-2); }
.geo__tentative[aria-expanded="true"] { background: var(--fill-2); }
.geo__tentative-name { color: var(--ink); min-width: 0; flex: 1; }
.geo__tentative-detail { padding: 4px 12px 12px 34px; border-bottom: 1px solid var(--rule-faint); }
.geo__tentative-detail .pipe__detail { padding: 0; border: 0; }
.geo__tentative-stage { color: var(--ink-3); font-size: 12px; white-space: nowrap; }

/* A due date that has gone by, in the colour the hub keeps for "this failed"
   rather than "this is owed". */
.cell--date.is-overdue { color: var(--critical); font-weight: 700; }

@media (max-width: 720px) {
  /* The four marks stay together; the count is what carries on a small screen. */
  .cell--closeout { width: 88px; }
}

/* ============================================================ on a phone == */

/* The hub was built for a 1680px desk and shrunk from there. On a 375px screen
   that left 454px of furniture — masthead, connection bar and the whole
   navigation — before a single row of content on an 812px screen, and 51 of its
   75 controls under 40px tall.

   This is the phone layer rather than another round of hiding columns. All of
   it is below 720px and none of it changes anything above. */

@media (max-width: 720px) {

  /* ---------------------------------------------------------- the frame -- */

  body { padding: 0; }

  .app {
    border: 0;
    border-radius: 0;
    min-height: 100vh;
  }

  /* The masthead is a brand and a row of tabs. On a phone the brand is worth
     about a third of what it costs, so it shrinks and the tabs take the room. */
  .masthead { align-items: center; }
  .masthead__brand { width: 84px; padding: 8px 10px; }
  .masthead__brand img { width: 58px; }

  /* Tabs scroll sideways and now say so. Without the fade the strip looks like
     it simply ends at "Wo…", and nobody swipes something that looks finished. */
  .tabs {
    position: relative;
    gap: 0;
    padding: 0 8px;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  }

  .tabs::-webkit-scrollbar { display: none; }

  .tab {
    padding: 14px 12px;
    min-height: 48px;
    font-size: 14px;
  }

  /* ---------------------------------------------- where the data came from -- */

  /* Three lines and two buttons, for something whose job is to say "this is
     current" and then get out of the way. The endpoint and the detail are for
     somebody debugging at a desk, not for a phone. */
  .conn {
    gap: 8px;
    padding: 7px 12px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    font-size: 11px;
  }

  .conn::-webkit-scrollbar { display: none; }
  .conn__endpoint, .conn__detail { display: none; }

  /* Except when the figures are from an earlier day. The detail is hidden here
     because it is for somebody debugging at a desk — but when the bar is amber
     the detail is the warning: when the figures are from, and how many daily
     refreshes were missed. On a phone that is exactly the reader who has no
     Settings screen open to find it on. It wraps rather than scrolling
     sideways, because a warning you have to swipe to finish is one people stop
     reading halfway. */
  .conn--behind { flex-wrap: wrap; overflow-x: visible; row-gap: 2px; }
  .conn--behind .conn__detail { display: inline; flex-basis: 100%; order: 3; }
  .conn .btn { min-height: 34px; padding: 6px 10px; white-space: nowrap; }

  /* ------------------------------------------------------ the navigation -- */

  /* Twelve links stacked came to 282px on every view — you scrolled past the
     whole hub to reach the thing you had opened. It becomes one thumb-height
     strip under the tabs, scrolling sideways, with the group headings folded
     away: the tabs are the top level and these are that tab's views, which is
     the shape a phone expects. */
  .sidebar {
    width: 100%;
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 0;
    padding: 0 8px;
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
  }

  .sidebar::-webkit-scrollbar { display: none; }
  .sidebar__group { flex-direction: row; align-items: center; gap: 0; }
  .sidebar__label { display: none; }

  /* A group boundary still reads without its heading: a hairline between the
     last item of one and the first of the next. */
  .sidebar__group + .sidebar__group {
    border-left: 1px solid var(--rule-faint);
    margin-left: 6px;
    padding-left: 6px;
  }

  .sidebar__item {
    margin: 6px 2px;
    padding: 0 12px;
    min-height: 44px;
    display: flex;
    align-items: center;
    white-space: nowrap;
    border-left: 0;
    border-bottom: 3px solid transparent;
    border-radius: 6px;
    font-size: 13px;
  }

  .sidebar__item[aria-current="true"] {
    border-left: 0;
    border-bottom-color: var(--accent);
  }

  /* --------------------------------------------------------- the content -- */

  .main { padding: 16px 12px; gap: 14px; }
  .main__head { flex-direction: column; align-items: flex-start; gap: 10px; }
  .main__title { font-size: 17px; }

  /* The rail is a second column on a desk and a long tail on a phone. It keeps
     its place at the bottom with a rule to say a new thing has started. */
  .rail { padding: 16px 12px; border-top: 2px solid var(--rule); }

  /* ---------------------------------------------------------- the counts -- */

  /* Four across becomes two, which is as many as 375px holds without the
     numbers wrapping. */
  .stats { flex-wrap: wrap; border-radius: 6px; }
  .stat { flex: 1 1 calc(50% - 1px); min-width: 140px; border-bottom: 1px solid var(--rule); }
  .stat:nth-last-child(-n+2) { border-bottom: 0; }
  .stat__value { font-size: 20px; }

  /* ---------------------------------------------------------- the thumbs -- */

  /* Everything you tap gets a thumb's worth of height. 44px is the floor the
     platform guidelines use, and the reason a dense desk tool feels hostile on
     a phone is that almost nothing here reached it: pagination at 24, the nav
     at 25, buttons at 26. */
  .btn,
  .page-btn,
  .chip,
  .fd__control,
  .field input,
  .field select,
  .search {
    min-height: 44px;
    padding-top: 10px;
    padding-bottom: 10px;
  }

  .btn, .chip { padding-left: 14px; padding-right: 14px; font-size: 13px; }
  .page-btn { min-width: 44px; }

  /* 16px is the size below which iOS zooms the page on focus. */
  .search { width: 100%; font-size: 16px; }

  /* The small inline actions — Note, Done, 7d, the sort toggles — are text
     rather than buttons by design, so they take their target from padding
     instead of a box, which would turn every row into a toolbar. */
  .loose__act,
  .linkish,
  .sort-toggle,
  .siteplan__all,
  .chat__edit {
    min-height: 40px;
    display: inline-flex;
    align-items: center;
    padding: 0 8px;
  }

  .loose__acts { width: auto; flex-wrap: wrap; justify-content: flex-start; }

  /* The strips on Today — the lab queue, the invoice queues, who is off — are
     the quickest way into anything from a phone, so they get a full target
     rather than the 27px they inherit from the desk layout. */
  .lab { gap: 8px; }
  .lab__job { min-height: 44px; padding: 8px 12px; font-size: 13px; }
  .lab__label { flex: 1 1 100%; }
  .rank__row { min-height: 44px; }

  /* The two in the connection bar were the last left short. */
  .conn .btn { min-height: 40px; }

  /* The little escape hatches — the ↗ out to Aldoa beside a loose end, the
     dash on a break row, a link inside a table cell. They are secondary by
     design and making them 44px tall would push every row apart to serve the
     thing nobody came for. So the box grows and the layout does not: padding
     out, margin back in, which is hit area without height. */
  .loose__away,
  .brk__dash,
  .brk__num a,
  .cell a {
    padding: 14px;
    margin: -14px;
    display: inline-block;
  }

  /* These are rows in their own right rather than something tucked beside
     another control, so they simply get the height. */
  .planlink { min-height: 44px; align-items: center; }
  .attention__item { min-height: 40px; }
  .table__sort { min-height: 40px; display: inline-flex; align-items: center; }

  /* A row you can open is the largest thing on the screen you will aim at, so
     it should be the easiest to hit rather than the tightest. */
  button.table__row { min-height: 44px; }

  /* Sorting headers earn a target too — they are how a table gets reordered on
     a phone, where there is no wide row to scan instead. */
  .sorth { min-height: 40px; }

  /* The last two: a verdict chip on Reporting and a booking on the Schedule. */
  .verdict { min-height: 40px; display: inline-flex; align-items: center; }
  .sch__card { min-height: 44px; }

  /* -------------------------------------------------------- the filtering -- */

  .filters { gap: 8px; }
  .fd__bar { gap: 12px; padding-bottom: 10px; }
  .fd__group { flex: 1 1 calc(50% - 6px); min-width: 140px; }
  .fd__bar .chip, .fd__bar .linkish { font-size: 13px; }

  /* ----------------------------------------------------------- the lists -- */

  /* A loose end was five columns on one line, which at this width is four words
     a column. Stacked, each row reads as a small block: what it is, the detail,
     then what can be done about it. */
  .loose__item { flex-wrap: wrap; gap: 4px 10px; padding-bottom: 10px; }
  .loose__num { width: auto; }
  .loose__name { width: auto; flex: 1 1 100%; }
  .loose__detail { flex: 1 1 100%; }
  .loose__body { padding-left: 14px; }
  .loose__head { padding: 12px; min-height: 48px; }
  .loose__label { font-size: 13px; }

  /* The project table keeps its shape and scrolls, because the columns are the
     comparison — but the name leads and takes the width. */
  .cell--status, .cell--wos, .cell--review { display: none; }
  .table__row__select { padding: 12px 10px; }
  .project__name { font-size: 13px; }

  /* An expanded field report is a document; on a phone it is one column. */
  .detail { padding: 14px 12px; }
  .detail__prose { padding-left: 10px; }

  /* ----------------------------------------------------------- the modal -- */

  /* Full screen, because a 375px dialog inside a 375px window is a dialog with
     nowhere to sit. The footer sticks, so Cancel and Finish are reachable
     without scrolling a long form back down. */
  .modal { padding: 0; }

  .modal__panel {
    width: 100%;
    max-width: 100%;
    max-height: 100vh;
    border-radius: 0;
    border: 0;
  }

  .modal__head { padding: 14px 12px; }
  .modal__body { padding: 16px 12px; gap: 16px; }

  /* The panel is a flex column and the body is the only part that scrolls, so
     the footer is already pinned to the bottom — it was also given sticky,
     which does nothing here except let body content show through beside it. */
  .modal__foot {
    padding: 12px;
    flex-wrap: wrap;
    gap: 8px;
    background: var(--fill);
  }

  .modal__foot .btn { flex: 1 1 auto; }

  /* The service type picker is six chips and the one choice on the form that
     decides a folder template, so it is worth a proper tap. */
  .type { min-height: 44px; padding: 10px 14px; font-size: 13px; }
  .modal__readiness { flex: 1 1 100%; order: -1; }
  .steps { padding: 0 12px; overflow-x: auto; scrollbar-width: none; }
  .steps::-webkit-scrollbar { display: none; }
  .step { min-height: 44px; white-space: nowrap; }

  /* ------------------------------------------------------------ the board -- */

  .chat__thread { max-height: 52vh; }

  /* -------------------------------------------------------- the cell list -- */

  /* The six cells for the tracking sheet are a reference to read off, not a
     table to scan across. */
  .cells__row { grid-template-columns: 46px minmax(0, 1fr); gap: 2px 8px; }
  .cells__why { grid-column: 2; font-size: 10px; }
  .rowout { font-size: 10.5px; }
}

/* Below about 360px two counts across stop fitting — 375px holds them. */
@media (max-width: 360px) {
  .stat { flex: 1 1 100%; }
  .stat:nth-last-child(-n+2) { border-bottom: 1px solid var(--rule); }
  .stat:last-child { border-bottom: 0; }
}

/* -- clients ----------------------------------------------------------------
   A row per organisation rather than per job. Fixed column widths, because the
   four counts in the middle are meant to be read down the page — a client with
   fifty open work orders should be visible as a shape before anybody reads the
   number. */

.cl { border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }

.cl__row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 11.5px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--rule-faint);
}

.cl__row:last-child { border-bottom: 0; }

.cl__row--head {
  background: var(--fill);
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  font-weight: 700;
}

.cl__name {
  width: 240px;
  flex: none;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 700;
}

.cl__row--head .cl__name { font-weight: 700; }

.cl__jobs { width: 200px; flex: none; display: flex; gap: 4px; flex-wrap: nowrap; overflow: hidden; }

.cl__job {
  border: 0;
  background: none;
  padding: 0;
  font-family: inherit;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--link);
  text-decoration: underline;
  cursor: pointer;
}

.cl__more { color: var(--ink-3); font-size: 11px; }

.cl__num {
  width: 46px;
  flex: none;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Reports written up and not signed off is the one count that is somebody's
   fault rather than somebody's workload. */
.cl__num--flag { color: var(--critical); font-weight: 700; }

.cl__contact { flex: 1; min-width: 0; display: flex; gap: 8px; align-items: baseline; }
.cl__who { flex: none; }
.cl__mail {
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cl__nil { color: var(--ink-3); }

/* An Outlook draft: the button, then what happened. */
.draft { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; margin: 2px 0 8px; }
.draft__done { color: var(--ink-2); font-size: 12px; }
.draft__err { color: var(--critical); font-size: 12px; }
.draft__note { color: var(--ink-3); font-size: 12px; }
.draft__copy { flex-basis: 100%; display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }
.draft__text {
  width: 100%; max-width: 640px; font: inherit; font-size: 12px; color: var(--ink);
  background: var(--fill); border: 1px solid var(--rule); border-radius: 4px; padding: 8px; resize: vertical;
}

/* Records to fix: one line per record, number first, where it is fixed above. */
.rec { display: flex; flex-direction: column; }
.rec__line {
  display: grid; grid-template-columns: 96px minmax(0, 1fr) minmax(0, 0.8fr); gap: 0 12px;
  align-items: baseline; padding: 6px 0; border: 0; border-bottom: 1px solid var(--rule-faint);
  background: none; font: inherit; font-size: 12.5px; color: inherit; text-align: left; width: 100%;
}
button.rec__line { cursor: pointer; }
button.rec__line:hover .rec__main { text-decoration: underline; }
.rec__num { color: var(--ink-2); font-variant-numeric: tabular-nums; font-size: 12px; }
button.rec__line .rec__num { color: var(--link); }
.rec__main { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.rec__sub { display: block; color: var(--ink-3); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rec__meta { color: var(--ink-3); font-size: 11.5px; }
.rec__spelling { display: block; }
@media (max-width: 720px) {
  .rec__line { grid-template-columns: 84px minmax(0, 1fr); }
  .rec__meta { grid-column: 2; }
}

/* Since you last looked: one line per change, number first. */
.changes .report__head { align-items: baseline; }
.changes__seen { margin-left: auto; font-size: 12px; }
.changes__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.changes__item {
  display: grid; grid-template-columns: 92px minmax(0, 1.2fr) minmax(0, 1fr); gap: 0 12px;
  align-items: baseline; padding: 5px 0; border-bottom: 1px solid var(--rule-faint); font-size: 12.5px;
}
.changes__num { font-variant-numeric: tabular-nums; font-size: 12px; text-align: left; }
.changes__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.changes__what { color: var(--ink-2); }
.changes__item--new .changes__what, .changes__item--proposal .changes__what { font-weight: 600; color: var(--ink); }
@media (max-width: 720px) {
  .changes__item { grid-template-columns: 84px minmax(0, 1fr); }
  .changes__what { grid-column: 2; }
}

/* A client's row opens to their projects, proposals and people. */
.cl__toggle {
  border: 0; background: none; padding: 0; margin: 0; font: inherit; font-weight: 700;
  color: inherit; text-align: left; cursor: pointer; display: flex; gap: 6px; align-items: baseline;
}
.cl__toggle .disclosure { color: var(--ink-3); flex: none; }
.cl__row--open { background: var(--fill-2); }
.cl__detail {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 20px;
  padding: 10px 14px 14px 28px; border-bottom: 1px solid var(--rule-faint); background: var(--fill-2);
}
.cl__section { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cl__line {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 8px; align-items: baseline;
  border: 0; background: none; padding: 3px 0; font: inherit; font-size: 12px; color: inherit;
  text-align: left; cursor: pointer; border-bottom: 1px solid var(--rule-faint);
}
.cl__line:hover .cl__line-name { text-decoration: underline; }
.cl__line-num { color: var(--link); font-variant-numeric: tabular-nums; }
.cl__line-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cl__line-meta { grid-column: 2; color: var(--ink-3); font-size: 11px; }
.cl__person { display: flex; flex-direction: column; gap: 1px; padding: 4px 0; border-bottom: 1px solid var(--rule-faint); font-size: 12px; }
.cl__person-name { font-weight: 600; }
.cl__person-role { font-weight: 400; color: var(--ink-3); margin-left: 6px; font-size: 11px; }
.cl__person-reach { display: flex; flex-wrap: wrap; gap: 2px 10px; }
@media (max-width: 900px) {
  .cl__detail { grid-template-columns: minmax(0, 1fr); padding-left: 14px; }
}
.cl__ok { color: var(--ink-2); }

@media (max-width: 900px) {
  .cl__jobs { display: none; }
  .cl__name { width: 170px; }
}


/* =================================================================== phone -- */

/* The rows that were still desk tables on a phone.

   The first phone pass (the 720px block further up) made the frame, the
   navigation and the tap targets work. Every row type built since was laid out
   in fixed-width columns — 140px for a number, 220px for a name, 96px for a
   date — and on a 375px screen the columns ran off the right edge. The frame's
   overflow: hidden then cut them off without a scrollbar, so nothing looked
   broken except that half of each row was missing: Break Schedule ran to 693px,
   Strength Results to 807px. Field Data and Contacts were worse the other way —
   a 900px rule hid the project and the organisation outright, and the fixed
   columns squeezed "what was filed" and the email address to 0px, leaving a
   list of dates and technicians with no job on any of them.

   So on a phone each of these rows stops being a table and becomes a short
   block in the order it is read: what it is, then the result, then the detail.
   Cells that were only a bare number under a column heading say what they count
   instead (data-label), because the heading row does not survive the stack.

   It sits at the end of the file on purpose. The earlier phone block comes
   before several of these components are declared, and at equal specificity
   the later rule wins — an override placed there would lose to the desk rule it
   was meant to replace. */

@media (max-width: 720px) {

  /* ------------------------------------------------------------ the type -- */

  /* The small letter-spaced labels — 9.5px stat labels and column heads — are
     legible at a desk and a squint at arm's length. A point up across the
     board rather than per screen, so the hierarchy between them holds. */
  .stat__label, .table__head, .table__head--roomy, .today__label, .project__type,
  .sch__dayname, .rail__eyebrow, .rail__title, .visit__ago, .worktype, .wx__label,
  .detail__table th, .contacts-table__head, .area, .brk__flag, .chat__daymark,
  .chat__topic, .colophon, .plans__group, .planlink__kind, .cell--plan-kind {
    font-size: 10.5px;
  }

  .stat__note, .report__title, .report__note, .project__number, .today__note,
  .lab__label, .rank__more, .rank__meta, .fd__label, .filters__label, .loose__act,
  .loose__hidden, .loose__away, .stale, .pill, .pill--roomy, .detail__omitted,
  .cnt__project-name, .rowout {
    font-size: 11.5px;
  }

  /* ---------------------------------------------------------- field data -- */

  /* Which job, then when and where it stands, then what was filed, then who.
     The project comes back — it was display: none below 900px — and the scope
     gets a line of its own instead of a 0px column. */
  .table--fd .table__head,
  .table--fd .table__row { flex-wrap: wrap; row-gap: 4px; }
  .table--fd .cell--fd-project { display: block; width: auto; flex: 1 0 100%; order: 1; }
  .table--fd .cell--date { width: auto; order: 2; }
  .table--fd .cell--form-status { width: auto; order: 3; margin-left: auto; }
  .table--fd .cell--grow { flex: 1 0 100%; order: 4; }
  .table--fd .cell--tech { width: auto; order: 5; }
  .table--fd .cell--pm { width: auto; order: 6; }
  .table--fd .table__row .cell--pm::before { content: "PM "; color: var(--ink-3); }
  .table--fd .cell--pics { width: auto; order: 7; }

  /* ------------------------------------------------------------ contacts -- */

  /* The person, how many jobs they are on, then the company and how to reach
     them. The company was hidden below 900px, on a screen whose point is half
     who the person works for. */
  .table--cnt .table__head,
  .table--cnt .table__row { flex-wrap: wrap; row-gap: 3px; }
  .table--cnt .cell--name { width: auto; flex: 1 1 auto; order: 1; font-weight: 700; }
  .table--cnt .cell--cnt-projects { width: auto; order: 2; }
  .table--cnt .table__row .cell--cnt-projects::after { content: " " attr(data-label); color: var(--ink-3); }
  .table--cnt .cell--cnt-org { display: block; width: auto; flex: 1 0 100%; order: 3; }
  .table--cnt .cell--grow { flex: 1 0 100%; order: 4; overflow-wrap: anywhere; }
  .table--cnt .cell--phone { width: auto; order: 5; }

  /* ------------------------------------------------------------- clients -- */

  .cl__row--head { display: none; }
  .cl__row { flex-wrap: wrap; gap: 4px 14px; padding: 10px 12px; }
  .cl__name { width: auto; flex: 1 0 100%; white-space: normal; font-size: 13px; }
  .cl__num { width: auto; text-align: left; font-weight: 700; }
  .cl__num::after { content: " " attr(data-label); font-weight: 400; color: var(--ink-3); }
  /* A count of none is not worth a line on a phone. The desk table keeps its
     dash, because there the dash holds the column. */
  .cl__num:has(.cl__nil) { display: none; }
  .cl__contact { flex: 1 0 100%; flex-wrap: wrap; gap: 2px 8px; }
  .cl__mail { white-space: normal; overflow-wrap: anywhere; }

  /* ------------------------------------------------------ break schedule -- */

  /* The job and what is owed on it, then the site, then the set. */
  .brk__set { flex-wrap: wrap; gap: 3px 10px; padding: 8px 0; }
  .brk__num { width: auto; order: 1; }
  .brk__state { width: auto; order: 2; margin-left: auto; }
  .brk__name { width: auto; flex: 1 0 100%; order: 3; white-space: normal; }
  .brk__setid { width: auto; order: 4; }
  .brk__spec { flex: 1 1 auto; order: 5; }
  .brk__design { width: auto; order: 6; text-align: left; }

  /* ---------------------------------------------------- strength results -- */

  /* The job, then the result — the average against the design strength and the
     margin — then the set, then the note. The pseudo-element is a line break:
     flex items honour `order`, and so do pseudo-elements. */
  .str__row { flex-wrap: wrap; gap: 3px 10px; padding: 9px 0; }
  .str__job { width: auto; flex: 1 0 100%; order: 1; flex-wrap: wrap; }
  .str__name { white-space: normal; }
  .str__avg { width: auto; order: 2; text-align: left; }
  .str__design { width: auto; order: 3; }
  .str__margin { width: auto; order: 4; text-align: left; }
  .str__row::before { content: ""; flex-basis: 100%; height: 0; order: 5; }
  .str__set { width: auto; order: 6; }
  .str__cast { width: auto; order: 7; }
  .str__spec { width: auto; order: 8; }
  .str__note { flex: 1 0 100%; order: 9; }

  /* ------------------------------------------------------------ rankings -- */

  /* The name on its own line and the bar underneath at full width. A name
     squeezed beside the bar truncates to "Massanutten Wat…", which on a
     ranking is the one part that must be read. */
  .rank__row { flex-wrap: wrap; row-gap: 2px; padding: 4px 0; }
  .rank__label,
  .rank--wide .rank__label,
  .rank__label--project { width: auto; flex: 1 0 100%; white-space: normal; }

  /* ------------------------------------------------------------ settings -- */

  .srcs__row--head { display: none; }
  .srcs__row { flex-wrap: wrap; gap: 2px 12px; padding: 9px 0; }
  .srcs__file { width: auto; flex: 1 0 100%; }
  .srcs__what { flex: 1 0 100%; }
  .srcs__rows,
  .srcs__age { width: auto; text-align: left; }
  .srcs__rows[data-label]::before,
  .srcs__age[data-label]::before { content: attr(data-label) " "; color: var(--ink-3); }
}


/* ============================================================ phone, pass two -- */

/* The first two passes made every row fit a phone. This one is about what the
   first screen shows, judged from real 390px renders rather than measurements:
   three stacked cards spending 210px on three numbers, full-size Export and
   Add buttons above every screen, a pager in three rows of buttons, a column
   header broken into a four-line stack, and the same six project facts twice
   on Basic Info. Nothing here changes the desk layout. */

@media (max-width: 720px) {

  /* ------------------------------------------------------------ the frame -- */

  /* When the figures are from is the one thing a reader needs from this bar,
     and the phone layout hid it with the debugging detail. The snapshot note
     is for whoever publishes the page, not whoever reads it. */
  .conn:not(.conn--behind) .conn__detail {
    display: inline;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .conn__snap { display: none; }

  /* A CSV is a desk thing. Add project stays, a size smaller. */
  .main__head .btn--export { display: none; }
  .main__head .btn-row { flex-wrap: wrap; gap: 8px; }
  .main__head .btn { min-height: 40px; padding: 8px 12px; font-size: 13px; }
  .step-nav { display: inline-flex; align-items: center; gap: 6px; }

  /* Chip rows scroll sideways on one line, the way the tabs do, rather than
     spending three rows of the first screen before the first project. */
  .filters {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
    padding-right: 24px;
  }
  .filters::-webkit-scrollbar { display: none; }
  .filters > * { flex: none; }

  /* The pager: where you are, then back and forward. The page numbers and the
     rows-per-page choice were three rows of 44px buttons. */
  .pager { flex-wrap: wrap; gap: 8px; }
  .pager > .pager__range:first-child { flex: 1 0 100%; }
  .pager .page-btn:not(.page-btn--step),
  .pager .page-btn--size,
  .pager .pager__rule,
  .pager > .pager__range:last-child:not(:first-child),
  .pager .u-spacer { display: none; }
  .pager .page-btn--step { flex: 1 1 0; }

  /* ---------------------------------------------------------------- today -- */

  /* Three tiles across: the count is what the tile is for, and the note under
     it wrapped to three lines at this width. */
  .today__row { grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 8px; }
  .today { padding: 10px; gap: 4px; min-height: 0; }
  .today__value { font-size: 24px; }
  .today__note { display: none; }

  /* Last week's weather, not every week on file: the rest was 700px at the
     bottom of the screen people open on site. */
  .wx__week + .wx__week { display: none; }

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

  /* One short line of column names — they are the sort controls — instead of
     a column header broken across four lines by the stacked row layout. */
  .table--fd .table__head,
  .table--cnt .table__head,
  .wo .table__head { flex-wrap: wrap; gap: 2px 14px; }
  .table--fd .table__head > .cell,
  .table--cnt .table__head > .cell,
  .wo .table__head > .cell {
    display: block; flex: 0 0 auto; width: auto; order: 0; margin: 0;
  }
  .table--fd .table__head > .cell:empty,
  .wo .table__head > .cell:empty { display: none; }

  /* The ▸ that says a row opens sits beside the job, not on a line of its own. */
  .table--fd .table__row .cell--fd-project { flex: 1 1 calc(100% - 32px); }

  /* Work Orders and Geotech: the job leads, then what it is waiting on. */
  .wo .table__row { flex-wrap: wrap; row-gap: 4px; }
  .wo .table__row > .disclosure { order: -2; }
  .wo .table__row .cell--fd-project {
    display: block; width: auto; flex: 1 1 calc(100% - 32px); order: -1;
  }
  /* Geotech's last column is a sentence that restates the step beside it, the
     same fifteen times down the list. On a phone the step is enough. */
  .app[data-tab="Geotech"] .table__row .cell--grow,
  .app[data-tab="Geotech"] .table__head .cell--grow { display: none; }

  /* ------------------------------------------------------------ basic info -- */

  .panel { padding: 12px; }
  .panel__row { gap: 10px; }
  .panel__key { width: 104px; }
  /* A folder path has no spaces to break at, and ran off the edge. */
  .panel__val { overflow-wrap: anywhere; }

  /* The rail's header and "at a glance" repeat, in full, what Basic Info has
     just shown above them. On a desk they sit beside it; on a phone they were
     the same six facts twice. */
  .app[data-view="basic"] .rail__head,
  .app[data-view="basic"] .rail__section--glance { display: none; }

  /* ------------------------------------------------------------ loose ends -- */

  /* Note, Done and snooze beside the detail they act on, not on a line below
     it — a third of each item's height, on a list of up to forty-four. */
  .loose__detail { flex: 1 1 auto; }
  .loose__acts { margin-left: auto; }
}

/* ------------------------------------------------------------- pipeline -- */

.pipe__nums { width: 250px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; }
.pipe__num { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }
.pipe__arrow { color: var(--ink-3); }
.pipe__job { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; }
.pipe__name { font-weight: 600; color: var(--ink); }
.pipe__where { color: var(--ink-3); font-size: 11px; }
.pipe__client { width: 220px; }
.pipe__stand { width: 230px; color: var(--ink-2); }
.pipe__stand--signed, .pipe__stand--sent { color: var(--ink); font-weight: 600; }
.chip__count { color: var(--ink-3); margin-left: 4px; font-variant-numeric: tabular-nums; }

@media (max-width: 720px) {
  .pipe__row { flex-wrap: wrap; row-gap: 4px; }
  .pipe__nums, .pipe__job, .pipe__client, .pipe__stand { width: auto; flex: 1 0 100%; }
  /* The arrow sits on the numbers' line, the rest indents under it. */
  .pipe__nums { flex-basis: calc(100% - 22px); }
  .pipe__job, .pipe__client, .pipe__stand { padding-left: 22px; }
  .table__head .pipe__client, .table__head .pipe__stand, .table__head .cell--grow { display: none; }
}

/* ============================================================ the look ==

   Tried on Today first and adopted everywhere on 29 September 2026. Scoped to
   .app so each rule outranks the component rule it refines without !important.

   What stays: the service colours, amber for "still owed", red for "failed",
   and the serif for page titles. What changed: everything scanned rather than
   read is the sans with aligned figures; the rules are warm greys; black is
   kept for the one primary button; and the page sits on a faint ground with
   the blocks on white. */

body { background: var(--ground); }

.app {
  font-family: var(--sans);
  border-color: var(--rule);
  border-radius: 10px;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* Chrome: one weight of rule throughout, and the brand column quieter. */
.app .masthead { border-bottom: 1px solid var(--rule-soft); }
.app .masthead__brand { border-right-color: var(--rule-soft); }
.app .masthead__brand img { width: 76px; }
.app .tabs { font-size: 14px; gap: 4px; }
.app .tab { font-weight: 500; color: var(--ink-3); padding: 14px 12px 12px; }
.app .tab:hover { color: var(--ink); }
.app .tab[aria-selected="true"] { font-weight: 600; color: var(--ink); }
.app .search { font-size: 13px; border-color: var(--rule); }

.app .conn {
  background: var(--fill);
  border-bottom-color: var(--rule-soft);
  font-size: 12px;
  padding: 7px 22px;
}
.app .conn__state { font-weight: 600; }

.app .sidebar { border-right-color: var(--rule-soft); font-size: 13.5px; gap: 20px; padding-top: 22px; }
.app .sidebar__label { font-size: 11px; letter-spacing: 0.08em; font-weight: 600; color: var(--ink-3); }
.app .sidebar__item { color: var(--ink-2); padding: 6px 10px; }
.app .sidebar__item:hover { background: var(--ground); }
.app .sidebar__item[aria-current="true"] { background: var(--ground); font-weight: 600; }

/* The page: blocks sit on the ground rather than inside more lines. */
.app .main {
  background: var(--ground);
  border-right: 0;
  padding: 28px 32px 36px;
  gap: 22px;
}

.app .main__head { border-bottom: 0; padding-bottom: 0; align-items: center; }
.app .main__title {
  font-family: var(--serif);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--ink);
  text-wrap: balance;
}
.app .main__subtitle { font-size: 14px; color: var(--ink-3); margin-top: 6px; }

.app .btn {
  font-size: 13px;
  padding: 7px 13px;
  border-color: var(--rule);
  background: var(--fill);
  border-radius: 7px;
}
.app .btn:hover { border-color: var(--ink-3); }
.app .btn { white-space: nowrap; }
.app .btn--primary { background: var(--ink); border-color: var(--ink); font-weight: 600; }
.app .btn-row { font-size: 13px; }

/* The three figures the morning starts from. */
.app .today__row { gap: 14px; }
.app .today {
  gap: 6px;
  padding: 16px 18px 15px;
  border-color: var(--rule-soft);
  border-radius: 10px;
}
.app .today:hover { border-color: var(--rule); }
.app .today__label { font-size: 11px; letter-spacing: 0.08em; font-weight: 600; color: var(--ink-3); }
.app .today__value { font-size: 34px; font-weight: 600; color: var(--ink); }
.app .today__note { font-size: 13px; color: var(--ink-3); }
.app .today--attention { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 9%, var(--fill)); }
.app .today--critical { border-color: var(--critical); }
.app .today--critical .today__value { color: var(--critical); }
.app .today--clear .today__value { color: var(--rule); font-weight: 400; }

.app .lab__label { font-size: 11px; letter-spacing: 0.08em; font-weight: 600; color: var(--ink-3); }
.app .lab__job { font-size: 13px; padding: 5px 12px; border-color: var(--rule); background: var(--fill); }
.app .lab__job:hover { border-color: var(--ink-3); background: var(--fill); }

/* The two lists. White on the ground, one soft rule, room inside. */
.app .report__cols { gap: 18px; }
.app .report__block {
  background: var(--fill);
  border-color: var(--rule-soft);
  border-radius: 10px;
  padding: 16px 18px 12px;
  gap: 12px;
}
.app .report__head { gap: 10px; }
.app .report__title { font-size: 11.5px; letter-spacing: 0.08em; font-weight: 600; color: var(--ink); }
.app .report__note { font-size: 12.5px; font-style: normal; color: var(--ink-3); }

.app .tlist__row,
.app .tlist__select { font-size: 13.5px; }
.app .tlist__row { border-bottom-color: var(--rule-soft); padding: 11px 6px; }
.app .tlist__select { padding: 11px 6px; }
.app .tlist__row.tlist__row--drop { padding: 0; }
.app .tlist__row:hover,
.app .tlist__select:hover { background: var(--ground); }
.app .tlist__project .project__name { font-size: 14px; font-weight: 600; color: var(--ink); }
.app .tlist__project .project__number { font-size: 12px; color: var(--ink-3); }
.app .tlist__project .project__head { flex-wrap: wrap; row-gap: 3px; }
.app .tlist__who,
.app .tlist__what { color: var(--ink-2); }
.app .area { font-size: 11px; }

/* Columns sized for the serif. The sans is wider at the same size, so a PM's
   name in Loose Ends ran under the Note and Done buttons beside it: the name
   now ends in an ellipsis inside its column, and the buttons take the width
   they need rather than a width chosen for narrower letters. */
.app .loose__meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.app .loose__acts { width: auto; }

/* A switch between two views of the same thing: one bordered track, the
   chosen side filled. Not chips, which read as filters that stack. */
.seg {
  align-self: flex-start;
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--rule);
  border-radius: 9px;
  background: var(--fill);
}
.seg__btn {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 14px;
  border: 0;
  border-radius: 6px;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--ink-3);
  cursor: pointer;
}
.seg__btn:hover { color: var(--ink); }
.seg__btn[aria-pressed="true"] { background: var(--fill-2); color: var(--ink); font-weight: 600; }
.seg__count { font-variant-numeric: tabular-nums; font-weight: 400; color: var(--ink-3); }

/* A way to reach somebody: the number or address as text you can select, the
   link underneath it a convenience, and its kind in small quiet type. One per
   line wherever there is more than one. */
.reach { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; font-variant-numeric: tabular-nums; }
.reach__value { user-select: all; }
.reach__type { font-size: 11.5px; color: var(--ink-3); }
.pipe__phone { display: block; font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* A project's timeline: dates down the left, a rule joining the dots, and
   today last, where what the job is waiting on is said in words. */
.tl__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.tl__item {
  display: grid;
  grid-template-columns: 92px 16px minmax(0, 1fr);
  align-items: baseline;
  gap: 0 8px;
  padding: 5px 0;
  position: relative;
  font-size: 13px;
}
.tl__date { color: var(--ink-3); font-size: 12px; font-variant-numeric: tabular-nums; text-align: right; }
.tl__dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--fill); border: 2px solid var(--mark);
  justify-self: center; align-self: center;
  position: relative; z-index: 1;
}
/* The rule between dots: from this dot to the next one. */
.tl__item:not(:last-child)::after {
  content: "";
  position: absolute;
  left: calc(92px + 8px + 7px);
  top: 50%;
  height: 100%;
  border-left: 1px solid var(--rule);
}
.tl__what { color: var(--ink); min-width: 0; }
.tl__detail { display: block; color: var(--ink-3); font-size: 12px; }
.tl__item--now .tl__date { color: var(--ink); font-weight: 600; }
.tl__item--now .tl__dot { background: var(--accent); border-color: var(--accent); }
.tl__item--now .tl__what { font-weight: 600; }
@media (max-width: 720px) {
  .tl__item { grid-template-columns: 70px 16px minmax(0, 1fr); }
  .tl__item:not(:last-child)::after { left: calc(70px + 8px + 7px); }
}

/* The work order table has seven fixed columns around Scope, and on a laptop
   (about 930px of table at a 1280px window) they took all of it: Scope was
   0px wide and broke one word per line over the technician's name. Below
   1400px the PM column goes — it is on the project, and this table is about
   the work order and who is doing it — and Scope keeps a floor. */
@media (max-width: 1400px) {
  .wo .cell--pm { display: none; }
  .wo .cell--tech { width: 112px; }
}
.wo .table__row .cell--grow,
.wo .table__head .cell--grow { min-width: 160px; }

.app .colophon { font-family: var(--sans); color: var(--ink-3); }

/* A phone has no ground to sit on: the frame is gone at this width already, so
   the blocks keep their white and the page keeps its gutter. */
@media (max-width: 720px) {
  /* The rail is a sideways strip here; the desktop padding above would open a
     band of nothing over it, and the head stacks with its button at the left. */
  .app .sidebar { padding: 0 8px; gap: 0; border-bottom-color: var(--rule-soft); }
  .app .main__head { align-items: flex-start; }
  .app .main { padding: 20px 16px 28px; gap: 18px; }
  .app .main__title { font-size: 26px; }
  .app .today { padding: 13px 14px; }
  .app .today__value { font-size: 28px; }
  .app .report__block { padding: 14px 14px 10px; }
}

/* The client contacts a database search found, one per line, to choose from.
   A list rather than a dropdown, so the company and title show beside each. */
.picks { display: flex; flex-direction: column; gap: 2px; }
.picks__item {
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 6px 10px;
  background: var(--fill);
  font: inherit;
  font-size: 11.5px;
  text-align: left;
  cursor: pointer;
}
.picks__item:hover { border-color: var(--ink-3); }
