/* CTRL-specific styles, layered over the shared app.css tokens
   (--orange, --ink, --cream, --paper, --muted, --line, --darkred, --disp, --font).
   Folds into staff_web/static alongside app.css per the migration checklist. */

/* ------------------------------------------------------------ record head */
.record-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 20px; margin-bottom: 22px; flex-wrap: wrap;
}
.record-name {
  font-family: var(--disp); font-stretch: var(--disp-stretch); font-style: italic; font-weight: 900;
  font-size: clamp(1.7rem, 3.4vw, 2.6rem); color: var(--ink);
  margin: 2px 0 6px; line-height: 1.03;
}
.record-sub { font-size: .95rem; font-weight: 600; }
.flag-warn { color: var(--darkred); font-weight: 800; }

/* ------------------------------------------------------- record columns */
.record-cols {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px;
}
/* app.css's `.card + .card` owl margin is for stacked cards; inside this grid it
   pushed every card after the first 22px down its cell — the same staircase the
   front page had. The grid's own gap is the spacing here. */
.record-cols > .card + .card { margin-top: 0; }
.record-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px 20px;
}
.record-out { margin-top: 16px; }

.field-val {
  font-weight: 700; color: var(--ink); font-size: 1rem; margin-top: 3px;
  word-break: break-word;
}
.field-val.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .92rem; }
.field-val.is-warn { color: var(--darkred); }
.field-val .empty { color: var(--muted); font-weight: 600; }

/* ------------------------------------------------------- sensitive fields */
.field.sensitive > span { display: inline-flex; align-items: center; gap: 8px; }
.tag {
  font-size: .6rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  padding: 2px 7px; border-radius: 999px; line-height: 1.4;
}
.tag-hidden { background: var(--cream); color: var(--muted); border: 1px solid var(--line); }
.tag-shown { background: var(--orange); color:var(--on-orange); }

.field.is-masked .field-val {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--muted); letter-spacing: .04em;
}
.field.is-revealed .field-val {
  /* a quiet ring so a revealed value is unmistakably the real thing */
  display: inline-block; background: rgba(255,100,70,.10);
  box-shadow: 0 0 0 1px rgba(255,100,70,.35) inset;
  border-radius: 8px; padding: 2px 8px;
}

/* --------------------------------------------------------- the PII banner */
.pii-banner {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  background: #64231e; color: #f6f0e3;
  border-radius: 14px; padding: 14px 18px; margin-bottom: 22px;
  box-shadow: 0 10px 30px rgba(100,35,30,.35);
}
.pii-banner strong { color: #fff; }
.pii-banner span:last-child { color: #e1d2aa; font-size: .95rem; }
.pii-banner-dot {
  width: 11px; height: 11px; border-radius: 50%; background: var(--orange);
  box-shadow: 0 0 0 0 rgba(255,100,70,.7); animation: pii-pulse 1.8s infinite;
  flex: none;
}
@keyframes pii-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(255,100,70,.6); }
  70%  { box-shadow: 0 0 0 9px rgba(255,100,70,0); }
  100% { box-shadow: 0 0 0 0 rgba(255,100,70,0); }
}
@media (prefers-reduced-motion: reduce) { .pii-banner-dot { animation: none; } }

/* ---------------------------------------------------------- the toggle */
.pii-toggle-wrap { display: inline-flex; align-items: center; gap: 10px; }
.pii-toggle-label {
  font-size: .72rem; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}
.pii-switch {
  border: 0; background: none; padding: 4px; cursor: pointer; line-height: 0;
}
.pii-switch-track {
  display: inline-block; width: 46px; height: 26px; border-radius: 999px;
  background: var(--line); position: relative;
  transition: background .18s ease;
}
.pii-switch-thumb {
  position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--paper);
  box-shadow: 0 1px 3px rgba(0,0,0,.28); transition: left .18s ease;
}
.pii-switch.is-on .pii-switch-track { background: var(--orange); }
.pii-switch.is-on .pii-switch-thumb { left: 23px; }
.pii-switch:focus-visible .pii-switch-track { box-shadow: 0 0 0 3px rgba(255,100,70,.35); }

/* ------------------------------------------------------------ responsive */
@media (max-width: 760px) {
  .record-cols { grid-template-columns: 1fr; }
  .record-head { flex-direction: column; }
}
@media (max-width: 460px) {
  .record-grid { grid-template-columns: 1fr; }
}

/* ------------------------------------------------------------- fleet view */
.fleet-head { display:flex; align-items:center; justify-content:space-between; gap:16px;
  flex-wrap:wrap; margin-bottom:20px; }

.metrics { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  gap:14px; margin-bottom:22px; }
.metric-sub { font-size:.72rem; font-weight:700; color:var(--muted); margin-top:2px; }
.metric-value .metric-sub { display:inline; font-size:.85rem; }
.metric-warn { border-color:var(--orange); box-shadow:0 0 0 1px rgba(255,100,70,.25) inset; }
.metric-warn .metric-value { color:var(--darkred); }

