/* JHL Constructors brand.
   Every hex below is copied from ~/.claude/brand/brand.css (generated from the
   JHL Brand Guidelines 2026, rev. 01.2026). Do not retype a brand hex from
   memory -- near-miss golds (#F4A700, #F6A800) are in circulation and wrong.

   Rich Gold #F5A800 is only ~2:1 against white, so it cannot carry text on a
   light ground and cannot be a 1px hairline there either. The prominence order
   in BRAND.md is Warm Gray -> Rich Gold -> White -> Black, and that is how the
   roles below are split: Warm Gray does the heavy lifting, gold marks things. */

/* ---- What the four colours mean -----------------------------------------
   One hue per role. Colour states a fact here; it never decorates, and it is
   never added because an area looked plain.

     RED    --danger   A problem, a refusal, a hard block. Something is wrong,
                       or something cannot happen: an error container, a
                       rejected request, a bus period, a gate with no room left.
     GREEN  --ok       Approved, confirmed, all clear. A discrete state that IS
                       true -- the approved pill, the notice after a write.
                       Never an absence: an empty list is quiet, not green.
     GOLD   --accent   Action owed by the person looking at the screen, and
                       nothing else below the menu bar: the pending count, the
                       pending badge, the view-as banner. Never a state, never
                       caution (BRAND.md), never text or a 1px line on a light
                       ground (~2:1 -- it is a fill under #323436, or a 2px line).
     BLUE   --info     Informational and factual: today, now, counts of things
                       that are simply happening.

   Two carve-outs, both pre-existing and both deliberate:
     - The chrome is brand, not semantics: the 2px gold rule under the menu bar
       and the gold badge on the current job site sit above the page, not in it.
     - Focus and hover are gold everywhere. That is the interface answering the
       pointer, not a meaning being assigned.

   Everything else is carried by Warm Gray. If you are reaching for a fifth hue,
   or a second shade of one of these four, the answer is Warm Gray.

   Every pair below states its measured ratio: 4.5:1 for text, 3:1 for a line or
   an edge. Adding a pair means adding the number. */
:root {
  /* Tells the browser which way its own widgets should render -- without this,
     UA defaults (scrollbars, date pickers, and the default link colour) stay in
     light mode even when the page is dark. theme-dark.css sets the other half;
     if you ever add a third theme, it needs this line too. */
  color-scheme: light;

  /* Primary palette, verbatim. */
  --jhl-gold: #F5A800;        /* Rich Gold  - Pantone 130 C */
  --jhl-warm-gray: #53575A;   /* Warm Gray  - Pantone 425 C */
  --jhl-light-gray: #BBBBBB;  /* Light Gray - Pantone Cool Gray 4 C */
  --jhl-blue: #005589;        /* Blue       - Pantone 7462 C */

  /* Surfaces and ink. The whole neutral ramp is Warm Gray and Light Gray. */
  --bg: #F1F1F1;              /* Light Gray 20% tint */
  --card: #ffffff;
  --text: #323436;            /* Warm Gray 60% shade -- 12.5:1 on white */
  --muted: var(--jhl-warm-gray);  /* 7.3:1 on white, 6.5:1 on --bg */
  --border: #E4E4E4;          /* Light Gray 40% tint */

  /* Colour roles. Read these, not the raw palette, in the rules below. */
  --accent: var(--jhl-gold);  /* rules, focus rings, active marks, fills behind dark ink */
  --accent-edge: #C48600;     /* Rich Gold 80% shade -- 3.1:1 on white, so gold-as-a-line works */
  --on-accent: #323436;       /* ink on a gold fill -- 6.2:1. Never white on gold. */
  --action: var(--jhl-warm-gray);  /* primary buttons: gold cannot carry white text, Warm Gray can */
  --on-action: #ffffff;       /* 7.3:1 on Warm Gray */
  --link: var(--jhl-blue);    /* 7.9:1 on white */
  /* Danger is semantic, never gold: BRAND.md is explicit that Rich Gold must
     not mean "warning". This is deliberately not a brand colour. */
  --danger: #B3261E;          /* 6.5:1 on white, both as text and behind white text */
  --on-danger: #ffffff;

  /* Green is a state that IS true -- an approved request, a write that landed
     -- and never the absence of one. An empty list stays quiet. Not a brand
     colour, for the same reason --danger is not: meaning outranks branding. */
  --ok: #1F7A3D;              /* 5.4:1 on --card, 4.8:1 on --bg */
  --on-ok: #ffffff;           /* 5.4:1 on --ok */
  /* The auxiliary hue is the brand's own Blue rather than an amber: a second
     warm yellow would be a near-miss gold. It is deliberately the same value
     as --link, not a neighbour of it -- one hue doing two jobs. */
  --info: var(--jhl-blue);    /* 7.9:1 on --card, 7.0:1 on --bg */
  --on-info: #ffffff;         /* 7.9:1 on --info */
  /* A booked slot of time. This was gold, which is how gold came to mean five
     different things at once. */
  --booked: var(--jhl-warm-gray);  /* 7.3:1 against the white track */
  --on-booked: #ffffff;       /* 7.3:1 on --booked */
  --booked-edge: #323436;     /* 12.5:1 on the white track */

  /* Corners. This is an instrument -- a dispatch console, not a consumer app
     -- so the corners are close to square and the scale is deliberately
     tiny: 2px reads as "machined edge" while still keeping a 1px border from
     looking ragged where it turns. Anything on the schedule grid itself is
     fully square, because a gold block with a soft corner reads as a badge
     rather than a slot of time.

     Assigned by the role of the element, never by its size. A control with a
     corner of its own is a bug, not a variant. */
  --radius-mark: 0;            /* chips on the grid: blackout label, legend key */
  --radius-sm: 0;              /* things that sit on the grid: delivery blocks */
  --radius-control: 2px;       /* everything clickable, typable or focusable */
  --radius-lg: 2px;            /* scroll containers */
  --radius-panel: 2px;         /* dropdowns and the block panel */
  --radius-container: 2px;     /* cards and panes */
  --radius-pill: 999px;        /* status pills, and nothing else */
}
/* The dark answer to every token above lives in public/theme-dark.css, which
   page-top.ejs links when the theme calls for it -- unconditionally if the
   viewer chose Dark, under a prefers-color-scheme media attribute if they
   left it on System, and not at all if they chose Light. Add a token here,
   add its dark value there. */

/* Reserve the scrollbar's width whether or not there is one to show. Without
   this, moving from a short page to a tall one slides the centred card
   sideways by the scrollbar's width -- visible on every Windows and Linux
   browser, and distracting on a screen someone keeps open all day.

   This applies to the viewport scroller: `html` has the default
   `overflow: visible`, which propagates there. Do not also add
   `overflow-y: scroll` -- that is the older hack this replaces, and having
   both is just confusing.

   Safari only shipped this in 18.2. Older Safari ignores it and keeps today's
   behaviour, which is harmless: macOS and iOS use overlay scrollbars, so there
   is no shift there to fix.

   Inner scroll containers are not covered -- .calendar-scroll and
   .switcher-menu still nudge their own content. That is pre-existing and
   accepted; this is a page-level fix. */
html { scrollbar-gutter: stable; }
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  /* A column holding one .frame, which grows to fill it -- that is what lets
     the footer's margin-top:auto pin it to the bottom of a short page. */
  display: flex;
  flex-direction: column;
  padding: 16px;
  background: var(--bg);
  color: var(--text);
  /* Roboto and Arial are the brand faces. Neither is webfont-loaded here -- no
     build step, and a utility app should not pay for 119KB of woff2 -- but
     Roboto is the system face on Android (our primary external client) and
     Arial is the universal fallback the guide names for cross-device work. */
  font: 16px/1.55 Roboto, system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
}
/* Links that no other rule claims. Without this they fall back to the browser's
   default link blue, which is off-brand on white and nearly unreadable on the
   dark card. Every more specific link rule below still wins. */
a { color: var(--link); }
/* The page frame: the same column on every signed-in page, with the menu bar
   and the footer spanning it. It exists so the chrome never moves -- it used
   to sit inside the card and took the card's width, so the menus jumped
   between 560, 1100 and 2000px as you changed page. Content keeps a readable
   width and is centred in the frame. 1600px rather than the full monitor, so
   no page runs edge to edge and the schedule still has room for its rail. */
.frame {
  width: 100%;
  max-width: 1600px;
  margin: 0 auto;
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
}
/* Sign-in has no chrome to hold still, so its frame is the card's own width
   and the card sits centred, as it always has. */
.chrome--logo .frame { max-width: 560px; }
.frame > .card { align-self: center; }
@media (min-width: 720px) {
  body { padding: 20px 24px 32px; }
}
/* On a monitor, a margin you can see on both sides before the frame's own
   bound takes over. */
@media (min-width: 1100px) {
  body { padding: 24px 40px 32px; }
}
.card {
  width: 100%;
  max-width: 560px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-container);
  padding: 32px;
}
@media (max-width: 420px) {
  body { padding: 12px; }
  .card { padding: 20px 18px; }
}
/* The SiteSync lockup: the leaned Switchback S and the Roboto 900 italic
   wordmark, as one piece of artwork built from the brand guide ("SiteSync on
   JHL Brand"). The wordmark is outlined in the SVG rather than typed, because
   Roboto is not installed on the Windows desktops the admins use and any
   fallback face would draw a different wordmark.

   On light grounds the wordmark and the mark's upper half are Charcoal and
   its lower half is gold. The guide's light lockup is one colour, but in
   Charcoal alone the two halves merge into one heavy letter and stop reading
   as a mark. The mark carries no information, so gold's ~2:1 here is not the
   problem it is for text. theme-dark.css swaps Bone in for Charcoal, so the
   markup
   never names a file. background-size:contain never distorts the artwork, and
   only the variant the theme needs is ever fetched -- a background-image the
   cascade discards is never requested. These url()s carry no ?v= stamp, and
   the host caches static files for 31 days: changing the artwork means a new
   file name, or the old drawing stays in every returning browser. Every size
   below keeps the artwork's
   own ratio, 750.8 x 140.48 (5.34:1). */
.brand-logo {
  display: block;
  width: 192px;
  height: 36px;
  margin-bottom: 14px;
  background: url("/sitesync-lockup.svg") left center / contain no-repeat;
}
/* Sign-in carries the endorsed lockup: the same artwork over a 2px gold rule
   and "A JHL CONSTRUCTORS PLATFORM". It is the one screen every vendor is sure
   to see, so it is where SiteSync says whose it is. 240px wide, where the rule
   lands at exactly 2px; the guide's floor is 160px, below which the
   descriptor closes up. Ratio 750.8 x 280.74 (2.67:1). The descriptor stays
   Charcoal, because it is text. */
.brand-logo--endorsed {
  width: 240px;
  height: 90px;
  margin-bottom: 20px;
  background-image: url("/sitesync-endorsed.svg");
}
h1 { margin: 0 0 8px; font-size: 26px; }
p { margin: 0 0 16px; color: var(--muted); }
code {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
  font-size: 14px;
}
.status { font-size: 14px; color: var(--muted); }
/* ---- Menu bar -----------------------------------------------------------
   One row of chrome on every signed-in page: the lockup, the job site you are
   in, and every screen you can reach. It replaced a flat row of links plus a
   page of tiles, because a function should be one hop from where you already
   are rather than three.

   Every dropdown is a <details> disclosure. Not a preference: helmet's CSP
   blocks inline script outright, and the one script the app ships is for the
   calendar workspace - a menu that stops working when a file fails to load
   would be a bad trade. */
.menubar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  padding-bottom: 12px;
  margin-bottom: 18px;
  /* The one gold rule on the page. Gold as a line is exactly what the guide
     reaches for, and 2px carries where a 1px gold hairline would vanish. */
  border-bottom: 2px solid var(--accent);
}
.brand {
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
}
/* The bar's lockup is smaller than the sign-in page's and carries no bottom
   margin, because here it sits in a row rather than above a heading. 150px
   wide, well clear of the guide's 96px floor for showing the wordmark at all. */
.brand .brand-logo { width: 150px; height: 28px; margin-bottom: 0; }
/* The lockup is a link home, and with nothing on hover it read as a title.
   A 2px gold line under it answers the pointer the way the menu links do:
   gold is ~2:1 on a light ground, so it is a 2px line, never a hairline.
   Drawn as a positioned pseudo-element so hovering never moves the bar --
   the wordmark is artwork now, so there is no text to underline. */
.brand { position: relative; }
.brand:hover::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -5px;
  height: 2px;
  background: var(--accent);
}
.menubar-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 4px;
  min-width: 0;
  margin-left: auto;
}
/* Top-level items are 13px muted text, the same weight the old flat links
   carried: the bar is chrome, and it should not compete with the schedule.

   Every item on the bar -- these and the job-site switcher -- is the same box:
   7px padding, an 18px line and a 1px border, 34px in all. The switcher used
   to be two lines and ~50px, so the schedule's bar was taller than every other
   page's and the gold rule and the page under it moved. The line-height is
   fixed rather than inherited so the heights match by construction. */
