/* Base shell styles — every themed value reads var(--token, <406Blue fallback>) (AC-U1).
   Screen-specific styles land with their slices. */

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  min-height: 100%;
}

body {
  background: var(--pg, #f3f5f9);
  color: var(--ink, #101c33);
  font-family: var(--f-body, 'Figtree', sans-serif);
  font-size: 14px;
  font-weight: 500;
}

h1,
h2,
h3 {
  font-family: var(--f-head, 'Bricolage Grotesque', sans-serif);
  font-weight: var(--w-head, 800);
  text-transform: var(--tt, none);
}

h1 {
  font-size: 24px;
}

/* Watermark (AC-U5): 5% opacity, never intercepts a click; phone 31%/40%,
   desktop app screens 5px under the topbar (top ~56px) at 240px. */
.watermark {
  position: fixed;
  top: 31%;
  left: 50%;
  transform: translateX(-50%);
  width: 40%;
  opacity: 0.05;
  pointer-events: none;
  z-index: 10;
}

@media (min-width: 900px) {
  .watermark {
    top: 56px;
    width: 240px;
  }
}

.app-main {
  max-width: 1200px;
  margin: 0 auto;
  padding: 16px;
}

.card {
  background: var(--card, #ffffff);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-lg, 24px);
  box-shadow: var(--sh-card, 0 6px 18px rgba(16, 28, 51, 0.07));
  padding: 22px;
}

/* --- Auth screens (login card per o-login: centred card, pill button, footer copy) --- */

.login-page .watermark {
  top: 10%;
}

.login-card {
  max-width: 420px;
  margin: 18vh auto 0;
  padding: 28px;
}

.card-sub {
  color: var(--mut, #5c6b85);
  margin-top: 4px;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 16px;
}

.input {
  height: 52px;
  padding: 0 16px;
  font-size: 15px;
  font-family: inherit;
  color: var(--ink, #101c33);
  background: var(--pg, #f3f5f9);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
}

/* TIMTRK-311 — the literal beside every var(--faint, …) in this file is a fallback, not a value:
   the token is read first and the hex only if it fails to resolve. The house rule is the handoff's
   ("Every themed style reads var(--token, <406Blue fallback>)", docs/handoff/SPEC.md), so all 25
   carry 406 Blue's value and they move WITH the token, never after it. Left stale they would be 25
   shadow copies of the exact sub-4.5:1 colour this ticket closed, each one rendering at the moment
   the token fails to resolve. Said here, at the first occurrence, and deliberately not repeated at
   the other 24 — they are the same line. */

/* A field the rules have closed (a PO code with hours against it, a username) — visibly
   inert, with the reason spelled out beside it rather than left to be guessed. */
.input:disabled {
  color: var(--faint, #627085);
  background: var(--chip-bg, #eef1f6);
  cursor: not-allowed;
}

.field-hint {
  color: var(--mut, #5c6b85);
  font-size: 13px;
  margin-top: 4px;
}

/* A validation message that belongs to one input rather than to the whole form. */
.field-error {
  font-size: 13px;
  margin-top: 4px;
}

.input-code {
  text-align: center;
  letter-spacing: 0.2em;
  font-size: 18px;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 0 22px;
  font-size: 16px;
  font-weight: 700;
  font-family: inherit;
  text-decoration: none;
  border-radius: var(--r-pill, 99px);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  cursor: pointer;
}

.btn-primary {
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
  box-shadow: var(--sh-btn, 0 8px 20px rgba(43, 51, 121, 0.30));
}

.btn-ghost {
  background: var(--card, #ffffff);
  color: var(--chip-fg, #2b3379);
}

/* TIMTRK-233 (D7): two outline variants of .btn, for the PO lifecycle pills. Geometry is inherited
   whole from .btn — D1's governing words are "same pill geometry as Cancel", and Cancel is
   .btn.btn-ghost, so the pill is 52px and --r-pill by construction rather than by repetition. The
   ticket's own "48px" was superseded by the prelim; 48 was never what the app shipped.

   Colour comes from a token and nothing else. D7 is explicit that no new token and no new hex may
   enter, and none does: --danger is #b3261e in the default skin and #cc0000 in slab, both already
   in tokens.css, so the dark treatment is settled by the token rather than by a second rule here.

   Named .btn-ghost-* rather than .btn-danger because there is no solid danger button in this app and
   this build is not the mandate to invent one. site.css already carries .badge-btn-danger and
   .link-btn-danger — the same colour worn by two other component families — so this is the third
   member of an established pattern, not a new idea. (The ticket text's "outline variant of
   .btn-danger" described a class that has never existed here.)

   Known and accepted (FA-2): [data-skin="slab"] sets --pri: #111111, so the "navy outline" Reopen
   pill reads BLACK in that skin. Inventing a second token to keep it navy there is precisely the new
   hex D7 forbids, and the pill's meaning — "the safe, reversible one" — survives the change of hue.
   Flagged for the PM's hand pass. */
.btn-ghost-danger {
  background: var(--card, #ffffff);
  color: var(--danger, #b3261e);
  border-color: var(--danger, #b3261e);
}

.btn-ghost-navy {
  background: var(--card, #ffffff);
  color: var(--pri, #2b3379);
  border-color: var(--pri, #2b3379);
}

/* D1's left-push. .modal-foot is already display:flex; flex-wrap:wrap; justify-content:flex-end, so
   one auto margin on the first child is the whole layout change — the pill goes far left and stays
   apart from Save, and at a narrow width the row simply wraps as it already knew how to.

   The precedent is the element this replaces: .modal-microcopy is `margin: 0 auto 0 0`, doing
   exactly this job in exactly this footer. */
.modal-foot-lead {
  margin-right: auto;
}

.btn-block {
  width: 100%;
}

.form-error {
  background: var(--chip-bg, #e8e9f4);
  color: var(--ink, #101c33);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  padding: 10px 14px;
  font-weight: 600;
}

.login-foot {
  margin-top: 18px;
  text-align: center;
  color: var(--faint, #627085);
  font-size: 13px;
}

/* TIMTRK-59: the build stamp — quiet enough to ignore, present enough to read out on a support
   call. One rule for both surfaces; --faint carries it into either skin. Sized and spaced so it
   never becomes the second line of anything above it, at any width. */
.version-stamp {
  margin: 0;
  color: var(--faint, #627085);
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
}

.login-card .version-stamp {
  margin-top: 14px;
  text-align: center;
}

.office-main .version-stamp {
  margin-top: 28px;
  text-align: right;
}

.enroll-qr {
  display: flex;
  justify-content: center;
  margin: 14px 0;
}

.manual-key {
  display: inline-block;
  margin-top: 6px;
  padding: 6px 10px;
  background: var(--pg, #f3f5f9);
  border-radius: var(--r-sm, 14px);
  font-size: 15px;
}

.recovery-codes {
  list-style: none;
  padding: 0;
  margin: 16px 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.recovery-codes code {
  display: block;
  padding: 8px 10px;
  background: var(--pg, #f3f5f9);
  border-radius: var(--r-sm, 14px);
  text-align: center;
}

/* --- Settings --- */

.stack-card {
  margin-top: 14px;
}

.profile-card {
  display: flex;
  align-items: center;
  gap: 14px;
}

.profile-avatar {
  width: 48px;
  height: 48px;
  flex: 0 0 auto;
  border-radius: var(--r-pill, 99px);
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
}

/* TIMTRK-56: the profile card's edit control. Badge/pill shape language borrowed from .chip, but
   sized as a real touch target (44px, the PM's touch-first provision) rather than a label — a chip
   you can tap has to be big enough to tap. Tokens only, so both skins come for free: the slab skin
   swaps --r-pill to 12px and --bw to 2px and this squares off with everything else. */
.badge-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
  padding: 0 16px;
  border-radius: var(--r-pill, 99px);
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  font-weight: 700;
  font-size: 14px;
  text-decoration: none;
}

/* The card is flex; this pins the control to the far edge without a wrapper element. Kept out of
   .badge-btn itself so the shape stays reusable where that alignment would be wrong. */
.profile-card .badge-btn {
  margin-left: auto;
}

/* TIMTRK-58: the destructive twin, coloured from the same --danger pairing .chip-danger uses so
   Delete reads as Delete in both skins without a skin-specific rule. */
.badge-btn-danger {
  background: var(--danger, #b3261e);
  color: var(--danger-fg, #ffffff);
  border-color: var(--danger, #b3261e);
}

.chip {
  display: inline-flex;
  align-items: center;
  padding: 3px 12px;
  border-radius: var(--r-pill, 99px);
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  font-size: 12px;
  font-weight: 700;
}

.theme-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-top: 10px;
}

.theme-card {
  min-height: 64px;
  padding: 12px;
  border-radius: var(--r-sm, 14px);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  font-family: inherit;
}

.theme-card-blue {
  background: #2b3379;
  color: #ffffff;
  border: 1px solid #ffc820;
}

.theme-card-slab {
  background: #ffffff;
  color: #111111;
  border: 2px solid #111111;
}

/* Which skin is live was invisible before — the swatches are the only selection feedback. */
.theme-card[aria-checked="true"] {
  outline: 3px solid var(--acc, #ffc820);
  outline-offset: 2px;
}

.theme-card-name {
  font-weight: 800;
  font-size: 15px;
}

.theme-card-note {
  font-size: 12px;
  opacity: 0.8;
}

.device-list {
  list-style: none;
  padding: 0;
  margin: 8px 0 0;
}

.device-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  border-bottom: 1px solid var(--chip-bg, #e8e9f4);
}

.device-last-seen {
  margin-left: auto;
  color: var(--mut, #5c6b85);
  font-size: 13px;
}

/* --- Office shell (topbar + 200px sidebar per SPEC §Screens) --- */

.office {
  background: var(--pg, #f3f5f9);
}

a.topbar-user {
  min-height: 44px;
  text-decoration: none;
  cursor: pointer;
}

a.topbar-user:hover,
a.topbar-user:focus-visible {
  border-color: var(--pri, #2b3379);
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 22px 28px;
  background: var(--card, #ffffff);
  border-bottom: var(--bw, 1px) solid var(--line, #ffc820);
}

.topbar-title {
  font-family: var(--f-head, 'Bricolage Grotesque', sans-serif);
  font-weight: var(--w-head, 800);
  text-transform: var(--tt, none);
  font-size: 22px;
}

/* The topbar is as tall as its tallest child, and that is the 44px user chip (a.topbar-user's
   min-height), not this logo — so .office-shell's subtraction below tracks the chip, and 42px
   here changes nothing about it. Raising this past 44px is what would move the topbar, and
   that calc has to move with it (TIMTRK-24, TIMTRK-54). */
.topbar-logo {
  display: block;
  height: 42px;
  width: auto;
}

/* 44px chip + 44px topbar padding + 1px border = 89px of topbar, rounded up. */
.office-shell {
  display: flex;
  min-height: calc(100vh - 90px);
}

.sidebar {
  width: 200px;
  flex-shrink: 0;
  padding: 22px 14px;
  background: var(--card, #ffffff);
  border-right: var(--bw, 1px) solid var(--line, #ffc820);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.sidebar-item {
  display: block;
  padding: 10px 16px;
  border-radius: var(--r-pill, 99px);
  color: var(--ink, #101c33);
  text-decoration: none;
  font-weight: 700;
  font-size: 15px;
  border: var(--bw, 1px) solid transparent;
}

.sidebar-item.active {
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  border-color: var(--line, #ffc820);
}

.nav-badge {
  display: inline-flex;
  align-items: center;
  margin-left: 8px;
  padding: 1px 9px;
  border-radius: var(--r-pill, 99px);
  background: var(--danger, #b3261e);
  color: var(--danger-fg, #ffffff);
  font-size: 11px;
  font-weight: 700;
}

.office-main {
  position: relative;
  flex: 1;
  padding: 22px 28px;
  max-width: 1200px;
}

.office-main .watermark {
  position: absolute;
  top: 5px;
  width: 240px;
}

/* --- Tabs, page head, tables (data-table component) --- */

.page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 18px;
  flex-wrap: wrap;
}

.page-head h1 {
  margin: 0;
}

.page-actions {
  display: flex;
  gap: 10px;
}

.tab-row {
  display: flex;
  gap: 8px;
}

.tab {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 18px;
  border-radius: var(--r-pill, 99px);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  background: var(--card, #ffffff);
  color: var(--chip-fg, #2b3379);
  text-decoration: none;
  font-weight: 700;
  font-size: 14px;
}

.tab.active {
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
}

.btn-accent {
  background: var(--acc, #ffc820);
  color: var(--acc-fg, #101c33);
  box-shadow: var(--sh-btn, 0 8px 20px rgba(43, 51, 121, 0.30));
}

.table-labels {
  display: grid;
  gap: 12px;
  padding: 0 18px 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mut, #5c6b85);
}

.table-row {
  display: grid;
  gap: 12px;
  align-items: center;
  /* TIMTRK-307: the ground is a variable so one row kind can retint it and the post-save flash
     (further down) can fade back to whatever this row's ground is. Declared here, not on :root, so a
     flash target that is not a .table-row (a calendar cell) keeps its transparent end-state. */
  --row-bg: var(--card, #ffffff);
  /* TIMTRK-307-1: the post-save pulse colour, beside the ground it fades to. A service row overrides
     it (below) because its ground IS the accent; a flash target that is not a .table-row keeps the
     accent through the fallback in .row-flash. */
  --row-flash: var(--acc, #ffc820);
  background: var(--row-bg);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  padding: 14px 18px;
  margin-bottom: 7px;
  color: var(--ink, #101c33);
  text-decoration: none;
  font-size: 14px;
}

.po-grid {
  grid-template-columns: 70px 1.8fr 1.2fr 0.7fr 1fr 0.9fr;
}

.people-grid {
  grid-template-columns: 1.6fr 0.9fr 1fr 0.8fr 1.4fr;
}

/* TIMTRK-165: the contractors list. Columns only — every colour, border and radius comes from
   .table-row's tokens, so both skins are already handled. Five columns is what fits 1024 without
   the row wrapping, which is why contact phone, email, address and notes live on the edit form. */
.contractors-grid {
  grid-template-columns: 1.8fr 1.2fr 1fr 0.5fr 1.1fr;
}

/* TIMTRK-195: the CSV import's three lists. Columns only, following the rule the contractors list
   above states — every colour, border and radius comes from .table-row's tokens, so both skins are
   free and tokens.css (which is generated) stays untouched. The 70px code column is the width
   .po-grid already uses for a short fixed-width identifier. */
.import-map-grid {
  grid-template-columns: 1.2fr 1.8fr 1fr;
}

.import-merge-grid {
  grid-template-columns: 110px 1fr;
}

/* TIMTRK-300: columns only — colours come from .table-row's own tokens, and tokens.css is
   generated from the design handoff and never hand-edited. */
.import-convention-grid { grid-template-columns: 1.6fr 0.7fr 0.9fr 1fr; }
.import-needs-code-grid { grid-template-columns: 70px 1.4fr 1fr 0.7fr 1fr; }

.import-skip-grid {
  grid-template-columns: 70px 1fr;
}

/* TIMTRK-27: the pending device-approval queue. Columns only — every colour, border and radius
   on the row comes from .table-row's tokens, so both skins are already handled. */
.approvals-queue-grid {
  grid-template-columns: 1.4fr 0.8fr 1.1fr 1.1fr 1.2fr 0.9fr 1.3fr;
}

/* Divider between a card's main action and its alternatives (the enrollment method chooser).
   Token-coloured rather than the browser's default rule, which ignores the skin entirely. */
.card-rule {
  border: 0;
  border-top: var(--bw, 1px) solid var(--line, #ffc820);
  margin: 18px 0 14px;
}

/* TIMTRK-307 / 307-1: service-call rows on the PO board are the full accent — the design's own
   colour for a service call (calendar chips, the + Service call button), used flat, so the All tab
   separates the two row kinds at list scale. The 12% mix this replaced (blue #fff8e4, slab #ffebe0)
   read as almost-white and was rejected. Reading the token rather than a hex is what keeps both
   skins coherent: blue is the client's yellow, slab is that skin's orange, each matching its own
   + Service call button, and tokens.css (generated) stays untouched. Sets only the ground variable,
   so it never out-specifies .row-flash; .row-closed's opacity mutes it on the Closed tab by design.
   --ink on the fill measures 10.95:1 (blue) and 6.04:1 (slab). There is no hover or selected state
   on these rows and this adds none. */
.table-row.row-service {
  --row-bg: var(--acc, #ffc820);
  /* The pulse is the card colour here: accent-on-accent would be no feedback at all, and the post-save
     highlight is the whole of it (locked decision 7 — a highlight, never a toast). */
  --row-flash: var(--card, #ffffff);
}

/* TIMTRK-307-1: muted text on the accent fill. --faint is 1.69:1 (blue) / 1.27:1 (slab) on the
   accent, so inside a service row it becomes the live accent darkened toward black — the idiom the
   restart-flash keyframes use for --danger — which is a warm dark ink in whichever skin is live:
   blue #473809 (7.37:1 on the fill; the design mock's #4a3a00 measures 7.14:1), slab #471900
   (4.77:1). 28% is the largest accent share that keeps slab above the 4.5:1 floor with margin
   (32% fails it); --ink is not the dark end because ink-into-accent reads grey, not warm. Scoped
   to the service row, so every other .faint run in the app is untouched.
   TIMTRK-312: the first colour line below is not redundant. The support floor (docs/OPERATIONS.md,
   "Supported browsers": Safari / iPadOS 16.2, Chrome / Edge 111) is exactly where color-mix()
   shipped, so a browser below it drops the second line at parse time — and then .faint's own rule
   (further down, (0,1,0)) wins over the row's inherited --ink, leaving these runs at 1.69:1 / 1.27:1
   on the fill: the failure 307-1 fixed, back again. With this line in place a sub-floor browser reads
   --ink instead — 10.95:1 (blue) / 6.04:1 (slab), the row's own ink without the secondary hierarchy —
   a chosen degradation, not an accidental one. At or above the floor the second line wins and this
   one is inert. */
.table-row.row-service .faint {
  color: var(--ink, #101c33);
  color: color-mix(in srgb, var(--acc, #ffc820) 28%, #000000);
}

/* TIMTRK-307-1: the status pill on the accent fill. Blue's --chip-bg is lavender and would sit on
   the yellow as a smudge — the mock shows a white pill; slab's --chip-bg is already white, so this
   is a no-op there. --chip-fg and the --line ring are untouched: in blue the ring is the accent and
   disappears into the fill, which is exactly what the mock shows. Also reaches .chip-ghost on a
   closed service call, whose ground is already var(--card) — no visible change.
   TIMTRK-313: scoped to the base pill. Unscoped, this (0,3,0) rule out-specified every single-class
   .chip-* variant (0,1,0), so a future .chip-solid / .chip-accent / .chip-danger on a service row —
   the SPEC's "Approved = solid --pri; In-progress/warning = solid --acc" — would have kept the
   variant's light text on a white ground. Each :not() names one variant whose ground differs from
   var(--card); .chip-ghost is deliberately NOT listed, because its ground IS var(--card) and excluding
   it would be a selector for no behaviour — that value-identical case is what 307-1's rejection of
   :not(.chip-ghost) was about, and it does not reach these three. Specificity is now (0,6,0); the
   one declaration is unchanged, and nothing else targets .chip under a .table-row. */
.table-row.row-service .chip:not(.chip-solid):not(.chip-accent):not(.chip-danger) {
  background: var(--card, #ffffff);
}

.row-closed {
  opacity: 0.55;
}

.cell-code,
.cell-hours,
.cell-strong {
  font-weight: 700;
}

/* TIMTRK-311 — WCAG 1.4.3, 4.5:1 for normal-size text. --faint is the de-emphasis TEXT tier (the
   handoff's "sub 12px --faint" on a row card), and both skins shipped under the floor: 406 Blue
   #93a1b8 measured 2.62:1 on --card and 2.40:1 on --pg; slab #a3a5a7 measured
   2.47:1 on white.

   Derived in OKLCH, not HSL: hold hue and chroma, descend lightness only, so the token gets darker
   without changing colour. The space never ships — the output is the hex in design-tokens.json —
   so there is no browser-support question here and no interaction with the color-mix() support
   floor TIMTRK-312 is about.
     406 Blue  #93a1b8 oklch(70.57% 0.0373 260.36)  ->  #627085 oklch(54.13% 0.0371 258.37)
     Slab      #a3a5a7 oklch(72.09% 0.0037 247.87)  ->  #747577 oklch(56.22% 0.0033 264.53)

   The four governing numbers — two skins x two grounds, and four is the whole set, not four of six:
     406 Blue   on --card #ffffff  5.03:1     on --pg #f3f5f9  4.61:1
     Slab       on --card #ffffff  4.61:1     on --pg #ffffff  4.61:1   (slab's two
     grounds are both white, so its two numbers are one measurement reported twice.)

   TARGET 4.60, ASSERT 4.50 — two numbers doing two jobs, and they must not be collapsed. The value
   above is the LIGHTEST one whose WORST governing ground clears 4.60 (one step lighter,
   #637085, measures 4.595 on --pg and does not qualify). 4.50 is a cliff, where a
   token nudge or another calculator's rounding flips it red; 4.75 over-darkens, and every point of
   margin is paid for in de-emphasis. FaintContrastTests asserts 4.50 — the actual WCAG floor, not
   4.60 — because a non-standard threshold in a test is one the next person correctly "fixes" down
   to 4.5, losing a margin they never knew existed. The 4.60 targeting rule lives here instead.

   --chip-bg IS DELIBERATELY NOT A GOVERNING GROUND. Do not add it back. .faint does render on it —
   a bare .tray-chip, a person not on the week, in Pages/Office/Approvals/Index.cshtml — but on
   #e8e9f4 BOTH de-emphasis tokens are sub-floor: --faint 4.17:1 and --mut 4.46:1.
   When two independent tokens fail one background, the background is the defect, and driving
   --faint past --mut to paper over it would invert the two tiers. Filed as its own finding instead.

   --faint stays LIGHTER than --mut in both skins — 5.03 under 5.39 in blue, 4.61
   under 5.21 in slab — which is what governing on two grounds rather than six bought. The tier
   order is pinned by FaintContrastTests, so a future value change cannot quietly invert it. */
.faint {
  color: var(--faint, #627085);
  font-weight: 500;
}

.chip-solid {
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
}

.chip-accent {
  background: var(--acc, #ffc820);
  color: var(--acc-fg, #101c33);
}

.chip-ghost {
  background: var(--card, #ffffff);
  color: var(--faint, #627085);
}

/* TIMTRK-393: a pre-logged figure — hours entered ahead of the day (the word is defined in
   MinuteDuration's type doc). Dashed --pri outline on the card ground: distinguished by fill and
   outline, never hue alone (v0.5 SPEC's pill rule), so it reads in both skins.
   Applied beside the other chip variants. The grid's day box has its own marker, .day-chip-prelog
   (TIMTRK-417): a dash drawn over the box's state fill rather than this card-ground treatment. */
.chip-prelog {
  background: var(--card, #ffffff);
  color: var(--pri, #2b3379);
  border: var(--bw, 1px) dashed var(--pri, #2b3379);
}

.chip-danger {
  background: var(--danger, #b3261e);
  color: var(--danger-fg, #ffffff);
  border-color: var(--danger, #b3261e);
}

/* TIMTRK-417 (§4): a finished week's status chip — "Locked". Chip geometry inherited whole (it is
   the size of the status chips, not of the action pills); the ring is the danger token, the same red
   as the Close PO pill (.btn-ghost-danger), marking a final, past state. The fill is the Finish
   pill's pale grey, derived — the 307-1/312 shape: the first background line is the sub-floor
   fallback (a white chip keeps its ring and its navy text), the color-mix line wins at or above the
   support floor. Navy text is --pri, which equals --chip-fg in both skins. */
.chip-locked {
  background: var(--card, #ffffff);
  background: color-mix(in srgb, var(--mut, #5c6b85) 15%, var(--card, #ffffff));
  color: var(--pri, #2b3379);
  border-color: var(--danger, #b3261e);
}

.budget-bar {
  display: inline-block;
  width: 90px;
  height: 10px;
  vertical-align: middle;
  background: var(--chip-bg, #e8e9f4);
  border: 1px solid var(--line, #ffc820);
  border-radius: var(--r-pill, 99px);
  overflow: hidden;
}

.budget-fill {
  display: block;
  height: 100%;
  background: var(--pri, #2b3379);
}

.budget-bar.over .budget-fill {
  background: var(--acc, #ffc820);
}

.budget-label {
  margin-left: 6px;
}

.table-foot {
  margin-top: 12px;
  font-size: 13px;
}

/* --- Forms & inline actions --- */

.form-card {
  max-width: 560px;
}

.field-label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-weight: 600;
  font-size: 13px;
  color: var(--mut, #5c6b85);
}

.input-area {
  min-height: 90px;
  padding: 12px 16px;
  resize: vertical;
}

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

.form-notice {
  background: var(--chip-bg, #e8e9f4);
  border: var(--bw, 1px) solid var(--pri, #2b3379);
  border-radius: var(--r-sm, 14px);
  padding: 10px 14px;
  font-weight: 600;
}

.actions-cell {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
}

.actions-cell form {
  display: inline;
}

.link-btn {
  background: none;
  border: none;
  padding: 0;
  min-height: 44px;
  color: var(--pri, #2b3379);
  font-weight: 700;
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
}

/* The `display` above is an author rule, so it beats the browser's own
   `[hidden] { display: none }` no matter how the selectors compare — meaning a link-btn marked
   hidden would still render. TIMTRK-51 needs this pinned: its collapse-all control ships hidden
   and is unhidden by script, because with scripting off it cannot collapse anything and a control
   that does nothing is worse than none. Same trap, same fix as .restart-banner[hidden] below. */
.link-btn[hidden] {
  display: none;
}

.device-panel {
  margin: -2px 0 10px;
  padding: 12px 18px;
}

.link-btn-danger {
  color: var(--mut, #5c6b85);
}

/* --- Worker shell (phone-first, bottom nav) --- */

.worker-main {
  max-width: 480px;
  padding-bottom: 96px;
}

.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-around;
  gap: 6px;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  background: var(--card, #ffffff);
  border-top: var(--bw, 1px) solid var(--line, #ffc820);
}

.bottom-nav-item {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 16px;
  border-radius: var(--r-pill, 99px);
  color: var(--mut, #5c6b85);
  font-weight: 700;
  font-size: 14px;
  text-decoration: none;
}

.bottom-nav-item.active {
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  border: var(--bw, 1px) solid var(--line, #ffc820);
}

/* Today */

.today-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.today-head h1 {
  font-size: 32px;
  margin: 0;
}

.week-total-chip {
  font-size: 14px;
  padding: 8px 16px;
}

.week-strip {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
  margin: 14px 0;
}

.week-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 8px 2px;
  background: var(--card, #ffffff);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  font-size: 12px;
}

.week-cell-letter {
  color: var(--faint, #627085);
  font-weight: 700;
}

.week-cell-value {
  font-weight: 700;
}

.week-cell.on {
  background: var(--cell-on, #2b3379);
  color: var(--cell-on-fg, #ffffff);
}

.week-cell.on .week-cell-letter {
  color: inherit;
}

.entry-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
  padding: 16px 18px;
  border-radius: var(--r-sm, 14px);
}

.hour-chip {
  font-size: 15px;
  padding: 6px 14px;
}

.entry-breakdown summary {
  cursor: pointer;
  color: var(--pri, #2b3379);
  font-size: 13px;
  font-weight: 700;
  min-height: 44px;
  display: flex;
  align-items: center;
}

.entry-breakdown ul {
  margin: 0;
  padding-left: 18px;
  color: var(--mut, #5c6b85);
  font-size: 13px;
}

.cta-add {
  margin-top: 16px;
  min-height: 56px;
  font-family: var(--f-head, 'Bricolage Grotesque', sans-serif);
  font-size: 17px;
}

.timer-card h2 {
  margin: 6px 0 0;
}

/* Add hours */

.add-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
}

.add-head h1 {
  font-size: 22px;
}

.back-link {
  color: var(--mut, #5c6b85);
  text-decoration: none;
  font-weight: 700;
}

.search-box {
  width: 100%;
  margin-bottom: 6px;
}

.group-label {
  margin: 14px 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mut, #5c6b85);
}

.picker-row {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--card, #ffffff);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  padding: 12px 16px;
  margin-bottom: 8px;
  cursor: pointer;
}

.picker-row input {
  position: absolute;
  opacity: 0;
}

.picker-body {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.picker-check {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: none;
  align-items: center;
  justify-content: center;
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
  font-weight: 700;
}

.picker-row:has(input:checked) {
  border: 2px solid var(--pri, #2b3379);
}

.picker-row:has(input:checked) .picker-check {
  display: inline-flex;
}

/* TIMTRK-293 (/add follow-up): each of Scheduled today / Jobs / Service calls clips to five rows
   behind a native <details> — the Today card's pattern (.today-more, further down) re-cut on its own
   class names, because .picker-row is shared with the Office GC picker (gc-picker.ts builds its rows
   on it) and must not change. Every rule below is a new class or is scoped under .po-picker, which
   exists only on /add. "Your assignment" never clips (D1).

   The wrapper carries three states as attributes the add-entry island owns — none (clipped list),
   data-po-searching (every expander forced open, summary hidden, the row filter decides — D3) and
   data-po-collapsed (only the chosen row, label singular, "Change ▾" moved onto the card — D2, TIMTRK-298). With scripting off
   no attribute is ever set, so the button never appears and the list is the clipped one (A1).

   The expander row is a real tap target (44px floor) with the dashed outline the design comment
   names; the native disclosure marker is hidden because the label carries its own caret, as on
   Today. No motion is added, so prefers-reduced-motion has nothing to gate; every colour is a token,
   so both skins apply. :has() is already load-bearing three rules up (.picker-row:has(input:checked)),
   so the collapsed rules lean on it too rather than on a class the island would have to place. */
.picker-more {
  display: block;
}

.picker-more-summary {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin-bottom: 8px;
  border: var(--bw, 1px) dashed var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  cursor: pointer;
  list-style: none;
  color: var(--pri, #2b3379);
  font-size: 15px;
  font-weight: 700;
}

.picker-more-summary::-webkit-details-marker {
  display: none;
}

.picker-more-summary:focus-visible {
  outline: 3px solid var(--acc, #ffc820);
  outline-offset: 2px;
}

/* One label element per state; the [open] attribute picks which shows, so the flip needs no script
   and a scripting-off browser still shows the right words. */
.picker-more-open {
  display: none;
}

.picker-more[open] > .picker-more-summary .picker-more-open {
  display: inline;
}

.picker-more[open] > .picker-more-summary .picker-more-closed {
  display: none;
}

/* Searching: the island has opened every expander; hide the row that would now say "Show fewer". */
.po-picker[data-po-searching] .picker-more-summary {
  display: none;
}

/* The singular label exists only for the collapsed state. */
.group-label-one {
  display: none;
}

/* TIMTRK-298: "Change ▾" is a text-style control that lives INSIDE the chosen card in the collapsed
   state — right-hand edge, vertically centred, in the slot the ✓ circle gives up (D2, D3) — not a
   full-width ghost pill under the card. The server renders the one button as the wrapper's last
   child; the add-entry island moves it into the checked row on collapse and back on "Change ▾".
   It is displayed ONLY inside that row (rule further down), so a page whose script never ran (A1)
   or a button the island failed to move can never show it. Navy, bold, no border, no fill, as
   drawn. The tap box is the 44px floor by min-height and the word's own width (wider than 44px),
   so it needs no padding and the text ends exactly at the card's 16px inset. .picker-row is
   align-items: center, so a wrapped two-line name centres the control against the block (D4).
   font: inherit rather than a size of its own — it matches the job name it sits beside (14px/700).
   In the slab skin --pri is #111111, so it reads black there: FA-2's accepted precedent. */
.picker-change {
  display: none;
  flex: none;
  align-items: center;
  min-height: 44px;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  font-weight: 700;
  color: var(--pri, #2b3379);
  cursor: pointer;
}

.picker-change:focus-visible {
  outline: 3px solid var(--acc, #ffc820);
  outline-offset: 2px;
}

.po-picker[data-po-collapsed] .search-box,
.po-picker[data-po-collapsed] .picker-group:not(:has(input:checked)),
.po-picker[data-po-collapsed] .picker-group:has(input:checked) .picker-row:not(:has(input:checked)),
.po-picker[data-po-collapsed] .picker-more-summary,
.po-picker[data-po-collapsed] .group-label-many {
  display: none;
}

.po-picker[data-po-collapsed] .group-label-one {
  display: inline;
}

/* Specificity is load-bearing: `.picker-row:has(input:checked) .picker-check` above is (0,3,1), so
   a plain `.po-picker[data-po-collapsed] .picker-check` (0,3,0) would LOSE and the ✓ would stay.
   Both rules therefore carry the checked row — which is also the only row this state renders. */
.po-picker[data-po-collapsed] .picker-row:has(input:checked) .picker-check {
  display: none;
}

.po-picker[data-po-collapsed] .picker-row:has(input:checked) .picker-change {
  display: inline-flex;
}

.stepper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 18px 0 10px;
}

.stepper-btn {
  width: 56px;
  height: 56px;
  font-size: 24px;
  font-weight: 700;
  background: var(--card, #ffffff);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  cursor: pointer;
  color: var(--ink, #101c33);
}

.stepper-center {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1;
}

.stepper-value {
  width: 130px;
  text-align: center;
  font-family: var(--f-head, 'Bricolage Grotesque', sans-serif);
  font-size: 40px;
  font-weight: var(--w-head, 800);
  color: var(--ink, #101c33);
  background: none;
  border: none;
  border-bottom: 2px dotted var(--faint, #627085);
}

.stepper-hint {
  font-size: 12px;
  color: var(--mut, #5c6b85);
  margin-top: 4px;
}

/* TIMTRK-319: the value box is content-driven, with a floor at today's 130px and a ceiling at the room
   the flex row actually has, so a thousands-separated h:mm renders whole instead of scrolling inside a
   fixed 130px box.

   `field-sizing: content` is the only mechanism that content-sizes an <input> in CSS alone. The two the
   ticket first proposed do not work and were measured before being dropped: `width: auto` on an input is
   the `size` attribute's intrinsic width — 20 characters, ~536px at this font size — not the content's,
   and a `ch` constant is fixed, so either one would move `8:00` off its current 130px and break the very
   thing this change protects.

   Guarded by @supports on purpose: the rule above is left byte-identical, so a browser without
   field-sizing keeps today's behaviour exactly. The fallback is the status quo, never a wider box.

   `.stepper-center` needs `min-width: 0` in the same breath. As a flex item its automatic minimum size is
   its min-content width, and with a content-sized input that is the WHOLE typed string — 227.80px for
   "999,999:59" against the 188px the row has at 320. That, not the input's own width, is what would push
   ＋ off a phone screen. With the column free to shrink to its share, `max-width: 100%` caps the box at
   the room that exists and the buttons never move: `.stepper-center` is `flex: 1`, so it claims the same
   space whatever it holds.

   Both skins are covered by construction rather than by a tuned constant — the slab skin swaps --f-head to
   Archivo Black, which is ~8-15% wider, and a px or ch value tuned on Bricolage would clip there. */
@supports (field-sizing: content) {
  .stepper-center {
    min-width: 0;
  }

  .stepper-value {
    field-sizing: content;
    width: auto;
    min-width: 130px;
    max-width: 100%;
  }
}

.note-box {
  width: 100%;
  margin: 8px 0 14px;
}

.cta-save {
  min-height: 56px;
  font-family: var(--f-head, 'Bricolage Grotesque', sans-serif);
  font-size: 17px;
  margin-bottom: 8px;
}

.cta-save:disabled,
#timer-cta:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Week */

.week-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.week-head h1 {
  font-size: 24px;
}

.week-pager a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: var(--ink, #101c33);
  text-decoration: none;
  font-size: 20px;
}

/* The labelled variant borrows the .tab pill so both skins resolve for free. The gap is what
   keeps two opposite actions reading as two controls — on slab --r-pill drops to 12px and
   abutting pills would fuse back into one bar. Must follow .week-pager a: equal specificity. */
.week-pager-labeled {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.week-pager-labeled a {
  width: auto;
  height: auto;
  min-width: 44px;
  min-height: 44px;
  padding: 0 18px;
  border-radius: var(--r-pill, 99px);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  background: var(--card, #ffffff);
  color: var(--chip-fg, #2b3379);
  font-size: 14px;
  font-weight: 700;
}

.week-banner {
  background: var(--acc, #ffc820);
  color: var(--acc-fg, #101c33);
  border-radius: var(--r-sm, 14px);
  padding: 14px 18px;
  font-weight: 700;
  margin-bottom: 12px;
}

.day-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 14px 18px;
  margin-bottom: 8px;
  border-radius: var(--r-sm, 14px);
}

.day-row.today {
  border: 2px solid var(--pri, #2b3379);
}

.day-row.empty {
  opacity: 0.6;
}

.day-total {
  font-weight: 800;
  font-size: 17px;
}

.week-total-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 18px;
  font-size: 15px;
}

.week-total-row strong {
  font-size: 24px;
  font-family: var(--f-head, 'Bricolage Grotesque', sans-serif);
}

/* --- Calendar board (o-calendar) --- */

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 10px;
}

.calendar-cell {
  min-height: 104px;
  background: var(--card, #ffffff);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.calendar-cell.today {
  border: 2px solid var(--pri, #2b3379);
}

.calendar-cell.weekend {
  opacity: 0.6;
}

/* TIMTRK-166: a day the company does not work. Layered OVER .weekend rather than replacing it —
   the two overlap but are not the same set (under a Mon-Thu week Friday is an off day and not the
   weekend, and a company that works Saturdays has a weekend day that is not an off day), and a
   cell that is both should read as more muted than either alone. The hatch is what distinguishes
   "nobody works this day" from "this cell is merely de-emphasised": opacity alone was already
   spoken for. Off days stay fully interactive — explicit bookings on them must remain visible and
   editable. */
.calendar-cell.non-work {
  opacity: 0.55;
  background-image: repeating-linear-gradient(
    135deg,
    transparent,
    transparent 7px,
    var(--line, #ffc820) 7px,
    var(--line, #ffc820) 8px
  );
}

.calendar-cell.non-work.weekend {
  opacity: 0.45;
}

.calendar-cell-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--mut, #5c6b85);
}

.calendar-chip-form {
  display: block;
}

.calendar-chip {
  display: block;
  width: 100%;
  text-align: left;
  padding: 6px 10px;
  border-radius: var(--r-pill, 99px);
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  font-size: 12px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
}

.calendar-chip.service {
  background: var(--acc, #ffc820);
  color: var(--acc-fg, #101c33);
}

.legend-swatch {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  margin: 0 4px 0 10px;
  vertical-align: middle;
}

.legend-job {
  background: var(--chip-bg, #e8e9f4);
  border: 1px solid var(--line, #ffc820);
}

.legend-service {
  background: var(--acc, #ffc820);
}

/* --- Today's job card + schedule (w-today / w-schedule) --- */

.today-job-card .today-job {
  margin-top: 10px;
}

.today-job-card h2 {
  margin: 4px 0 2px;
  font-size: 20px;
}

.today-job .btn {
  margin-top: 10px;
  min-height: 56px;
}

/* TIMTRK-293: the Today card clips to five jobs; the sixth onward sit inside a native <details>
   whose summary is the "Show all N jobs ▾" / "Show fewer ▴" row. The disclosure marker is hidden
   here because the label carries its own caret — the ticket's wording — and two triangles on one
   row read as two controls. (.entry-breakdown and .permission-station-summary keep the native
   marker because their labels carry none; same reasoning, opposite outcome.) No motion is added,
   so prefers-reduced-motion has nothing to gate. Every colour is a token, so both skins apply. */
.today-more {
  display: block;
}

.today-more-summary {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin-top: 10px;
  cursor: pointer;
  list-style: none;
  color: var(--pri, #2b3379);
  font-size: 15px;
  font-weight: 700;
}

.today-more-summary::-webkit-details-marker {
  display: none;
}

.today-more-summary:focus-visible {
  outline: 3px solid var(--acc, #ffc820);
  outline-offset: 2px;
}

/* One label element per state; the [open] attribute picks which is displayed, so the flip needs
   no script and a scripting-off browser still shows the right words. */
.today-more-open {
  display: none;
}

.today-more[open] > .today-more-summary .today-more-open {
  display: inline;
}

.today-more[open] > .today-more-summary .today-more-closed {
  display: none;
}

/* The empty state replaces the whole card. Same ink as a card-sub, sitting where the card would. */
.today-empty {
  margin: 14px 0 0;
  color: var(--mut, #5c6b85);
}

.schedule-row.today-card {
  border: 2px solid var(--acc, #ffc820);
}

/* --- Approvals grid (o-approvals) --- */

/* TIMTRK-58: the day columns each carry a 44px control now, so every track gets a floor and the
   row's own gap and padding tighten to pay for it. The arithmetic, at the PM's 1024 target: the
   office main is 1024 − 200 sidebar − 1 border − 56 padding = 767px; the row keeps 767 − 2 border
   − 24 padding = 741px, less 11 × 8px gaps = 653px across 8.30fr, so 1fr = 78.7px and a 0.55fr day
   column (43.3px) sits ON its 44px floor — as do the name (110), total (52) and status (76) tracks —
   leaving the actions track the remainder, ≈107px. The floors bind at 1024 by design; narrower than
   that, .approvals-scroll scrolls rather than squishing.

   TIMTRK-417 (§3c): the actions share rose 1.1fr → 1.35fr (its 84px floor unchanged) so the compact
   Approve and Return pills share one line where the content is at its 1200px cap — at ≥1400px the
   office main is 1200, the row 1118, the tracks 1030px over 8.30fr, 1fr = 124px and the actions
   track ≈167px against the ≈150px two pills need. At 1024 with the sidebar the same rule gives
   ≈107px and the pills stack, Approve above Return. Both widths are measured in
   ApprovalsStateColourTests, not argued; the day columns were not re-weighted. */
.approvals-grid {
  grid-template-columns:
    minmax(110px, 1.4fr) repeat(7, minmax(44px, 0.55fr))
    minmax(52px, 0.7fr) minmax(76px, 1fr) minmax(84px, 1.35fr);
  gap: 8px;
}

.table-row.approvals-grid,
.table-labels.approvals-grid {
  padding-left: 12px;
  padding-right: 12px;
}

.approvals-scroll {
  overflow-x: auto;
}

.cell-center {
  text-align: center;
}

/* TIMTRK-200: the cell holds ONE control. TIMTRK-192 put two 44px targets in here — the
   approve/unapprove toggle and a separate editor button — and the arithmetic above says a day
   column is 44.6px at the office's 1024 target, so the pair overflowed into its neighbour on
   every logged day. Per-day approval retired (PM 2026-08-22) and the cell went back to the v0.5
   handoff's single control, so there is no gap to set and nothing to lay out beside anything.

   The wrapper stays because it is still the grid cell and still the centering container, and its
   min-height is what states the design's "≥44px row cell" independently of what sits in it. */
.day-cell-wrap {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  min-height: 44px;
}

/* The v0.5 cell, in the handoff's own words (v0.5 SPEC line 20): "Logged: solid --pri chip,
   radius 12px, min 46x40, and the chip IS the edit target. Empty weekday: 40px ghost + circle,
   --line outline."

   Two literals rather than tokens, both blessed by the spec itself under "New surfaces reuse
   existing tokens only … ghost `+` fill #eef0f6. No token additions.": the #eef0f6 fill and the
   12px radius (no 12px radius token exists in the Blue skin — --r-sm is 14 and --r-pill is 99).
   Everything else is tokens, so both skins come for free.

   The sizes are 44px where the spec says 40, and that is the one place this deviates from its
   own quote. The spec's numbers are a FLOOR — "min 46x40" — and the house rule puts the touch
   floor at 44px, so 46x44 and a 44px circle satisfy both while a 40px box would satisfy only the
   spec. Width is where the real constraint bites and it is honoured: the chip keeps the spec's
   46px min-width, 1.4px wider than the 44.6px column by design, which the grid's 8px gap absorbs
   — the bounding-box assertions in ApprovalsRosterTests measure exactly that, against the column
   AND against the neighbouring cell.

   TIMTRK-417 (§2a/§2b): the fill and text colour are the DAY'S OWN approval state now — this base
   is the approved look (navy/white); .day-chip-added and .day-chip-waiting below override it, and
   .day-chip-prelog draws the pre-logged dash over whichever fill is live. Geometry, radius, padding
   and the 44px target are unchanged. */
.day-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 46px;
  min-height: 44px;
  padding: 0 6px;
  border-radius: 12px;
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
  font-weight: 700;
  text-decoration: none;
}

/* TIMTRK-417 (§2b) — the state classes. One per logged box; the base above IS the approved look.
   "added": accent fill, accent foreground; "waiting": chip background/foreground — lavender/navy in
   Blue, white/black in Slab (the Slab edge is restored below). Tokens only, no new hex, so both
   skins come for free. */
.day-chip-added {
  background: var(--acc, #ffc820);
  color: var(--acc-fg, #101c33);
}

.day-chip-waiting {
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
}

/* TIMTRK-393 → TIMTRK-417: the pre-logged marker is a 2px dash drawn ON TOP of the state fill, in
   the box's text colour — dark on yellow (B2), navy on lavender (B4) — with one exception: on navy
   it is the accent (B6/B8/B10), because navy-on-navy is no marker. The dash is the only difference
   between a pre-logged box and its plain twin; the state colour is kept (this replaces 393's white
   card-ground treatment, which lost the state). A real border, exactly as 393 drew it, inside the
   border-box minimums — so a pre-logged box keeps 393's geometry and a plain box keeps 200's; a
   transparent border on every box would widen a content-driven "24:00" box by 4px on the very axis
   the overflow guard measures. */
.day-chip-prelog {
  border: 2px dashed currentColor;
}

.day-chip-approved.day-chip-prelog {
  border-color: var(--acc, #ffc820);
}

/* TIMTRK-417 (§5, ruled): Slab's --chip-bg is white, so a waiting box would have no edge on the
   white row. Slab alone gives it a 2px outline in the skin's primary (black) — dashed when
   pre-logged, where --pri equals Slab's --chip-fg, so B4's "text colour" dash holds there too. The
   first [data-skin] selector in this file: no token can say "only where the chip ground is white". */
[data-skin="slab"] .day-chip-waiting {
  border: 2px solid var(--pri, #111111);
}

[data-skin="slab"] .day-chip-waiting.day-chip-prelog {
  border-style: dashed;
}

/* TIMTRK-393: the worker's week strip — a day holding pre-logged hours underlines its value. */
.week-cell-prelog .week-cell-value {
  border-bottom: 2px dashed var(--pri, #2b3379);
}

/* Today's cell sits on --cell-on, which the Blue skin sets to the same value as --pri, so the
   underline there takes the cell's own foreground (--cell-on-fg) or it would vanish into the
   ground — the very case the flag outlives its date for (R6). */
.week-cell.on.week-cell-prelog .week-cell-value {
  border-bottom-color: currentColor;
}

.day-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: #eef0f6;
  border: var(--bw, 1px) solid var(--line, #ffc820);
  /* --mut, NOT --faint. Round-2 review measured the first attempt at ~2.2:1 in both skins (--faint
     was #93a1b8 / #a3a5a7 then) on the #eef0f6 fill, under WCAG 2.2 SC 1.4.11's 3:1 floor for a
     graphical control — and the `＋` is the entire visible content of the only add affordance on
     the office's iPad. The control this replaces drew --chip-fg on --chip-bg at roughly 10:1, so
     shipping --faint on the GLYPH would have been a real drop rather than a neutral restyle. That
     finding is settled: the reduced-emphasis signal is carried by the BORDER, which is where D17
     puts it, and nothing about the non-workday treatment depends on the glyph being hard to see.

     TIMTRK-205 re-measured the BORDERS against the token TIMTRK-311 shipped (--faint is now
     #627085 blue / #747577 slab). A border sits between two grounds, so both were measured — the
     #eef0f6 fill inside the circle and the roster row card behind it, which is --card (#ffffff in
     both skins; .table-row sets --row-bg from it and the approvals grid never overrides it):

       blue  --faint on the fill 4.41:1   on the row card 5.03:1
       slab  --faint on the fill 4.05:1   on the row card 4.61:1
       blue  --line  on the fill 1.36:1   on the row card 1.55:1   <-- sub-floor, see below
       slab  --line  on the fill 16.57:1  on the row card 18.88:1

     and the two borders against EACH OTHER, which is a house question rather than a WCAG one:
     blue 3.24:1, slab 4.09:1.

     So the RECESSIVE border — the one this cell was filed over — clears 3:1 on both grounds in both
     skins, and the pair separates above 3:1 too. No token moves for it.

     The one sub-floor reading is --line on the BLUE skin: 1.36:1 on the fill. That is the WORKDAY
     border, the common case, and it is a real 1.4.11 failure on a shipped surface. It is filed as
     TIMTRK-374 and deliberately NOT fixed here, because --line is a global accent — 56 rules in
     this file draw it, including every row card's border (.table-row) and the submitted banner
     (.banner-submitted) — and moving it needs its own ticket. Do not "fix" it from this rule.
     Note for whoever takes TIMTRK-374: --acc is a DIFFERENT token that shares --line's #ffc820
     fallback in blue but not in slab (--acc #ff5a00 vs --line #111111). Every :focus-visible
     outline and .day-row.locked draw --acc and --pri respectively, NOT --line, so they are not in
     this token's blast radius however similar they look in the 406 Blue skin.

     The numbers above are the measurement. The ASSERTION lives in ApprovalsRosterTests, over both
     skins, reading the colours back out of the browser; only the --faint readings are asserted, so
     that TIMTRK-374 stays a filed finding rather than a red build. */
  color: var(--mut, #5c6b85);
  font-weight: 700;
  font-size: 15px;
  line-height: 1;
  text-decoration: none;
}

/* The NON-WORKDAY ghost, at reduced emphasis: same geometry, same behaviour, no accent border. A
   PM-approved DEPARTURE from the approved design, whose words are "Empty weekend: faint dash"
   (PM + Design, 2026-08-23). The dash was literal — a span, not a control — which left the FIRST
   such entry unreachable from this grid, while the model has always accepted hours on any day.
   Do not "fix" this back to the spec text; that edit is Design's to make.

   TIMTRK-204/D18 (Design, 2026-08-23) renamed the idea, not the styling: "weekend" in that rule
   means NON-WORKDAY PER COMPANY SETTINGS, so the class is applied from the company work week the
   office calendar already reads (`day-plus-wknd` before this ticket — a company on a Mon–Thu week
   was shipping Friday cells classed "weekend", which is exactly the two-ideas-merging-back-into-one
   that WorkWeekSingleImplementationTests exists to stop). One declaration, unchanged.

   The reduced-emphasis signal is the BORDER and only the border — see .day-plus above for the
   round-2 WCAG 1.4.11 measurement that put it there. Do not move it onto the glyph. */
.day-plus-nonwork {
  border-color: var(--faint, #627085);
}

/* The retired toggle carried the approvals block's only focus treatment, and this file has no
   global :focus-visible rule to fall back on — every focusable surface states its own. Both new
   controls are real links, so both state it here, in the same shape the toggle used. */
.day-chip:focus-visible,
.day-plus:focus-visible {
  outline: 3px solid var(--acc, #ffc820);
  outline-offset: 2px;
}

/* TIMTRK-417 (§3): the approvals grid's action pills. The <button class="link-btn"> is untouched
   and stays the 44px tap target (no fill, no border — the shared .link-btn is byte-unchanged, A3);
   the label sits in this face, which is what the eye sees: hugging the 14px bold text (A2: today's
   size, the ticket's "13px" was superseded) with 6px/9px padding and the skin's pill radius, about
   30px tall. Scoped to this grid by name — People, Contractors, Device approvals, Settings and the
   edit-person modal share .link-btn and keep their plain-text buttons.

   The base face is the primary pill (Submit, Approve): navy fill, white text, navy ring. */
.approvals-pill {
  display: inline-flex;
  align-items: center;
  padding: 6px 9px;
  border-radius: var(--r-pill, 99px);
  border: var(--bw, 1px) solid var(--pri, #2b3379);
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
}

/* Finish: pale grey — the muted token mixed 15% into the card — navy text, accent ring. Two
   background lines on purpose (TIMTRK-307-1/312): a browser below the support floor drops the
   color-mix line at parse time and keeps a white pill with its ring and text, a chosen degradation.
   The accent ring is decorative — the pill is identified by its text (WCAG 1.4.11 does not require
   a boundary to contrast when the text does); the state marks that must contrast are the box dashes
   and the Locked ring, and they do. */
.approvals-pill-finish {
  background: var(--card, #ffffff);
  background: color-mix(in srgb, var(--mut, #5c6b85) 15%, var(--card, #ffffff));
  color: var(--pri, #2b3379);
  border-color: var(--acc, #ffc820);
}

/* Return and Un-finish: pale accent — the accent mixed 30% into the card — dark text (--acc-fg,
   which equals --ink in both skins), accent ring. Same fallback shape as Finish. */
.approvals-pill-return {
  background: var(--card, #ffffff);
  background: color-mix(in srgb, var(--acc, #ffc820) 30%, var(--card, #ffffff));
  color: var(--acc-fg, #101c33);
  border-color: var(--acc, #ffc820);
}

/* §3a: 6px between pills — the flex gap between the 44px targets, so two pills on one line are
   6px apart and two stacked ones (1024 with the sidebar) sit 6 + 14 = 20px apart face to face;
   pulling the faces closer would overlap the tap targets. The shared .actions-cell keeps its 12px
   for every other screen. */
.approvals-grid .actions-cell {
  gap: 6px;
}

.chip-mini {
  font-size: 10px;
  padding: 1px 8px;
  margin-left: 6px;
}

.banner-submitted {
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  border: var(--bw, 1px) solid var(--line, #ffc820);
}

.banner-approved {
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
}

.day-row.locked {
  border-color: var(--pri, #2b3379);
}

/* --- Reports (o-reports) --- */

.filter-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.filter-select {
  height: 48px;
  min-width: 150px;
}

.summary-cards {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  margin-bottom: 20px;
}

.summary-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 18px 22px;
}

.summary-card strong {
  font-family: var(--f-head, 'Bricolage Grotesque', sans-serif);
  font-weight: var(--w-head, 800);
  font-size: 30px;
}

/* TIMTRK-180: a leading 28px column for the disclosure marker. Every row type carries it —
   summaries draw a marker in it, plain rows and day rows leave it empty, and the header labels
   leave it empty too — which is what keeps four different row shapes on one set of column edges.
   The PO column is now blank on a total row and filled only inside a breakdown. */
.report-grid {
  grid-template-columns: 28px 1.3fr 1.1fr 1.8fr 0.7fr;
}

.share-bar {
  width: 100%;
  max-width: 260px;
  height: 12px;
}

/* --- TIMTRK-180: the worker drill-down --- */

/* The <details> is a pure grouping box; the summary inside it is the row, so this must add no
   chrome of its own or every worker would sit in a second, larger card. */
.worker-drill {
  display: block;
}

/* DELIBERATE DIVERGENCE, announced to the stage-5 reviewer: the app's two other disclosures
   (.permission-station-summary, .entry-breakdown summary) keep the native ::marker triangle, and
   site.css even says so — "the disclosure triangle is the only affordance saying a branch can
   close, so it stays". This one cannot keep it. A <summary> has to be `display: grid` to lay out
   as a report row, and that suppresses the marker box in Chromium and WebKit; the alternative,
   `list-style-position: outside`, renders the triangle beyond the card's own border where it is
   clipped. So the marker becomes a real grid cell, which also buys the alignment the other
   approach loses. The affordance and the 44px target are unchanged — only who draws the triangle. */
.worker-summary {
  cursor: pointer;
  min-height: 44px;
  list-style: none;
}

.worker-summary::-webkit-details-marker {
  display: none;
}

.worker-summary:focus-visible {
  outline: 3px solid var(--acc, #ffc820);
  outline-offset: 2px;
}

/* Drawn with borders rather than a glyph so it inherits the row's ink in both skins and needs no
   font to be present. Rotation is a transform on the pseudo-element, so the 28px cell never
   reflows as it turns. */
.drill-marker::before {
  content: "";
  display: block;
  width: 0;
  height: 0;
  border-left: 7px solid currentColor;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  transform-origin: 3px 50%;
}

.worker-drill[open] .drill-marker::before {
  transform: rotate(90deg);
}

/* Motion is opt-IN, matching .row-flash (site.css) — the rotated state above is unconditional, so
   someone who asked their system for less motion still sees the marker turn; they just do not see
   it travel. Putting the transform inside the query instead would leave them no open/closed
   affordance at all, which is the bug that block's comment records. */
@media (prefers-reduced-motion: no-preference) {
  .drill-marker::before {
    transition: transform 120ms ease-out;
  }
}

/* Subordinate to the summary above it: inset, quieter ground, a dashed edge to read as a
   continuation rather than a peer.

   NAMED `.drill-day-row`, not `.day-row`: this file already has a `.day-row` (the worker Week
   screen's rows, ~line 1048, with .today/.empty/.locked modifiers). An unscoped `.day-row` here
   would sit LATER in the file at equal specificity and silently restyle that screen — caught by
   grepping the class before trusting it, not by any test, since no assertion reads margins. */
.drill-day-row {
  margin-left: 28px;
  background: var(--pg, #f3f5f9);
  border-style: dashed;
  font-size: 13px;
}

/* --- Audit screens --- */

.audit-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.audit-head h1 {
  margin: 0;
}

.audit-grid {
  grid-template-columns: 0.8fr 0.9fr 3fr;
}

.audit-chip {
  font-size: 12px;
  padding: 2px 10px;
}

.tab-disabled {
  opacity: 0.55;
  cursor: default;
}

.pager-row {
  display: flex;
  gap: 16px;
  align-items: center;
  margin-top: 10px;
}

/* TIMTRK-18: per-user permission panel, sharing the device-panel geometry. */
.permission-panel {
    margin: -2px 0 10px;
    padding: 12px 18px;
}

.permission-station {
    margin-bottom: 10px;
}

.permission-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 44px;
    border-bottom: var(--bw, 1px) solid var(--line, #ffc820);
}

.permission-station .permission-row:last-child {
    border-bottom: none;
}

/* TIMTRK-51: the batched permissions panel — laid out for iPad landscape (1024 CSS px) FIRST per
   the PM's 2026-08-08 UI provision, then allowed to breathe at desktop widths. Every control here
   is at least 44px tall, nothing is revealed by hover alone, and every state that matters is
   carried by :checked or :focus-visible rather than a pointer state a touch device never has.
   Colours come only from the existing tokens, so both skins work by construction — tokens.css is
   generated from the design-token source and must never be hand-edited. */

/* Labels that exist for screen readers only. The panel needs one per row: each row's three radios
   are a group, and without a legend they are announced as three unrelated choices. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.permission-toolbar {
    display: flex;
    justify-content: flex-end;
    min-height: 44px;
    align-items: center;
}

.permission-station-summary {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 4px 2px;
    cursor: pointer;
    font-weight: 700;
    color: var(--mut, #5c6b85);
    text-transform: var(--tt, none);
    border-bottom: var(--bw, 1px) solid var(--line, #ffc820);
}

/* The disclosure triangle is the only affordance saying a branch can close, so it stays. */
.permission-station-summary::marker {
    color: var(--faint, #627085);
}

.permission-station-summary:focus-visible {
    outline: 3px solid var(--acc, #ffc820);
    outline-offset: 2px;
}

/* The three choices per row, as one segmented control. */
.permission-choice {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0;
    padding: 0;
    border: 0;
}

.choice-pill {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0 14px;
    cursor: pointer;
    font-weight: 600;
    color: var(--mut, #5c6b85);
    background: var(--card, #ffffff);
    border: var(--bw, 1px) solid var(--line, #ffc820);
    border-radius: var(--r-pill, 99px);
}

/* The radio itself is hidden from sight but not from the keyboard or the accessibility tree: it
   still receives focus, still answers to arrow keys, and still carries the label. Only its default
   rendering is replaced — the pill IS the radio, visually. */
.choice-pill input[type="radio"] {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    pointer-events: none;
}

.choice-pill:has(input:checked) {
    background: var(--chip-bg, #e8e9f4);
    color: var(--chip-fg, #2b3379);
    border-color: var(--chip-fg, #2b3379);
}

/* The focus ring has to live on the pill, since the input it belongs to is not visible. */
.choice-pill:has(input:focus-visible) {
    outline: 3px solid var(--acc, #ffc820);
    outline-offset: 2px;
}

.permission-save-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    margin-top: 12px;
    padding-top: 12px;
    border-top: var(--bw, 1px) solid var(--line, #ffc820);
}

.permission-save-bar .btn {
    min-height: 44px;
}

/* At 1024 a row's name and its three pills do not fit side by side, so the row stacks and the
   pills get the full width to stay finger-sized. The rule is min-width rather than max-width on
   purpose: the narrow layout is the default and the wide one is the enhancement. */
@media (min-width: 1200px) {
    .permission-choice {
        flex-wrap: nowrap;
    }
}

@media (max-width: 1199px) {
    .permission-row {
        flex-wrap: wrap;
        align-items: flex-start;
        padding: 8px 0;
    }

    .permission-choice {
        width: 100%;
    }

    .choice-pill {
        flex: 1 1 auto;
    }
}

/* TIMTRK-52: the company-wide two-step enforcement rows on the Settings Security card. The
   control moved here from the retired /security-settings screen, where its rows were plain
   .field-label with a browser-default ~16px radio — under the PM's binding 2026-08-08 provision
   (iPad landscape 1024 CSS px first, 44px+ targets). Enlarging the row and its radio is
   compliance with that provision, not a re-rendering of the three-option control. Every state
   that matters is carried by :checked or :focus-visible rather than a pointer state a touch
   device never has, and the colours come only from existing tokens so both skins work by
   construction — tokens.css is generated and must never be hand-edited. */
.policy-choice {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    cursor: pointer;
}

.policy-choice input[type="radio"] {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    accent-color: var(--acc, #ffc820);
}

/* The ring goes on the row, not the radio: at 24px the browser default ring is easy to miss on
   a card this dense, and the whole row is the touch target anyway. */
.policy-choice:has(input:focus-visible) {
    outline: 3px solid var(--acc, #ffc820);
    outline-offset: 2px;
}

/* TIMTRK-18: friendly crash page — the code is the whole point, so it reads large. */
.error-code-row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}

.error-code {
    font-family: var(--tt, monospace);
    font-size: 28px;
    font-weight: 700;
    letter-spacing: 2px;
    padding: 8px 16px;
    border-radius: var(--r-sm, 8px);
    background: var(--chip-bg, #fff6d6);
    color: var(--chip-fg, #1a1a1a);
    border: var(--bw, 1px) solid var(--line, #ffc820);
}

/* TIMTRK-18: diagnostics list + stack trace. */
.diagnostics-grid {
    grid-template-columns: 1fr 0.8fr 1fr 1.6fr 1.2fr;
}

.stack-trace {
    overflow-x: auto;
    white-space: pre;
    font-family: var(--tt, monospace);
    font-size: 12px;
    padding: 12px;
    border-radius: var(--r-sm, 8px);
    background: var(--chip-bg, #fff6d6);
    color: var(--chip-fg, #1a1a1a);
}

/* TIMTRK-34: audit drill-down modals.
   Every value here comes from an existing token, because the PM's prelim decision was that the
   modal language is DERIVED from the app's own components rather than from a design station that
   turned out not to exist. A modal is a card that floats. */
dialog.modal {
  background: var(--card, #ffffff);
  color: var(--ink, #101c33);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-lg, 24px);
  box-shadow: var(--sh-card, 0 8px 30px rgba(16, 28, 51, 0.18));
  width: min(92vw, 640px);
  max-height: 85vh;
  overflow: auto;
  padding: 20px;
}

dialog.modal::backdrop {
  background: var(--overlay, rgba(16, 28, 51, 0.45));
}

.modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.modal-head h2 {
  margin: 0;
}

/* TIMTRK-53: seats the edit badge against the name instead of letting space-between strand it in
   the middle of a three-child row — an auto margin claims the free space before justify-content
   ever sees it. Scoped like .profile-card .badge-btn rather than folded into .badge-btn, so the
   shape stays reusable where this alignment would be wrong. flex:none because a long name must
   compress the h2, never the 44px touch target. */
.modal-head .badge-btn {
  margin-right: auto;
  flex: none;
}

.modal-close {
  min-width: 44px;
  min-height: 44px;
  font-size: 22px;
  line-height: 1;
  background: none;
  border: 0;
  color: var(--mut, #5c6b85);
  cursor: pointer;
}

.modal-subtitle {
  margin: 4px 0 12px;
}

.modal-subhead {
  margin: 18px 0 6px;
  font-size: 14px;
  color: var(--mut, #5c6b85);
}

.modal-facts {
  display: grid;
  grid-template-columns: minmax(90px, auto) 1fr;
  gap: 4px 14px;
  margin: 8px 0;
}

.modal-facts dt {
  color: var(--faint, #627085);
}

.modal-facts dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.modal-summary {
  margin: 12px 0;
}

/* Locked decision 1: the token keeps exactly the weight and colour it has today. The only
   affordance is an underline, and only on hover or keyboard focus — the lists must read
   unchanged at rest. */
.audit-pivot {
  color: inherit;
  font-weight: inherit;
  text-decoration: none;
  cursor: pointer;
}

.audit-pivot:hover,
.audit-pivot:focus-visible {
  text-decoration: underline;
}

/* The whole row is clickable via a stretched anchor rather than a click handler: it stays
   keyboard-reachable, works with no script, and never nests one anchor inside another. */
.row-linked {
  position: relative;
}

.row-stretch {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.row-linked > span {
  position: relative;
  z-index: 1;
}

.row-linked .audit-pivot {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* The user pivot also appears on rows that are not themselves clickable — People, Approvals and
   the approvals queue — so the 44px floor cannot ride on `.row-linked` alone. Caught in review
   round 2: those three anchors were only as tall as their 14px line box. */
.people-grid .audit-pivot,
.approvals-grid .audit-pivot,
.approvals-queue-grid .audit-pivot {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* --- Restart announcement (TIMTRK-37, reworked by TIMTRK-47) ----------------
   Sticky to the top edge so it stays in the viewport however far the page is
   scrolled — that is what earned the removal of the per-form inline warnings at
   stage 1. The colours reuse existing tokens rather than inventing any: neutral
   is the chip pairing and red is --danger. Nothing here touches tokens.css.

   TIMTRK-47 retired the amber band. TIMTRK-37 ramped neutral -> amber (<=60s)
   -> red (<=15s); the superseding spec colours the WHOLE last minute red, which
   leaves amber no window at all, so the class and its toggle are gone rather
   than left dead. Three states remain: neutral while counting, flashing red for
   the last 60 seconds, and flat red for the terminal "Restart imminent" phase.
   The ticking text is present in every one of them — colour is never the only
   signal, and the flash is motion-gated below. */
.restart-banner {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  border-bottom: var(--bw, 1px) solid var(--line, #ffc820);
  padding: 12px 18px;
  font-weight: 700;
  text-align: center;
}

/* [hidden] is only a default, and the rule above sets no display of its own —
   but a future layout change that adds one would silently un-hide the banner,
   so pin it. */
.restart-banner[hidden] {
  display: none;
}

/* The last 60 seconds, and the terminal state, share one red treatment — they
   differ only in whether it moves. */
.restart-banner.is-red,
.restart-banner.is-imminent {
  background: var(--danger, #b3261e);
  color: var(--danger-fg, #ffffff);
  border-bottom-color: var(--danger, #b3261e);
}

.restart-alert.is-red .restart-alert-text,
.restart-alert.is-imminent .restart-alert-text {
  color: var(--danger, #b3261e);
}

/* The flash, gated so that reduced-motion users simply get the steady red above
   with no extra rules to keep in step. Only is-red animates: is-imminent is
   specified as permanent and STATIC red, and a warning that pulses forever is
   the thing an accessibility rule exists to prevent.

   Two reds rather than red<->white, at 1Hz: WCAG 2.3.1 draws its line at three
   flashes a second, and keeping the luminance delta small means this reads as a
   pulse in peripheral vision instead of a strobe for someone sitting in front of
   it for a minute. color-mix keeps the darker tone derived from whichever skin's
   --danger is live, so neither skin needs its own keyframes. */
@keyframes restart-flash {
  from {
    background: var(--danger, #b3261e);
  }

  to {
    background: color-mix(in srgb, var(--danger, #b3261e) 72%, #000000);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .restart-banner.is-red {
    animation: restart-flash 1s steps(2, jump-none) infinite;
  }

  .restart-alert.is-red .restart-alert-text {
    animation: restart-flash-text 1s steps(2, jump-none) infinite;
  }
}

/* The dialog's text sits on the card, not on a red bar, so it pulses its own
   colour rather than a background. */
@keyframes restart-flash-text {
  from {
    color: var(--danger, #b3261e);
  }

  to {
    color: color-mix(in srgb, var(--danger, #b3261e) 72%, #000000);
  }
}

/* The one-time alert. A card that floats, same vocabulary as dialog.modal. */
dialog.restart-alert {
  background: var(--card, #ffffff);
  color: var(--ink, #101c33);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-lg, 24px);
  box-shadow: var(--sh-card, 0 8px 30px rgba(16, 28, 51, 0.18));
  width: min(92vw, 420px);
  padding: 24px;
  text-align: center;
}

dialog.restart-alert::backdrop {
  background: var(--overlay, rgba(16, 28, 51, 0.45));
}

.restart-alert-title {
  margin: 0 0 8px;
}

.restart-alert-text {
  margin: 0 0 8px;
  font-weight: 700;
  font-size: 18px;
}

.restart-alert-note {
  margin: 0 0 18px;
  color: var(--mut, #5c6b85);
}

/* SPEC: hit targets >= 44px. The banner carries no control, so this button is
   the only place the rule bites in this feature. */
.restart-alert .btn {
  min-height: 44px;
  width: 100%;
}

/* --- Office closing countdown (TIMTRK-55) -----------------------------------
   The same shapes as .restart-banner above, and deliberately so: two sticky
   warnings that looked different would read as two unrelated kinds of urgency.
   Only two states here rather than three — neutral while counting, flashing red
   for the last 60 seconds — because this countdown has no terminal phase to
   sit in: at zero the island navigates and the session is over.

   Both banners can be on screen at once (a restart announced during the closing
   window). They stack rather than overlap: each is sticky at its own position in
   normal flow, and the closing one is rendered second, so it settles beneath. */
.closing-banner {
  position: sticky;
  top: 0;
  z-index: 39;
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  border-bottom: var(--bw, 1px) solid var(--line, #ffc820);
  padding: 12px 18px;
  font-weight: 700;
  text-align: center;
}

/* Same trap as .restart-banner[hidden]: the rule above sets no display, so a
   later layout change adding one would silently un-hide this. Pin it. */
.closing-banner[hidden] {
  display: none;
}

.closing-banner.is-red {
  background: var(--danger, #b3261e);
  color: var(--danger-fg, #ffffff);
  border-bottom-color: var(--danger, #b3261e);
}

/* Reuses restart-flash and restart-flash-text: same 1Hz two-red pulse, same
   WCAG 2.3.1 reasoning, same motion gate. Defining a second identical keyframe
   set would be two things to keep in step for no visual difference. */
@media (prefers-reduced-motion: no-preference) {
  .closing-banner.is-red {
    animation: restart-flash 1s steps(2, jump-none) infinite;
  }

  .closing-alert.is-red .closing-alert-text {
    animation: restart-flash-text 1s steps(2, jump-none) infinite;
  }
}

dialog.closing-alert {
  background: var(--card, #ffffff);
  color: var(--ink, #101c33);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-lg, 24px);
  box-shadow: var(--sh-card, 0 8px 30px rgba(16, 28, 51, 0.18));
  width: min(92vw, 420px);
  padding: 24px;
  text-align: center;
}

dialog.closing-alert::backdrop {
  background: var(--overlay, rgba(16, 28, 51, 0.45));
}

.closing-alert-title {
  margin: 0 0 8px;
}

.closing-alert-text {
  margin: 0 0 8px;
  font-weight: 700;
  font-size: 18px;
}

.closing-alert-note {
  margin: 0 0 18px;
  color: var(--mut, #5c6b85);
}

/* SPEC: hit targets >= 44px. The banner carries no control, so this button is
   the only place the rule bites in this feature. */
.closing-alert .btn {
  min-height: 44px;
  width: 100%;
}

/* --- Approvals day editor (TIMTRK-58) --------------------------------------
   The modal that corrects one person's one day. Laid out as a stack of rows so
   a narrow dialog on a tablet wraps instead of clipping; every control is at
   least 44px tall, which is what the badge-btn shape already guarantees. */

.day-entry-list {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.day-entry {
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  padding: 10px 12px;
  background: var(--card, #ffffff);
}

.day-entry-po {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}

.day-entry-fields {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.day-entry-form {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  flex: 1;
}

.day-entry-hmm {
  width: 84px;
  min-height: 44px;
}

.day-entry-note {
  flex: 1;
  min-width: 140px;
  min-height: 44px;
}

.day-add {
  margin-top: 16px;
  padding-top: 14px;
  border-top: var(--bw, 1px) solid var(--line, #ffc820);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.day-add h3 {
  margin: 0;
  font-size: 15px;
}

.day-add select,
.day-add input {
  min-height: 44px;
}

.day-add .badge-btn {
  align-self: flex-start;
}

/* --- Worker self-correction on /today (TIMTRK-68) ---------------------------
   The .day-entry-* shapes above were written for the office modal, where each
   row is a card on a plain dialog. On the phone they sit INSIDE an .entry-row
   card already, so the borrowed border and background would draw a box in a
   box. Only that nesting is corrected here; the 44px control sizing carries
   over untouched, which is the whole reason for reusing the classes. */

.worker .day-entry {
  border: 0;
  border-top: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: 0;
  background: none;
  padding: 10px 0 0;
}

.worker .day-entry-list {
  gap: 4px;
}

/* The note is the field that suffers first on a narrow screen: at 140px it
   shares a line with the hours box and the Save button and every one of them
   ends up too tight to hit. Full width below 420px, buttons on their own row. */
@media (max-width: 420px) {
  .worker .day-entry-note {
    flex: 1 1 100%;
    min-width: 0;
  }
}

.today-lock {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 12px;
}

/* ── TIMTRK-70: the four batched toggle surfaces ─────────────────────────────────────────────
   Designed at 1024 CSS px (iPad landscape) first and expanded upward, per the PM's binding UI
   provision: every interactive target clears 44px, nothing is revealed on hover alone, and the
   coarse-pointer case is handled rather than inherited. Tokens only, so both skins come free. */

/* Surface 2 — the day cell as a toggle — RETIRED by TIMTRK-200 along with per-day approval and
   its batch form (.day-toggle, .day-toggle-face, the :checked / :not(:checked) / :focus-visible
   sibling rules, the hover block and .day-batch-form). The cell's one control is .day-chip /
   .day-plus, defined with the rest of the approvals grid above. */

/* Surface 1 — the device-limit cell.
   The cell holds a count line and, for an at-limit person, the editor under it. It lives inside a
   grid column, so it stacks rather than sitting in a row. */
.device-limit-cell {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
}

.device-limit-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 100%;
}

.stepper {
    display: flex;
    align-items: center;
    gap: 6px;
}

.stepper-btn {
    min-height: 44px;
    min-width: 44px;
    border-radius: var(--r-pill, 99px);
    border: var(--bw, 1px) solid var(--line, #ffc820);
    background: var(--chip-bg, #e8e9f4);
    color: var(--chip-fg, #2b3379);
    font-size: 20px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
}

.stepper-btn:focus-visible {
    outline: 3px solid var(--acc, #ffc820);
    outline-offset: 2px;
}

/* Wide enough for two digits plus the native spinner, and tall enough to be a touch target in
   its own right — the steppers are the enhancement, not the only way in. */
.stepper-input {
    min-height: 44px;
    width: 5.5rem;
    text-align: center;
}

.device-limit-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.device-limit-actions .btn {
    min-height: 44px;
}

/* Surface 4 — the Security-policy panel. It is a block inside the existing Security card rather
   than a card of its own, so it inherits the card's padding and only needs its own rhythm. */
.security-policy-panel {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* ── TIMTRK-113 work assignments ────────────────────────────────────────────────────────────────
   Office calendar: assignee pills on chips, the people/crews tray and its drag affordances, and
   the roster side sheet. Worker: the new-assignment banner and ack cards. Every declaration reads
   a token with a 406-Blue fallback, so both skins follow without a second rule set. Touch first:
   nothing here depends on hover, and the hit targets are 44px+ at the 1024 iPad width. */

/* Cells grow only where assignments can be seen, so a view-only viewer keeps the compact board. */
.board-assign .calendar-cell { min-height: 150px; }

.board-assign .calendar-chip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    text-decoration: none;
}

.chip-label { flex: 1 1 auto; }

.chip-unassigned {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    opacity: .75;
}

.chip-pills { display: flex; flex-wrap: wrap; gap: 3px; }

/* Solid = the worker has acknowledged it; outlined = not yet (AC-23). */
.initials-pill {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 999px;
    font-size: 9px;
    font-weight: 700;
    background: var(--card, #fff);
    color: var(--pri, #2b3379);
    border: 1.5px solid var(--pri, #2b3379);
}

.initials-pill.seen {
    background: var(--pri, #2b3379);
    color: var(--pri-fg, #fff);
}

.tray { margin-top: 14px; }
.tray-chips { display: flex; flex-wrap: wrap; gap: 8px; }

.tray-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    padding: 8px 14px;
    border-radius: var(--r-pill, 99px);
    border: var(--bw, 1px) solid var(--line, #ffc820);
    background: var(--chip-bg, #e8e9f4);
    color: var(--chip-fg, #2b3379);
    font: inherit;
    font-size: 13px;
    font-weight: 700;
    /* Required for a reliable long-press: without it the browser claims the gesture as a scroll
       and fires pointercancel before the chip can lift. */
    touch-action: none;
    user-select: none;
    cursor: pointer;
}

.tray-chip-crew {
    background: var(--pri, #2b3379);
    color: var(--pri-fg, #fff);
}

.tray-chip-crew .faint { color: var(--pri-fg, #fff); opacity: .7; }

.tray-chip.disabled,
.tray-chip:disabled { opacity: .5; cursor: not-allowed; }

.tray-chip.dragging { opacity: .35; }

.drag-ghost {
    position: fixed;
    z-index: 60;
    pointer-events: none;
    transform: translate(-50%, -50%) scale(1.04);
    box-shadow: var(--sh-btn, 0 2px 6px rgba(16, 28, 51, .25));
}

/* Class-driven, never :hover — a coarse pointer has no hover to speak of. */
.drop-target { outline: 2px solid var(--pri, #2b3379); outline-offset: 1px; }

.drop-popover {
    position: fixed;
    z-index: 61;
    max-width: 260px;
    padding: 12px;
    background: var(--card, #fff);
    border: var(--bw, 1px) solid var(--line, #ffc820);
    border-radius: var(--r-sm, 12px);
    box-shadow: var(--sh-card, 0 2px 10px rgba(16, 28, 51, .12));
}

.drop-popover-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }

/* TIMTRK-162: PO pills drag between days, so in manage mode they need the same gesture claim the
   tray chips make. Scoped to .board-assign so a read-only board keeps ordinary touch scrolling
   over its chips; the cell background and the gaps between chips scroll either way. */
.board-assign .calendar-chip { touch-action: none; user-select: none; }

.calendar-chip.dragging { opacity: .4; }

/* TIMTRK-178 AC-5: the roster sheet's day picker. A row per week, seven cells per row, so a
   booking's gaps are visible as gaps rather than described in prose. Sized for a thumb on the
   office iPad: every cell is at least 44px on both axes, which is also why this is a grid of
   labels rather than a list of checkboxes with text beside them. */
.day-picker {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px;
  margin: 6px 0 10px;
}

.day-pick {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 4px 2px;
  border-radius: var(--r-sm, 8px);
  border: var(--bw, 1px) solid var(--line, #ffc820);
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  font-size: 11px;
  font-weight: 700;
  cursor: pointer;
}

.day-pick input { margin: 0 0 2px; }
.day-pick-dow { font-size: 10px; opacity: .7; text-transform: uppercase; }
.day-pick-num { font-size: 13px; }

/* Outside the booking. Not selectable, and it has to LOOK not-selectable — the office should read
   "the picker stops here on purpose", never "this sheet is broken". The reason is stated once
   above the grid rather than as a tooltip per day. */
.day-pick-off {
  opacity: .38;
  cursor: not-allowed;
  background: transparent;
  border-style: dashed;
}

/* The day the sheet IS: ticked, and not untickable. */
.day-pick input:disabled:checked { accent-color: var(--acc, #ffc820); }

/* TIMTRK-178 AC-6: the booking edge handles. Only the booking's first and last pills draw one.

   Sized for the office's iPad first: the strip spans the pill's full height and is 22px wide, and
   `touch-action: none` is what stops the browser claiming the gesture as a scroll before the lift
   engine sees it — the same reason the pill itself sets it. There is no hover-only affordance
   here: the handle is always visible on a coarse pointer, because an edge you have to discover by
   hovering does not exist on a touch screen at all. */
.calendar-chip { position: relative; }

.chip-edge {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 22px;
  cursor: ew-resize;
  touch-action: none;
  border-radius: var(--r-pill, 99px);
}

.chip-edge-start { left: 0; }
.chip-edge-end { right: 0; }

/* The grip itself — a short bar, centred, drawn in the chip's own foreground so it inherits the
   service-call palette without a second rule. Kept low-contrast so a board full of bookings does
   not read as a board full of handles. */
.chip-edge::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 2px;
  height: 12px;
  border-radius: 2px;
  background: currentColor;
  opacity: .45;
}

.chip-edge:hover::after,
.chip-edge.dragging::after { opacity: .9; }

/* The label must not run under a handle, or the grip sits on top of the PO code. */
.calendar-chip .chip-label { display: block; }
.chip-edge-start ~ .chip-label { padding-left: 14px; }
.chip-edge-end ~ .chip-label { padding-right: 14px; }

/* A coarse pointer gets a wider target; 44px total across the two handles on a small pill would
   otherwise leave nothing between them to tap for the roster sheet. */
@media (pointer: coarse) {
  .chip-edge { width: 28px; }
  .chip-edge::after { height: 16px; width: 3px; opacity: .6; }
}

/* A day the office may not write to. Distinct from .drop-target rather than a variant of it: the
   point is that this one is NOT a target, and the two must never read the same at a glance. */
.drop-refused {
    /* --mut, not --muted (TIMTRK-199): the latter is not a token, so this outline always fell
       through to a literal that is neither skin's muted grey — blue-tinted beside Slab's neutral
       lines. A dashed outline is a GRAPHICAL cue (WCAG 1.4.11, 3:1). On the board's own ground
       (--pg) --mut measures 4.94:1 in 406 Blue and 5.21:1 in Slab. */
    outline: 2px dashed var(--mut, #5c6b85);
    outline-offset: 1px;
    cursor: not-allowed;
}

/* Revealed only while a board pill is lifted, so it costs nothing the rest of the time. */
.trash-zone {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    margin-bottom: 12px;
    padding: 10px 12px;
    border: 2px dashed var(--line, #ffc820);
    border-radius: var(--r-sm, 12px);
    /* --mut, not --muted (TIMTRK-199): the latter is not a token, so this text always fell through
       to a literal that is neither skin's muted grey. On its own ground (--pg) --mut measures
       4.94:1 in 406 Blue and 5.21:1 in Slab. */
    color: var(--mut, #5c6b85);
    font-size: 13px;
    font-weight: 700;
    text-align: center;
}

.trash-zone[hidden] { display: none; }

.chip-menu {
    position: fixed;
    z-index: 61;
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 200px;
    padding: 8px;
    background: var(--card, #fff);
    border: var(--bw, 1px) solid var(--line, #ffc820);
    border-radius: var(--r-sm, 12px);
    box-shadow: var(--sh-card, 0 2px 10px rgba(16, 28, 51, .12));
}

.chip-menu[hidden] { display: none; }

/* 44px targets: this menu is opened by a long press on a touch screen, and the finger that opened
   it is the one about to pick from it. */
.chip-menu .btn { min-height: 44px; justify-content: flex-start; text-align: left; }

/* The roster sheet's own parts. Its geometry is not here — it is a shell width variant like any
   other, and lives with the rest of them further down (`dialog.modal-roster-sheet`). */
.sheet-body { flex: 1 1 auto; overflow-y: auto; }

.sheet-foot {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    justify-content: space-between;
    padding-top: 10px;
    border-top: var(--bw, 1px) solid var(--line, #ffc820);
}

.roster-card { margin-bottom: 10px; }

.roster-card-head {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    justify-content: space-between;
}

.roster-readonly { margin: 6px 0 0; padding-left: 18px; }

.span-toggle { margin-bottom: 6px; }
.span-fields { display: flex; flex-wrap: wrap; gap: 8px; }

/* 44px toggle rows; the selected state rides :checked rather than a pointer state (AC-27). */
.toggle-row {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 4px 8px;
    border-radius: var(--r-sm, 12px);
    border: var(--bw, 1px) solid transparent;
}

.toggle-row:has(input:checked) {
    border-color: var(--pri, #2b3379);
    border-width: 2px;
}

.toggle-name { flex: 1 1 auto; font-size: 14px; font-weight: 700; }

/* Worker: the next unacknowledged assignment, above their job for today (screen 16). */
.assignment-banner {
    background: var(--acc, #ffc820);
    color: var(--acc-fg, #101c33);
    border-radius: var(--r-lg, 24px);
}

.banner-label {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
}

.banner-sub { margin: 2px 0 0; font-size: 13px; font-weight: 700; }

.got-it-btn { min-height: 52px; }

.ack-card.unseen { border-color: var(--acc, #ffc820); border-width: 2px; }

.ack-card-head {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    justify-content: space-between;
}

/* The Schedule tab's unseen count. .nav-badge already exists for the office sidebar; this puts it
   on a bottom-nav item and recolours it to the accent the handoff specifies. */
.bottom-nav-item { position: relative; }

.bottom-nav-item .nav-badge {
    position: absolute;
    top: 4px;
    right: 10px;
}

.nav-badge-accent {
    background: var(--acc, #ffc820);
    color: var(--acc-fg, #101c33);
}

/* Crews on the People screen, sharing the disclosure-panel geometry already used there. */
.crew-head { margin-top: 22px; }

.crew-grid {
    display: flex;
    gap: 12px;
    align-items: center;
    justify-content: space-between;
}

.crew-actions { display: flex; gap: 12px; align-items: center; }

.crew-panel { margin: 0 0 10px; }

.add-job-days { gap: 8px; }

/* TIMTRK-114: the overlay shell.

   Widths are the only thing that separates the variants, and they live here rather than in the
   island so a skin stays a pure token swap. `dialog.modal` above keeps its 640px default, which is
   what every pre-existing drill-down still gets — the variants are opt-in through the invoker's
   data-modal-variant, so nothing that shipped before this ticket changes shape. */
dialog.modal-form { width: min(92vw, 560px); }
/* TIMTRK-237: the PO edit dialog's own width. Same min(92vw, Npx) form as its three siblings
   rather than the handoff's calc(100vw - 32px), so one rule shape governs every dialog in the app.
   720 is the designed width: it fits 1024x768 with margins either side and stops growing after
   that, because a form wider than about 720 only stretches its line lengths. The PO row is the one
   invoker; the two create links stay on "form" at 560. */
dialog.modal-form-wide { width: min(92vw, 720px); }
dialog.modal-compact { width: min(92vw, 480px); }
dialog.modal-confirm { width: min(92vw, 400px); }

/* The two side sheets. Both dock right and run full height; they differ only in width — 340px for
   the roster, 420px for permissions — and in the roster's keeping the base card shadow where the
   permissions sheet casts its own. Those are two separate design decisions, which is why they are
   two variants rather than one with an override: neither can drift into the other.

   Column layout on both, so a long roster or permission tree scrolls inside .sheet-body while the
   header and the action bar stay put.

   TIMTRK-123 moved the roster sheet here. It used to be selected on the content it contained
   (`dialog.modal:has([data-component="roster-sheet"])`) because the shell once hardcoded every
   fetched dialog's class; the variant map removed that limitation, so the sheet is now chosen by
   its invoker like every other overlay. Note the specificity: these are single-class selectors that
   tie with `dialog.modal` above, so for the properties both rules set — width, max-height,
   border-radius, and box-shadow on the permissions one — the winner is decided by source order
   alone, and these must therefore stay BELOW the base. The rest (inset, max-width, height, margin,
   display, flex-direction) the base never sets, so they are uncontested and are what actually docks
   the sheet: dropping one does not fall back to a sane default, it un-docks. */
dialog.modal-roster-sheet {
    inset: 0 0 0 auto;
    width: min(340px, 100vw);
    max-width: 100vw;
    height: 100dvh;
    max-height: 100dvh;
    margin: 0;
    border-radius: 0;
    display: flex;
    flex-direction: column;
}

dialog.modal-sheet {
    inset: 0 0 0 auto;
    width: min(420px, 100vw);
    max-width: 100vw;
    height: 100dvh;
    max-height: 100dvh;
    margin: 0;
    border-radius: 0;
    box-shadow: -18px 0 40px rgba(16, 28, 51, .35);
    display: flex;
    flex-direction: column;
}

/* Header/footer anatomy shared by every overlay that carries actions. The footer reads
   microcopy-left, actions-right; on a narrow viewport it wraps rather than crushing the 44px
   targets. */
.modal-foot {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    justify-content: flex-end;
    margin-top: 18px;
}

.modal-microcopy {
    margin: 0 auto 0 0;
    color: var(--mut, #5c6b85);
    font-size: 13px;
}

/* Overlay bodies scroll internally so the footer never leaves the viewport (a long Devices list or
   permission tree would otherwise push Save off the bottom). */
.modal-body {
    overflow-y: auto;
    max-height: 60vh;
}

dialog.modal-sheet .modal-body {
    flex: 1 1 auto;
    max-height: none;
}

dialog.modal-sheet .modal-foot {
    flex: none;
    padding-top: 10px;
    border-top: var(--bw, 1px) solid var(--line, #ffc820);
}

/* The staged-change marker: a row edited in an overlay but not yet saved. 8px accent dot, per
   SPEC §Components. Applied by the island on the row itself, so it costs the markup nothing. */
[data-staged-row][data-staged] > .permission-label::before,
[data-staged-row][data-staged] > label::before {
    content: "";
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 8px;
    border-radius: 50%;
    background: var(--acc, #ffc820);
    vertical-align: middle;
}

/* Post-save feedback is a row highlight and nothing else (locked decision 7 — no toasts). One
   shot: the island adds the class and removes it on animationend, so there is no state to clear
   and re-saving the same row plays it again. Both skins get it for free — the tint is the accent
   token. Motion is gated, like the restart banner's flash above: someone who asked their system
   for less of it still gets the colour, just not the fade. */
/* Review round 3: the tint is unconditional and only the FADE is gated. The whole rule used to sit
   inside the media query, so someone who had asked their system for less motion got no post-save
   feedback at all — and since there is no toast by design, their save was simply invisible. The
   comment above already promised the opposite; this is the code catching up with it.

   It also fixed a quieter bug: with no animation there is no animationend, so the class the island
   adds was never removed, and the focus fallback that looks for `[data-row-id].row-flash` could send
   focus to a row saved minutes earlier. The island now clears the class on a timer as well. */
.row-flash {
    /* TIMTRK-307-1: the pulse reads the row's own flash colour — the accent on a job row and on a
       calendar cell (fallback), the card on a service row whose ground is already the accent. */
    background-color: var(--row-flash, var(--acc, #ffc820));
}

@media (prefers-reduced-motion: no-preference) {
    .row-flash {
        /* TIMTRK-307: the fade ends on the row's own ground — the tint on a service row, the card
           on a job row — and still on transparent for a flash target that is not a .table-row. */
        background-color: var(--row-bg, transparent);
        animation: tt-row-flash 1200ms ease-out 1;
    }
}

@keyframes tt-row-flash {
    from { background-color: var(--row-flash, var(--acc, #ffc820)); }
    to { background-color: var(--row-bg, transparent); }
}

/* While any overlay is open the page behind must not scroll under the scrim. Set on <html> rather
   than <body> because the office layout scrolls the root element. */
html.tt-scroll-lock {
    overflow: hidden;
}

/* TIMTRK-114: the per-cell "add a job on this day" affordance.

   Absolutely positioned, so it costs the cell no height. That is not cosmetic: as an ordinary flex
   child it added a line to every cell on the board, which made the whole grid taller and pushed the
   people/crews tray below the fold — and TIMTRK-113's drag test, which drives a real mouse from a
   tray chip to a job chip, then had nothing to grab. The board's geometry is load-bearing for a
   feature that shipped before this one, so the new control stays out of the flow.

   Always visible rather than revealed on hover: office screens are iPad-first, where there is no
   hover at all, and a control you cannot discover by looking is not a control. */
.calendar-cell {
    position: relative;
}

.calendar-cell-add {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    line-height: 1;
    text-decoration: none;
    color: var(--mut, #5c6b85);
    border-radius: var(--r-sm, 14px);
}

.calendar-cell-add:hover,
.calendar-cell-add:focus-visible {
    color: var(--pri, #2b3379);
    background: var(--chip-bg, #e8e9f4);
}


/* ---- TIMTRK-165: contractors and the Customer / GC picker ---------------------------------- */

/* A phone number on a tablet is there to be tapped, so it is sized as a real touch target (44px,
   the PM's touch-first provision) rather than as a line of text that happens to be a link. */
.tel-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    color: var(--ink, #101c33);
    text-decoration: none;
    border-bottom: var(--bw, 1px) solid var(--line, #d7dae8);
}

.tel-link:hover,
.tel-link:focus-visible {
    color: var(--pri, #2b3379);
    border-bottom-color: var(--pri, #2b3379);
}

/* What the list filter hides. A class rather than an inline style so a row the filter never
   touched is never left with a display value it has to be given back. */
.filtered-out {
    display: none;
}

/* TIMTRK-412 AC-9: the PO list's search empty state, shown by CSS rather than by script — the shape
   Approvals' [data-roster-empty] rule established. Hidden by default; revealed only when the search
   box holds text (:not(:placeholder-shown) — true for a typed query and for a server-rendered
   value="…" alike, which is what lets a scripting-off submit show the same line) AND no PO row
   survives — whether the island or the server put .filtered-out on it. An empty tab with an empty
   box shows nothing, exactly as before. */
[data-po-search-empty] { display: none; }

.po-search:has(.search-box:not(:placeholder-shown))
    ~ [data-list-panel]:not(:has(.table-row:not(.filtered-out))) [data-po-search-empty] {
    display: block;
}

.gc-picker {
    display: block;
    margin-top: 6px;
    /* TIMTRK-225: the positioning context the dropdown anchors to. Everything else in the shell
       stays in flow; only the list leaves it. */
    position: relative;
}

/* The queued selection. Reads as a chip, but it is a row: the Change control beside it has to be
   a 44px target of its own, and a chip-sized one would not be. */
/* An author-level `display` outranks the UA's `[hidden] { display: none }`, so the chip below
   needs this or `hidden` is inert on it — which is exactly what happened: an empty chip with a
   lone Change button sat above the search box on every unselected picker. Found by looking at the
   ticket's own screenshot, not by an assertion. */
.gc-picker-chip[hidden],
.gc-picker-list[hidden] {
    display: none;
}

.gc-picker-chip {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    padding: 6px 8px 6px 14px;
    background: var(--chip-bg, #e8e9f4);
    border: 2px solid var(--pri, #2b3379);
    border-radius: var(--r-sm, 12px);
}

.gc-picker-chip-name {
    flex: 1 1 auto;
    font-size: 14px;
    font-weight: 700;
    color: var(--ink, #101c33);
}

.gc-picker-change {
    flex: none;
    min-height: 44px;
}

/* TIMTRK-225: the dropdown, as an overlay anchored under the search box.

   Out of flow is the point, not the styling: an in-flow list made the dialog taller on every
   keystroke and, on open, pushed the rest of a 6-field form behind `dialog.modal`'s own
   `overflow: auto`. Absolute means the dialog's height cannot move while typing at all.

   The cap replaces `max-height: 40vh`, which was the wrong instrument for an overlay: 40vh at the
   768-tall house reference is 307px, measured from a box that no longer stops at the dialog's edge.

   The cap is MEASURED, and it has to track the viewport rather than be a constant — a round-1
   review finding, because the first cut was a flat 224px and that is only safe at one screen
   height. The dialog is `max-height: 85vh`; the form content above this anchor is a fixed 421px
   (measured at 1024x768: the dialog sits at y=57.6, the shell's bottom edge — search box plus
   `.search-box`'s 6px margin — is y=478.6). So the room below the anchor is `0.85vh - 421`, which
   is 231.8px at a 768-tall screen but only 191px at 720, where a flat 224 would have hung 33px
   past the dialog and re-created the clipping this ticket removes.

   Hence the clamp, with 430 rather than 421 to keep a few pixels in hand: about four rows plus a
   sliver of the fifth at 768, fewer on a short laptop, and up to 260px (five rows) on a tall
   screen where the room genuinely exists. The 88px floor keeps at least one whole row reachable
   rather than letting the expression collapse on a very short window.

   Why not the six rows AC 3 asks for: the New job PO form is 730px of content in a 651px dialog
   even with this picker fully COLLAPSED — a 79px overflow that predates this ticket and belongs to
   the six fields themselves, not to the picker. It pushes this anchor 79px lower than it should
   sit, and it is the only reason the sixth row does not fit. Raised as its own ticket at Ship. If
   that overflow is ever fixed, this constant is the one line to raise.

   Rows are deliberately NOT tightened to fit more of them: `.picker-row`'s 44px box and 8px rhythm
   are the iPad touch floor (AC 8) and the house card language the prompt forbids re-inventing.

   Downward-only is the spec, not an oversight: AC 2 says "anchored under the input". Flipping the
   overlay above the input when the room below is short WOULD buy the six rows AC 3 asks for — there
   is ~421px of space up there — and it is ruled out here deliberately rather than missed, because it
   contradicts AC 2 and this build may not re-decide what the product does.

   The binding check is geometric, not numeric, and lives in
   TIMTRK225_a_long_result_set_scrolls_inside_the_dropdown_and_stays_in_the_dialog, which asserts
   this box stays inside the dialog's AT TWO viewport heights — 1024x768 and 1280x720. One viewport
   is what let the flat constant through round 1, so a single-height assertion is not enough here.

   The opaque background is mandatory rather than cosmetic: it floats over Budget and Notes. */
.gc-picker-list {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 2;
    max-height: clamp(88px, calc(85vh - 430px), 260px);
    overflow-y: auto;
    padding: 4px;
    background: var(--card, #ffffff);
    border: var(--bw, 1px) solid var(--line, #ffc820);
    border-radius: var(--r-sm, 14px);
    box-shadow: var(--sh-card, 0 8px 30px rgba(16, 28, 51, 0.18));
}

/* Rides .picker-row for its box, and adds what a <button> does not inherit from a <label>: full
   width, left alignment, and the inherited font a button resets. The 44px floor is explicit here
   rather than implied by padding, matching .toggle-row. */
.gc-picker-row {
    width: 100%;
    min-height: 44px;
    text-align: left;
    font: inherit;
    color: var(--ink, #101c33);
}

/* Selection is carried by aria-selected, not by a pointer state — a touch device has no hover,
   and this list is used on one. */
.gc-picker-row[aria-selected="true"] {
    border: 2px solid var(--pri, #2b3379);
}

/* TIMTRK-225: the KEYBOARD's current row, which is a different claim from aria-selected's "this is
   the chosen one" — arrowing past a row chooses nothing until Enter. Deliberately the same border
   treatment rather than a new one: the prompt bans new visual vocabulary, and the two never
   compete for attention because only one row can be active at a time. The inline add takes it too,
   being a row of the listbox for arrow purposes. */
.gc-picker-row.is-active,
.gc-picker-add.is-active {
    border: 2px solid var(--pri, #2b3379);
}

.gc-picker-row:hover,
.gc-picker-row:focus-visible {
    border-color: var(--pri, #2b3379);
}

/* The inline add. Deliberately the accent rather than another identical row: it is the one entry
   in the list that writes something new, and it must not be tapped by mistake. */
.gc-picker-add {
    width: 100%;
    min-height: 44px;
    text-align: left;
    font: inherit;
    font-weight: 700;
    color: var(--pri, #2b3379);
    border-style: dashed;
}

.gc-picker-more {
    margin: 4px 0 0;
    font-size: 12px;
}

.gc-picker-error {
    margin-top: 6px;
}

/* ── TIMTRK-305: crew at creation ───────────────────────────────────────────────────────────────
   The CREW · OPTIONAL section on both create modals, and the stacked "Add workers" picker.

   Tokens only, with ONE literal: #e5e8f0 for the hairline and the row borders, following the
   precedent recorded further down this file (#dfe4ee for the row hairline) — a hairline is a
   drawn edge rather than a colour anything is coloured WITH, and tokens.css is generated from the
   design handoff, so a new token here would be a hand-edit of a generated file.

   Numbers are measured off flats 1a/1b/1c and are the spec, not suggestions. Both skins: every
   colour is a token or an alpha wash over one, so slab's --pri:#111 turns the chips and rings
   black there — accepted, the same way .btn-ghost-navy's FA-2 note records it, and flagged for
   the PM's hand pass. */

/* §4 decision 15 (PM-settled): the hairline sits ABOVE the section, between the last field and the
   label. Nothing draws a line between the helper text and the footer — the prompt's prose bullet
   is superseded by the flats on this one detail, so this is not a deviation to "fix". */
.crew-field {
    margin-top: 16px;
    padding-top: 14px;
    border-top: var(--bw, 1px) solid #e5e8f0;
}

.crew-label {
    margin: 0 0 10px;
}

/* Flat 1c lays the chips and the "+ Add" pill on ONE wrapping row. */
.crew-field-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
}

.crew-chips {
    display: contents;
}

.crew-add {
    min-height: 44px;
    padding: 0 20px;
    font-size: 15px;
}

/* With at least one chip the pill shortens to "+ Add" and goes dashed (crew-field.ts sets the
   class; the label is its text). */
.crew-add.is-dashed {
    border: 2px dashed var(--faint, #627085);
    background: var(--card, #fff);
    color: var(--pri, #2b3379);
}

.crew-help {
    margin: 10px 0 0;
}

.crew-chip {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    padding: 4px 4px 4px 6px;
    border-radius: var(--r-pill, 99px);
    background: var(--pri, #2b3379);
    color: var(--pri-fg, #fff);
    font-weight: 700;
    font-size: 15px;
}

.crew-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 999px;
    background: var(--chip-bg, #e8e9f4);
    color: var(--pri, #2b3379);
    font-weight: 700;
    font-size: 14px;
    flex: none;
}

/* Flat 1c: a lighter navy disc with white initials — an alpha wash over the token, no new hex. */
.crew-avatar-chip {
    width: 32px;
    height: 32px;
    background: rgba(255, 255, 255, .18);
    color: var(--pri-fg, #fff);
    font-size: 12px;
}

.crew-chip-remove {
    min-width: 44px;
    min-height: 44px;
    margin-left: -4px;
    background: none;
    border: 0;
    color: var(--pri-fg, #fff);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    border-radius: 999px;
}

.crew-picker-subhead {
    margin: 4px 0 14px;
    color: var(--mut, #5c6b85);
}

.crew-picker-list-wrap {
    position: relative;
}

/* Flat 1b shows exactly eight rows; the ninth sits under the fade. */
.crew-picker-list {
    max-height: calc(8 * 52px + 7 * 8px);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.crew-picker-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 52px;
    padding: 6px 12px;
    text-align: left;
    font: inherit;
    color: var(--ink, #101c33);
    background: var(--card, #fff);
    border: var(--bw, 1px) solid #e5e8f0;
    border-radius: var(--r-sm, 12px);
    cursor: pointer;
}

/* The app's existing toggle-row vocabulary (F24): 2px --pri border plus a filled check. */
.crew-picker-row[aria-selected="true"] {
    background: var(--chip-bg, #e8e9f4);
    border: 2px solid var(--pri, #2b3379);
}

.crew-picker-row.filtered-out {
    display: none;
}

.crew-row-text {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
}

.crew-row-name {
    font-weight: 700;
    font-size: 15px;
}

.crew-row-role {
    font-size: 13px;
    color: var(--mut, #5c6b85);
}

.crew-check {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 999px;
    border: 2px solid var(--faint, #627085);
    color: transparent;
    flex: none;
}

.crew-picker-row[aria-selected="true"] .crew-check {
    background: var(--pri, #2b3379);
    color: var(--pri-fg, #fff);
    border-color: var(--pri, #2b3379);
    box-shadow: 0 0 0 2px var(--acc, #ffc820);
    font-size: 14px;
    font-weight: 700;
}

.crew-picker-fade {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 64px;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding-bottom: 6px;
    background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--card, #fff) 60%);
    pointer-events: none;
}

.crew-picker-fade[hidden] {
    display: none;
}

.crew-picker-more {
    font-size: 14px;
    font-weight: 700;
    color: var(--mut, #5c6b85);
}

.crew-picker-foot {
    margin-top: 14px;
}

.crew-picker-count {
    margin-right: auto;
    font-weight: 700;
    color: var(--ink, #101c33);
}

.crew-picker .btn-primary:disabled {
    background: var(--faint, #627085);
    box-shadow: none;
    cursor: default;
}

.crew-picker .search-box {
    margin-bottom: 10px;
}

/* ── TIMTRK-192: the standing roster, the elevated group and the search bar ─────────────────────
   Existing tokens only — this build changes no token and hand-edits nothing generated. */

/* AC-39: the name column stays put while the day block scrolls at 1024. An opaque --card
   background, because a sticky cell with a transparent one lets the scrolled days read through it. */
.approvals-grid .name-cell {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--card, #fff);
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    min-height: 44px;
}

/* The label row's twin of the rule above. Its background is the PAGE colour, not --card: the header
   row has no card behind it, so borrowing the row background would paint a white block above the
   table. And no 44px floor — a label is not a touch target, and forcing one would deepen the header
   on every screen to fix a column that only scrolls on narrow ones. */
.approvals-grid .name-cell-head {
    background: var(--pg, #f3f5f9);
    min-height: 0;
    justify-content: flex-end;
}

/* AC-1: 11px uppercase under the name. Letter-spaced because at 11px uppercase without it reads as
   a smudge, and this line has to be legible at arm's length on the iPad. */
.role-label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    /* --mut, not --muted: the latter is not a token, so it would always fall through to the literal
       and render blue-tinted in the slab skin while every other muted line went neutral (AC-39). */
    color: var(--mut, #5c6b85);
}

/* AC-6: ONE hairline between the roster and the elevated group, inside the same card. A border on
   the first elevated row rather than a separate block — the accent colour says "a different kind of
   row", and the single line says "still the same table". */
.table-row.approvals-grid.row-elevated-first {
    border-top: 2px solid var(--line, #ffc820);
    margin-top: 6px;
}

/* AC-40: the elevated half is marked by more than a hue — the hairline above and the name column's
   weight carry it too, so nothing here depends on telling two colours apart. */
.table-row.approvals-grid.row-elevated .name-cell strong { font-weight: 800; }

/* AC-20: the note under a week nobody in front of the screen can approve. Spans the whole row
   because it is a sentence, and the action column is 84px wide. */
.approver-note {
    grid-column: 1 / -1;
    margin: 2px 0 0;
    font-size: 12px;
}

.roster-off-form { grid-column: 1 / -1; margin-top: 4px; }

/* AC-8: the search box and the three role pills, on one line at every width the office uses. */
.roster-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin: 10px 0;
}

.roster-filters .search-box { flex: 1 1 220px; }

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

/* A pill is a link, so it needs the chip's shape without the chip's assumption of being text. The
   44px floor is AC-39's, and it applies to a pointer as much as to a thumb. */
.role-pills .chip {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 8px 16px;
    text-decoration: none;
}

/* AC-8's search empty state, shown by CSS rather than by script.

   Hidden by default and revealed only when BOTH halves hold: the search box actually has something
   in it (`:not(:placeholder-shown)`), and no roster row survives the filter. The first half is not
   decoration — without it the message fires whenever the roster half is empty for a reason that has
   nothing to do with searching (the "Office" pill in a company with no active Office account), and
   blames a search nobody made. The server-rendered sentence beside it covers that case instead.

   With scripting off nothing carries .filtered-out, the :has() matches, and this never shows —
   correct, because with scripting off every row really is visible. */
[data-roster-empty] { display: none; margin-top: 10px; }

.roster-filters:has(.search-box:not(:placeholder-shown))
    ~ * .approvals-scroll:not(:has([data-roster-row="true"]:not(.filtered-out))) [data-roster-empty] {
    display: block;
}

/* AC-10/AC-14: a chip already on the week takes the solid treatment the calendar's crew chips use,
   so "on" and "off" differ in fill and in wording, never in hue alone (AC-40). */
/* .tray-chip carries `touch-action: none` because the CALENDAR tray needs a long-press to lift a
   drag without the browser claiming the gesture as a scroll. These chips are plain submit buttons
   with no drag island at all, so inheriting it only means a scroll that happens to start on a chip
   is swallowed and the page will not pan — on the PM's iPad, where the tray sits under a table
   people scroll past. Overridden here rather than by changing the calendar's rule, which still
   needs it. */
.tray-chip-form { display: contents; }

.tray-chip-form .tray-chip {
    touch-action: manipulation;
    user-select: auto;
}

.tray-chip-on {
    background: var(--pri, #2b3379);
    color: var(--pri-fg, #fff);
}

.tray-chip-on .faint { color: var(--pri-fg, #fff); opacity: .75; }

/* TIMTRK-192 (P4): the faint boundary line under a permission's name. Its own block rather than an
   inline span, because the two it exists for read as a sentence and would otherwise trail off the
   end of the label line at 390px. */
.permission-helper {
    display: block;
    font-size: 12px;
    margin-top: 2px;
}

/* TIMTRK-193: a locked row has nothing on its right-hand side, so the label takes the whole width
   rather than leaving a column of empty space where the radios used to be. */
.permission-row-locked { grid-template-columns: 1fr; }

/* ── TIMTRK-214: blueprints on POs — the office panel and its two upload overlays ───────────────
   Design frame 1c (panel) and 1d (replace modal), v0.5.5. Columns, gaps and the row's own hairline
   are the PM's prelim decision 1, given as literal values rather than chosen here; everything else
   is a token, per AC-U1. The two literals below (#dfe4ee for the row hairline, #fff6d9 for the
   notice fill) follow the v0.5 SPEC's own precedent — "New surfaces reuse existing tokens only:
   roster row hairline #eef1f7 …" — where a hairline or a wash is a deliberate literal because the
   palette has no token at that weight. tokens.css and design-tokens.json are untouched.

   Appended at the end of the file so these single-class rules sit below the base .table-labels and
   .btn rules they refine; they tie with them on specificity, so order is what decides. */

.blueprints-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.blueprints-title {
    margin: 0;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--mut, #5c6b85);
}

/* TIMTRK-214 (reopen), the PM's re-cut grid: type badge / name+meta / revision / actions. The
   date-and-author column is GONE — that information moved into the file cell's meta line, which is
   what buys the filename its 240px floor. Four tracks now, and the fixed ones plus gaps come to
   418px against the old 604, so the name column gets the rest instead of being squeezed to ~90px
   and wrapping. The action group is a fixed 256px shared by the header row and every data row, so
   the columns align. The e2e fit test measures the LAID-OUT tracks: 4 of them, filename ≥ 240,
   actions exactly 256, and uniform row height. */
.blueprints-grid {
    display: grid;
    grid-template-columns: 44px minmax(240px, 1fr) 88px 256px;
    gap: 10px;
    align-items: center;
}

.table-labels.blueprints-grid {
    padding: 0 8px 8px;
}

.blueprints-row {
    padding: 8px;
    margin-bottom: 7px;
    background: var(--card, #ffffff);
    border: 1px solid #dfe4ee;
    border-radius: 14px;
    font-size: 14px;
    color: var(--ink, #101c33);
}

/* The CAD row: dashed and quieted, because it is the one row nothing can render in place. Not the
   55%-opacity "closed row" treatment — this file is perfectly current, it simply has to be
   downloaded, and dimming it would say the wrong thing about its status. */
.blueprints-row-cad {
    border-style: dashed;
    color: var(--mut, #5c6b85);
}

.blueprint-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 10px;
    background: var(--chip-bg, #e8e9f4);
    color: var(--mut, #5c6b85);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
}

/* min-width:0 is load-bearing, not tidiness: a grid item defaults to min-width:auto, which refuses
   to shrink below its content's min-content width — so a long unbroken filename would push this
   track wider than 1fr and shove the actions group out of its own track instead of wrapping. */
.blueprint-file {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.blueprint-name {
    font-weight: 700;
    overflow-wrap: anywhere;
}

.blueprint-meta {
    font-size: 12px;
    color: var(--faint, #627085);
    overflow-wrap: anywhere;
}

.blueprint-rev {
    justify-self: start;
    white-space: nowrap;
}

.blueprint-actions {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-end;
    align-items: center;
    gap: 6px;
}

/* 44px tall, because SPEC's hit-target floor is a touch rule and this screen is iPad-first — but
   narrower and smaller-typed than a standing .btn, which at 52px and 22px of padding would put
   three of these well past the 256px the grid gives them (284px before the TIMTRK-214 reopen — the
   track narrowed, so the headroom here is smaller than it was and the fit test is what watches it). */
.blueprint-action {
    min-height: 44px;
    padding: 0 10px;
    font-size: 13px;
    white-space: nowrap;
}

/* "Remove faintest" (frame 1c): the same outlined pill, one step quieter, so the destructive action
   is the last thing the eye lands on rather than the first. No red — the palette has none, and
   TIMTRK-114's locked decision 5 declined to invent one. */
.blueprint-action-quiet {
    border-color: #dfe4ee;
    color: var(--mut, #5c6b85);
}

.blueprints-note {
    margin: 14px 0 0;
    font-size: 13px;
    color: var(--mut, #5c6b85);
}

/* ---- TIMTRK-214 (reopen): the way in, and the page it leads to ---- */

/* The edit-PO overlay's one navigating row (flat 2a). A plain anchor with no data-modal, so
   modal-host's delegation ignores it and the browser navigates — the row's whole mechanism is an
   attribute that is NOT here, which is why the e2e test asserts its absence rather than trusting
   this comment. Sized as a real target: 44px is SPEC's floor and this is an iPad-first screen. */
.blueprints-row-link {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 44px;
    margin-top: 14px;
    padding: 10px 12px;
    border: var(--bw, 1px) solid #dfe4ee;
    border-radius: var(--r-sm, 14px);
    color: var(--ink, #101c33);
    text-decoration: none;
}

/* The rounded tile flat 2a draws to the left of the text. Same 44px square and the same chip wash
   as .blueprint-badge, deliberately: the panel this row leads to is full of those tiles, so reusing
   the vocabulary makes the row read as a door onto that screen (assumption I — a text glyph, no
   icon font and no image asset; the pixel judgment is the PM's). */
.blueprints-row-tile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    border-radius: 10px;
    background: var(--chip-bg, #e8e9f4);
    color: var(--chip-fg, #2b3379);
    font-size: 20px;
    line-height: 1;
}

/* min-width:0 for the same min-content reason .blueprint-file documents: a long sub-line must wrap
   inside the row rather than pushing the chevron off the end of it. */
.blueprints-row-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1 1 auto;
}

.blueprints-row-title {
    font-weight: 700;
}

.blueprints-row-sub {
    font-size: 12px;
    color: var(--faint, #627085);
}

.blueprints-row-chevron {
    flex: 0 0 auto;
    font-size: 20px;
    line-height: 1;
    color: var(--mut, #5c6b85);
}

/* The manage page's breadcrumb line (flat 2b: "POs · PO-2107 · Blueprints"). Small and muted — it
   orients, it does not compete with the h1 under it. */
.blueprints-breadcrumb {
    margin: 0 0 2px;
    font-size: 12px;
    color: var(--mut, #5c6b85);
}

/* ---- the two overlays (frame 1d) ---- */

.blueprint-drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 20px 16px;
    border: 2px dashed #dfe4ee;
    border-radius: var(--r-sm, 14px);
    text-align: center;
}

.blueprint-drop-over {
    border-color: var(--acc, #ffc820);
    background: var(--chip-bg, #e8e9f4);
}

.blueprint-drop-copy {
    font-weight: 700;
    cursor: pointer;
}

.blueprint-drop-browse {
    text-decoration: underline;
}

.blueprint-drop-hint {
    font-size: 13px;
    color: var(--faint, #627085);
}

/* The native input stays visible and usable: it is the whole feature with scripting off, and hiding
   it behind the label would leave a no-script visitor a drop zone that does nothing. */
.blueprint-file-input {
    max-width: 100%;
    font: inherit;
}

.blueprint-drop-chosen {
    font-size: 13px;
    font-weight: 700;
    overflow-wrap: anywhere;
}

.blueprint-rev-field {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}

.blueprint-rev-input {
    width: 96px;
    text-align: center;
    font-weight: 700;
}

.blueprint-rev-hint {
    font-size: 13px;
    color: var(--faint, #627085);
}

/* Frame 1d's amber notice. A statement of consequence, not a warning: the border is the skin's own
   accent so it re-tints in slab, and the wash is a literal because no token sits at that weight. */
.blueprint-notice {
    margin: 0;
    padding: 12px 14px;
    background: #fff6d9;
    border: var(--bw, 1px) solid var(--acc, #ffc820);
    border-radius: var(--r-sm, 14px);
    color: var(--acc-fg, #101c33);
    font-size: 14px;
}

/* At 1920 the grid simply widens the name column; nothing else moves. Below the iPad the fixed
   tracks stop fitting, so the row stacks — this screen is office-side and not framed for a phone,
   and stacking is the honest fallback for a visitor who arrives on one anyway. */
@media (max-width: 900px) {
    .blueprints-grid {
        grid-template-columns: 44px 1fr;
    }

    .table-labels.blueprints-grid {
        display: none;
    }

    /* Re-derived against the four-track template, not preserved: the .blueprint-when selector went
       with the span it styled, so listing it here would be a dead rule pointing at nothing. */
    .blueprint-rev,
    .blueprint-actions {
        grid-column: 2;
        justify-content: flex-start;
        flex-wrap: wrap;
    }
}

/* ============================================================================
   TIMTRK-215 — the worker blueprint list (frame 1a) and the shared viewer (1b)
   ============================================================================ */

/* ---- the list screen (frame 1a) ---- */

.blueprint-head {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin-bottom: 14px;
}

/* A real 44px target, not a decorative glyph: this is the only way back on a screen with no active
   nav tab, and it is pressed with a work glove on. */
.blueprint-back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    min-width: 44px;
    min-height: 44px;
    margin-left: -10px;
    font-size: 28px;
    line-height: 1;
    color: var(--ink, #101c33);
    text-decoration: none;
}

/* min-width:0 so a long PO name wraps inside the flex row instead of widening it — the same
   min-content trap the office panel's .blueprint-file comment documents. */
.blueprint-head-text {
    min-width: 0;
}

.blueprint-head-text h1 {
    margin: 0;
    font-size: 20px;
    overflow-wrap: anywhere;
}

.blueprint-list {
    gap: 8px;
}

/* One row shape for both branches, so a CAD row and an openable row are the same object with
   different affordances rather than two layouts that have to be kept in step. */
.blueprint-row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 64px;
    padding: 8px;
    background: var(--card, #ffffff);
    border: 1px solid #dfe4ee;
    border-radius: var(--r-sm, 14px);
    color: var(--ink, #101c33);
    text-align: left;
}

/* The whole row is the tap target — the biggest one the layout can offer, which is the right call
   on a phone held in one hand at the top of a ladder. */
.blueprint-row-open {
    text-decoration: none;
}

/* Dashed and quieted, exactly as the office panel treats the same row and for the same reason: the
   file is perfectly current, it simply cannot be rendered here, so it is marked as different rather
   than dimmed as stale. */
.blueprint-row-cad {
    border-style: dashed;
    color: var(--mut, #5c6b85);
}

.blueprint-link {
    margin-top: 8px;
}

/* ---- the viewer (frame 1b) ---- */

/* Skin-invariant by the ticket: a drawing is judged against one consistent ground, never one that
   moves with a display preference. The CHROME inside it is tokened, which is why this layout still
   needs the anti-FOUC skin line. */
.viewer-shell {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    background: #101c33;
    color: #ffffff;
    overflow: hidden;
}

/* nowrap plus a shrinkable title is the whole 320 story: the three controls hold their size and the
   name gives way, rather than the bar wrapping to two lines and eating the drawing. */
.viewer-bar {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: 8px;
    padding: 6px 8px;
    background: rgba(0, 0, 0, 0.25);
}

.viewer-close,
.viewer-download {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.12);
    color: #ffffff;
    font-size: 22px;
    line-height: 1;
    text-decoration: none;
}

/* min-width:0 is what actually lets the ellipsis happen — without it the flex item refuses to
   shrink below its content and pushes the download button off a 320 screen. */
.viewer-title {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
    line-height: 1.2;
}

.viewer-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 700;
    font-size: 14px;
}

.viewer-pages {
    font-size: 12px;
    color: rgba(255, 255, 255, 0.7);
}

/* Never allowed to shrink or wrap. It is the one piece of chrome a worker must be able to read at a
   glance on every screen size — wiring from the wrong revision is the failure this feature exists
   to prevent. */
.viewer-rev {
    flex: 0 0 auto;
    white-space: nowrap;
    background: var(--acc, #ffc820);
    color: var(--acc-fg, #101c33);
}

.viewer-stage {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    /* The gesture handler owns panning and zooming, so the browser must not also try to. Without
       this the first pinch scrolls the page underneath the drawing instead of scaling it. */
    touch-action: none;
}

/* The compositor transforms both of these during a pinch; the expensive redraw happens once, on
   release (and for an <img>, never). */
.viewer-canvas,
.viewer-image {
    transform-origin: center center;
    will-change: transform;
}

/* The canvas gets NO max-* caps, deliberately (review round 1).
   It is sized in script to fit the stage on both axes, so it needs no clamping at scale 1 — and a
   cap here would actively break zoom: a baked re-render at 4x sets an explicit width of four stage
   widths, which max-width would drag straight back to fit-size the instant the fingers lift. The
   zoom would appear to work during the gesture and silently undo itself on release, which is
   exactly what AC-7 asks for and the one criterion no test in this build can judge.
   The overflow is contained by .viewer-stage's own overflow:hidden, and reaching it is what panning
   is for. */

/* An <img> keeps its caps: nothing ever rewrites its intrinsic size, so at rest it must be told to
   fit the stage, and its zoom lives entirely in the transform, which max-* does not touch. */
.viewer-image {
    max-width: 100%;
    max-height: 100%;
}

.viewer-error {
    margin: 0;
    padding: 24px;
    text-align: center;
    color: rgba(255, 255, 255, 0.75);
}

.viewer-hint {
    margin: 0;
    padding: 6px 0;
    text-align: center;
    font-size: 12px;
    color: rgba(255, 255, 255, 0.6);
}

/* Horizontally scrollable rather than wrapping: a 30-page drawing set would otherwise build a
   tile wall that swallows the screen. */
.viewer-strip {
    display: flex;
    gap: 6px;
    padding: 8px;
    overflow-x: auto;
    background: rgba(0, 0, 0, 0.25);
    /* The strip scrolls itself, so it opts back IN to touch handling the stage opted out of. */
    touch-action: pan-x;
}

.viewer-strip[hidden] {
    display: none;
}

/* flex-shrink:0 keeps the 44px floor real — without it flex would happily squeeze twelve tiles into
   a 320 screen and quietly break the touch-target rule the test measures. */
.viewer-tile {
    flex: 0 0 auto;
    min-width: 44px;
    min-height: 44px;
    border: 1px solid rgba(255, 255, 255, 0.35);
    border-radius: 8px;
    background: transparent;
    color: #ffffff;
    font-size: 13px;
    font-weight: 700;
}

.viewer-tile[aria-current="true"] {
    background: var(--acc, #ffc820);
    border-color: var(--acc, #ffc820);
    color: var(--acc-fg, #101c33);
}

/* From the iPad up there is room for the filename to breathe; nothing else about the layout
   changes, so the shell stays honest all the way out to 1920. */
@media (min-width: 768px) {
    .viewer-bar {
        padding: 10px 16px;
    }

    .viewer-name {
        font-size: 16px;
    }
}

/* The row's flex sizing, stated explicitly rather than left to the defaults.
   .blueprint-file carries min-width:0 (shared with the office panel, whose comment explains why),
   and without an explicit flex-grow it defaults to `0 1 auto` — so at 320 the badge, the rev pill
   and the CAD row's Download button take their content width first and the filename is squeezed to
   almost nothing. Observed at 320 during stage 4: "riser-diagram.dwg" wrapped ONE CHARACTER PER
   LINE, because overflow-wrap:anywhere will happily break anywhere once the box is 20px wide. */
.blueprint-row .blueprint-file {
    flex: 1 1 auto;
}

.blueprint-row .blueprint-rev,
.blueprint-row .blueprint-action {
    flex: 0 0 auto;
}

/* Even with the filename given every spare pixel, a phone-width CAD row cannot hold a badge, a
   name, a rev pill AND a button on one line. So the button drops to its own full-width line, which
   also makes it a considerably better target than a 90px pill wedged against the screen edge. */
.blueprint-row-cad {
    flex-wrap: wrap;
}

.blueprint-row-cad .blueprint-action {
    flex: 1 0 100%;
    margin-top: 8px;
}

/* From 480 up there is room for the original single-line arrangement. */
@media (min-width: 480px) {
    .blueprint-row-cad .blueprint-action {
        flex: 0 0 auto;
        margin-top: 0;
        margin-left: auto;
    }
}

/* The strip sits flush against the bottom edge of a fixed full-screen shell, which on a notched
   phone is exactly where the home indicator lives. */
.viewer-strip {
    padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
}

/* The three chrome bands never give way — only the drawing does.
   All three defaulted to `flex: 0 1 auto`, i.e. shrinkable, inside a fixed-height flex column whose
   stage wants to be far taller than the shell. Flex therefore distributed the shrinkage across the
   bands too, and the shell's overflow:hidden cropped what was left: the page-tile strip rendered
   with only the top third of each tile visible (seen at 320 and 1024 in the stage-5 captures).
   Note which tests did NOT catch it — the >=44px assertion passes because the TILES keep their
   min-height while their container clips them, and the 320 fit assertion measures horizontal
   overflow only. */
.viewer-bar,
.viewer-hint,
.viewer-strip {
    flex: 0 0 auto;
}

/* ================================================================================================
   TIMTRK-224: the in-dialog flow's views, inside the Edit PO dialog.
   TIMTRK-233 (D8): the SHELL half of this block is now neutrally named.

   Two vocabularies here, and the split is the point:

     * `.flow-view` / `.flow-head` / `.flow-head-text` / `.flow-kicker` — the shell every view
       wears: the column, the head row, the title block, the kicker line. TIMTRK-233 added a close
       confirm and a reopen view that are not about blueprints at all, so a `bp-` prefix on the
       shell would have made every non-blueprint view claim to be one. Renamed rather than
       duplicated: a second identical set of rules under a second prefix is two owners for one
       layout.
     * `.bp-flow-*` — the file cards and their furniture (`-card`, `-card-head`, `-card-cad`,
       `-tile`, `-file`, `-name`, `-name-row`, `-meta`, `-actions`, `-upload`). These stay
       blueprint-named because they ARE blueprints: nothing outside a blueprint view renders a
       revision label, a CAD chip or an upload CTA. Renaming them would have widened the diff
       without making one of them reusable.

   Both kept their own vocabulary rather than re-skinning `.blueprints-*`, and both prefixes are
   deliberately greppable. Those classes belong to _BlueprintsPanel — the 1024-wide data table the
   no-JS edit page still renders, with a fixed four-track grid — and this is a 560 card list. One
   set of rules serving both would be one owner for two unrelated layouts, and it would put the
   dialog's geometry inside the partial the no-script tier draws. (Since TIMTRK-237 that dialog is
   720 rather than 560; the argument is about which layout owns which rules, not about the number.)

   Every value below comes from an existing token or from PM decision 3's measured layout facts
   (card padding 12/14, tile 40×40 radius 12, meta 12px --faint, action row of equal 44px pills at
   8px gap, upload CTA full-width 48px). No new colours, no new type — decision 3 is explicit, and
   the palette has no danger colour to reach for in any case.

   And no `dialog.` selector anywhere in this block, deliberately. The dialog was opened by the PO
   row and carries ONE class constant for its whole life — a width rule here would be a second
   owner for a number the invoker already set (PM answer to Q1, 2026-08-25).

   TIMTRK-237 revises the NUMBER in that answer and nothing else about it. The 2026-08-26 hours
   design widens the PO edit dialog, so the row's invoker now says
   data-modal-variant="form-wide" and the dialog is `modal modal-form-wide` — 720 — for its whole
   life. Every part of the mechanism Q1 defends is intact: the invoker still owns the width, the
   class is still constant for the dialog's lifetime, and the two create modals were deliberately
   left on "form" at 560 precisely so this change could not reach them. The card list below is now
   laid out inside 720 rather than 560; it is a fluid flex column and is expected to stretch, which
   is a named item on the PM's hand-check list for this ticket.
   ============================================================================================== */

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

.flow-head {
    display: flex;
    align-items: flex-start;
    gap: 8px;
}

/* .blueprint-back is TIMTRK-215's shipped chevron pill, reused rather than re-cut. Two adjustments
   for living in a dialog instead of a page header: the page version hangs 10px into the left margin
   to optically align with a heading below it, which is wrong against the dialog's own 20px padding;
   and view 2's back is a <button> (there is no URL for "back to the edit view"), so it needs the
   button chrome reset that an <a> never did. */
.flow-head .blueprint-back {
    margin-left: 0;
    padding: 0;
    background: none;
    border: 0;
    cursor: pointer;
    font-family: inherit;
}

/* min-width:0 so a long PO name wraps inside the flex row instead of widening it — the same
   min-content trap .blueprint-file and .blueprint-head-text both document. */
.flow-head-text {
    min-width: 0;
    flex: 1 1 auto;
}

.flow-head-text h2 {
    margin: 0;
    font-size: 20px;
    overflow-wrap: anywhere;
}

/* The kicker line: "{code label} · {display name}". The same treatment as .blueprints-breadcrumb,
   which is the shipped word for "small muted line that orients above a title". */
.flow-kicker {
    margin: 0 0 2px;
    font-size: 12px;
    color: var(--faint, #627085);
    overflow-wrap: anywhere;
}

/* TIMTRK-233: view six's consequence list. Layout only — no colour and no type scale of its own, so
   the bullets inherit the dialog's body text exactly as the paragraphs around them do. The gap is
   the flow shell's own 10px stepped down one notch: five consecutive sentences need separating from
   each other more than they need separating from the note beneath them. */
.flow-consequences {
    margin: 0;
    padding-left: 20px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* One file. Decision 3's 12/14, and the card border/radius the panel's rows already use. */
.bp-flow-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 12px 14px;
    border: var(--bw, 1px) solid #dfe4ee;
    border-radius: var(--r-sm, 14px);
}

/* CAD keeps the dashed treatment flat 4b draws — the same signal .blueprints-row-cad carries on the
   panel, so a file that cannot be opened in place says so the same way on both surfaces. */
.bp-flow-card-cad {
    border-style: dashed;
}

.bp-flow-card-head {
    display: flex;
    align-items: flex-start;
    gap: 10px;
}

/* 40×40 at radius 12 — decision 3's number, and smaller than the row link's 44px tile on purpose:
   this one is an identifier inside a card, not a touch target. */
.bp-flow-tile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    border-radius: 12px;
    background: var(--chip-bg, #e8e9f4);
    color: var(--chip-fg, #2b3379);
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
}

.bp-flow-file {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1 1 auto;
}

.bp-flow-name-row {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

/* AC-2's ellipsis. min-width:0 on both this and its parents is what makes it possible at all: a
   flex item's default min-width is min-content, so without it the name would refuse to shrink and
   would push the rev pill out of the card instead of clipping. */
.bp-flow-name {
    min-width: 0;
    flex: 1 1 auto;
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Folded UNDER the name at 560 (decision 3). On the 1024 panel this shares a row with the filename;
   there is no room for that here, and flat 4b folds rather than truncating either. */
.bp-flow-meta {
    font-size: 12px;
    color: var(--faint, #627085);
}

/* Decision 3's action row: equal-width pills, 8px gap, 44px floor. flex-basis 0 with flex-grow 1 is
   what makes them EQUAL rather than content-sized — at 560 outer / 520 content / 492 card-inner
   that is roughly 158px each, comfortably over any of the six labels. min-width:0 again, so a pill
   whose label is wider than its share clips instead of stretching the row past the card. */
.bp-flow-actions {
    display: flex;
    gap: 8px;
}

.bp-flow-actions > .btn {
    flex: 1 1 0;
    min-width: 0;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-align: center;
}

/* The full-width 48px CTA, below the list. Taller than the 44px floor because decision 3 says so
   and because it is the one primary action on the view. */
.bp-flow-upload {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 48px;
}

/* ==============================================================================================
   TIMTRK-237 — the PO hours strip, the wide PO form grid, and the phone layouts for both.

   Every value below comes from an existing token. Both skins are correct with zero skin-specific
   rules: the stripe derives from --pri against --chip-bg, which is #2b3379 on #e8e9f4 in the
   default skin and #111111 on #ffffff in slab — both comfortably separated, so the handoff's
   "swap --chip-bg for --faint if slab's is too close to white" escape hatch is NOT needed and is
   deliberately not taken. It stays available as a one-token change if the PM's eye disagrees.

   Media queries, never container queries: this stylesheet has never used one (grep @container
   across wwwroot/css finds nothing), and introducing the first instance inside a feature build is
   how a codebase ends up with two responsive vocabularies.
   ============================================================================================== */

.po-hours {
    border: var(--bw, 1px) solid var(--line, #ffc820);
    border-radius: var(--r-sm, 14px);
    background: var(--pg, #f3f5f9);
    padding: 14px 16px;
}

.po-hours-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
}

/* Two cells, not three, whenever there are no carried hours to show — the imported cell is absent
   from the markup rather than rendered empty, so the track count has to follow it.

   A server-rendered modifier class rather than a `:has()` selector on the cell count. Not because
   `:has()` is unavailable — this stylesheet already uses it in six places and it stays — but
   because the shape is ALREADY decided, by PoHoursStripModel, before any of this reaches a
   browser. Asking the browser to re-derive it from the markup would put a second owner on a
   decision that has one, which is the same reasoning that keeps every width on the invoker. */
.po-hours-grid--pair { grid-template-columns: repeat(2, 1fr); }

.po-stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.po-stat-value {
    font-family: var(--f-head, 'Bricolage Grotesque', sans-serif);
    font-weight: var(--w-head, 800);
    font-size: 22px;
    color: var(--ink, #101c33);
}

.po-stat-value small {
    font-size: 13px;
    color: var(--faint, #627085);
    font-weight: 600;
}

.po-stat-sub {
    font-size: 12px;
    color: var(--faint, #627085);
}

/* The degenerate form: a service call, or a job with neither a budget nor carried hours. One quiet
   line, no bar, no percentage — there is nothing to compare against. */
.po-hours-line {
    margin: 0;
    color: var(--mut, #5c6b85);
    font-size: 13px;
}

/* Legend dots. They carry the SAME fills as the bar segments, which is what makes striped-vs-solid
   readable without relying on hue — it survives both skins and colour blindness. */
.po-key {
    width: 10px;
    height: 10px;
    border-radius: calc(var(--r-sm, 14px) / 5);
    display: inline-block;
    margin-right: 6px;
    vertical-align: baseline;
}

.po-key--imported,
.po-bar-seg--imported {
    background: repeating-linear-gradient(45deg,
        var(--pri, #2b3379) 0 6px, var(--chip-bg, #e8e9f4) 6px 12px);
}

.po-key--logged,
.po-bar-seg--logged { background: var(--pri, #2b3379); }

.po-bar {
    margin-top: 12px;
    height: 10px;
    border-radius: var(--r-pill, 99px);
    background: var(--chip-bg, #e8e9f4);
    overflow: hidden;
    display: flex;
}

.po-bar-caption {
    margin-top: 8px;
    display: flex;
    justify-content: space-between;
    gap: 12px;
    font-size: 12px;
    color: var(--mut, #5c6b85);
}

.po-bar-caption strong { color: var(--pri, #2b3379); }

/* Over budget. The handoff's own rule set background AND color on both selectors, which put
   danger-coloured text on a danger-coloured background in the caption — a mock artifact with a
   visible intent. Split: the segment takes the fill, the caption takes the ink. */
.po-hours--over .po-bar-seg--imported {
    background: repeating-linear-gradient(45deg,
        var(--danger, #b3261e) 0 6px, var(--chip-bg, #e8e9f4) 6px 12px);
}

.po-hours--over .po-bar-seg--logged { background: var(--danger, #b3261e); }

.po-hours--over .po-bar-caption strong { color: var(--danger, #b3261e); }

/* The kind chip. Ghost-styled, as the design draws it, but its OWN class: a closed PO's status
   chip is a .chip-ghost in the same heading, and sharing the class would make any "the ghost chip
   in the header" selector — markup, CSS or test locator — match two different facts. */
.chip-kind {
    background: var(--card, #ffffff);
    color: var(--faint, #627085);
}

/* The chips beside the modal title. .modal-head is a flex row whose last child is the close
   button, so the chips need a wrapper to sit as one item beside the heading rather than pushing it. */
.modal-head-chips {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-right: auto;
}

/* The job edit form at 720. Name and Notes span both tracks — the handoff's "things wrong" list
   names them by name, because "6022 Helena Christian School" truncates in a half-width field. */
.po-form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    align-items: start;
}

.po-form-grid > .span-2 { grid-column: 1 / -1; }

/* One grid cell holding a field AND whatever it says about itself — a locked-code hint, a field
   error. Without it those sentences are grid items of their own and land in the next field's
   column. Column direction so the message sits under its own control, never beside it. */
.po-form-cell {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

/* Under a 640px viewport the wide dialog becomes a full-width bottom sheet, top corners only. */
@media (max-width: 640px) {
    dialog.modal-form-wide {
        width: 100vw;
        max-width: 100vw;
        max-height: 100dvh;
        border-radius: var(--r-lg, 24px) var(--r-lg, 24px) 0 0;
        margin: auto 0 0;
    }
}

/* The handoff's @container (max-width: 560px) block, as the media query its own comment names as
   the fallback. 600 rather than 560 because the dialog is 92vw at these sizes: a 600px viewport
   gives a ~552px dialog, which is already past the point the 3-up cells stop fitting.

   The two grid rules are safe globally — both classes are new and PO-only. The FOOTER rule is
   scoped, and that scoping is the point: .modal-foot is shared by every modal in the app, so an
   unscoped stacking rule would silently restack People, the blueprint overlays and the confirms. */
@media (max-width: 600px) {
    .po-hours-grid,
    .po-hours-grid--pair,
    .po-form-grid { grid-template-columns: 1fr; }

    /* Label left, value right — the phone flat's shape. The bar and the percentage stay. */
    .po-hours-grid .po-stat {
        flex-direction: row;
        align-items: baseline;
        justify-content: space-between;
        flex-wrap: wrap;
        gap: 8px;
    }

    .po-hours-grid .po-stat-sub {
        flex-basis: 100%;
        text-align: right;
    }

    dialog.modal-form-wide .modal-foot,
    .po-form-grid .modal-foot,
    .po-form-grid .form-actions {
        flex-direction: column-reverse;
        align-items: stretch;
    }

    dialog.modal-form-wide .modal-foot .btn,
    .po-form-grid .modal-foot .btn,
    .po-form-grid .form-actions .btn { width: 100%; }
}

/* ---------------------------------------------------------------------------------------------
   TIMTRK-238 — the task pill + popover, and the two admin panels.

   Tokens only, no new tokens (D4): design-tokens.json is a byte copy of v0.4 and this build adds
   nothing to it. The design's literals map straight onto what is already there — #2b3379 is --pri,
   #ffc820 is --acc — so both skins render both pills correctly without a skin-specific rule. The
   Slab skin's pri (#111111) and acc (#ff5a00) simply flow through.
   --------------------------------------------------------------------------------------------- */

/* The anchor — for the ISLAND's measurement, not for the CSS's own positioning any more
   (TIMTRK-238 clip fix, PM 2026-09-03). Out of flow is still the whole no-reflow mechanism, and the
   e2e tier still asserts the form's geometry is unchanged while the popover is open — the failure a
   320px phone would meet first is the Save button pushed off-screen. What changed is WHERE out of
   flow puts it: the popover is `position: fixed` and placed from the pill button's own rect, so
   this box no longer resolves its offsets. `position: relative` is kept deliberately rather than
   deleted — `.task-pick` is the mount the outside-tap handler's `closest()` walk resolves to and
   the box the office/worker skin rules key on, and removing a declaration this fix does not need is
   a change nobody asked for. */
.task-pick {
  position: relative;
  margin: 14px 0;
}

/* The office pill sits on the action row rather than in the form flow, so it takes no margin of
   its own and lets .day-add-actions do the spacing. */
.task-pick-office {
  margin: 0;
}

.task-pick-label {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mut, #5c6b85);
}

/* SPEC: solid --pri pill, 44px min-height (the PM's touch-first provision, same number
   .badge-btn uses), caret on the right. */
.task-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  max-width: 100%;
  padding: 0 14px;
  border-radius: var(--r-pill, 99px);
  border: var(--bw, 1px) solid transparent;
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}

/* D2 / artboard 1h: yellow shell, ink text, blue outline. Office only — the worker pill stays
   blue, and the two are told apart by the variant class the partial renders, never by the page. */
.task-pick-office .task-pill {
  background: var(--acc, #ffc820);
  color: var(--acc-fg, #101c33);
  border-color: var(--pri, #2b3379);
}

/* Long task names ellipsize rather than wrapping: a wrapped pill would grow the form, which is
   the one thing the popover being out of flow (`position: fixed`, placed by the island — see
   .task-popover below) exists to prevent. */
.task-pill-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The DEFAULT badge — accent on the blue pill, blue on the yellow one (1a / 1h). Shown iff the
   selection equals the role default; the island owns that, this owns only the look. */
.task-pill-badge {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: var(--r-pill, 99px);
  background: var(--acc, #ffc820);
  color: var(--acc-fg, #101c33);
  font-size: 10px;
  letter-spacing: 0.06em;
}

.task-pick-office .task-pill-badge {
  background: var(--pri, #2b3379);
  color: var(--pri-fg, #ffffff);
}

.task-pill-caret {
  flex: 0 0 auto;
  font-size: 12px;
}

/* SPEC, verbatim: width 240, white card, --line border, r 18, shadow 0 18px 44px rgba(16,28,51,.28),
   48px rows. The radius and the shadow are the only literals here — SPEC states both as exact
   values and neither has a token, and D4 forbids inventing one.

   `fixed`, not `absolute` (TIMTRK-238 clip fix, PM 2026-09-03): `dialog.modal` is its own
   scroll container (`overflow: auto`), and an absolutely-positioned descendant is clipped at
   its padding box — which cut the office popover to a sliver. A fixed box positions against
   the viewport instead (no ancestor establishes a fixed containing block — the only
   transform/filter/contain/will-change hits in this file are unrelated components), and since
   the day modal is a `showModal()` dialog the popover stays INSIDE the dialog's top layer, so
   it paints above the modal and its backdrop with no z-index war. Fixed coordinates are
   viewport coordinates, so the [data-direction] rules that used to anchor it here moved into
   task-pill.ts (`positionPopover`), which sets inline top/left (down) or bottom/right (up)
   from the pill's rect on every open, scroll and resize; direction still arrives on the mount
   as data-direction. The overflow-y pairs with a max-height the island computes, and it is NOT
   dormant: the room above the pill is min(content above it, 85vh - 64), so it shrinks with the
   viewport, and at 1024x480 — near the ~520px window this clip was reported at — 398px of popover
   meets ~343px of room and the list scrolls internally. That is the trade, deliberately: never
   clipped beats never scrolls. The plan's "never engages at any supported viewport" was measured
   false and is corrected here.

   Deliberately DIVERGES from .gc-picker-list (TIMTRK-225), which clamps and stays inside this
   same dialog: its AC said "anchored under the input"; the PM ruled the OPPOSITE here on
   2026-09-03 — never clipped, escape the modal's edge. Two dropdowns, two rulings; neither is
   a bug. */
.task-popover {
  position: fixed;
  z-index: 20;
  width: 240px;
  max-width: calc(100vw - 32px);
  overflow-y: auto;
  padding: 6px;
  border-radius: 18px;
  border: var(--bw, 1px) solid var(--line, #ffc820);
  background: var(--card, #ffffff);
  box-shadow: 0 18px 44px rgba(16, 28, 51, 0.28);
}

.task-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  min-height: 48px;
  padding: 0 12px;
  border: 0;
  border-radius: var(--r-sm, 14px);
  background: transparent;
  color: var(--ink, #101c33);
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
}

/* Selected row: --chip-bg at weight 700 — the parent canvas's own rule, and the reason the
   selection is legible without a tick column. */
.task-option[aria-selected="true"] {
  background: var(--chip-bg, #e8e9f4);
  font-weight: 700;
}

/* SPEC: the "No task" row is muted --- UNLESS it is the selection. The second rule is not
   redundant with the selected rule above: that one sets background and weight but no colour, so
   without this a chosen "No task" would sit on the selected background still greyed out. */
.task-option-none {
  color: var(--mut, #5c6b85);
}

.task-option-none[aria-selected="true"] {
  color: var(--ink, #101c33);
}

.task-option-badge {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: var(--r-pill, 99px);
  background: var(--chip-bg, #e8e9f4);
  color: var(--chip-fg, #2b3379);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
}

/* The one animation, and it is opt-in: nothing moves for anyone who asked for less motion (D4).
   Kept to opacity so the popover never changes size — a scaling entrance would reflow nothing but
   would read as the form jumping. */
@media (prefers-reduced-motion: no-preference) {
  .task-popover {
    animation: task-popover-in 120ms ease-out;
  }

  @keyframes task-popover-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}

/* The day modal's action row: pill right-aligned OPPOSITE the Add hours button (SPEC screen 0). */
.day-add-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 12px;
}

/* The per-row task select. Sized like .day-entry-hmm's sibling inputs so the row keeps one
   vocabulary — this is the affordance no artboard drew, so it follows the row it lives in. */
.day-entry-task {
  min-height: 44px;
  max-width: 160px;
}

/* --- Admin panels (artboards 1d and 1e), iPad-first 1024 → 1920 --- */

.task-panel-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
}

.task-add-form,
.task-rename-form,
.role-default-form {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.task-list,
.role-default-list {
  list-style: none;
  margin: 14px 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.task-row,
.role-default-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 8px 0;
  border-bottom: var(--bw, 1px) solid var(--line, #ffc820);
}

/* Inactive rows read muted, and the service already sorts them last (1d). */
.task-row-off {
  opacity: 0.62;
}

.task-name-input {
  min-height: 44px;
  min-width: 180px;
}

.task-row-meta,
.role-default-name {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  flex: 1;
  font-size: 14px;
}

/* --- TIMTRK-299: the catalogue's active control as the pill switch artboard 1d draws ------------
   Nothing about the CONTROL changed: it is still a submit button inside its own form, posting
   the state it moves to, because this page works with scripting off and a checkbox cannot post
   on its own. These rules are a skin on that button. 1d's numbers are used as drawn — a 42×24
   track and an 18px knob inset 3px — but drawn by the button's two pseudo-elements INSIDE a
   44×44 button, so the touch target .badge-btn gave it (the PM's touch-first provision) survives
   the smaller pill. Colours are tokens only: --pri is #111111 in the slab skin and this card is
   captured under both, so a literal #2b3379 would be right in exactly one of them. */
.task-toggle {
  position: relative;
  display: inline-block;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}

/* The track: 42×24 at (1, 10) inside the 44×44 button. --chip-bg is #ffffff in the slab skin,
   where an unbordered off-track would vanish into the white card, so the track carries the
   --line ring every .chip and .badge-btn already wears; the on-state paints that ring --pri so the
   on-track is still the flat solid pill 1d draws. The border width is the same in both states,
   so switching never changes the pill's size. */
.task-toggle::before {
  content: "";
  position: absolute;
  left: 1px;
  top: 10px;
  width: 42px;
  height: 24px;
  border-radius: var(--r-pill, 99px);
  background: var(--chip-bg, #e8e9f4);
  border: var(--bw, 1px) solid var(--line, #ffc820);
}

.task-toggle[aria-pressed="true"]::before {
  background: var(--pri, #2b3379);
  border-color: var(--pri, #2b3379);
}

/* The knob: 18px, 3px inside the track's left edge when off (1 + 3 = 4) and its right edge when
   on (43 − 3 − 18 = 22), vertically centred on the track (10 + 3 = 13). 1d gives the off-knob a
   hairline (#c3cbda, a colour no token carries) so white-on-pale-grey has an edge; --faint is the
   nearest token and it disappears on the on-state, as drawn. */
.task-toggle::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 13px;
  width: 18px;
  height: 18px;
  border-radius: var(--r-pill, 99px);
  background: var(--pri-fg, #ffffff);
  border: 1px solid var(--faint, #627085);
}

.task-toggle[aria-pressed="true"]::after {
  left: 22px;
  border-color: transparent;
}

/* No global :focus-visible rule to fall back on — every focusable surface states its own. */
.task-toggle:focus-visible {
  outline: 3px solid var(--acc, #ffc820);
  outline-offset: 2px;
  border-radius: var(--r-pill, 99px);
}

/* The warning is a full-width line under the row it belongs to, not a chip beside it: it names
   what was taken away and why, which is more than a chip can carry (A5). */
.role-default-warning {
  flex-basis: 100%;
  margin: 6px 0 0;
}

/* --- TIMTRK-299: the role-default select as the dropdown artboard 1e draws ---------------------
   Scoped to this card, deliberately: a bare `select { … }` would also repaint the report and
   audit filters (.filter-select), .day-add's select and .day-entry-task, none of which this
   hotfix owns — the app carries two select vocabularies until a later ticket unifies them (PM,
   2026-09-04). appearance: none drops the browser's own arrow; the caret is the same "▾" span
   _TaskPill.cshtml renders, hidden from the accessible name and lifted out of the hit-test so a
   tap on the glyph still opens the select under it. 1e's #dfe4ee border and 12px radius map to
   --line and --r-sm — the call .input already makes — so both skins come for free. */
.role-default-select {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.role-default-select select {
  appearance: none;
  -webkit-appearance: none;
  min-height: 44px;
  min-width: 210px;
  padding: 0 40px 0 14px;
  border: var(--bw, 1px) solid var(--line, #ffc820);
  border-radius: var(--r-sm, 14px);
  background: var(--card, #ffffff);
  color: var(--ink, #101c33);
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}

/* 1e: the "No default" state is muted. Decided by the row model (TaskId is null) and rendered as
   a modifier class rather than read back from the markup with :has() — the same reasoning as
   .po-hours-grid--pair: one owner for a decision already made server-side. Specificity matches
   the rule above (0,2,1 beats 0,1,1), which is why the modifier is written on the select. */
.role-default-select select.role-default-select-none {
  color: var(--faint, #627085);
}

/* The list itself stays legible whatever the closed control's colour: some engines paint the
   dropdown's options in the select's own colour. */
.role-default-select option {
  color: var(--ink, #101c33);
}

.role-default-caret {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  color: var(--faint, #627085);
  font-size: 14px;
  line-height: 1;
}

.role-default-select select:focus-visible {
  outline: 3px solid var(--acc, #ffc820);
  outline-offset: 2px;
}

/* ---- TIMTRK-389: the worker job-details block (address / contact / phone) ---------------- */

/* Under the code line of every worker job card — /today job cards, the /today new-assignment
   banner and /schedule cards — drawn by one partial (_JobDetails.cshtml). A label column and a
   value column, so "Address / Contact / Phone" read as plain words (A9) and a wrapped address
   stays in its own cell instead of running under the label. */
.job-details {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 12px;
  row-gap: 2px;
  align-items: center;
  margin: 8px 0 0;
}

.job-details dt {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--faint, #627085);
}

.job-details dd {
  margin: 0;
  font-size: 15px;
  color: var(--ink, #101c33);
  overflow-wrap: anywhere;
}

/* The .tel-link treatment (TIMTRK-165), copied rather than shared because that class is named for
   phones and a maps link under it misleads: a present address or phone is there to be tapped in
   gloves, so it is a 44px target, not a line of text that happens to carry an href. */
.job-details-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: inherit;
  text-decoration: none;
  border-bottom: var(--bw, 1px) solid var(--line, #d7dae8);
}

.job-details-link:hover,
.job-details-link:focus-visible {
  color: var(--pri, #2b3379);
  border-bottom-color: var(--pri, #2b3379);
}

/* On the accent banner the card's ink is --acc-fg; the block follows it rather than painting the
   muted greys and navy hover onto yellow. */
.assignment-banner .job-details dt,
.assignment-banner .job-details dd {
  color: var(--acc-fg, #101c33);
}

.assignment-banner .job-details-link:hover,
.assignment-banner .job-details-link:focus-visible {
  color: var(--acc-fg, #101c33);
  border-bottom-color: var(--acc-fg, #101c33);
}

/* --- TIMTRK-413: "Hours by worker" on the PO modal ---------------------------------------------
   The Reports drill-down (TIMTRK-180, .worker-drill above) scoped to one PO. Everything the
   drill-down already owns is REUSED — .worker-drill, .worker-summary, .drill-marker,
   .drill-day-row, and _CapacityBar's .share-bar — and these rules are only what one PO needs on
   top of it: the section frame, the two grids the approved mockup draws (Share of logged in place
   of Capacity; a day row is date · note · state chip · h:mm), the dialog-sized share bar, and the
   pre-logged dash on a solid chip. Tokens only; every fallback hex here already appears in this
   file. */
.po-worker-hours {
  margin-top: 14px;            /* + the host form's 12px gap = the mockup's 26px above the rule */
  padding-top: 18px;
  border-top: var(--bw, 1px) solid var(--line, #ffc820);
}

.po-worker-hours-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.po-worker-hours-title {
  margin: 0;
  font-size: 18px;
}

.po-worker-hours-lead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin: 10px 0;
  font-size: 13px;
}

.po-worker-legend {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}

/* The same leading 28px marker column .report-grid carries; the other tracks are the mockup's. */
.po-worker-grid {
  grid-template-columns: 28px 1.7fr 0.55fr 1.35fr 0.6fr;
}

/* A day row inside a worker: date · note · state chip · h:mm. Inset and dashed by .drill-day-row. */
.po-worker-day-grid {
  grid-template-columns: 0.9fr 1.7fr 0.9fr 0.55fr;
}

.po-worker-sub {
  display: block;
  font-size: 12px;
}

.po-worker-share {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* _CapacityBar's .share-bar is sized for a Reports row (max 260px); the 720 dialog gets the
   mockup's 150. Scoped to this grid so Reports keeps its width. */
.po-worker-grid .share-bar {
  max-width: 150px;
}

/* TIMTRK-393/417's pre-logged dash on the section's SOLID (approved) chip: currentColor is white
   on navy, so the dash takes the accent — the same exception .day-chip-approved.day-chip-prelog
   makes on the approvals grid. The accent chip keeps its dark currentColor dash. */
.chip-solid.day-chip-prelog {
  border-color: var(--acc, #ffc820);
}

/* The mockup's empty state: one dashed row on the page ground. */
.po-worker-empty {
  grid-template-columns: 1fr;
  border-style: dashed;
  background: var(--pg, #f3f5f9);
}

.po-worker-harvest {
  margin: 2px 0 0;
  font-size: 12px;
}