.version-spread { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
.vchip { font-family:ui-monospace,"SF Mono",Menlo,monospace; font-size:.78rem;
  background:var(--cream); border:1px solid var(--line); border-radius:999px;
  padding:3px 10px; color:var(--muted); }
.vchip b { color:var(--ink); margin-left:6px; }
.vchip-latest { background:var(--orange); color:var(--on-orange); border-color:var(--orange); }
.vchip-latest b { color:var(--on-orange); }

.fleet-table td { vertical-align:middle; }
.cell-gym a { font-weight:800; }
.cell-gym a:hover { color:var(--orange-mid); }

.dot { display:inline-block; width:10px; height:10px; border-radius:50%; }
.dot-ok { background:var(--green); }
.dot-bad { background:var(--darkred); }

.pill { display:inline-block; font-size:.6rem; font-weight:800; letter-spacing:.08em;
  text-transform:uppercase; padding:2px 7px; border-radius:999px; margin-left:6px; }
.pill-warn { background:var(--protein); color:var(--darkred); }
.pill-bad { background:var(--darkred); color:var(--on-darkred); }

/* One icon chip per checklist task, coloured by the sweep's state. The glyph
   says which task at a glance and the title says it in words — seven
   three-letter codes in a row was a legend nobody had. Sized square so a row
   of them lines up whatever the emoji's own metrics are, and the state colour
   still carries the meaning that matters most. */
.task { display:inline-flex; align-items:center; justify-content:center;
  width:22px; height:22px; font-size:.78rem; line-height:1;
  border-radius:6px; margin-right:3px; cursor:help; }
/* The state is carried by a RING on a pale field, never by a dark fill. An
   emoji brings its own colours, and several of these are red — on the solid
   dark-red the overdue chip used, the glyph disappeared into its own alarm.
   Pale field, coloured ring: the row still scans as red-means-overdue, and you
   can still tell which task it is. */
.task-ok { background:var(--tint-green); box-shadow:inset 0 0 0 1px var(--tint-green-line); }
.task-dueSoon { background:var(--protein); box-shadow:inset 0 0 0 1px var(--tint-amber-line); }
.task-overdue { background:var(--tint-red); box-shadow:inset 0 0 0 2px var(--darkred); }
.task-never { background:var(--line); opacity:.5; }

/* Admin at the machine this week + software behind = the update is not taking. */
.cell-alert { color:var(--darkred) !important; font-weight:800; }
/* The admin's name rides under the time. Capped so one long name cannot widen
   a column the whole table has to live with; the full name is on hover. */
.admin-name { max-width:19ch; overflow:hidden; text-overflow:ellipsis;
  white-space:nowrap; font-weight:400; }

/* A nickname field that lives inside a table cell, not a form card. */
.input-inline { width:11ch; padding:4px 8px; font-size:.85rem; }

/* ── What members wrote, as a strip rather than a card ───────────────────
   A median gym gets five written messages a YEAR, so this is the rarest thing
   on the page — but it was rendering as three full quote blocks above the
   tabs, which put the actual work of the page below the fold. One line each,
   truncated, with the full text on hover and the tab one click away. */
.voice-strip { display:flex; align-items:center; gap:14px; flex-wrap:wrap;
  padding:10px 16px; margin:0 0 18px; background:var(--paper);
  border-radius:14px; border-left:3px solid var(--orange); }
.voice-strip-kicker { font-size:.68rem; font-weight:800; letter-spacing:.08em;
  text-transform:uppercase; color:var(--muted); white-space:nowrap; }
.voice-strip-list { flex:1 1 320px; list-style:none; margin:0; padding:0;
  min-width:0; }
.voice-strip-list li { display:flex; align-items:baseline; gap:8px;
  padding:2px 0; min-width:0; }
.voice-strip-list li + li { border-top:1px dotted var(--line); }
.voice-strip-list a { flex:1 1 auto; min-width:0; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; color:var(--ink);
  text-decoration:none; }
.voice-strip-list a:hover { color:var(--orange-mid); text-decoration:underline; }
.voice-strip-when { flex:0 0 auto; font-size:.76rem; color:var(--muted); }

.fleet-foot { margin-top:14px; font-size:.85rem; }

@media (max-width:760px) {
  .fleet-table thead { display:none; }
  .fleet-table tbody tr { display:grid; grid-template-columns:1fr auto; gap:4px 10px;
    padding:12px 14px; border-bottom:1px solid var(--line); }
  .fleet-table td { border:0; padding:0; }
  .fleet-table td.cell-gym { grid-column:1 / -1; font-size:1.02rem; }
}

/* ── Umbrella nav ───────────────────────────────────────────────────── */
/* Shakeventory's app.css grew this block when the gym locator landed; CTRL's
   copy of app.css predates it and is otherwise byte-identical. Added here rather than
   in that copy so the two files stay one hunk apart instead of two, and so the whole
   fork is easy to delete when app.css is promoted to staff_web/static/. */
.topbar-nav { display:flex; gap:18px; margin-left:28px; font-weight:700; font-size:.95rem; }
.topbar-nav a { color:var(--muted); padding:4px 0; border-bottom:2px solid transparent;
  white-space:nowrap; }
.topbar-nav a:hover { color:var(--ink); }
.topbar-nav a.is-current { color:var(--darkred); border-bottom-color:var(--orange); }
/* The nav drops to a row of its own before it runs into the sign-out button.
   860, not the 680 this was written at: a fourth section (Products, 2026-08-25)
   pushed the single-row layout past the viewport at 785px and the button went
   off the right edge with no way to scroll to it. Measure before moving this —
   the number is the width the four labels plus the lockup plus the button
   actually need, not a device size. */
@media (max-width:860px) {
  .topbar-inner { flex-wrap:wrap; padding-bottom:6px; }
  .topbar-nav { order:3; width:100%; margin:0 0 2px; gap:16px; font-size:.9rem; }
}

/* A bare .mono was never defined anywhere, so every `class="mono"` in fleet.html has
   been rendering in the body font. Only `.field-val.mono` existed. */
.mono { font-family:ui-monospace,"SF Mono",Menlo,monospace; font-size:.92rem; }

/* ── Per-gym panels ─────────────────────────────────────────────────── */
/* A quiet line under a panel heading: what the number does and does not mean. Used
   wherever a figure needs a caveat travelling with it rather than in a tooltip
   nobody opens. */
.panel-note { font-size:.85rem; color:var(--muted); margin:-8px 0 16px; max-width:70ch; }
.panel-note code { font-family:ui-monospace,"SF Mono",Menlo,monospace; font-size:.82rem; }

/* A rate we are deliberately not showing, in the place the rate would have been —
   an em dash with a reason attached, so "we can't say yet" never reads as zero. */
.suppressed { color:var(--muted); font-style:italic; cursor:help;
  text-decoration:underline dotted var(--line); text-underline-offset:3px; }

/* The review text feed. Ratings carry text so rarely that when one does, it should
   look like something somebody wrote, not another table row. */
.review-list { display:grid; gap:12px; }
.review { border-left:3px solid var(--line); padding:2px 0 2px 14px; }
.review.is-low { border-left-color:var(--darkred); }
.review-text { font-size:1.02rem; line-height:1.45; }
.review-meta { font-size:.8rem; color:var(--muted); margin-top:4px;
  display:flex; gap:10px; flex-wrap:wrap; }
.stars { color:var(--orange); letter-spacing:1px; white-space:nowrap; }
.stars .off { color:var(--line); }

/* Star distribution as a text-first bar, so the shape survives with charts blocked. */
.star-rows { display:grid; gap:6px; }
.star-row { display:grid; grid-template-columns:52px 1fr 76px; align-items:center; gap:10px;
  font-size:.85rem; }
.star-bar { height:10px; border-radius:999px; background:var(--line); overflow:hidden; }
.star-bar > span { display:block; height:100%; background:var(--orange); border-radius:999px; }

/* A one-word note in the written feed — kept, but visually subordinate to a sentence. */
.review.is-brief .review-text { font-size: .95rem; color: var(--muted); }

/* ── Metric tile alignment ───────────────────────────────────────────────── */
/* A row of tiles is read by scanning the NUMBERS, so the numbers have to sit on one
   line. They did not: "ONLINE NOW" fits on one line while "BEHIND ON SOFTWARE" wraps to
   two, which pushed its value a whole line lower than its neighbour's, and tiles with a
   sub-caption ("latest is v108") sat differently again from tiles without one.
   
   So the tile becomes a three-row grid — label, value, caption — with the label row
   reserving two lines whether it needs them or not, and the caption pinned to the
   bottom. Every value now starts at the same y, every caption ends at the same y, and a
   one-word label simply leaves air above its number instead of moving it. */
.metric { display: grid; grid-template-rows: auto auto 1fr; }
.metric-label {
  /* Two lines at this font-size and line-height. Longer labels than that are a copy
     problem, not a layout one — they wrap and push, which is the visible signal. */
  line-height: 1.25;
  min-height: 2.5em;
}
.metric > .metric-value { align-self: start; }
/* Only the block caption gets pinned; `.metric-value .metric-sub` is the inline
   "/ 276" and must stay where it is. */
.metric > .metric-sub { align-self: end; }

/* A metric tile that is also a filter. Every headline number answers "which ones?",
   so the tiles carrying a filter look pressable and the active one stays lit. */
a.metric { text-decoration: none; cursor: pointer; transition: border-color .12s, transform .12s; }
a.metric:hover { border-color: var(--orange); transform: translateY(-1px); }
a.metric:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.metric-on { border-color: var(--darkred); box-shadow: 0 0 0 1px var(--darkred) inset; }
.metric-on .metric-label { color: var(--darkred); }

/* Action cards on the front page. The grid already stretches them to equal height; this
   makes the body take the slack so the button rows line up too, instead of each sitting
   directly under copy of a different length. */
.action-card-body { flex: 1 1 auto; margin-bottom: 0; }
.action-card-buttons { align-items: center; }
/* app.css's `.stack > * + *` owl margin is for vertical stacks, but `.grid.stack` uses
   .stack only for its gap override — the margin leaks in and pushes every grid cell
   after the first 22px down, which is exactly the staircase the front page had. */
.grid.stack > * + * { margin-top: 0; }

/* Second-level nav. Gyms is one section with three views of the same question — the
   roster, the map, and the fleet — so they sit under it rather than competing with it
   for space in the top bar. */
.subnav { display: flex; gap: 20px; padding: 10px 0 0; font-weight: 700; font-size: .9rem; }
.subnav a { color: var(--muted); padding: 2px 0; border-bottom: 2px solid transparent; }
.subnav a:hover { color: var(--ink); }
.subnav a.is-current { color: var(--darkred); border-bottom-color: var(--orange); }

/* ── Customer record & insight panels ───────────────────────────────── */
/* A panel title with a control on its right — Edit, Adjust drinks, a caption.
   Flex, not grid: the right side can be a button or a whisper of text. */
.panel-head { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.panel-head .panel-title { margin-bottom: 0; }
.panel-head .btn, .panel-head .btn-sm { padding: 6px 16px; font-size: .82rem; }
.card > .panel-note:first-of-type { margin-top: 0; }

.small { font-size: .82rem; }

/* The one-line profile read — the sentence a rep says out loud first. */
.profile-read { font-size: 1.08rem; font-weight: 700; color: var(--darkred);
  margin: 0 0 10px; }

.badge-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 6px; }

/* A quiet kicker over each chart / sub-list inside a panel. */
.chart-kicker { font-size: .74rem; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 8px; }

/* Where-they-train (and spend-by-family) bars: label / bar / count. */
.venue-split { margin-top: 18px; }
.venue-rows { display: grid; gap: 8px; }
.venue-row { display: grid; grid-template-columns: minmax(120px, 1.2fr) 2fr minmax(150px, 1.1fr);
  align-items: center; gap: 12px; font-size: .88rem; }
.venue-name { font-weight: 700; }
.venue-bar { height: 10px; border-radius: 999px; background: var(--cream);
  border: 1px solid var(--line); overflow: hidden; }
.venue-bar > span { display: block; height: 100%; background: var(--orange);
  border-radius: 999px; min-width: 2px; }
.venue-count { text-align: right; white-space: nowrap; }
@media (max-width: 680px) {
  .venue-row { grid-template-columns: 1fr; gap: 4px; }
  .venue-count { text-align: left; }
}

/* A refunded row reads as an aside, not a sale. */
.row-refund td { color: var(--darkred); background: var(--tint-red); }

/* ── Service notes ──────────────────────────────────────────────────── */
.note-form { display: flex; gap: 10px; align-items: flex-start; margin: 4px 0 14px; }
.note-form textarea { flex: 1; resize: vertical; }
.note-list { display: grid; gap: 12px; }
.note { border-left: 3px solid var(--protein); padding: 2px 0 2px 14px; }
.note-text { white-space: pre-wrap; line-height: 1.45; }
.note-meta { font-size: .8rem; color: var(--muted); margin-top: 4px;
  display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
/* A destructive control that should whisper: plain text until hovered. */
.btn-quiet { border: 0; background: none; padding: 0; font: inherit;
  font-size: .8rem; color: var(--muted); cursor: pointer;
  text-decoration: underline dotted; text-underline-offset: 3px; }
.btn-quiet:hover { color: var(--darkred); }

/* ── Inline adjust / edit forms inside a record card ────────────────── */
.inline-adjust { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap;
  background: var(--cream); border: 1px solid var(--line); border-radius: 12px;
  padding: 12px 14px; margin: 0 0 16px; }
/* display:flex outranks the [hidden] UA style, which un-hides the closed form. */
.inline-adjust[hidden] { display: none; }
.edit-form { background: var(--cream); border: 1px solid var(--line);
  border-radius: 12px; padding: 14px 16px; margin: 0 0 16px; }

/* ── Proportion pass (2026-08-15) ───────────────────────────────────── */
/* The record page had buttons shouting over the data and half a dozen font
   sizes competing. One scale now: field values are the loudest thing in a
   card, panel controls are quiet tools, and every button inside the record
   shares one compact size. */
.record .btn, .record .btn-sm { padding: 6px 16px; font-size: .82rem; }
.record .btn-primary { box-shadow: 0 6px 16px rgba(255,100,70,.25); }
.record .panel-title { font-size: 1.02rem; }
.record .field-val { font-size: .95rem; }
.record .field-val.mono { font-size: .78rem; }
.record-sub { font-size: .92rem; }
.record .metric-value { font-size: clamp(1.2rem, 2vw, 1.6rem); }
.record .record-out { margin-top: 12px; }

/* ── The jump box ───────────────────────────────────────────────────── */
/* Type a name anywhere on a record, land on the next member. The dropdown is
   just a positioned fragment; empty response = closed. */
.jump-box { position: relative; display: inline-block; margin-left: 10px;
  vertical-align: middle; }
.jump-input { padding: 5px 12px; font-size: .82rem; width: 170px;
  border-radius: 999px; transition: width .15s ease; }
.jump-input:focus { width: 240px; }
.jump-results { position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;
  min-width: 280px; display: block; background: var(--paper);
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  box-shadow: 0 14px 40px rgba(26,20,17,.18); }
.jump-results:empty { display: none; }
.jump-row { display: block; padding: 8px 14px; font-size: .88rem;
  border-bottom: 1px solid var(--line); }
.jump-row:last-child { border-bottom: 0; }
a.jump-row:hover { background:var(--hover); }
.jump-name { font-weight: 700; display: block; }

/* ── The reporter-note banner (service notes) ───────────────────────── */
/* Same visual grammar as Shakeventory's "what the reporter wrote": an icon
   tile, a red kicker, and the note itself big enough to be the first thing
   read — because if CS wrote it, it matters. */
.reporter-note { display: flex; gap: 16px; align-items: flex-start;
  padding: 6px 2px 14px; }
.reporter-note-icon { flex: none; width: 52px; height: 52px; display: flex;
  align-items: center; justify-content: center; font-size: 1.5rem;
  background: var(--paper); border: 1px solid var(--line); border-radius: 14px;
  box-shadow: 0 4px 14px rgba(26,20,17,.08); }
.reporter-note-kicker { font-size: .72rem; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--orange-mid); margin: 2px 0 6px; }
.reporter-note-text { font-size: 1.15rem; font-weight: 700; line-height: 1.4;
  color: var(--ink); margin: 0 0 4px; white-space: pre-wrap; }
.reporter-note-body { min-width: 0; }

/* ── Role chip & header cues ────────────────────────────────────────── */
/* The role is the single most-asked fact on a call, so it reads as a label,
   not another grey chip. */
.chip-role { background: var(--ink); color: var(--cream); border-color: var(--ink);
  font-weight: 800; }
.home-gym-link { font-weight: 700; }
.home-gym-link:hover { color: var(--orange-mid); }
.back-to-search { margin-left: 12px; padding: 4px 14px; font-size: .8rem; }

/* ── The index-gap hint ─────────────────────────────────────────────── */
/* Honest but quiet: a market is temporarily missing from the directory. */
.index-gap { margin: 0 0 10px; color: var(--darkred); }

/* ── Sortable column headers ────────────────────────────────────────── */
.th-sort { cursor: pointer; user-select: none; white-space: nowrap; }
.th-sort:hover { color: var(--orange-mid); text-decoration: underline;
  text-underline-offset: 3px; }
.sort-arrow { color: var(--orange); font-size: .7rem; }

/* ── Reset filters ──────────────────────────────────────────────────── */
/* Quiet until a filter narrows the view, then it lights up. */
.btn-reset { align-self: flex-end; }
.btn-reset.is-active { background: var(--orange); color:var(--on-orange);
  border-color: transparent; box-shadow: 0 6px 18px rgba(255,100,70,.35); }
.btn-reset.is-active:hover { background: var(--orange); color:var(--on-orange); }

/* ── Service notes, when they matter ────────────────────────────────── */
/* Same logic as Shakeventory's inventory reports: content = attention. */
.card.notes-live { border-left: 5px solid var(--orange); background: var(--tint-orange-strong);
  box-shadow: 0 8px 26px rgba(255,100,70,.14); }

/* ── Shakers ────────────────────────────────────────────────────────── */
.shaker-list { display: grid; gap: 14px; }
.shaker { border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; }
.shaker.is-away { border-color: var(--darkred);
  box-shadow: 0 0 0 1px var(--darkred) inset; background: var(--tint-red); }
.shaker-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.shaker-id { font-weight: 700; }

/* ── The live Stripe strip ──────────────────────────────────────────── */
.stripe-note { margin: 14px 0 0; font-size: .88rem; }
.stripe-note code { font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: .8rem; background: var(--cream); padding: 1px 5px; border-radius: 6px; }
.stripe-live { margin-top: 14px; border-top: 1px dashed var(--line); padding-top: 4px; }
.stripe-sub { margin-top: 10px; }
.stripe-sub > div:first-child { display: flex; align-items: baseline; gap: 8px;
  flex-wrap: wrap; }

/* ── Shakebase ─────────────────────────────────────────────────────────
   `.inline-adjust` is a flex row that sizes every field to its own content.
   That is right for two or three controls and wrong for twelve: the machine
   record came out as a ragged stack of boxes at six different widths, with a
   full-width single-line "Notes" box and an oversized Save. A grid gives every
   field one column, lets the long ones span, and groups them under the three
   questions they answer — what is it, who owns it, when did it happen. */
.sb-form { background: var(--paper); border: 1px solid var(--line);
  border-radius: 12px; padding: 18px; margin: 0 0 16px;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px 16px; align-items: stretch; }
.sb-form[hidden] { display: none; }
/* A label that wraps to two lines ("Gym name — if not on the roster") used to
   push its input down and out of line with the one beside it. Fields in a row
   stretch to the same height and the input sits at the bottom, so the inputs
   line up whatever the labels do. Hints inside labels lose the uppercase. */
.sb-form .field { min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.sb-form .field > .input, .sb-form .field > select.input { margin-top: auto; }
.sb-form .field > textarea.input { margin-top: auto; }
.sb-form .field > span .muted { text-transform: none; letter-spacing: 0; font-weight: 600; }
.sb-form .wide { grid-column: 1 / -1; }
.sb-form textarea.input { resize: vertical; min-height: 70px; }
.sb-form-group { grid-column: 1 / -1; font-size: .7rem; font-weight: 800;
  letter-spacing: .11em; text-transform: uppercase; color: var(--muted);
  border-top: 1px solid var(--line); padding-top: 14px; margin: 4px 0 -4px; }
.sb-form-group:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
.sb-form-actions { grid-column: 1 / -1; display: flex; gap: 10px;
  align-items: center; border-top: 1px solid var(--line); padding-top: 16px;
  margin-top: 4px; }
.sb-form-actions .btn { padding: 7px 18px; font-size: .86rem; }
.sb-form-actions .sb-form-note { margin-left: auto; font-size: .78rem;
  color: var(--muted); text-align: right; max-width: 46ch; }

/* An urgent parcel has to look expensive to ask for, both when you are asking
   and when somebody else is looking at the list you put it on. */
.sb-alert { grid-column: 1 / -1; display: flex; gap: 10px; align-items: flex-start;
  background: var(--tint-red); border: 1px solid var(--tint-red-line); border-left: 4px solid var(--orange);
  border-radius: 10px; padding: 12px 14px; color: var(--darkred); font-size: .88rem; }
.sb-alert strong { font-weight: 800; }
.sb-alert[hidden] { display: none; }

.chip-urgent { background: var(--orange); color:var(--on-orange); border-color: var(--orange);
  font-weight: 800; letter-spacing: .04em; }
tr.row-urgent > td { background: var(--tint-red); }
tr.row-urgent > td:first-child { box-shadow: inset 3px 0 0 var(--orange); }

/* The banner that says this is not production yet. Loud on purpose — it is the
   one thing on the page that must not be mistaken for chrome. */
.sb-testbar { background: repeating-linear-gradient(135deg,
    #64231e, #64231e 14px, #4d1a16 14px, #4d1a16 28px);
  color: #fff; font-size: .82rem; font-weight: 700; letter-spacing: .02em; }
.sb-testbar .container { display: flex; gap: 12px; align-items: center;
  padding-top: 8px; padding-bottom: 8px; flex-wrap: wrap; }
.sb-testbar .sb-testbar-tag { background: var(--orange); color:var(--on-orange);
  border-radius: 999px; padding: 2px 12px; font-weight: 900;
  letter-spacing: .1em; font-size: .72rem; text-transform: uppercase; }
.sb-testbar a { text-decoration: underline; }

/* A label the group heading above it already gives sighted readers, kept for
   screen readers rather than dropped. */
.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; }

/* Machine photos, migrated out of AppSheet. A row of thumbnails that scrolls rather
   than a grid that reflows: they are almost all portrait phone shots of one machine,
   and eleven of them must not push the service history off the screen. */
.sb-photos { display: flex; gap: 12px; overflow-x: auto; padding: 4px 0 8px; }
.sb-photo { flex: 0 0 auto; width: 150px; margin: 0; position: relative; }
.sb-photo .inline-form { position: absolute; top: 6px; right: 6px; margin: 0; }
.sb-photo .inline-form .btn { min-width: 30px; min-height: 30px; padding: 0 8px;
  background: rgba(255,255,255,.9); border-color: rgba(0,0,0,.15);
  font-size: .8rem; line-height: 1; }
.sb-photo:not(:hover) .inline-form { opacity: .55; }
.sb-photo .inline-form:focus-within, .sb-photo:hover .inline-form { opacity: 1; }
.sb-photo img { width: 150px; height: 150px; object-fit: cover;
  border-radius: 10px; border: 1px solid var(--line); background: var(--cream);
  display: block; }
.sb-photo figcaption { margin-top: 6px; line-height: 1.35; }

/* ── Shakebase: taking a photo ──────────────────────────────────────────
   The one flow that is mobile-first. A bare file input is unstyleable and, on a
   phone, a ~20px target; the input is hidden and a 48px label drives it, which is
   what you can hit one-handed in a gym car park. */
.sb-capture { display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  background: var(--cream); border: 1px solid var(--line); border-radius: 12px;
  padding: 12px 14px; margin: 0 0 14px; }
.sb-shoot { min-height: 48px; display: inline-flex; align-items: center; gap: 8px;
  cursor: pointer; font-size: .95rem; padding: 0 22px; }
.sb-capture-note { flex: 1 1 220px; min-width: 0; }
.sb-capture-preview { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
/* `display:flex` outranks the UA's `[hidden]`, so without this the empty preview
   frame sits in the form before anything has been picked — the same trap
   `.inline-adjust` already carries a note about. */
.sb-capture-preview[hidden] { display: none; }
.sb-capture-preview img { width: 54px; height: 54px; object-fit: cover;
  border-radius: 8px; border: 1px solid var(--line); }

/* ── Shakebase: the mobile way in ──────────────────────────────────────── */
.sb-find { max-width: 30rem; margin: 8px 0 26px; }
.sb-find-label { display: block; font-size: .74rem; font-weight: 800;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
  margin-bottom: 8px; }
.sb-find-input { width: 100%; font-family: var(--disp); font-stretch: var(--disp-stretch); font-weight: 900;
  font-size: clamp(2rem, 9vw, 3rem); text-align: center; letter-spacing: .04em;
  padding: 14px 12px; }
.sb-find-go { width: 100%; min-height: 52px; margin-top: 12px; font-size: 1rem; }
.sb-find-note { font-size: .82rem; margin: 12px 0 0; }
.sb-find-metrics { margin-top: 26px; }

/* ── Shakebase on a phone ───────────────────────────────────────────────
   Only the screens a technician actually stands in front of. The machine list and
   the picking list stay desktop on purpose — a 374-row table is not a phone screen
   and pretending otherwise costs more than it returns. */
@media (max-width: 700px) {
  /* The record grids are two columns of nothing at this width.
     `minmax(0, 1fr)`, NOT `1fr`: a bare `1fr` is `minmax(auto, 1fr)`, so a wide
     child — the service-history table — pushes the column past the viewport and the
     whole page scrolls sideways. The zero minimum is what lets the table's own
     `overflow-x` do its job instead. */
  .record-cols, .sb-form, .record-grid { grid-template-columns: minmax(0, 1fr); }
  .sb-form .wide { grid-column: auto; }
  .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Every tap target reaches 44px, the smallest thing a thumb reliably hits. */
  .btn, .btn-sm { min-height: 44px; display: inline-flex; align-items: center;
    justify-content: center; }
  /* 1rem, not smaller: iOS zooms the whole page in on focus for anything under
     16px, and a page that jumps when you tap a field is unusable one-handed. */
  .input, select.input, input.input { font-size: 1rem; padding: 12px 14px; }
  /* ...except the one field this page exists for, which stays a big number. Restated
     inside the query because the rule above comes later in the file and would
     otherwise flatten it to 16px. */
  input.sb-find-input { font-size: clamp(2.2rem, 13vw, 3rem); padding: 16px 12px; }

  /* A full-width shutter is the point of the page it sits on. `flex-basis` on the
     note field was written for a ROW; in a column it becomes a 220px-tall text box,
     so it is reset here rather than guessed at. */
  .sb-capture { flex-direction: column; align-items: stretch; }
  .sb-shoot { width: 100%; justify-content: center; font-size: 1rem; min-height: 52px; }
  .sb-capture-note { flex: 0 0 auto; width: 100%; }
  .sb-capture-preview { justify-content: flex-start; }

  /* Bigger thumbnails: on a phone the strip is the content, not a garnish. */
  .sb-photo, .sb-photo img { width: 132px; }
  .sb-photo img { height: 132px; }

  /* Chrome, tightened. Two nav rows, a striped banner and a subnav row were ~700px
     before the reader reached the field the page exists for. Everything still
     present, none of it shouting. */
  .topbar-inner { flex-wrap: wrap; row-gap: 6px; padding-top: 10px;
    padding-bottom: 10px; }
  .topbar-nav, .subnav { flex-wrap: wrap; row-gap: 2px; column-gap: 14px; }
  .topbar-nav a, .subnav a { font-size: .92rem; }
  .topbar-user { margin-left: 0; }
  .topbar-user .btn { min-height: 36px; padding: 0 14px; font-size: .82rem; }
  .sb-testbar { font-size: .78rem; }
  .sb-testbar .container { padding-top: 6px; padding-bottom: 6px; }
  .sb-testbar-more { display: none; }
  .record-head { margin-bottom: 14px; }
  .sb-find { margin-top: 0; }
}

/* ── Shakebase: working a ticket ────────────────────────────────────────
   Three transitions carry most of the field work. They are buttons, not a
   dropdown, because a dropdown on a phone is two taps and a scroll wheel to say
   the thing you already knew when you opened the page. */
.sb-quick { display: flex; gap: 8px; flex-wrap: wrap; }
.sb-quick .btn { flex: 1 1 auto; min-height: 44px; }
.sb-quick .btn.is-current { border-color: var(--orange); color: var(--darkred);
  box-shadow: inset 0 0 0 1px rgba(255,100,70,.4); }
.sb-more-statuses > summary { cursor: pointer; font-size: .84rem; padding: 4px 0; }
.sb-more-statuses .sb-form-actions { gap: 8px; }
.sb-more-statuses select.input { flex: 1 1 auto; }
.sb-take { width: auto; }
/* A resolution is prose, and three rows of it is a slot to peer through. Written at
   the same specificity as `.sb-form textarea.input` above, which would otherwise pin
   it back to the generic 70px floor. */
.sb-form textarea.sb-grow { min-height: 96px; }
.sb-label-form .field { min-width: 0; }

@media (max-width: 700px) {
  /* On a phone the reader is AT the machine: they know what is wrong and came to
     record it. The action goes first, the report second. On a laptop they are
     reading the queue and the order is the other way round, which is why this is a
     media query and not the markup. */
  .record-cols { display: flex; flex-direction: column; }
  .record-cols > .sb-act { order: -1; }
  .sb-quick { flex-direction: column; }
  .sb-quick .btn { width: 100%; min-height: 48px; }
  .sb-take { width: 100%; }
  .sb-form textarea.sb-grow { min-height: 132px; }
  /* Serial and SIM are transcribed digit by digit off a sticker; give them room. */
  .sb-label-form .input { letter-spacing: .04em; }
  /* The explanatory note sat BESIDE the button and squashed it to a third of the
     row. On a phone the button is the thing; the note goes under it. */
  .sb-form-actions { flex-wrap: wrap; }
  .sb-form-actions .btn { flex: 1 1 auto; }
  .sb-form-actions .sb-form-note { margin-left: 0; text-align: left; flex: 1 1 100%;
    max-width: none; }
}

/* ── Tables that become cards on a phone ────────────────────────────────
   OPT-IN, via `.is-cards`. A blanket rule on `.data-table` would catch all
   sixteen tables in CTRL, and the thirteen without `data-label` on their cells
   would collapse into unlabelled stacks — worse than the sideways scroll they
   have now. The four that opt in are the ones somebody works FROM in a gym.

   The shape is one card per row: an identity line that leads, then label/value
   pairs, then any action full-width at the foot. The header row is hidden — its
   job is done by the per-cell labels — but it is hidden accessibly, not with
   `display:none`, so a screen reader still gets the table semantics. */
@media (max-width: 700px) {
  .data-table.is-cards, .data-table.is-cards tbody { display: block; }
  .data-table.is-cards thead { position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

  .data-table.is-cards tr { display: block; background: var(--paper);
    border: 1px solid var(--line); border-radius: 12px;
    padding: 12px 14px; margin-bottom: 10px; }
  .data-table.is-cards tr.row-urgent { border-color: var(--orange);
    box-shadow: inset 3px 0 0 var(--orange); }
  .data-table.is-cards tr.row-urgent > td { background: none; }
  .data-table.is-cards tr.row-urgent > td:first-child { box-shadow: none; }

  .data-table.is-cards td { display: flex; gap: 14px; align-items: baseline;
    justify-content: space-between; text-align: right;
    border: 0; padding: 4px 0; white-space: normal; }
  .data-table.is-cards td::before { content: attr(data-label);
    flex: 0 0 auto; text-align: left; font-size: .68rem; font-weight: 800;
    letter-spacing: .09em; text-transform: uppercase; color: var(--muted); }

  /* The thing the row IS: full width, no label, leading the card. */
  .data-table.is-cards td.is-primary { display: block; text-align: left;
    font-size: 1rem; padding: 0 0 8px; }
  .data-table.is-cards td.is-primary::before { content: none; }
  .data-table.is-cards td.is-primary a { font-weight: 800; }

  /* A cell with nothing in it is noise on a card. Marked in the template,
     because CSS cannot tell an em-dash from a value. */
  .data-table.is-cards td.is-empty { display: none; }

  /* Buttons get the full width at the foot — this is the tap the card exists for. */
  .data-table.is-cards td.is-action { display: block; text-align: left;
    padding-top: 10px; margin-top: 6px; border-top: 1px solid var(--line); }
  .data-table.is-cards td.is-action::before { content: none; }
  .data-table.is-cards td.is-action .inline-form { display: block; }
  .data-table.is-cards td.is-action .btn { width: 100%; }
  .data-table.is-cards td.is-action .chip { display: inline-block; margin-bottom: 8px; }

  /* The wrapper's horizontal scroll has nothing left to scroll. */
  .table-wrap:has(.is-cards) { overflow-x: visible; }
}

/* The card layout gives `td.is-action` buttons the full width, which is right for
   the one action the card exists for and wrong for a destructive one sitting under
   it. Secondary controls stay small and step aside. */
.sb-only-cards { display: none; }
@media (max-width: 700px) {
  .data-table.is-cards td.is-action .sb-secondary { display: flex;
    justify-content: flex-end; margin-top: 8px; }
  .data-table.is-cards td.is-action .sb-secondary .btn { width: auto;
    padding: 0 16px; font-size: .82rem; }
  .sb-only-cards { display: inline; }
}

/* An `.htmx-indicator` is opacity-0, not display-none, so it keeps its box. In a
   filter bar on a phone that box wraps to a line of its own and leaves a blank strip
   under the search field for the 99% of the time nothing is loading. Give it no box
   until the form is actually requesting. */
@media (max-width: 700px) {
  .filter-bar .htmx-indicator { display: none; }
  .filter-bar.htmx-request .htmx-indicator { display: inline; }
}

/* ------------------------------------------------ a machine's life
   The event stream on the machine dossier. A list, not a table: the rows are
   different shapes (an installation names a gym, a service names a resolution)
   and forcing them into shared columns is what made the ticket table read as
   four facts about nothing. */
.sb-life { list-style: none; margin: 0 0 var(--s3, 18px); padding: 0; }
.sb-life li {
  position: relative; padding: 0 0 12px 18px; margin: 0;
  border-left: 2px solid var(--line);
}
.sb-life li:last-child { border-left-color: transparent; padding-bottom: 0; }
.sb-life li::before {
  content: ''; position: absolute; left: -5px; top: 6px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--orange);
}
.sb-life-when {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: .78rem; color: var(--muted); margin-right: 8px;
}
.sb-life-what { font-weight: 700; }

/* A group of fields inside .sb-form that shows or hides as one. `display:
   contents` keeps the grid flowing through it; the [hidden] rule must come
   after, or the attribute loses to the class. */
.sb-form .sb-sub { display: contents; }
.sb-form .sb-sub[hidden] { display: none; }
.input.input-tiny { width: 9.5rem; padding: 4px 8px; font-size: .85rem; }

/* The owner warning: loud on purpose, and only drawn once the select moved. */
.sb-alert-owner { line-height: 1.45; }
.sb-alert-owner[hidden] { display: none; }
.sb-alert-owner input { margin-right: 6px; }

/* The record's facts in two columns, the actions on its header. */
.meta-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 4px 32px; }
.sb-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.sb-actions[hidden] { display: none; }
#sb-record .sb-form { margin-top: 16px; margin-bottom: 0; }
.card + .card { margin-top: 16px; }
.sb-fault { font-size: 1.02rem; line-height: 1.5; margin: 0 0 14px; }
.sb-resolution { background: var(--tint-green); border: 1px solid var(--tint-green-line); border-radius: 10px;
  padding: 12px 14px; margin: 0 0 14px; }
.sb-resolution .panel-title { margin-bottom: 4px; font-size: .95rem; }
.sb-actions .inline-form { display: inline-flex; gap: 8px; }

/* The row's other exits, behind one fold. */
.sb-more { position: relative; display: inline-block; }
.sb-more > summary { list-style: none; cursor: pointer; }
.sb-more > summary::-webkit-details-marker { display: none; }
.sb-more-menu { position: absolute; right: 0; top: calc(100% + 6px); z-index: 5;
  background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
  padding: 8px; display: flex; flex-direction: column; gap: 6px; min-width: 220px;
  box-shadow: 0 10px 30px rgba(0,0,0,.12); }
.sb-more-menu .btn { width: 100%; justify-content: flex-start; }
.data-table.is-cards td.is-action .sb-more-menu { position: static; box-shadow: none;
  border: 0; padding: 0; }
.sb-capture[hidden] { display: none; }
[data-sb-gym-photos] { margin-top: 14px; }
/* The admin half of the sub-nav sits to the right, behind a gap. */
.subnav-gap { flex: 1 1 auto; }
.subnav a.is-current + .subnav-gap { margin-left: 0; }

.pii-banner .sb-key-fix { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }

/* ── Products ───────────────────────────────────────────────────────── */
/* The product master. Built on the existing table, chip and filter-bar
   classes — it is another list of records — so only what is genuinely new
   lives here. */

/* The portion price is the column the page exists for, so it carries the
   weight the eye goes to first. Tabular figures because four decimals in a
   proportional font do not line up, and a column of prices that does not line
   up is a column nobody can scan. */
.pr-price { font-weight: 700; font-variant-numeric: tabular-nums; }
.pr-table .num { font-variant-numeric: tabular-nums; }
/* The non-Regular sizes sit under the headline price rather than beside it:
   57 of 131 products have a Smaller, and inline they push the column wide
   enough to squeeze the product name. */
.pr-other-size { display: block; font-weight: 400; }

/* A row the sanity check is complaining about. A tint and a rule rather than
   a red row: these are usually a handful among a hundred correct ones, and a
   fully red row reads as an error rather than as a thing to fix. */
.pr-trouble > td { background: var(--tint-red); }
.pr-trouble > td:first-child { box-shadow: inset 3px 0 0 var(--darkred); }

.pr-trouble-list { margin: 6px 0 10px; padding-left: 20px; }
.pr-trouble-list li { margin: 2px 0; }

/* The product picker on the schedule page. Tall on purpose — an announcement
   covers a product line, and scrolling a 6-row box to ⌘-click eight flavours
   is where the half-applied price rise comes from. */
.pr-picker { height: auto; min-height: 260px; font-family: inherit; }

/* ── Hours ─────────────────────────────────────────────────────────────────
   The monthly timesheet. One card per month: heading and status, hours
   against the quota beside a mini calendar, the quick-add row, the ledger of
   days, and the submit line. Ported from the artifact it started as, drawn
   with CTRL's own tokens so it sits beside the other sections in both
   schemes. */
.hr-card { padding: 0; overflow: hidden; }
.hr-head { display: flex; align-items: center; gap: 12px; padding: 18px 22px 8px; flex-wrap: wrap; }
.hr-apprhead { padding: 0 0 10px; }
.hr-monthnav { display: flex; align-items: center; gap: 6px; }
.hr-nb { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center;
  font-size: 22px; line-height: 1; color: var(--muted); text-decoration: none; }
.hr-nb:hover { background: var(--hover); color: var(--ink); }
.hr-month { font-size: 1.55rem; margin: 0; min-width: 250px; text-align: center;
  text-transform: uppercase; color: var(--ink); }
.hr-now { margin-left: 4px; }
.hr-head > .chip { margin-left: auto; }
.hr-band { margin: 6px 22px 0; padding: 12px 14px; border-radius: 10px; font-size: .92rem;
  display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.hr-band form { margin-left: auto; }
.hr-band.is-submitted { background: var(--tint-amber); color: var(--amber-ink); border: 1px solid var(--tint-amber-line); }
.hr-band.is-approved { background: var(--tint-green); color: var(--green); border: 1px solid var(--tint-green-line); }
.hr-band.is-returned { background: var(--tint-red); color: var(--darkred); border: 1px solid var(--tint-red-line); }
.hr-band.is-carry { background: var(--tint-blue); color: var(--blue-ink); border: 1px solid var(--tint-blue-line); }
.hr-target { margin: 8px 0 0; font-size: .85rem; max-width: 60ch; }
.hr-ledger { display: flex; flex-direction: column; gap: 4px; font-size: .92rem; margin: 0 0 12px;
  padding: 10px 12px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; }
.hr-over { color: var(--amber-ink); }
.hr-monthquota { margin-top: 12px; }
.hr-mode { min-width: 150px; padding: 6px 10px; }
.hr-summary { display: grid; grid-template-columns: 1fr auto; gap: 24px; padding: 14px 22px 20px; align-items: start; }
.hr-hero { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.hr-hero b { font-size: 3rem; font-weight: 800; letter-spacing: -.02em; line-height: 1;
  font-variant-numeric: tabular-nums; }
.hr-hero span { color: var(--muted); }
.hr-meter { height: 8px; border-radius: 4px; background: var(--tint-orange); overflow: hidden;
  margin: 14px 0 10px; max-width: 420px; box-shadow: inset 0 0 0 1px var(--tint-red-line); }
.hr-meter > i { display: block; height: 100%; border-radius: 4px; background: var(--orange);
  transition: width .35s ease; }
.hr-meter.is-over { background: var(--tint-amber); box-shadow: inset 0 0 0 1px var(--tint-amber-line); }
.hr-meter.is-over > i { background: var(--amber-ink); }
.hr-meter.mini { height: 6px; margin: 5px 0 0; max-width: 140px; }
.hr-sub { display: flex; gap: 16px; font-size: .9rem; color: var(--muted); flex-wrap: wrap; }
.hr-sub b { color: var(--ink); }
.hr-sub .is-over { color: var(--amber-ink); font-weight: 700; }
.hr-cal { display: grid; grid-template-columns: repeat(7, 30px); gap: 3px; align-content: start; }
.hr-dh { font-size: .62rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
  text-align: center; font-weight: 800; padding-bottom: 2px; }
.hr-dc { height: 28px; border-radius: 7px; display: grid; place-items: center; font-size: .78rem;
  font-family: var(--font); color: var(--muted); background: var(--hover); border: 0; padding: 0;
  font-variant-numeric: tabular-nums; position: relative; cursor: pointer; }
.hr-dc.we { background: transparent; border: 1px dashed var(--line); }
.hr-dc.has { background: var(--orange); color: var(--on-orange); font-weight: 800; border: 0; }
.hr-dc.today::after { content: ""; position: absolute; inset: -3px; border: 2px solid var(--orange);
  border-radius: 9px; pointer-events: none; }
.hr-dc.is-blank { background: transparent; cursor: default; }
.hr-dc:not(:disabled):not(.is-blank):hover { outline: 2px solid var(--orange); outline-offset: -1px; }
.hr-dc:disabled { cursor: default; }
.hr-add { display: grid; grid-template-columns: 160px 100px 1fr auto; gap: 10px;
  padding: 16px 22px 14px; border-top: 1px solid var(--line); align-items: end; }
.hr-add .field, .hr-editform .field { gap: 5px; }
.hr-hours { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.hr-add .input, .hr-editform .input { height: 42px; }
.hr-addbtn { height: 42px; padding: 0 26px; font-size: .95rem; }
.hr-chips { grid-column: 1 / -1; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.hr-chip { font: inherit; font-size: .8rem; font-weight: 700; color: var(--darkred);
  background: var(--tint-orange); border: 1px solid var(--tint-red-line); border-radius: 999px;
  padding: 3px 10px; cursor: pointer; }
.hr-chip:hover { background: var(--tint-red); }
.hr-lock { padding: 14px 22px; border-top: 1px solid var(--line); margin: 0; font-size: .92rem; }
.hr-rows { border-top: 1px solid var(--line); }
.hr-row { display: grid; grid-template-columns: 58px 1fr 72px auto; align-items: center; gap: 12px;
  padding: 10px 14px 10px 22px; border-top: 1px solid var(--line); }
.hr-row:first-child { border-top: 0; }
.hr-row:hover { background: var(--hover); }
.hr-d { display: flex; flex-direction: column; line-height: 1.05; }
.hr-d b { font-size: 1.1rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.hr-d span { font-size: .66rem; color: var(--muted); text-transform: uppercase; letter-spacing: .07em;
  font-weight: 800; margin-top: 2px; }
.hr-row.we .hr-d b { color: var(--muted); }
.hr-n { min-width: 0; overflow-wrap: anywhere; }
.hr-h { text-align: right; font-variant-numeric: tabular-nums; }
.hr-h small { color: var(--muted); font-size: .7rem; margin-left: 2px; }
.hr-editbtn { opacity: 0; }
.hr-row:hover .hr-editbtn, .hr-row:focus-within .hr-editbtn { opacity: 1; }
.hr-row.is-editing .hr-editbtn { display: none; }
.hr-editform { grid-column: 1 / -1; display: grid; grid-template-columns: 160px 100px 1fr; gap: 10px;
  padding: 10px 0 4px; align-items: end; }
/* The house rule: a class that sets display must also honour hidden, or the
   attribute silently loses (compare .sb-form[hidden], .inline-adjust[hidden]). */
.hr-editform[hidden], .hr-add[hidden] { display: none; }
.hr-editacts { grid-column: 1 / -1; display: flex; gap: 8px; align-items: center; }
.hr-spacer { flex: 1; }
.hr-empty { padding: 26px 22px; text-align: center; margin: 0; font-size: .92rem; }
.hr-foot { display: flex; align-items: center; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--line);
  background: var(--hover); flex-wrap: wrap; }
.hr-h2 { font-size: .72rem; text-transform: uppercase; letter-spacing: .12em; color: var(--muted);
  font-weight: 800; margin: 22px 0 8px 4px; }
.hr-hist { display: flex; flex-direction: column; gap: 6px; }
.hr-hrow { display: grid; grid-template-columns: 1fr auto auto; gap: 14px; align-items: center;
  padding: 10px 16px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
  color: var(--ink); text-decoration: none; }
.hr-hrow:hover { border-color: var(--line-strong); background: var(--hover); }
.hr-hm { font-weight: 800; }
.hr-elsewhere { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 0 0 14px; }
.hr-elsewhere .chip { text-decoration: none; }
.hr-apprhours { min-width: 160px; }
.hr-apprbtn { text-align: right; }
.hr-reviewrow > td { padding: 0 !important; border-top: 0 !important; }
.hr-reviewrow > td:empty { display: none; }
.hr-review { padding: 14px 18px 16px; background: var(--hover); border-top: 1px solid var(--line); }
.hr-cmt { color: var(--darkred); margin: 0 0 10px; font-size: .92rem; }
.hr-mini { width: 100%; border-collapse: collapse; font-size: .9rem; }
.hr-mini td { padding: 6px 8px 6px 0; border-top: 1px dotted var(--line); vertical-align: top; }
.hr-mini tr:first-child td { border-top: 0; }
.hr-mini-d { white-space: nowrap; width: 1%; padding-right: 16px !important; }
.hr-mini-h { white-space: nowrap; width: 1%; text-align: right; padding-right: 16px !important; }
.hr-mini-total td { font-weight: 800; border-top: 1px solid var(--line-strong); }
.hr-reviewacts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 12px; }
.hr-return { display: flex; gap: 6px; align-items: center; }
.hr-return .input { min-width: 220px; padding: 6px 10px; }
.hr-quota { display: flex; gap: 6px; align-items: center; }
.theme-toggle { min-width: 34px; padding-left: 8px; padding-right: 8px; font-size: 1rem; line-height: 1; }
@media (max-width: 720px) {
  .hr-summary { grid-template-columns: 1fr; }
  .hr-cal { grid-template-columns: repeat(7, 1fr); }
  .hr-add { grid-template-columns: 1fr 1fr; }
  .hr-add .hr-f-note, .hr-add .hr-addbtn { grid-column: 1 / -1; }
  .hr-editform { grid-template-columns: 1fr 1fr; }
  .hr-editform .hr-f-note { grid-column: 1 / -1; }
  .hr-row { grid-template-columns: 48px 1fr 60px auto; gap: 8px; padding-left: 14px; }
  .hr-editbtn { opacity: 1; }
  .hr-month { font-size: 1.2rem; min-width: 0; }
}