.menubar-link,
.menu > summary {
  display: block;
  padding: 7px 10px;
  font-size: 13px;
  line-height: 18px;
  color: var(--muted);
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  white-space: nowrap;
  cursor: pointer;
}
/* The grey fill alone was too faint to say "this is a link", so hover also
   draws a 2px gold bar along the bottom edge. An inset shadow rather than a
   border, so nothing shifts and the focus ring's reserved border is left alone. */
.menubar-link:hover,
.menu > summary:hover {
  color: var(--text);
  background: var(--bg);
  box-shadow: inset 0 -2px 0 var(--accent);
}
/* The ring pairs gold with a darkened edge, the same way a form field does:
   gold alone is ~2:1 on a light ground and a focus indicator has to clear
   3:1. The 1px transparent border above is reserved for exactly this, so
   nothing shifts when it appears. */
.menubar-link:focus-visible,
.menu > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--text);
}

.menu { position: relative; min-width: 0; }
.menu > summary { padding-right: 24px; list-style: none; }
.menu > summary::-webkit-details-marker { display: none; }
/* The caret, drawn rather than typed so it cannot pick up an emoji font. */
.menu > summary::after {
  content: "";
  position: absolute;
  right: 10px;
  top: 50%;
  width: 5px;
  height: 5px;
  margin-top: -4px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
}
.menu[open] > summary { color: var(--text); background: var(--bg); }
.menu[open] > summary::after { margin-top: -2px; transform: rotate(225deg); }
/* Clicking away closes the menu, with no script: an open disclosure lays a
   transparent sheet over the viewport, and because the sheet belongs to the
   summary, a click anywhere on it toggles the menu shut. The panel sits above
   the sheet so its own links still work. */
.menu[open] > summary::before,
.switcher[open] > summary::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 10;
}
.menu-account { margin-left: 4px; }
.menu-account-email {
  display: block;
  max-width: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.menu-panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 6px);
  right: 0;
  min-width: 220px;
  max-width: min(320px, 90vw);
  padding: 6px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-panel);
  /* Same justification as the switcher's menu: it floats over the page, and a
     border alone does not separate it from the card behind it. */
  box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
}
.menu-group {
  margin: 8px 8px 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}
.menu-group:first-child { margin-top: 4px; }
.menu-note {
  margin: 0 8px 6px;
  font-size: 13px;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.menu-item {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: 1px solid transparent;   /* reserved for the focus ring's dark edge */
  border-radius: var(--radius-control);
  font-size: 14px;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}
.menu-item:hover { background: var(--bg); }
.menu-form { margin: 4px 0 0; border-top: 1px solid var(--border); padding-top: 4px; }
.menu-item-button {
  font: inherit;
  font-size: 14px;
  text-align: left;
  background: transparent;
  cursor: pointer;
}
.menu-item-button:focus-visible,
.menu-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--text);
}

/* Narrow screens: the switcher gets its own full-width row under the brand,
   which is the only way all of brand, switcher and menus fit a phone. */
@media (max-width: 860px) {
  .menubar { gap: 4px 8px; }
  .menubar-link,
  .menu > summary { padding: 6px 8px; }
  .menu > summary { padding-right: 22px; }
  .switcher { order: 3; flex: 1 1 100%; }
  .switcher > summary { width: 100%; }
  .menu-account-email { max-width: 11ch; }
}
/* Wide enough to share the row: the switcher gives up width rather than
   pushing the links onto a second line, which would make the schedule's bar
   taller than every other page's. The bar stops wrapping, the lockup and the
   links keep their width, and the switcher is the one item that shrinks: the
   location is truncated first, the job site only once that is gone. */
@media (min-width: 861px) {
  .menubar { flex-wrap: nowrap; }
  .brand,
  .menubar-nav { flex-shrink: 0; }
}

/* Phone: the bar is the lockup and one Menu button, and the menu opens as a
   full-width list under the bar rather than a dropdown hanging off a word.
   Six links, two dropdowns and a truncated email wrapped to three ragged
   rows on a phone; this is one row whatever the role.

   The panel is positioned against the bar, not the button - the button's
   .menu is made static so the bar is the nearest positioned ancestor - which
   is what lets the list span the width. Items are 44px tall, the size a
   thumb needs. Mobile-first: the phone menu is the default, and 720px, where
   the calendar grid appears, swaps in the desktop links. */
.menubar { position: relative; }
.menubar-nav { display: none; }
.menubar-phone { position: static; margin-left: auto; }
/* ...but the caret still hangs off the button, so the summary keeps it. Flex
   and a 44px floor, because this button is hidden only from 720px (below) -
   there is no width where it renders below thumb size. */
.menubar-phone > summary {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 44px;
}
.menubar-phone > .menu-panel {
  left: 0;
  right: 0;
  top: calc(100% + 2px);   /* just under the gold rule */
  max-width: none;
  padding: 6px 6px 8px;
}
.menubar-phone .menu-item { padding: 12px 10px; font-size: 15px; white-space: normal; }
@media (min-width: 720px) {
  .menubar-nav { display: flex; }
  .menubar-phone { display: none; }
}

.datenav { display: flex; align-items: stretch; gap: 6px; }
.datenav-form { display: flex; gap: 6px; flex: 1; min-width: 0; }
.datenav input {
  flex: 1;
  width: 100%;
  min-width: 0;
  padding: 9px 8px;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
}
.datenav button,
.datenav .step {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  padding: 9px 11px;
  font: inherit;
  font-weight: 600;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  cursor: pointer;
  text-decoration: none;
  line-height: 1.2;
}
.datenav .step:hover, .datenav button:hover { border-color: var(--accent-edge); }
.datenav-today { margin: 10px 0 0; font-size: 14px; }
/* 14px text is a 17px tap target on its own; below 720px it gets the same
   44px floor every other phone control does, reset once a pointer is
   assumed. */
.datenav-today a { display: inline-flex; align-items: center; min-height: 44px; color: var(--link); }
@media (min-width: 720px) {
  .datenav-today a { min-height: 0; }
}
.crumb { margin: 0 0 10px; font-size: 14px; }
/* Same 44px phone floor as .datenav-today above, for the same reason: a
   14px "&larr; Companies" is a 17px target otherwise. */
.crumb a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--link);
  text-decoration: none;
}
.crumb a:hover { color: var(--text); }
@media (min-width: 720px) {
  .crumb a { min-height: 0; }
}
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.form { margin-top: 20px; display: flex; flex-direction: column; gap: 6px; }
.form label { font-size: 14px; font-weight: 600; }
.form input {
  padding: 11px 12px;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
}
/* The satellite preview of a gate's saved pin. A fixed height, because an
   iframe has no content size of its own to follow; shorter on a phone, where
   the form is already one long column. The frame's own border is dropped and
   replaced with the same token a field uses, so it reads as part of the form
   in both themes. */
.map-preview {
  display: block;
  width: 100%;
  height: 180px;
  border: 1px solid var(--border);
  border-radius: var(--radius-container);
  background: var(--bg);
}
@media (min-width: 720px) {
  .map-preview { height: 240px; }
}
/* Gold focus ring, but the border darkens too: gold alone is ~2:1 against a
   white field, and a focus indicator has to clear 3:1 to be seen. */
.form input:focus, .form select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--text);
}
.form button {
  margin-top: 10px;
  padding: 12px 16px;
  font: inherit;
  font-weight: 600;
  color: var(--on-action);
  background: var(--action);
  border: 0;
  border-radius: var(--radius-control);
  cursor: pointer;
}
.form button.secondary { color: var(--text); background: transparent; border: 1px solid var(--border); }
/* On a phone a form is one column and its button should fill that column, the
   same way a delivery's Approve or a company's Save does. On a desktop pane a
   full-width submit under a single 640px-wide column of fields reads as the
   loudest thing on the page for an action that usually is not - .block-form
   already made this call for the blackout panel, so admin lists get the same
   answer rather than a second one. A search bar is the one form on these
   pages that is meant to run the width of the row, so it opts back out. */
@media (min-width: 720px) {
  .card-wide .form > button { align-self: flex-start; min-width: 150px; }
  .card-wide .form { max-width: 640px; }
  .card-wide .form.filter-bar { max-width: none; }
}
/* .alert is only ever an error container in this app, so it stays red, and
   .notice directly below is its counterpart at exactly the same weight: a 1px
   edge with body ink, never a filled bar. A write that landed is worth
   confirming, not worth shouting -- and before this existed the app confirmed
   nothing at all, so a vendor submitted a delivery request and saw a page that
   looked like nothing had happened. */
.alert {
  padding: 11px 12px;
  border: 1px solid var(--danger);
  border-radius: var(--radius-control);
  color: var(--text);
  font-size: 14px;
}
/* Renders on a pane ground as well as a card, so it sets its own surface. */
.notice {
  margin: 0 0 16px;
  padding: 11px 12px;
  background: var(--card);
  border: 1px solid var(--ok);   /* 5.4:1 on --card, 8.1:1 on the dark card */
  border-radius: var(--radius-control);
  color: var(--text);            /* 12.5:1 */
  font-size: 14px;
}
/* A file input sizes itself to the chosen filename by default, which ran a
   phone's viewport wide on the import screen - "Choose the exported file"
   plus a real filename is longer than a 375px screen has. */
.form input[type="file"] { max-width: 100%; }
.form select {
  padding: 11px 12px;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
}
.form button.danger { color: var(--on-danger); background: var(--danger); }

/* A group heading inside a form. The request form asks two different questions
   -- where and when, then who is coming -- and typography is enough to say so:
   12px uppercase muted over a hairline, the same weight the rail titles use. A
   fieldset box would draw more attention to the grouping than the grouping is
   worth. */
.form-section {
  margin: 24px 0 2px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);              /* 7.3:1 on --card */
}
.form .form-section:first-of-type { margin-top: 4px; padding-top: 0; border-top: 0; }

/* The time blocks. A dropdown hid the one thing the form is for behind a press
   and a scroll; these are the whole answer at a glance, and one press each on
   a phone. The radio itself is the control and is only visually hidden -- it
   still takes focus and still answers the arrow keys, which is what makes a
   group of boxes a real radio group rather than a row of divs pretending. */
.timeblocks { margin: 0; padding: 0; border: 0; }
/* A legend cannot be flex-aligned reliably across browsers, so it is styled to
   match .form label rather than being one. */
.timeblocks legend { padding: 0; font-size: 14px; font-weight: 600; }
/* A grid, not wrapped flex: flex sizes each block to its own text, so a row
   of them came out ragged - "7:00 AM - 7:30 AM" is narrower than "10:00 AM -
   10:30 AM" and the column edges went everywhere. Equal tracks line them up,
   which is the cheapest way to make a grid of anything look deliberate. The
   minimum is what the longest label needs; auto-fill decides how many fit. */
.timeblock-list {
  margin-top: 6px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
  gap: 6px;
}
.timeblock span {
  display: block;
  text-align: center;
  /* The same padding as .form input, so a block is the same height as the
     fields above it and clears a thumb on a phone, which is where most of
     these are pressed. */
  padding: 11px 12px;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text);                 /* 12.5:1 on --card */
  /* A choice, not a field: an unchosen block used to share .form input's own
     grey fill and hairline border, which is exactly what made a whole page of
     them read as disabled text inputs. --card is the same surface a real
     button sits on, and --muted is a border strong enough to be read as an
     edge rather than a suggestion -- 7.3:1 on --card in light mode, 8.2:1 in
     dark, both well past the 3:1 a UI edge needs. */
  background: var(--card);
  border: 1px solid var(--muted);
  border-radius: var(--radius-control);
  cursor: pointer;
}
.timeblock span:hover { border-color: var(--text); }
/* Gold marks the current thing, and the block you have chosen is exactly that.
   Dark ink on the gold fill, never gold text: on a light ground gold is ~2:1.
   The border goes gold too so the chosen block does not sit a pixel smaller
   than its neighbours. */
.timeblock input:checked + span {
  color: var(--on-accent);            /* 6.2:1 on Rich Gold */
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}
/* The ring has to be drawn by the span, because the input it belongs to is the
   one element nobody can see. */
.timeblock input:focus-visible + span {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--text);
}

/* What to try when the chosen gate, date and length gave nothing. Each
   suggested block is a real submit button rather than a radio label - picking
   one changes the fields above it, it does not answer the form by itself -
   so it borrows .timeblock's own look rather than a second style for what is
   visually the same thing. */
.suggestions { margin-top: 6px; }
.suggestion-label {
  margin: 14px 0 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--muted);                /* 7.3:1 on --card, same pairing as .form-section */
}
.suggestions .timeblock-list .timeblock-pick {
  display: block;
  width: 100%;
  text-align: center;
  font: inherit;
  padding: 11px 12px;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text);                 /* 12.5:1 on --card */
  /* Borrows .timeblock's own "choice" surface -- see the note there. */
  background: var(--card);
  border: 1px solid var(--muted);     /* 7.3:1 on --card, 8.2:1 dark */
  border-radius: var(--radius-control);
  cursor: pointer;
}
.timeblock-pick:hover { border-color: var(--text); }
.timeblock-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- The requester's page -----------------------------------------------
   Three panes on the workspace shell: the request, the week it books into,
   and the requester's own bookings. On a phone they stack in that order. From
   1200px the request is a 400px column down the left and the other two sit
   beside it, so the whole job - pick a slot, see why the others are taken,
   cancel one - is on one screen. It used to be one 1100px card with the week
   and the bookings below a form that was eight fields tall, which put the
   timeline a screen and a half down and still gave it a scrollbar.

   #book-planner is the one element public/book.js swaps, and it holds two of
   the three panes. display:contents lets the grid place those two directly,
   so swapping stays one replaceWith and the layout does not have to care. */
.book-layout {
  display: grid;
  gap: 16px;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
#book-planner { display: contents; }
@media (min-width: 1200px) {
  .book-layout {
    grid-template-columns: 400px minmax(0, 1fr);
    /* The request is usually the tallest thing here and spans both rows.
       Without 1fr on the second row its extra height would be shared between
       them and open a gap between the week and the bookings; this way any
       slack sits under the bookings, where it is only the end of the page. */
    grid-template-rows: auto 1fr;
  }
  .book-request { grid-column: 1; grid-row: 1 / span 2; }
  .book-week { grid-column: 2; grid-row: 1; }
  .book-mine { grid-column: 2; grid-row: 2; }
}

/* The form: one field per row on a phone, the short ones two to a row from
   480px. Spacing is by group, not by padding every field - 12px between
   fields, the section rule between the two questions. */
.book-form { margin-top: 0; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.field select, .field input { width: 100%; box-sizing: border-box; }
/* Two to a row even on a phone: a date, a length, a name and a phone number
   all fit in half of 375px. Job site beside gate, and date beside how long,
   both stack until 480px - a project name needs the width - and above that
   take the same 3:2 split, so the two rows' columns line up instead of one
   being an even split and the other not. */
.field-pair { display: grid; gap: 12px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.field-pair--wide-first { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 480px) {
  .field-pair--wide-first { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
}
/* .form.book-form rather than .book-form: "Who is coming" is the first <p> in
   this form, so .form .form-section:first-of-type would otherwise strip the
   rule that separates the two questions. */
.form.book-form .form-section { margin: 4px 0 -4px; padding-top: 12px; border-top: 1px solid var(--border); }
.book-form button { margin-top: 0; }
.book-request .status { margin: 12px 0 0; font-size: 13px; }

/* Two blocks to a row at every width. On a phone that means trimming their
   side padding, not their height: "10:00 AM – 11:00 AM" needs about 130px,
   half a 375px screen leaves about 150, and the height is what a thumb needs.
   It halves the length of a day's worth of blocks on the screen they are most
   often pressed on. */
.book-request .timeblock-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.book-request .timeblock span,
.book-request .suggestions .timeblock-list .timeblock-pick { padding-left: 4px; padding-right: 4px; }

/* On a desktop, with a pointer, 8px is plenty and it takes a full day of
   hourly blocks from five rows of 46px down to five of 38. */
@media (min-width: 1200px) {
  .book-request .timeblock span,
  .book-request .suggestions .timeblock-list .timeblock-pick { padding: 8px 10px; }
}

/* The week. The same grid and agenda the calendar draws, switching at the
   same breakpoint, but sized for a pane beside a form rather than a whole
   screen: gate names here are short, so the label column gives back 58px, and
   a quarter-hour floor of 16px - an hour is still 64px, wide enough for "10 AM"
   and a start time - lets a nine-hour day fit in 650px. That is what the pane
   has left beside a 400px form at 1200px, so the grid does not scroll at any
   width the two sit side by side. A longer day still can, as it should. Rows are a little shorter because this is for spotting a gap,
   not reading a record. */
.book-week .rail-title { margin-bottom: 0; }
.book-week .calendar-scroll { margin-top: 10px; }
.book-week .calendar-agenda { margin-top: 10px; }
@media (min-width: 720px) {
  .book-week .calendar-grid {
    --cal-slot-w: 16px;
    --cal-label-w: 74px;
    grid-auto-rows: minmax(36px, auto);
  }
  .book-week .calendar-day-label { padding: 5px 10px; }
  .book-week .calendar-gate { padding: 6px 10px; }
}

/* The bookings pane. The list keeps its .rows look; it only loses the top
   margin that separated it from a page heading it no longer sits under. */
.book-mine .site-group { margin: 0; }
.book-mine .rows { margin-top: 8px; }
.book-mine .rows li:last-child { padding-bottom: 0; }
.book-mine .status { margin: 10px 0 0; font-size: 13px; }



.rows { list-style: none; margin: 24px 0 0; padding: 0; }
.rows li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
.rows strong { display: block; font-size: 15px; word-break: break-all; }
.row-meta { display: block; margin-top: 2px; font-size: 13px; color: var(--muted); }
.rows button {
  flex: 0 0 auto;
  padding: 7px 11px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  cursor: pointer;
}
.rows button.danger { color: var(--danger); border-color: var(--danger); }

.detail { margin: 20px 0 0; display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; }
.detail dt { font-size: 14px; color: var(--muted); }
.detail dd { margin: 0; font-size: 14px; font-weight: 600; }
.location { margin-top: 28px; }
.location h2 {
  margin: 0 0 4px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.job-site { margin-top: 18px; }
.job-site h3 { margin: 0; font-size: 16px; font-weight: 700; }
.job-site h3 a { color: var(--text); text-decoration: none; }
.job-site h3 a:hover { text-decoration: underline; }
.widget { margin-top: 28px; }
.widget h2 { margin: 0 0 4px; font-size: 13px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.widget .rows { margin-top: 10px; }
.widget .rows a { display: block; color: var(--text); text-decoration: none; }
.widget .rows a:hover strong { color: var(--link); }
.project-number {
  display: inline-block;
  padding: 1px 7px;
  margin-right: 5px;
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  vertical-align: middle;
}
/* An empty state is a quiet statement of fact, at the same weight as any other
   supporting text -- it had no colour at all and fell back to body ink, which
   made "Nothing waiting for review." compete with the things that are. Not
   green: green is for something that IS approved, and an absence is not an
   all-clear. */
.empty { margin: 4px 0 0; font-size: 14px; color: var(--muted); }  /* 7.3:1 on --card, 6.5:1 on --bg */

/* Locations and job sites.

   The group heading is the same 12px uppercase muted treatment as .rail-title
   and .switcher-group: size and colour carry the hierarchy, so a location does
   not need a box drawn round it to read as a group. No new colour pair here,
   so no new contrast ratio to record. */
.site-group {
  margin: 30px 0 0;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}
.site-group + .empty { margin-top: 8px; }

/* The add-a-thing disclosure. A <details> because the content security policy
   blocks inline script, and nothing on this page loads a script at all, so it
   is the only way to keep a long form next to the thing it belongs to without
   spending a whole screen on it. */
.site-add { margin-top: 14px; }
.site-add > summary {
  display: inline-block;
  padding: 5px 9px;
  font-size: 14px;
  color: var(--link);
  cursor: pointer;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  list-style: none;
}
.site-add > summary::-webkit-details-marker { display: none; }
.site-add > summary:hover { color: var(--text); }
.site-add > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.site-add[open] > summary { color: var(--text); }
.site-add > .form { margin-top: 12px; }

/* ---- The ways to add to a list, side by side ------------------------------
   Companies has two - the Procore import and typing one in - and they belong
   together under the description, not one parked in the heading's far corner
   and the other below the search. The disclosure's summary takes the
   button's height so the pair reads as one row; opened, it stays where it
   was and its form runs down beneath it. Letting it take the whole row
   instead moved the summary under the button at the moment it was pressed. */
.add-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 10px 12px;
  margin: 16px 0 24px;
}
.add-actions > .site-add { margin-top: 0; }
.add-actions > .site-add > summary {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 14px;
}
.add-actions > .site-add[open] { flex: 1 1 320px; }

/* ---- A location's heading and its Manage menu ------------------------------
   Renaming and deactivating act on the location, not on its list of job
   sites, so they live in one quiet menu at the far end of its heading. The
   list's own "Add a job site here" stays under the list. The menu floats over
   the page like the switcher, rather than pushing the list down, so the
   heading and the rows under it never move when it opens. No new colour pair:
   the summary is --link on --card, the panel the switcher's own surface. */
.location-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-top: 30px;
}
.location-head > .site-group { margin: 0; }
.location-head + .empty { margin-top: 8px; }

.location-manage { position: relative; flex: none; }
.location-manage > summary {
  font-size: 13px;
  color: var(--link);
  cursor: pointer;
  list-style: none;
}
.location-manage > summary::-webkit-details-marker { display: none; }
.location-manage > summary:hover,
.location-manage[open] > summary { color: var(--text); text-decoration: underline; }
.location-manage > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.location-manage-panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 6px);
  right: 0;
  width: min(320px, calc(100vw - 32px));
  padding: 4px 14px 14px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-panel);
  /* Floats over the list like .switcher-menu, and for the same reason. */
  box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
}
.location-manage-panel > .form { margin-top: 10px; }
/* Deactivating is a different act from renaming, so a rule sets it apart. */
.location-manage-panel > .location-toggle {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.location-toggle > .status { margin: 0 0 6px; font-size: 13px; }

/* ---- A search or filter row above a list ----------------------------------
   .form already stacks one field per row, which is right for a form that
   builds a record but reads as a queue of separate questions for one that
   narrows a list. From 720px the fields run in a row instead, each keeping
   its own label above it so the row still reads left to right as "field,
   field, button" rather than a single wide control. */
.filter-bar { margin-top: 0; }
.filter-field { display: flex; flex-direction: column; gap: 6px; }
@media (min-width: 720px) {
  .filter-bar {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 10px 12px;
  }
  .filter-field { flex: 0 1 260px; }
  /* The row lines up on its bottom edge, so every control in it has to be one
     height: .form input, .form select and .form button each come out a few
     pixels apart (a select sizes its own line box), which dropped the labels
     out of line too. And .card-wide .form > button pins a lone submit to the
     top of its column - right for a stacked form, wrong for a row - so the
     search button is put back on the shared edge here. */
  .filter-bar input,
  .filter-bar select,
  .card-wide .form.filter-bar > button {
    height: 46px;
    padding-top: 0;
    padding-bottom: 0;
  }
  .card-wide .form.filter-bar > button { align-self: auto; margin-top: 0; }
  .filter-bar button { margin-top: 0; }
  .filter-clear { align-self: center; }
}
.filter-clear { font-size: 14px; color: var(--link); }

/* ---- A list read as a table on a desktop ----------------------------------
   Mobile-first: on a phone a <table> collapses to the same stacked rows
   .rows already draws, because a grid of columns has nowhere to go at
   375px. From 720px it becomes a real table - sortable-looking headers,
   ruled rows, numbers lined up on their own edge - which is how a desktop
   list of forty companies or a page of audit entries is actually scanned:
   down a column, not through a paragraph per row. */
.data-table { width: 100%; margin-top: 20px; border-collapse: collapse; }
.data-table thead { display: none; }
.data-table tr { display: block; padding: 12px 0; border-top: 1px solid var(--border); }
.data-table td { display: block; }
.data-table td:first-child { font-size: 15px; font-weight: 600; }
.data-table td:not(:first-child) { margin-top: 2px; font-size: 13px; color: var(--muted); }
.data-table a { color: var(--text); font-weight: 600; text-decoration: none; }
.data-table a:hover { text-decoration: underline; }
.data-table a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The name that opens a row - a company from the companies list today - is
   the tap target the stacked mobile layout is built around, but the text
   alone measured under 20px tall. 44px below 720px, where this is a thumb
   target in a list read as stacked rows; the desktop table resets it, where
   the row is read rather than tapped and every column is a compact line. */
.data-table td:first-child a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
@media (min-width: 720px) {
  .data-table td:first-child a { display: inline; min-height: 0; }
}
/* A deactivated row still reads company-first, but nothing on it competes
   with an active one for attention - the same demotion .row-meta already
   gives a company's second line. */
.data-table tr.is-inactive td { color: var(--muted); }
.data-table tr.is-inactive a { color: var(--muted); }
@media (min-width: 720px) {
  .data-table thead { display: table-header-group; }
  .data-table tr { display: table-row; padding: 0; border-top: 0; }
  .data-table td { display: table-cell; }
  .data-table th {
    padding: 0 14px 8px 0;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--muted);
    text-align: left;
    border-bottom: 1px solid var(--border);
  }
  .data-table td {
    padding: 8px 14px 8px 0;
    font-size: 14px;
    color: var(--text);
    vertical-align: top;
    border-top: 1px solid var(--border);
  }
  .data-table td:first-child,
  .data-table td:not(:first-child) { margin-top: 0; font-size: 14px; color: var(--text); }
  .data-table tr.is-inactive td { color: var(--muted); }
  .data-table .num { text-align: right; font-variant-numeric: tabular-nums; }
  .data-table .nowrap { white-space: nowrap; }
}
.data-table .change { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; word-break: break-word; }

/* A reference photo, not a hero image: capped so it cannot dominate the page,
   and bordered rather than shadowed - the one shadow in this stylesheet is on
   .switcher-menu, which genuinely floats. */
.gate-photo {
  display: block;
  max-width: 320px;
  width: 100%;
  height: auto;
  margin-top: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
}
.schedule { list-style: none; margin: 0; padding: 0; }
.schedule li {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
/* The agenda row as a link. Inside the li rather than replacing it, so
   .schedule li keeps the layout it also gives the gate-day view. */
.slot-link {
  display: flex;
  flex-direction: column;
  gap: 2px;
  color: inherit;
  text-decoration: none;
  min-height: 44px;
  justify-content: center;
}
.slot-link:hover .slot-company { text-decoration: underline; }
.slot-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.slot-time { font-variant-numeric: tabular-nums; font-weight: 600; }
.slot-company { font-size: 14px; color: var(--muted); }
@media (min-width: 480px) {
  .schedule li { flex-direction: row; align-items: baseline; gap: 16px; }
  .slot-link { flex-direction: row; align-items: baseline; gap: 16px; }
  .slot-time { min-width: 170px; }
}
/* The agenda's answer to .calendar-ghost on the grid: the same block the
   request form above would ask for, if submitted as it stands. Same
   treatment - a gold dashed edge, dark ink on --card, never gold text - so
   the two read as one idea in two layouts rather than two different ones. */
.agenda-ghost {
  margin: 8px 0 0;
  padding: 8px 10px;
  font-size: 14px;
  color: var(--text);                    /* 12.5:1 on --card */
  background: var(--card);
  border: 1px dashed var(--accent-edge); /* 3.1:1 on --card, an edge needs 3:1 */
  border-radius: var(--radius-control);
}

.request {
  padding: 14px 0;
  border-top: 1px solid var(--border);
}
.request strong { display: block; font-size: 15px; }
.decision { display: flex; gap: 8px; margin-top: 10px; }
.decision input {
  flex: 1;
  min-width: 0;
  padding: 8px 10px;
  font: inherit;
  font-size: 14px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
}
.decision button {
  flex: 0 0 auto;
  padding: 8px 14px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--on-action);
  background: var(--action);
  border: 0;
  border-radius: var(--radius-control);
  cursor: pointer;
}
.decision button.danger { color: var(--danger); background: transparent; border: 1px solid var(--danger); }
.status-pending, .status-approved, .status-rejected, .status-cancelled, .status-revoked {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 8px;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
}
/* Deliberately NOT branded. These pills carry meaning, and meaning outranks
   branding -- green is "approved" to everyone, and gold would read as
   "pending/warning", which BRAND.md forbids Rich Gold from meaning.

   Approved and rejected are the two decided states and they carry equal
   weight, because a rejection is the one a requester must not scroll past.
   They used to be wildly unequal: rejected was muted grey and identical to
   cancelled, so "JHL said no" and "I withdrew it" looked the same. Cancelled
   stays muted, because nobody decided anything about it.

   The Approve button on the queue is NOT green. Green is a state, not an
   action -- that is the likeliest drift from here. */
.status-approved  { color: var(--on-ok);     background: var(--ok);     border-color: var(--ok); }      /* 5.4:1 light, 6.4:1 dark */
.status-rejected  { color: var(--on-danger); background: var(--danger); border-color: var(--danger); }  /* 6.5:1 light, 5.2:1 dark */
.status-pending   { color: var(--text); }    /* 12.5:1 */
.status-cancelled { color: var(--muted); }   /* 7.3:1 light, 8.2:1 dark */
/* Revoked is a refusal, so it is red -- but outlined rather than filled, the
   same inversion .decision button.danger already uses. It has to be tellable
   apart from rejected at a glance: one was never on the schedule, the other
   was, and only the second means somebody may already be driving. */
.status-revoked   { color: var(--danger); border-color: var(--danger); }  /* 6.5:1 light, 6.6:1 dark */

.hint { font-size: 13px; color: var(--muted); }
/* .form's own gap already puts 6px above a hint - the same as above every
   other field - so it read as belonging to the field below it rather than the
   one above. Pulling it up and tightening what follows ties it to its own
   field instead. */
.form .hint { margin: -2px 0 8px; }
.alert div + div { margin-top: 4px; }

/* A delivery that overlaps a block, named wherever that delivery is read:
   the agenda row, the rail's "On the schedule" list, and the panel onto the
   record. Ink only, the same weight as .status-revoked's text colour and for
   the same reason -- this reads on a --card ground everywhere it appears, so
   one rule covers all three. */
.conflict-note {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--danger);  /* 6.5:1 light, 6.6:1 dark */
}

.day-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.day-toggle { display: flex; align-items: center; gap: 7px; flex: 1; min-width: 0; font-size: 14px; }
.day-toggle input { width: 18px; height: 18px; flex: 0 0 auto; }
.day-row input[type="time"] {
  flex: 0 0 auto;
  width: 108px;
  padding: 7px 8px;
  font: inherit;
  font-size: 14px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
}

.week-day { margin-top: 24px; }
.week-day h2 { margin: 0 0 4px; font-size: 15px; }
.week-day h2 a { color: var(--text); text-decoration: none; }
.week-day h2 a:hover { text-decoration: underline; }
.week-day .slot-company .row-meta { margin-top: 0; }


/* A gold field behind dark ink -- the one place gold gets to shout. This
   banner says "you are simulating another user", which is attention, not
   danger, so gold is the right signal as well as the on-brand one. */
.viewas-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin-bottom: 18px;
  padding: 10px 12px;
  background: var(--accent);
  border-radius: var(--radius-control);
  color: var(--on-accent);
  font-size: 14px;
}
.viewas-banner span { flex: 1 1 auto; min-width: 0; }
.viewas-banner a { color: var(--on-accent); text-decoration: underline; flex: 0 0 auto; }
.viewas-banner form { flex: 0 0 auto; margin: 0; }
/* This one sits in a bare <form>, so .form button.secondary never reached it
   and it was rendering as an unstyled browser button -- invisible as a dark
   slab on the gold field once color-scheme:dark is honoured. */
.viewas-banner button.secondary {
  padding: 5px 10px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--on-accent);
  background: transparent;
  border: 1px solid var(--on-accent);
  border-radius: var(--radius-control);
  cursor: pointer;
}

.card-wide { max-width: 1100px; }

.calendar-scroll {
  margin-top: 16px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.calendar-grid {
  --cal-slot-w: 18px;
  --cal-slot-max: calc(var(--cal-slot-w) * 2);
  --cal-label-w: 130px;
  /* The track sizing, kept here rather than in the inline style because an
     inline style cannot be overridden. The template supplies only --cal-slots,
     the number of quarter-hour columns in the visible range.

     The columns stretch to fill the pane at every width, between a floor and a
     ceiling. Neither is decoration.

     The floor is --cal-slot-w, below which a quarter-hour stops being readable.
     Past it the grid outgrows the pane and .calendar-scroll takes over, which is
     what a twelve-hour day does on a laptop. Stretching can only ever consume
     slack; it never compresses a slot.

     The ceiling is twice that. The visible range is the job site's gate hours,
     not a fixed working day, so a gate open 08:00-12:00 draws sixteen columns --
     unbounded, that would spread one hour across a third of a monitor. Capped,
     the slack sits at the right edge instead, which is the lesser evil and rare.

     min-width states the floor exactly rather than as max-content. An fr track
     sized under a max-content constraint takes its content's width, so one long
     company name on a block spanning four columns would widen the whole grid and
     raise a scrollbar nothing on screen asked for. Blocks clip to their slot: the
     grid is sized by the clock, never by what is booked into it. */
  --cal-track: minmax(var(--cal-slot-w), 1fr);
  display: grid;
  grid-template-columns: var(--cal-label-w) repeat(var(--cal-slots), var(--cal-track));
  grid-auto-rows: minmax(44px, auto);
  width: 100%;
  min-width: calc(var(--cal-label-w) + var(--cal-slots) * var(--cal-slot-w));
  max-width: calc(var(--cal-label-w) + var(--cal-slots) * var(--cal-slot-max));
}
.calendar-corner {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--card);
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.calendar-hour {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 6px 0 6px 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  border-bottom: 1px solid var(--border);
  border-left: 1px solid var(--border);
  white-space: nowrap;
}
.calendar-day-label {
  /* Padding moves to .calendar-day-label-text below, which carries it at
     every scroll position -- see the note there. */
  padding: 8px 0;
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  background: var(--bg);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
/* The row itself spans every column so its background reads as one bar behind
   the day, and that has to keep scrolling with the grid or it would stop
   lining up with the hour header above it. The text inside it does not: on a
   day with enough columns to need .calendar-scroll, scrolling right used to
   carry "Monday, June 15" off the left edge with nothing left to say which
   day the row beneath belongs to. Sticky, like .calendar-gate beside it,
   keeps the label pinned to the visible edge while the coloured bar underneath
   still moves with the grid it is labelling. */
.calendar-day-label-text {
  display: inline-block;
  position: sticky;
  left: 0;
  padding: 0 10px;
}
/* Today's row. In week view it was indistinguishable from Friday's, which is
   the one thing a week grid has to answer before anything else. Blue is the
   factual hue: this is a statement about which row you are in, not an alert and
   not work owed. The 2px bottom rule is a deliberate exception to the 1px
   border everywhere else -- it has to survive being one row among seven.

   The word "Today" is in the markup as well as the colour. Nothing on this grid
   may be carried by hue alone. */
.calendar-day-label.is-today {
  color: var(--info);                        /* 7.0:1 on --bg, 8.6:1 on the dark --bg */
  border-bottom: 2px solid var(--info);
}
.calendar-today-tag {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
}
/* The day on /book's week timeline that the request form's own date field
   currently shows. Not a state that IS true the way "today" is, and not an
   action owed the way gold would say - it is just which of seven days the
   form above is already talking about, so it takes the same quiet, muted
   treatment as a section label rather than a hue of its own. */
.calendar-chosen-tag {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);                        /* 7.3:1 on --card, 6.5:1 on --bg */
}
/* The quarter-hour the page was drawn in. z-index 0 and last in the DOM, so it
   paints over the track and the bands but under a delivery block and under the
   sticky gate column -- a time rule must never hide a booking or a gate name.
   It cannot update itself, so the workspace bar states the time it represents
   rather than letting the line quietly go stale. */
/* Sits beside the location in the workspace bar, at supporting weight: it is a
   caveat on the now line, not a headline. */
.workspace-drawn { color: var(--muted); }   /* 7.3:1 on --card */
.calendar-now {
  border-left: 2px solid var(--info);        /* 7.9:1 on the track */
  pointer-events: none;
  z-index: 0;
}
.calendar-gate {
  position: sticky;
  left: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  padding: 8px 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  background: var(--card);
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.calendar-gate:hover { color: var(--link); }
/* The name sits in its own span so the label can hold a second thing beside
   it (the gate's load, on the workspace). text-overflow does not reach
   through a flex parent to its item, so the ellipsis lives here - which is
   what keeps /book's narrow label cutting a long name cleanly. */
.calendar-gate-name {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.calendar-track {
  border-bottom: 1px solid var(--border);
}
.calendar-track.is-closed {
  background: repeating-linear-gradient(
    45deg,
    var(--bg),
    var(--bg) 8px,
    var(--border) 8px,
    var(--border) 9px
  );
}
/* An empty hour of track used to be inert - nothing to click, and no way to
   tell it was blockable without leaving the grid for Admin > Blackouts. One
   link per hour, invisible until a pointer rests on it or a keyboard tabs to
   it, so the grid does not fill up with a border around every open slot.
   z-index 0 and first in the DOM among the things at that level, so a real
   band still wins the cell it shares one with (.calendar-band is also z-index
   0, painted after it); .calendar-block sits at z-index 1 and always wins
   regardless of DOM order. Focus and hover are gold everywhere in this app,
   so this is no exception. */
.calendar-slot {
  z-index: 0;
  display: block;
}
.calendar-slot:hover { background: var(--bg); }
.calendar-slot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.calendar-block {
  margin: 4px 1px;
  padding: 3px 7px;
  display: flex;
  flex-direction: column;
  gap: 0;
  overflow: hidden;
  /* Warm Gray fill, white ink. Gold was here, and that is how gold came to
     mean five unrelated things at once. The block is still the densest mark on
     the grid -- it is the darkest one now instead of the brightest -- and it is
     still fully square, because a soft corner turns a slot of time into a
     badge. The dark edge is not there for the silhouette: 7.3:1 already gives
     it one. It is what stops two back-to-back deliveries on the same gate row
     reading as a single long block. */
  background: var(--booked);            /* 7.3:1 on the light track, 8.2:1 on the dark */
  color: var(--on-booked);              /* 7.3:1 light, 6.5:1 dark */
  border: 1px solid var(--booked-edge); /* 12.5:1 on the light track */
  border-radius: var(--radius-sm);
  font-size: 11px;
  line-height: 1.3;
  /* A block is a link now: it opens the delivery it draws. The underline has
     to go explicitly - .calendar-block beats the global `a` rule on colour by
     specificity, but nothing overrides the user agent's underline. */
  text-decoration: none;
}
/* Outline, never an inset shadow: .calendar-block sets overflow:hidden, which
   clips anything drawn inside the box. The offset stays at 2px because the
   sticky gate column sits at z-index 1, and a wider ring on a block at the
   left edge would be painted under it while the grid is scrolled. */
a.calendar-block:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
a.calendar-block:hover { border-color: var(--text); }
/* A request awaiting review holds the gate exactly as a booking does, so it
   takes up the same room on the grid - but it is not agreed yet and must not
   read as though it were. Outlined instead of filled, with a dashed edge: the
   slot is visibly taken and visibly provisional. Deliberately not hatched,
   because hatching already means blocked on this grid and a held slot is the
   opposite of blocked - it is a slot somebody wants. */
.calendar-block.is-held {
  background: var(--card);
  color: var(--text);                   /* 12.5:1 on --card */
  border: 1px dashed var(--booked);     /* 7.3:1 on --card, an edge needs 3:1 */
}
a.calendar-block.is-held:hover { border-color: var(--text); }
/* A delivery drawn over a block it overlaps - the exact "truck during bus
   dismissal" failure this app exists to prevent. An outline, not a wider
   border: the block's own edge sits on the --booked fill (or --card behind a
   held one), where --danger reads too close to warm gray to carry the
   warning. The outline paints just outside the border box instead, against
   the track underneath, where --danger already carries at 6.5:1. Nothing is
   cancelled by a block, so this stacks with is-held rather than replacing it
   - a request can be sitting on a bus period too. */
.calendar-block.is-conflict {
  outline: 2px solid var(--danger); /* 6.5:1 on --card, 6.6:1 dark */
  outline-offset: 1px;
}
.calendar-block-time {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.calendar-block-company {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A booking that runs past the edge of the visible range used to be marked by
   squaring off that corner. Blocks are square now, so the template carries a
   chevron on the time label instead. */

/* The block the request form above would ask for, if submitted as it stands
   right now. Gold marks the current/actionable thing everywhere else in this
   app, so it marks this too - but as a line, never as text: rule 1 in
   SKILL.md is that Rich Gold cannot carry text on a light ground (~2:1), so
   the fill stays --card and the ink stays --text, and gold is only the
   dashed edge. Dashed like a held slot, because both are provisional, but a
   different colour and a different shape of provisional: a held slot is
   someone else's real request waiting on a decision; this is a preview of
   one that has not been sent. Not a link and not clickable - there is
   nothing to open yet - and never drawn outside /book, where chosenDate and
   ghost are the only locals that ever set it. */
.calendar-ghost {
  margin: 4px 1px;
  padding: 3px 7px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--card);
  color: var(--text);                    /* 12.5:1 on --card */
  border: 1px dashed var(--accent-edge); /* 3.1:1 on --card, an edge needs 3:1 */
  border-radius: var(--radius-sm);
  font-size: 11px;
  line-height: 1.3;
}

.calendar-scroll { display: none; }
.calendar-agenda { display: block; }
@media (min-width: 720px) {
  .calendar-grid { --cal-slot-w: 26px; --cal-label-w: 132px; }
  .calendar-block, .calendar-ghost { font-size: 12px; }
  .calendar-scroll { display: block; }
  .calendar-agenda { display: none; }
}
/* The workspace grid, narrowed the same way .book-week already is. The rail
   sits beside the grid only from 1500px (below); short of that, a job site's
   ordinary 07:00-16:00 day (36 quarter-hour columns) needs more room than the
   26px floor leaves once the pane's own padding and the frame's margins are
   taken out -- an iPad in either orientation, and a laptop with a narrow
   window, both fell back to .calendar-scroll's sideways drag and lost the
   afternoon off the right edge. 16px keeps a nine-hour day inside the pane at
   every width up to where the rail claims some of it back. The scroller stays
   for a longer day than that, as it should. */
@media (min-width: 720px) and (max-width: 1499px) {
  #workspace .calendar-grid { --cal-slot-w: 16px; --cal-label-w: 74px; }
}

/* Gate names on the workspace wrap to two lines instead of being cut off.
   Real gates are named "Gate 1 - Delivery Entrance", and at one line the
   two gates that matter most read identically as "Gate 1 - Delivery Entr...".
   Two lines at 13px fit a 44-50px row; a name longer than that still ends in
   an ellipsis. Workspace only: /book's week keeps its one-line, 74px label
   (it is not inside #workspace), because its pane is narrower still. */
#workspace .calendar-gate {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 6px;
  align-items: center;
  white-space: normal;
  padding: 6px 8px;
}
/* A gate's load on this day, beside its name. Warm Gray while there is room,
   blue once it is nearly spoken for, red once it is full -- at 100% every
   bookable minute is taken, so an approval into that gate has to collide.
   Not gold at either level: gold may never mean caution. The number is the
   message and the colour only sorts it, so nothing rests on hue alone.
   Formerly the HUD's per-gate bars; see views/partials/hud.ejs. */
.calendar-gate-load {
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--muted);                      /* 7.3:1 on --card, 8.2:1 dark */
}
.calendar-gate-load.is-tight { color: var(--info); }   /* 7.9:1 on --card, 7.8:1 dark */
.calendar-gate-load.is-full  { color: var(--danger); font-weight: 700; }  /* 6.5:1 on --card, 6.6:1 dark */
/* Each level sets its own colour, so .calendar-gate:hover recolouring the
   label to link blue reaches the name and leaves the figure as it was. */
#workspace .calendar-gate-name {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  white-space: normal;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

/* How wide the label is, and how narrow a quarter-hour may get to pay for it.

   The range a job site shows is its gate hours widened to cover anything
   drawn on them, so an afternoon bus block to 16:30 makes an ordinary day
   07:00-17:00: forty columns. Available width G below is the viewport less
   the frame margins, the pane padding and, from 1500px, the 320px rail.

     720-1099   74px label, 16px floor: as before. 74 + 40 x 16 = 714.
                The label stacks (one column) so anything beside the name
                drops under it instead of squeezing it.
     1100-1499  176px label, 16px floor. G is about 997 at 1100;
                176 + 40 x 16 = 816.
     >= 1500    176px label, 22px floor. G is about 1061 at exactly 1500;
                176 + 40 x 22 = 1056. The old 132 + 40 x 26 = 1172 scrolled
                sideways on a 1920 monitor at 125%.

   176px is two lines of "Gate 1 - Delivery / Entrance" with room for a
   short figure beside it. The floor drops from 26 to 22 only at 1500 and
   up, and the columns still stretch to fill whatever is left, so it only
   shows as narrower in a day long enough to need it. --cal-slot-max is
   pinned at 52 because it otherwise follows the floor down, and a short
   gate day would stop stretching sooner than it did. */
@media (min-width: 720px) and (max-width: 1099px) {
  #workspace .calendar-gate { grid-template-columns: minmax(0, 1fr); }
}
@media (min-width: 1100px) {
  #workspace .calendar-grid { --cal-label-w: 176px; }
}
@media (min-width: 1500px) {
  #workspace .calendar-grid { --cal-slot-w: 22px; --cal-slot-max: 52px; }
}

.viewtabs {
  display: flex;
  gap: 6px;
  margin-top: 10px;
}
.viewtab {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 18px;
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  text-decoration: none;
}
.viewtab:hover { border-color: var(--accent-edge); color: var(--text); }
/* Selected, not shouted. A solid gold pill here outweighed the delivery
   blocks it sits beside, and the blocks are what the eye should find
   first -- so the current tab is marked the same quiet way the switcher
   marks its current job site. */
.viewtab.is-current { color: var(--text); background: var(--bg); border-color: var(--accent-edge); }

.calendar-agenda { margin-top: 16px; }
.calendar-agenda-day { margin-top: 20px; }
.calendar-agenda-day:first-child { margin-top: 0; }
/* h2 in the markup now (see calendar-agenda.ejs); still sized as supporting
   text, not a section heading -- the day is a subdivision of the schedule,
   not a new page. */
.calendar-agenda-day h2 { margin: 0 0 6px; font-size: 15px; }
.calendar-agenda-gate { margin-top: 14px; }
.calendar-agenda-gate h3 { margin: 0; font-size: 13px; font-weight: 600; }
/* The way into a gate's day from the phone agenda measured 36x18 as plain
   text. 44px below 720px, where this is a thumb target and the agenda is
   what renders; the desktop grid takes over from 720px (see .calendar-agenda
   above), where the reset is a formality rather than something anyone taps. */
.calendar-agenda-gate h3 a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--text);
  text-decoration: none;
}
.calendar-agenda-gate h3 a:hover { text-decoration: underline; }
@media (min-width: 720px) {
  .calendar-agenda-gate h3 a { display: inline; min-height: 0; }
}

/* ---- Job-site switcher --------------------------------------------------
   Location > Job Site > Gate is the hierarchy, and the job site is where the
   work happens, so the switcher is chrome on every page rather than something
   you go back to the dashboard to do. A <details> disclosure, needing no
   script, which keeps it working under the content security policy and with
   scripting off entirely. */
.switcher { position: relative; min-width: 0; max-width: 100%; }
/* One line, the same 34px box as the menu links beside it (see .menubar-link):
   location, a dot, then the job site. The location gives way first when space
   runs out, because the project number is what the team says out loud. */
.switcher > summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  padding: 7px 26px 7px 10px;
  line-height: 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  background: var(--card);
  list-style: none;          /* Safari keeps its own marker without this. */
  min-width: 0;
}
.switcher > summary::-webkit-details-marker { display: none; }
/* The caret, drawn rather than typed so it cannot pick up an emoji font. */
.switcher > summary::after {
  content: "";
  position: absolute;
  right: 10px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -5px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
}
.switcher[open] > summary::after { margin-top: -2px; transform: rotate(225deg); }
.switcher > summary:hover { border-color: var(--accent-edge); }
.switcher > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.switcher-location {
  /* Shrinks a thousand times faster than the job site, so it is truncated
     away almost entirely before the site loses a letter. */
  flex: 0 1000 auto;
  min-width: 0;
  font-size: 13px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.switcher-sep { flex: none; font-size: 13px; color: var(--muted); }
.switcher-site {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The badge's 14px line, 1px padding and 1px border make 18px, exactly the
   line box. Top-aligned, because vertical-align: middle sits it a pixel and a
   half off the baseline and that alone made the summary taller than a link. */
.switcher > summary .project-number { line-height: 14px; vertical-align: top; }

.switcher-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 6px);
  left: 0;
  min-width: 260px;
  max-width: min(360px, 90vw);
  max-height: 70vh;
  overflow-y: auto;
  padding: 6px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-panel);
  /* The one place a shadow earns its keep: this floats over the page, and a
     border alone does not separate it from the card behind it. */
  box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
}
.switcher-group {
  margin: 8px 8px 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}
.switcher-group:first-child { margin-top: 4px; }
.switcher-item {
  display: block;
  padding: 8px 10px;
  border-radius: var(--radius-control);
  font-size: 15px;
  color: var(--text);
  text-decoration: none;
}
.switcher-item:hover { background: var(--bg); }
.switcher-item.is-current { background: var(--bg); font-weight: 600; }
/* Gold marks the current thing. It is a fill behind dark ink, never ink. */
.switcher-item.is-current .project-number { background: var(--accent); color: var(--on-accent); }

/* ---- Desktop workspace --------------------------------------------------
   Mobile friendly, not mobile first. Everything above this point is the phone
   layout and stays that way; this is the layout the work is actually done in,
   on a desktop monitor, often a second one.

   The workspace fills the frame, which already carries the 1600px bound. */
.workspace { width: 100%; }
/* A flex column so the HUD and the panes beneath it can be reordered by
   width -- see the note on #workspace .hud below. Visually identical to the
   plain block it replaces at every width nothing reorders: two block-level
   children stack the same way in a flex column as they do without one. */
#workspace {
  display: flex;
  flex-direction: column;
}
/* Below 720px there is no grid beside the HUD to justify it going first: the
   four stats and the gate-load bars pushed today's deliveries below the fold,
   on the one width where a driver is most likely checking the schedule from
   a phone in the yard. The calendar pane -- and the rail beside it, since
   they are one flex item -- moves ahead of it instead; the HUD is still one
   scroll away, not gone. From 720px, where the grid appears and the screen
   has the room for both, it reverts to the order it is written in. */
#workspace .hud { order: 2; }
#workspace .workspace-panes { order: 1; }
@media (min-width: 720px) {
  #workspace .hud { order: 0; }
  #workspace .workspace-panes { order: 0; }
}
.workspace-panes {
  display: grid;
  gap: 16px;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
.pane {
  min-width: 0;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-container);
  padding: 18px;
}

/* ---- Heads-up display ---------------------------------------------------
   The state of the job site above the schedule that details it. Deliberately
   not a row of cards: the figures sit on one surface divided by 1px rules,
   each under a small uppercase label, which is the hierarchy the rail already
   uses. A grid of identical boxes would say that nothing had been ranked. */
.hud {
  display: grid;
  gap: 16px;
  grid-template-columns: minmax(0, 1fr);
  align-items: stretch;
  margin-bottom: 16px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-container);
  padding: 14px 18px;
}
.hud-figures {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  row-gap: 14px;
  align-items: start;
}
.hud-figure {
  min-width: 0;
  padding: 2px 14px;
  text-decoration: none;
  border-left: 1px solid var(--border);
}
/* Two up on a phone, four across from tablet width. The column count is
   explicit so the rule only ever falls between two figures and never at the
   start of a row, which is what a wrapping flex row could not promise. */
.hud-figure:nth-child(2n + 1) { border-left: 0; padding-left: 0; }
@media (min-width: 620px) {
  .hud-figures { grid-template-columns: repeat(4, minmax(0, 1fr)); row-gap: 0; }
  .hud-figure:nth-child(2n + 1) { border-left: 1px solid var(--border); padding-left: 14px; }
  .hud-figure:nth-child(4n + 1) { border-left: 0; padding-left: 0; }
}
a.hud-figure:hover .hud-value { text-decoration: underline; }
/* Nothing here has a border to darken, so the ring is drawn in the gold
   shade instead: #C48600 is 3.1:1 on white and becomes full gold on dark,
   which clears 7.9:1. */
a.hud-figure:focus-visible { outline: 2px solid var(--accent-edge); outline-offset: 3px; }
.hud-label {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The Headline step, reused. A figure wants to be readable from across a
   desk, but inventing an eleventh type size to get there would be the tool
   announcing itself. Tabular numerals so the row does not reflow as counts
   change. */
.hud-value {
  display: block;
  margin-top: 3px;
  font-size: 26px;
  font-weight: 700;
  line-height: 1.1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* The only gold in the display, and only when there is something waiting: a
   count that represents work owed is exactly what gold is for. A fill behind
   dark ink, never gold ink -- it is ~2:1 on a light ground. */
.hud-value.is-waiting {
  display: inline-block;
  padding: 0 9px;
  background: var(--accent);
  color: var(--on-accent);   /* 6.2:1 */
  border-radius: var(--radius-control);
}
/* A duration reads as words, not as a number, so it takes the body size. */
.hud-value-text { font-size: 20px; font-weight: 600; }
.hud-note { display: block; margin-top: 2px; font-size: 13px; color: var(--muted); }

/* ---- Gate load, as a bar per gate ----
   The bars came back here on 2026-09-23, beside the per-day figure each grid
   row prints, not instead of it: the bars are the range in view, the row
   labels say which day is the busy one. */
.hud-load { min-width: 0; }
.hud-load .hud-label { margin: 0 0 8px; }
.hud-bars { list-style: none; margin: 0; padding: 0; }
.hud-bar-row {
  display: grid;
  grid-template-columns: minmax(64px, 132px) minmax(0, 1fr) 54px;
  align-items: center;
  gap: 10px;
  padding: 3px 0;
  font-size: 13px;
}
.hud-bar-name {
  color: var(--muted);                      /* 7.3:1 on --card, 8.2:1 dark */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Warm Gray carries, gold marks. A gate running hot is a fact to read off the
   length of the bar, not a warning -- and gold may never mean caution. */
.hud-bar-track {
  display: block;
  height: 10px;
  background: var(--bg);
  border: 1px solid var(--border);
}
/* The bar's length says how full a gate is; the colour only answers "can
   anything else still fit?". Warm Gray while there is room, blue once a gate
   is nearly spoken for, red once it is full -- at 100% every bookable minute
   is taken, so an approval into that gate has to collide. The same three
   levels, and the same colours, as .calendar-gate-load on the grid. The
   percentage is printed beside the bar, so nothing here rests on hue alone. */
.hud-bar-fill { display: block; height: 100%; background: var(--muted); }  /* 6.5:1 on the --bg track */
.hud-bar-fill.is-tight { background: var(--info); }    /* 7.0:1 on the track, 8.6:1 dark */
.hud-bar-fill.is-full  { background: var(--danger); }  /* 5.8:1 on the track, 7.3:1 dark */
.hud-bar-value {
  text-align: right;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* Given the width, the figures sit two by two beside the bars; below it they
   stack, four across, with the bars full width underneath.

   Beside the bars, the figures grow with them. A job site with eight gates has
   a bar list twice the height of one with four, and four figures at a fixed
   26px would sit stranded at the top of it. So the figures' block stretches to
   the bars' height, spreads its two rows across it, and the numbers scale with
   the gate count the markup passes as --hud-gates:

     2 gates  26px (the Headline step, unchanged)
     4 gates  31px
     6 gates  36px
     8+ gates 40px, the cap

   The cap is "within reason": past it the numbers would start competing with
   the grid underneath for attention, and the grid is the subject of the page.
   Below 1100px the figures are a strip above the bars, not a column beside
   them, so there is no height to match and they stay at their own size. */
@media (min-width: 1100px) {
  .hud {
    grid-template-columns: minmax(260px, 1fr) minmax(320px, 520px);
    gap: 24px;
  }
  .hud-figures {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: 1fr;
    row-gap: 14px;
    align-content: stretch;
  }
  .hud-figure { align-self: start; }
  .hud-figure:nth-child(2n + 1) { border-left: 0; padding-left: 0; }
  .hud-figure:nth-child(2n) { border-left: 1px solid var(--border); padding-left: 14px; }
  .hud-value { font-size: clamp(26px, calc(21px + 2.5px * var(--hud-gates, 0)), 40px); }
  .hud-value-text { font-size: clamp(20px, calc(16px + 2px * var(--hud-gates, 0)), 30px); }
  .hud-note { font-size: clamp(13px, calc(12px + 0.5px * var(--hud-gates, 0)), 15px); }
  .hud-load { border-left: 1px solid var(--border); padding-left: 24px; }
}

/* The toolbar above the grid: where you are, when you are looking at, and the
   actions that belong next to a schedule rather than three menus away. */
.workspace-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  margin-bottom: 14px;
}
.workspace-bar .datenav { flex: 0 0 auto; }
.workspace-bar .viewtabs { margin: 0; }
.workspace-bar-spacer { flex: 1 1 auto; }
.workspace-bar .datenav-today { margin: 0; }
.workspace-bar .datenav-today .datenav-heading { display: none; }
/* One width for "Today" and "This week": both labels share a grid cell and
   the longer, invisible one sets it. See calendar-nav.ejs. */
.workspace-bar .datenav-today a { display: inline-grid; }
.workspace-bar .datenav-today a > span { grid-area: 1 / 1; }
.datenav-today-sizer { visibility: hidden; }
.workspace-heading { margin: 0; font-size: 17px; font-weight: 600; }
.workspace-sub { margin: 2px 0 0; font-size: 13px; color: var(--muted); }

/* Two panes. Below this the rail sits under the calendar rather than
   disappearing -- on a phone it is the fastest way to see what is pending. */
@media (min-width: 1500px) {
  .workspace-panes { grid-template-columns: minmax(0, 1fr) 320px; }
}
@media (min-width: 1800px) {
  .workspace-panes { grid-template-columns: minmax(0, 1fr) 360px; gap: 20px; }
}

/* One pane, no rail: what a requester is served. Without this the two-column
   rules above would strand an empty 320 or 360px track beside the calendar on
   a wide monitor, because there is no second child to fill it.

   Outside a media query and below both of them on purpose: it has the same
   specificity as .workspace-panes, so source order is what makes it win. Any
   new .workspace-panes grid rule must go ABOVE this one, or it will silently
   stop applying to the single-pane workspace. */
.workspace-panes--single { grid-template-columns: minmax(0, 1fr); }

/* ---- The workspace rail -------------------------------------------------
   The situational half: what needs a decision, and what is on the schedule.
   It sits beside the calendar on a desktop and underneath it on a phone, where
   it is the quickest read of the day. */
.rail { display: flex; flex-direction: column; gap: 22px; }
.rail-section { min-width: 0; }
/* Below 720px the agenda directly above already shows what is on the
   schedule; the rail keeps only the two sections nothing else on the page
   says: Pending and Blocked. From 720px, where the rail sits beside the grid
   rather than under a repeat of it, all three sections return. */
.rail-section--scheduled { display: none; }
@media (min-width: 720px) {
  .rail-section--scheduled { display: block; }
}
.rail-title {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}
.rail-count {
  /* A count is a fact. Only one of the three sections is work owed to anyone,
     and only that one takes gold -- three identical gold badges are what
     taught the eye to stop reading them. The base is neutral; the padding
     drops to 0 vertically because the new 1px border restores the height. */
  display: inline-block;
  min-width: 20px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text);        /* 12.5:1 on --card, 14.0:1 on the dark card */
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0;
  text-align: center;
}
/* Work owed to the viewer: a fill behind dark ink, never gold ink. This is the
   only fill left in the rail, which is how it still marks. */
.rail-count--owed {
  background: var(--accent);
  color: var(--on-accent);   /* 6.2:1 */
  border-color: var(--accent-edge);
}
/* Factual: what is actually happening in the range being looked at. Blue may
   be a 1px line on a light ground at 7.9:1, which gold may not. */
.rail-count--info { color: var(--info); border-color: var(--info); }  /* 7.9:1 light, 7.8:1 dark */
.rail-list { list-style: none; margin: 0; padding: 0; }
.rail-item {
  padding: 10px 0;
  border-top: 1px solid var(--border);
  min-width: 0;
}
.rail-item:first-child { border-top: 0; padding-top: 0; }
.rail-item strong { display: block; font-size: 15px; color: var(--text); }
.rail-meta { display: block; font-size: 13px; color: var(--muted); }
.rail-actions { margin: 8px 0 0; font-size: 14px; }
.rail-actions a { text-decoration: none; }
.rail-actions a:hover { text-decoration: underline; }

/* The company name, as the way into the panel. Not the whole row: the row
   holds buttons, and an anchor cannot contain them. */
.rail-item-link {
  /* A 15px line of text is a 23px tap target; flex and a 44px floor fix that
     below 720px, where this is pressed with a thumb. The reset below returns
     it to the compact row a mouse works fine with. */
  display: flex;
  align-items: center;
  min-height: 44px;
  font-size: 15px;
  font-weight: 700;
  color: var(--text);       /* 12.5:1 */
  text-decoration: none;
}
.rail-item-link:hover { text-decoration: underline; }
@media (min-width: 720px) {
  .rail-item-link { min-height: 0; }
}
.rail-item-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.rail-item-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }

/* Approve and Decline, at the weight the rail can carry: the metrics of
   .rows button, so a column of them beside the grid stays subordinate to it.

   Approve is filled green and Decline is outlined red -- the same
   filled-primary / outlined-danger pairing the approvals queue already uses,
   and the pairing is what makes them scannable at a glance in a list.

   Note this contradicts the note above .status-approved, which says the
   Approve control is deliberately NOT green because green is a state rather
   than an action. That was the standing rule and it was overruled on purpose:
   these two sit side by side in a dense rail, they are pressed quickly, and
   telling them apart by colour is worth more here than the distinction
   between a state and an action. The queue's own button is untouched. */
.rail-approve, .rail-decline {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  padding: 7px 11px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  border-radius: var(--radius-control);
  border: 1px solid transparent;
  text-decoration: none;
  cursor: pointer;
}
.rail-approve {
  color: var(--on-ok);          /* 5.4:1 light, 6.4:1 dark */
  background: var(--ok);
  border-color: var(--ok);
}
.rail-decline {
  color: var(--danger);         /* 6.5:1 light, 6.6:1 dark */
  background: transparent;
  border-color: var(--danger);  /* 3.9:1 against --card, above the 3:1 UI edge */
}
.rail-approve:focus-visible, .rail-decline:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A pending row, compacted where there is a mouse. Company and the two
   buttons share the first line, day/window/gate the second, so a row is
   about 50px rather than 128 and eight of them leave "On the schedule" on
   the same screen. Anything that does not fit is cut with an ellipsis and
   read in full from the row's title -- the gate, being last, is what goes.
   Below 720px the row stays stacked, because there the buttons are pressed
   with a thumb and keep their full size. */
@media (min-width: 720px) {
  .rail-item--pending {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "name actions" "meta meta";
    column-gap: 8px;
    align-items: center;
    padding: 6px 0;
  }
  .rail-item--pending:first-child { padding-top: 0; }
  .rail-item--pending .rail-item-link { grid-area: name; min-width: 0; }
  .rail-item--pending .rail-item-link,
  .rail-item--pending .rail-meta {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .rail-item--pending .rail-meta { grid-area: meta; font-size: 12px; }
  .rail-item--pending .rail-item-actions { grid-area: actions; flex-wrap: nowrap; gap: 6px; margin-top: 0; }
  .rail-item--pending .rail-approve,
  .rail-item--pending .rail-decline { padding: 4px 8px; font-size: 12px; }
}

/* Where the capped list hands over to the full queue. Plain link ink: it is
   a way to somewhere else, not an action on anything in the rail. */
.rail-more {
  display: inline-block;
  margin-top: 8px;
  font-size: 13px;
  color: var(--link);           /* 7.9:1 on white, as every link */
}

/* Removing a block, styled as the link beside it rather than as a button with
   a box. The rail supports the grid and should not compete with it, and a red
   bordered control in every blocked row would read as an alarm on a schedule
   where a bus period is an ordinary fact. Red still carries the meaning --
   this is the only destructive thing in the rail -- it just carries it in the
   ink instead of a border. */
.rail-remove {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--danger);   /* 6.5:1 on --card, 6.6:1 dark */
  cursor: pointer;
  /* A one-line text button is a 17px target; 44px below 720px, where this
     is pressed with a thumb next to a weekly rule it is easy to mis-tap. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.rail-remove:hover { text-decoration: underline; }
.rail-remove:focus-visible { outline: 2px solid var(--danger); outline-offset: 2px; }
@media (min-width: 720px) {
  .rail-remove { min-height: 0; }
}

/* ---- Blackout bands -----------------------------------------------------
   When nothing may be booked. Drawn from the same blackoutInterval the booking
   engine refuses against, so the band and the refusal cannot disagree.

   Deliberately not gold: BRAND.md is explicit that Rich Gold must not mean
   "warning". An ordinary blackout is not red either -- a closure is a normal
   fact of the schedule rather than an error -- so it reads as hatched-out
   ground with a label, which is what it is, distinguishable from the
   closed-gate hatch by being denser and by carrying the blackout's name.

   Every block looks the same, a bus period included: the band carries the
   block's name, and the name already says why it is there. There used to be a
   Type field that drew a bus period with a red edge and label; it changed no
   rule, so it was dropped. */
.calendar-band {
  display: flex;
  align-items: center;
  overflow: hidden;
  margin: 4px 1px;
  padding: 0 5px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: repeating-linear-gradient(
    135deg,
    var(--bg),
    var(--bg) 5px,
    var(--border) 5px,
    var(--border) 7px
  );
  /* Under the delivery blocks, above the empty track. */
  z-index: 0;
}
.calendar-band-name {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* A chip behind the label so the hatch does not run through the text. */
  padding: 0 4px;
  border-radius: var(--radius-mark);
  background: var(--card);
}
/* A delivery must always win the stacking contest against a band it overlaps. */
.calendar-block { position: relative; z-index: 1; }

/* A key to the grid, so it goes where the grid goes: below 720px there is only
   the agenda, which names every row in words and needs no key. */
.calendar-legend { display: none; }
@media (min-width: 720px) {
  .calendar-legend { display: flex; }
}
.calendar-legend {
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 16px;
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--muted);
}
.calendar-legend span { display: inline-flex; align-items: center; gap: 6px; }
.calendar-legend i {
  width: 28px;   /* wide enough for a dashed edge to read as dashed */
  height: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-mark);
}
.calendar-legend .key-booked { background: var(--booked); border-color: var(--booked-edge); }
.calendar-legend .key-held { background: var(--card); border: 1px dashed var(--booked); }
.calendar-legend .key-blocked {
  background: repeating-linear-gradient(
    135deg, var(--bg), var(--bg) 5px, var(--border) 5px, var(--border) 7px
  );
}

/* The agenda's equivalent: blocked rows read differently from booked ones. */
.schedule .is-blocked .slot-company { color: var(--muted); font-style: italic; }

/* The list equivalent of the dashed block. On a phone and on the gate day page
   there is no grid to outline, so the same idea becomes a chip on the row: one
   visual language for held, not two. The company name beside it is already
   muted, which is why the distinction has to be drawn rather than coloured. */
.slot-held {
  margin-left: 6px;
  padding: 0 4px;
  font-size: 11px;
  letter-spacing: .02em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--muted);               /* 7.3:1 on --card */
  border: 1px dashed var(--booked);  /* 7.3:1 on --card, an edge needs 3:1 */
  border-radius: var(--radius-mark);
}

/* ---- Block panel --------------------------------------------------------
   Blocking a time used to be Admin > Blackouts > pick the job site > a nine
   field form > land on a list nowhere near the schedule. It is now a panel on
   the schedule itself, opened by ?block=1 and closed by a link.

   Server-rendered, so it works with scripting off and a rejected submission
   can come back with what was typed still in it. It is a <dialog open> that
   also carries .rail-modal, so public/app.js promotes it to a modal exactly
   like the detail panel below, and opening it no longer shoves the grid down. */
.block-panel {
  margin: 0 0 16px;
  padding: 16px 18px 18px;
  /* Separated from the pane by surface, not by ornament: the pane is --card and
     this sits on --bg, which is the same inversion the rest of the app uses for
     a nested region. An accent bar down one edge would read as decoration, and
     it is the single most recognisable tell of generated UI. */
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-panel);
}
.block-panel-head { display: flex; align-items: baseline; gap: 12px; }
.block-panel-head h2 { margin: 0; font-size: 17px; }
.block-panel-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: auto;
  /* 44px below 720px, where this is a thumb target and not a mouse pointer -
     32px stays the desktop size, reset below. */
  width: 44px;
  height: 44px;
  border-radius: var(--radius-control);
  text-decoration: none;
  color: var(--muted);
}
.block-panel-close:hover { color: var(--text); background: var(--card); }
.block-panel-close:hover { color: var(--text); }
@media (min-width: 720px) {
  .block-panel-close { width: 32px; height: 32px; }
}
.block-panel-sub { margin: 4px 0 14px; font-size: 13px; color: var(--muted); }

/* Bounded, because a form is read in a column and stretching seven fields
   across a 1400px pane makes them harder to scan, not easier. The panel spans
   the pane; the form inside it does not. */
.block-form,
.booking-form { margin: 0; max-width: 620px; }
.block-form-row { display: flex; flex-wrap: wrap; gap: 10px 14px; }
.block-form-row > div { flex: 1 1 150px; min-width: 0; }
/* Label over control, the control filling its half. Left inline, a short
   label and a short select sat side by side whenever the pair happened to fit
   and stacked when it did not - which in a 480px modal meant Gate above its
   field and Type beside its own, on the same row. */
.block-form-row label { display: block; margin-bottom: 6px; }
.block-form-row select,
.block-form-row input { width: 100%; }
/* A submit sized to its label. Full-width made it the loudest thing on the
   page, for an action that is not the page's purpose. */
.block-form button,
.booking-form button { align-self: flex-start; min-width: 150px; }

/* Date and Day used to show at once, whichever Repeats said - a one-off rule
   next to a weekday nobody would read, or a weekly one next to a date
   parseBlackoutForm was never going to look at. :has() reads the selected
   <option> straight off the Repeats select and hides the field that is not
   in play; the form already carries wrapper classes for exactly this.
   parseBlackoutForm keeps deciding which field is read - this only stops the
   panel from showing the one it will not be. A browser with no :has() shows
   both, which is what every browser showed before this rule existed. */
.block-form:has(select[name="recurrence"] option[value="weekly"]:checked) .field-date {
  display: none;
}
.block-form:has(select[name="recurrence"] option[value="once"]:checked) .field-weekday {
  display: none;
}

/* Booking | Block. Two forms sit under one pair of radios and the one not
   chosen is hidden, with the same :has() reading of a control that Repeats
   uses above - no script, no CSP change. Separate forms, so a required field
   in the hidden one cannot stop the visible one from submitting. A browser with
   no :has() shows both, each with its own button, which loses nothing.

   The toggle borrows .timeblock's look, so gold marks the one chosen. Two
   equal tracks at every width: it is two words, and a phone gets two thumbs. */
.mode-toggle { margin: 0 0 14px; max-width: 620px; }
.mode-toggle .timeblock-list { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 0; }
.block-panel:has(input[name="mode"][value="block"]:checked) .booking-form,
.block-panel:has(input[name="mode"][value="booking"]:checked) .block-form {
  display: none;
}
/* A not-listed company is typed; every listed one already has a name. */
.booking-form:not(:has(select[name="companyId"] option[value="other"]:checked)) .field-company-other {
  display: none;
}
.field-company-other { display: flex; flex-direction: column; gap: 6px; }
.booking-form textarea { width: 100%; box-sizing: border-box; }

/* The detail panel.

   Unscripted it is a <dialog open>, which renders as an ordinary block, so it
   inherits .block-panel and looks like the panel it is. showModal() promotes
   the same element to the top layer, and :modal is what tells the two apart --
   it matches only the scripted case. */
/* The browser's own dialog rules have to be undone for the unscripted mode:
   a <dialog open> is position:absolute with width:fit-content by default, so
   left alone it lifts out of the flow and lies across the grid. Static is what
   makes it the ordinary block this mode is supposed to be. */
.rail-modal {
  position: static;
  display: block;
  width: auto;
  max-width: none;
  height: auto;
  max-height: none;
  margin: 0 0 16px;
  color: var(--text);
}
/* Closing has to actually close it. The display above is an author rule, so it
   overrides the user agent's own dialog:not([open]) { display: none } no matter
   how specific that is - and without this the panel reappeared in the flow the
   moment the modal was dismissed. */
.rail-modal:not([open]) {
  display: none;
}
.rail-modal:modal {
  position: fixed;
  inset: 0;
  width: min(480px, calc(100vw - 24px));
  /* fit-content, not auto: inset:0 with an auto height stretches the box to
     the full viewport, and the panel would sit at the top of a tall empty
     rectangle. Centring is what margin:auto against inset:0 is for. */
  height: fit-content;
  max-height: calc(100dvh - 32px);
  margin: auto;
  overflow: auto;
  background: var(--bg);
  /* The one shadow in the system, and the case it was written for: this
     genuinely overlays content that scrolls behind it. */
  box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
}
/* Works on both themes, so it needs no dark answer. */
.rail-modal::backdrop { background: rgb(0 0 0 / 45%); }

.rail-modal .detail { margin-top: 14px; }
/* Bounded like .block-form, and for the same reason: unscripted this panel is
   as wide as the pane, and a reason box stretched across 1400px is harder to
   read, not easier. In the modal the panel is 480px and this never bites. */
.rail-modal-decline { margin: 18px 0 0; max-width: 620px; }
.rail-modal-decline label { display: block; font-weight: 600; margin-bottom: 4px; }
.rail-modal-decline textarea {
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  color: var(--text);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  resize: vertical;
}
.rail-modal-decline textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--text);
}
.rail-modal-actions { margin-top: 10px; }
.rail-modal-approve { margin-top: 14px; }
/* Thumb-sized in the panel, where there is room and a mis-press costs a
   delivery. The rail's own buttons stay compact. */
.rail-modal .rail-approve, .rail-modal .rail-decline {
  min-height: 44px;
  padding: 10px 16px;
  font-size: 14px;
}
.rail-modal .status { margin-top: 14px; }
.rail-modal .tabular { font-variant-numeric: tabular-nums; }

/* What a blocked window would cover. An .alert, because it interrupts, but it
   carries a list rather than a sentence: "2 deliveries" is not something an
   admin can act on, and the companies and times are. */
.block-warning p { margin: 0; }
.block-warning ul { margin: 8px 0 0; padding-left: 18px; }
.block-warning li { margin-top: 4px; }
.block-warning strong { display: block; }
.block-warning .tabular { font-variant-numeric: tabular-nums; }

/* The confirm step. Framed by a red edge rather than a red fill: it is asking
   a question, not reporting a failure, and .alert already owns the second
   treatment. */
.rail-modal-danger {
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid var(--danger);   /* 3.9:1 against --bg, above the 3:1 UI edge */
  border-radius: var(--radius-panel);
}
.rail-modal-danger p { margin: 0 0 6px; }
.rail-modal-danger p:last-child { margin-bottom: 0; }
.rail-modal-danger .rail-meta { display: block; }
.rail-modal-confirm { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
/* Keeping it is the safe answer, so it is the one with the calm styling and
   the one focus lands on first. */
.rail-modal-confirm .button-link { min-height: 44px; }

/* The only thing public/app.js ever draws: a request that died in flight. It
   does not retry, because a write that may or may not have landed is not a
   thing to guess about. */
.rail-offline { margin: 0 0 16px; }

@media (max-width: 420px) {
  /* A bottom sheet: showModal() is in the top layer, so where the panel sits
     in the document does not matter. dvh, because the Android URL bar would
     otherwise clip the buttons off the end of it. */
  .rail-modal:modal {
    width: 100vw;
    max-width: none;
    max-height: 92dvh;
    margin: auto 0 0;
    border-bottom: 0;
  }
}
.hint-inline { font-weight: 400; color: var(--muted); }

/* A link that has to read as a button, for actions that are navigations. */
.button-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 14px;
  /* The control radius, like every other control. This was 8px, which made it
     the only 7px-system control with a different corner. */
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.button-link:hover { border-color: var(--accent-edge); }
.button-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The filled variant, for a page whose whole purpose is one action -- a
   requester's calendar, where "Request a delivery" is why they opened it.
   Deliberately rare: two filled buttons on a screen is two primary actions,
   which is none.

   Stated entirely in --action / --on-action, the pair .form button already
   uses, so no new colour pair enters this file and the hard rule holds by
   construction. On a light ground --action is Warm Gray (#ffffff on #53575A,
   7.3:1) and on a dark one it is Rich Gold (#323436 on #F5A800, 6.2:1). Gold
   never carries light text here, because it is never the light-ground fill.
   Both clear the 4.5:1 body-text target, and dark mode needs no answer of its
   own: all three tokens are already reassigned in the dark block. */
.button-link--primary {
  border-color: transparent;
  background: var(--action);
  color: var(--on-action);
}
/* The hover edge is --on-action against its own fill, so it reuses the two
   ratios above -- 7.3:1 light, 6.2:1 dark -- both far past the 3:1 a UI edge
   needs. The inherited --accent-edge hover would vanish on dark, where
   --accent-edge and --action are both gold. */
.button-link--primary:hover { border-color: var(--on-action); }
/* Full gold is ~2:1 on white, and the 2px offset puts the ring on the pane
   rather than on the fill, so the ring takes the gold shade: --accent-edge is
   #C48600 at 3.1:1 on white and full gold at 7.9:1 on dark. Same reasoning as
   a.hud-figure:focus-visible. */
.button-link--primary:focus-visible { outline-color: var(--accent-edge); }

/* ---- Page footer --------------------------------------------------------
   One quiet strip under every page. It holds the light/dark switch, which is
   here rather than in the account menu because the account menu does not exist
   on the sign-in page -- and the sign-in page is the one page a signed-out
   vendor sees.

   It spans the frame, like the menu bar, so the switch sits in the same place
   on every page. margin-top:auto against the frame, which fills body's
   min-height:100vh, pins it to the bottom of the viewport on a short page like
   /login instead of leaving it stranded under a small card; on a long page it
   simply follows the content. */
.page-foot {
  width: 100%;
  margin-top: auto;
  padding-top: 18px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 14px;
}
/* Which build you are looking at. Quiet on purpose -- nobody needs this until
   they need it badly, and then they need it to be on the page rather than at
   /health, because a deploy on this host does not delete files and a rendered
   page can be served from cache. --muted is 6.5:1 on --bg in light and 9.1:1
   in dark. */
.page-foot-version {
  font-size: 12px;
  color: var(--muted);
}
/* The version and the privacy link travel together on the left, so the theme
   switch keeps the right-hand end to itself. */
.page-foot-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 14px;
}
/* --link on --bg, the same pair every other link uses, so it inherits that
   pair's contrast in both themes; only the size is quieted to match the
   version beside it. */
.page-foot-link { font-size: 12px; }
@media (max-width: 420px) {
  .page-foot { padding-top: 12px; }
}

.theme-switch {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  margin: 0;
}
/* The same 12px uppercase muted label the rail uses for a section: the label
   is quiet and the thing it names is not. --muted is 6.5:1 on --bg in light
   and 9.1:1 in dark. WCAG has no small-text penalty -- only a large-text
   relaxation -- so 12px at 6.5:1 is compliant and does not want growing. */
.theme-switch-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.theme-switch-options { display: flex; gap: 4px; }
.theme-option {
  padding: 7px 12px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-control);   /* a control, like every other one */
  cursor: pointer;
  /* Padding alone leaves this at 36px; 44px below 720px, where the footer's
     three buttons are as much a thumb target as anything above them. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.theme-option:hover { color: var(--text); border-color: var(--border); }
@media (min-width: 720px) {
  .theme-option { min-height: 0; }
}
/* Selected, marked by surface and ink alone. .viewtab.is-current takes a gold
   edge as well, but it sits on --card and this sits on --bg, so the fill has
   to invert -- and a display preference is not "action owed by the person
   looking at the screen", which is the only thing gold means below the menu
   bar. --text on --card is 12.5:1 in light and 14.0:1 in dark. */
.theme-option[aria-current] {
  color: var(--text);
  background: var(--card);
  border-color: var(--border);
}
/* Nothing else here has a border to darken, so the ring is the whole
   indicator. Full gold is ~2:1 on --bg; --accent-edge is 3.1:1 on white and
   full gold at 7.9:1 on dark. Same reasoning as a.hud-figure:focus-visible. */
.theme-option:focus-visible {
  outline: 2px solid var(--accent-edge);
  outline-offset: 1px;
}

/* ------------------------------------------------------------------------
   Help: the manuals, read in the app (views/help, src/help).

   The page is a contents column beside the document from 1100px, and the
   contents stack above it on a phone. .help-doc styles what the converter and
   the Quick Start extractor emit - plain h2/h3, tables, figures - plus the
   Quick Start's own classes (flow, steps, call, pill, shot, two), which come
   from docs/quickstart/guide.html and were laid out for print. Colours are the
   tokens above, so the dark theme needs nothing of its own. */
.help-list { list-style: none; margin: 16px 0 0; padding: 0; max-width: 640px; }
.help-list li + li { margin-top: 10px; }
.help-list-item {
  display: block;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-container);
  text-decoration: none;
  color: var(--text);
}
.help-list-item:hover { border-color: var(--text); }
.help-list-item:focus-visible { outline: 2px solid var(--accent-edge); outline-offset: 2px; }
.help-list-title { display: block; font-weight: 700; color: var(--link); }
.help-list-audience { display: block; font-size: 14px; color: var(--muted); }

.help-layout { display: block; margin-top: 16px; }
.help-toc {
  margin-bottom: 20px;
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-container);
  font-size: 14px;
}
.help-toc-title {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.help-toc ul { list-style: none; margin: 0; padding: 0; }
.help-toc li { margin: 0; }
.help-toc a { display: block; padding: 4px 0; text-decoration: none; }
.help-toc a:hover { text-decoration: underline; }
@media (min-width: 1100px) {
  .help-layout { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 40px; align-items: start; }
  .help-toc { position: sticky; top: 16px; margin-bottom: 0; max-height: calc(100vh - 32px); overflow-y: auto; }
}

.help-doc { min-width: 0; max-width: 820px; }
.help-doc h2 {
  margin: 36px 0 12px;
  padding-top: 12px;
  border-top: 2px solid var(--accent);
  font-size: 22px;
  scroll-margin-top: 16px;
}
.help-doc > h2:first-child { margin-top: 0; }
.help-doc h3 { margin: 24px 0 8px; font-size: 17px; scroll-margin-top: 16px; }
.help-doc h4 { margin: 16px 0 4px; font-size: 15px; }
.help-doc ul, .help-doc ol { margin: 0 0 16px; padding-left: 24px; color: var(--muted); }
.help-doc li { margin-bottom: 4px; }
.help-doc strong { color: var(--text); }
.help-doc pre {
  margin: 0 0 16px;
  padding: 12px 14px;
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  font-size: 13px;
}
.help-doc pre code { padding: 0; border: 0; background: none; }
.help-doc .table-scroll { margin: 0 0 16px; overflow-x: auto; }
.help-doc table { width: 100%; border-collapse: collapse; font-size: 14px; }
.help-doc th {
  padding: 6px 10px;
  text-align: left;
  background: var(--action);
  color: var(--on-action);
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.help-doc td { padding: 6px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
.help-doc figure { margin: 0 0 20px; }
.help-doc img {
  display: block;
  max-width: 100%;
  height: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
}
.help-doc figcaption { margin-top: 6px; font-size: 13px; color: var(--muted); }
.help-meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 16px;
  margin: 0 0 24px;
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-container);
  font-size: 14px;
}
.help-meta dt { font-weight: 700; }
.help-meta dd { margin: 0; color: var(--muted); }

/* The Quick Start's own pieces. */
.help-doc .head { margin-bottom: 16px; }
.help-doc .sub { font-size: 17px; }
.help-doc .flow {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 10px;
  margin: 0 0 24px;
}
.help-doc .flow-step {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-top: 3px solid var(--accent-edge);
  border-radius: var(--radius-container);
  text-align: center;
}
.help-doc .flow-step .n { font-size: 12px; font-weight: 700; letter-spacing: 0.06em; color: var(--muted); }
.help-doc .flow-step .h { font-weight: 700; }
.help-doc .flow-step .d { font-size: 14px; color: var(--muted); }
.help-doc .two, .help-doc .phones { display: grid; gap: 20px; margin-bottom: 16px; }
.help-doc .two > *, .help-doc .phones > * { min-width: 0; }
@media (min-width: 900px) {
  .help-doc .two { grid-template-columns: 1fr 1fr; }
  .help-doc .phones { grid-template-columns: 220px 1fr; align-items: start; }
}
.help-doc ol.steps { list-style: none; padding: 0; counter-reset: step; }
.help-doc ol.steps > li { position: relative; padding-left: 36px; margin-bottom: 14px; counter-increment: step; }
.help-doc ol.steps > li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 0;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  background: var(--action);
  color: var(--on-action);
  font-size: 13px;
  font-weight: 700;
}
.help-doc ol.steps h4 { margin: 0 0 2px; }
.help-doc ol.steps p { margin: 0; font-size: 15px; }
.help-doc .call {
  margin: 0 0 16px;
  padding: 10px 14px;
  border-left: 3px solid var(--accent-edge);
  background: var(--bg);
  font-size: 15px;
  color: var(--muted);
}
.help-doc .call .t {
  margin-bottom: 2px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text);
}
.help-doc .pill {
  display: inline-block;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}
.help-doc span.status {
  display: inline-block;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
}
.help-doc .closer { margin-top: 24px; padding-top: 8px; border-top: 2px solid var(--accent); }
.help-doc td.nav-cell { font-weight: 700; color: var(--text); }
/* The first column stays on one line where there is room for it; on a phone
   it wraps, or the table would be wider than the screen. */
@media (min-width: 720px) {
  .help-doc td.nav-cell { white-space: nowrap; }
}

/* ---- Add a company ---------------------------------------------------------
   One page asking who signs in and where, with a readout under the box that
   says what the paste will let in. Nothing new in the palette: the same
   --danger and --muted pairings the rest of the form screens already clear. */

/* The paste box takes a field's look, which .form input gives inputs only. */
.form textarea {
  padding: 11px 12px;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  resize: vertical;
}
.form textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--text);
}

/* The readout is a quiet panel under the box, not a second form field: --bg
   ground, a hairline, no shadow and no coloured edge. Small type, because it is
   a check on what was typed and the box above is the thing to read. */
.signin-readout {
  margin: 0 0 8px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  font-size: 14px;
}
.signin-readout-summary { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
.signin-readout-list { list-style: none; margin: 0; padding: 0; }
.signin-readout-list:not(:empty) { margin-top: 8px; }
.signin-readout-list li { display: flex; gap: 8px; padding: 4px 0; }
.signin-readout-list strong { font-weight: 600; word-break: break-all; }
/* The mark gets a fixed width so every label starts on the same edge whichever
   mark leads the line. Colour is never the only signal: the mark differs too,
   and a screen reader is told the status in words instead. */
.signin-mark { flex: 0 0 1.25em; text-align: center; color: var(--muted); }
.signin-readout-list .is-refused strong,
.signin-readout-list .is-refused .signin-mark { color: var(--danger); }
/* Skipped is not a problem and must not look like one: muted ink all through,
   because JHL addresses are harmless, just not needed. */
.signin-readout-list .is-skipped strong { font-weight: 400; color: var(--muted); }

/* The job-site ticks are a fieldset only for the sake of the legend, which a
   screen reader reads before each box. The browser's own box around it is
   noise next to the form's other groups, so it is flattened. */
.choice-group { margin: 12px 0 0; padding: 0; border: 0; min-width: 0; }
.choice-group legend { padding: 0; font-size: 14px; font-weight: 600; }
/* A location's name over its job sites, in the same quiet small caps the form
   section headings use, so the two hierarchies do not compete. */
.choice-group-heading {
  margin: 12px 0 4px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.choice-grid { display: grid; gap: 8px; }
.choice-grid .day-toggle { flex: none; min-height: 44px; }  /* a thumb-sized row on a phone */
/* Two to a row once there is room, so a location with four job sites is not a
   screen of one-line rows. */
@media (min-width: 720px) {
  .choice-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .choice-grid .day-toggle { min-height: 0; }
}

/* The Remove tick on a sign-in already on a company: the same row the list had,
   with a checkbox where its button was. Ticking is not removing - nothing happens
   until Save - so it is plain ink, not the red of a button that acts at once. */
.rows .remove-tick { display: flex; align-items: center; gap: 7px; flex: 0 0 auto; font-size: 14px; }
.rows .remove-tick input { width: 18px; height: 18px; padding: 0; flex: 0 0 auto; }

.form-actions { display: flex; align-items: center; gap: 16px; margin-top: 10px; }
.form-actions button { margin-top: 0; }

/* A company on the list that admits nobody. The words carry it; the weight only
   makes them findable in a column of plain text. Body ink, not red: it is a gap
   to fill, not a refusal. */
.row-problem { font-weight: 600; color: var(--text); }

/* A section heading partway down a form keeps its hairline. The rule above that
   strips it from the first heading is for a form that opens with one; here the
   heading follows a field and needs the separation. */
.form .form-section--divided { margin-top: 24px; padding-top: 14px; border-top: 1px solid var(--border); }

/* A field that is the reason a save was refused. The message at the top says
   why; this says where. A 2px red edge rather than a red fill, so what was typed
   stays readable, and the field's own focus ring still wins when it is focused.
   The job sites are a group, so the group gets the edge, set off from its
   boxes by padding. */
.form input[aria-invalid="true"],
.form textarea[aria-invalid="true"] {
  border-color: var(--danger);   /* 3.9:1 against --card, above the 3:1 UI edge */
  box-shadow: 0 0 0 1px var(--danger);
}
.choice-group.is-invalid {
  padding: 4px 12px 12px;
  border: 2px solid var(--danger);
  border-radius: var(--radius-control);
}
.choice-group.is-invalid legend { padding: 0 4px; }

/* The reasons a save was refused, as one box over the form. Held to the form's
   own width, so the box and the fields it points at share a right edge rather
   than the box running the full card while the fields stop at 640px. */
.form-errors { margin: 16px 0 0; }
.form-errors p { margin: 0; font-weight: 600; }
.form-errors ul { margin: 6px 0 0; padding-left: 20px; }
.form-errors li + li { margin-top: 4px; }
@media (min-width: 720px) {
  .card-wide .form-errors { max-width: 640px; }
}
