/* src/web/static/app.css
   One stylesheet, served from this origin. No CDN, no webfont, nothing that
   a CSP has to make an exception for.

   Everything is a token, even though only one palette is defined: a later
   component still reads --bg / --text / etc rather than a literal colour. */

:root {
  color-scheme: light;

  /* Neutrals carry a trace of warmth. A pure grey UI reads clinical, and this
     is a tool somebody opens between serving customers. */
  --bg:            #f7f7f6;
  --surface:       #ffffff;
  --surface-2:     #f2f2f0;
  --surface-3:     #e9e9e6;
  --border:        #e3e3df;
  --border-strong: #c9c9c3;

  --text:          #1a1a19;
  --text-muted:    #53534e;
  --text-faint:    #6d6d66;

  /* One accent, used only for the current page and the primary action. When
     everything is emphasised nothing is. */
  --accent:        #2f5bd8;
  --accent-hover:  #2549ba;
  --accent-soft:   #eaf0ff;
  --accent-text:   #ffffff;

  --ok:            #12704a;
  --ok-soft:       #e2f3ea;
  --warn:          #8a5300;
  --warn-soft:     #fdf0d9;
  --danger:        #b3261e;
  --danger-soft:   #fceceb;
  --leave:         #6b3fa0;
  --leave-soft:    #f1e9fa;
  --visit:         #0f766e;
  --visit-soft:    #e3f4f2;

  /* The attendance strip draws one 13px square per day. The -soft fills above
     vanish at that size and the full-strength ones turn a normal month into a
     wall of colour, so the strip gets its own step between the two. */
  --ok-mid:        #7ec3a2;
  --warn-mid:      #e3b263;
  --danger-mid:    #dd8781;
  --leave-mid:     #b8a1d8;
  --accent-mid:    #93abec;

  --radius:        12px;
  --radius-sm:     8px;
  --gap:           16px;

  /* Nothing tappable is smaller than this. A mis-tap on a page that changes
     someone's pay is worse than a page that scrolls a little more. */
  --tap:           48px;

  --shadow:    0 1px 2px rgba(20, 20, 18, .05);
  --shadow-lg: 0 4px 16px rgba(20, 20, 18, .10), 0 1px 3px rgba(20, 20, 18, .06);

  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
}

/* The inline sprite sheet of <symbol> defs (partials/icons.html) must never
   paint. It used to carry style="display:none", but the CSP is style-src
   'self' with no unsafe-inline, so that attribute was silently dropped and
   the sprite rendered as a ~150px visible block at the top of every page,
   pushing the whole layout down. A class reaches the same rule through the
   stylesheet, which the CSP allows. */
.sprite-defs { display: none; }

/* ------------------------------------------------------------------- icons */
/* One weight, one size, aligned to the text it sits beside. Emoji were doing
   this job and rendered differently on every device, and at different sizes,
   so nothing lined up. */

.ico {
  width: 20px; height: 20px;
  flex: 0 0 auto;
  vertical-align: -.22em;
  stroke-width: 1.75;
}
.ico-sm { width: 18px; height: 18px; vertical-align: -.2em; }
.ico-lg { width: 26px; height: 26px; }
.ico-xl { width: 40px; height: 40px; stroke-width: 1.4; }

* { box-sizing: border-box; }

/* `hidden` must win everywhere. The phone layout below turns table rows into
   `display: block`, and an author rule beats the browser's own [hidden]
   style -- which is how every "Change" editor on Shops and on a person's
   roster stood wide open on a phone before anyone tapped anything, and why
   tapping the second one flung the page a screen and a half down. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 17px/1.6 var(--sans);
  -webkit-text-size-adjust: 100%;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Visible focus everywhere. Keyboard navigation is not optional. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ------------------------------------------------------------------ layout */
/* A left sidebar on any reasonable screen, an off-canvas drawer on a phone.
   Seven destinations do not fit across the top of a handset, and a menu that
   wraps onto two lines is a menu people stop reading. */

.shell { display: flex; min-height: 100vh; }

.sidebar {
  flex: 0 0 232px;
  width: 232px;
  background: var(--surface);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 18px 12px 12px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  z-index: 30;
}

.brand {
  display: flex; align-items: center; gap: 9px;
  font-weight: 700;
  letter-spacing: -.015em;
  color: var(--text);
  font-size: 16.5px;
  white-space: nowrap;
}
.brand .ico { color: var(--accent); }
.brand-mark { display: block; border-radius: 6px; flex: none; }
.brand-mark-lg { border-radius: 7px; }
.login-mark { display: block; margin: 0 auto; border-radius: 13px; box-shadow: var(--shadow-lg); }
.brand:hover { text-decoration: none; }
.sidebar-brand { padding: 4px 10px 12px; }

.dev-tag {
  background: var(--warn-soft); color: var(--warn);
  border-radius: var(--radius-sm); padding: 7px 10px; margin-bottom: 6px;
  font-size: 12px; font-weight: 650; text-align: center; line-height: 1.35;
}

nav.main { display: flex; flex-direction: column; gap: 2px; }

/* Groups, not dividers. A rule between each set would draw three lines across
   a 232px column to separate eight items; the space above a label does the
   same work and adds no ink. */
.nav-group { display: flex; flex-direction: column; gap: 2px; }
.nav-group + .nav-group { margin-top: 16px; }

/* The heading over each group. Sentence case at a size that can be read,
   not the 11px letter-spaced capitals this used to be: a word set in caps
   loses its shape, which is the thing the eye actually reads, and these
   are meant to be skimmed rather than studied. Quiet enough that the
   destinations under them still come first. */
.nav-label {
  padding: 0 12px 5px;
  font-size: 13.5px;
  font-weight: 650;
  color: var(--text-muted);
}


nav.main a {
  display: flex;
  align-items: center;
  gap: 11px;
  min-height: var(--tap);
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-weight: 550;
  font-size: 16.5px;
}
nav.main a .ico { color: var(--text-faint); transition: color .12s; }
nav.main a:hover .ico { color: var(--text-muted); }
nav.main a[aria-current="page"] .ico { color: var(--accent); }
nav.main a:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
nav.main a[aria-current="page"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }

/* Pinned to the bottom, because settings is where you go last, not first. */
.sidebar-foot { margin-top: auto; padding-top: 10px; border-top: 1px solid var(--border); }

.who-link {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 12px; border-radius: var(--radius-sm);
  color: var(--text); min-height: var(--tap);
}
.who-link:hover { background: var(--surface-2); text-decoration: none; }
.who-link.is-current { background: var(--accent-soft); }
.who-link .ico { color: var(--text-faint); }
.who-link span { display: block; min-width: 0; }
.who-name { font-weight: 650; font-size: 15.5px; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }
.who-role { font-size: 13.5px; color: var(--text-muted); }

.mainpane { flex: 1 1 auto; min-width: 0; }

/* The phone header. Hidden wherever the sidebar is permanent. */
.mobilebar { display: none; }
.menu-btn {
  min-height: 48px; min-width: 48px; padding: 0 12px;
  font-size: 20px; line-height: 1; border-radius: var(--radius-sm);
}
.scrim { display: none; }

main {
  max-width: 1180px;
  margin: 0 auto;
  padding: 24px 22px 72px;
}
main.anon { max-width: 640px; }

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px var(--gap);
  flex-wrap: wrap;
  margin-bottom: 18px;
}
/* The one slot on the right of every title: the page's filters, and at
   most one action. Filters submit themselves; the Show button is only for
   a browser with no script, and the name box, where Enter does the job. */
.page-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.page-tools form.row { gap: 8px; align-items: center; flex-wrap: wrap; }
.page-tools form.row > * { flex: 0 1 auto; min-width: 0; }
.page-tools form.row > label[for] { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.page-tools select, .page-tools input:not([type=checkbox]) { min-height: 44px; padding: 9px 12px; font-size: 16px; width: auto; }
.page-tools input[type=search] { min-width: 180px; }
/* Asked of the browser, not of a class app.js removes at the end of the
   page: the class version drew the button, then took it away, and the
   toolbar jumped on every page load. The class rule stays for browsers too
   old to answer the media query. */
@media (scripting: enabled) { .page-tools form[data-autosubmit] .show-btn { display: none; } }
html:not(.no-js) .page-tools form[data-autosubmit] .show-btn { display: none; }
.page-tools .clear-link { font-size: 14.5px; color: var(--text-muted); white-space: nowrap; }
/* A page reached from another page says where it came from, above the
   title, the same way everywhere -- not as a button competing with the
   page's own actions. */
.crumb { display: inline-flex; align-items: center; gap: 5px; font-size: 14.5px; color: var(--text-muted); margin-bottom: 6px; }
.crumb:hover { color: var(--accent); text-decoration: none; }
.crumb .ico { width: 14px; height: 14px; }

h1 { font-size: 26px; margin: 0; letter-spacing: -.02em; }
h2 { font-size: 18.5px; margin: 0 0 12px; letter-spacing: -.01em;
     display: flex; align-items: center; gap: 9px; }
h2 .ico { color: var(--text-faint); }
h3 { font-size: 15px; margin: 0 0 8px; color: var(--text-muted); font-weight: 650; }

.sub { color: var(--text-muted); font-size: 15px; margin: 3px 0 0; }

/* ------------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px;
  box-shadow: var(--shadow);
  margin-bottom: var(--gap);
}
.card.flush { padding: 0; overflow: hidden; }
.card > h2 { padding: 0; }
.card.flush > h2 { padding: 16px 18px 0; }

.grid { display: grid; gap: var(--gap); }
/* min() on the track floor, not a bare length. auto-fit will not shrink a
   track below its own minimum, so on a narrow phone a bare 320px lays a 320px
   column inside a card that has less than that to give, and the contents run
   past the column into the card padding and then past its border. Where that
   starts is a property of the page, not of this rule: the same class sits in
   a different card-inner width on /staff/7 and on /platform/businesses/1, so
   there is no single width to quote. The min() lets the floor fall to whatever
   the container actually has, which removes the question. Written the same way on every auto-fit grid in this
   file, including the ones that clear their own threshold on any real phone:
   a sheet where one floor is safe and six are bare is a copy-paste bug
   waiting for its author, and above the threshold min() changes nothing. */
.grid.two   { grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); }
.grid.three { grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr)); }
/* Side by side but not stretched: a short card next to a tall one keeps its
   own height instead of carrying a void inside its border. */
.grid.top   { align-items: start; }
.grid.stats {
  grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
  gap: 1px; background: var(--border);
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden; box-shadow: var(--shadow);
}
.grid.stats > .stat { border: 0; border-radius: 0; box-shadow: none; }
.card > .grid.stats { box-shadow: none; }

/* ------------------------------------------------------------------- stats */

.stat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
}
.stat .label {
  font-size: 13.5px; color: var(--text-muted); font-weight: 600;
}
.stat .value {
  font-size: 29px; font-weight: 620; letter-spacing: -.02em;
  margin-top: 5px; font-variant-numeric: tabular-nums;
}
.stat .value.small { font-size: 22px; }
.stat .note { font-size: 14px; color: var(--text-muted); margin-top: 4px; }

/* ------------------------------------------------------------------ tables */
/* Writing a `.mytable td` rule? Scope any width or `white-space: nowrap` in
   it to `@media print, (min-width: 721px)`. Below that width the stacking block at
   the foot of this file turns every `td` into a block inside a card, where a
   fixed width is not a column, it is a clipping box -- a `td:last-child
   { width: 1%; white-space: nowrap }` meant for a desktop column left a date
   hanging outside the card on a phone. The bare `td` rules here are 0,0,1 and
   lose to anything, so a component rule survives into a layout it was never
   written for, silently. See also the note above the form block. */

.table-wrap { overflow-x: auto; }

table { width: 100%; border-collapse: collapse; font-size: 15.5px; }

th {
  text-align: left;
  font-size: 13.5px;
  color: var(--text-muted);
  font-weight: 600;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  white-space: nowrap;
}

td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--surface-2); }

td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
td.mono { font-family: var(--mono); font-size: 14.5px; }
tfoot td { font-weight: 620; background: var(--surface-2); }

/* ------------------------------------------------------------------- chips */

.chip {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 100px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
}
.chip.ok      { background: var(--ok-soft);     color: var(--ok); }
.chip.warn    { background: var(--warn-soft);   color: var(--warn); }
.chip.danger  { background: var(--danger-soft); color: var(--danger); }
.chip.muted   { background: var(--surface-2);   color: var(--text-muted); }
.chip.accent  { background: var(--accent-soft); color: var(--accent); }
.chip.leave   { background: var(--leave-soft);  color: var(--leave); }
.chip.visit   { background: var(--visit-soft);  color: var(--visit); }

.dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--ok); margin-right: 6px; vertical-align: middle;
}

/* ------------------------------------------------------------------- forms */
/* The field rules below are attribute selectors: `input[type="number"]` is
   0,1,1, which outranks any single class. A `.my-narrow { width: 104px }`
   written later in the file still loses to the `width: 100%` here and does
   nothing at all. Reach for `.my-wrapper .my-narrow`, or an element in the
   selector, whenever you need to narrow a field. The table block above has
   the same hazard pointing the other way: bare `td` is weak enough that
   component rules override it in layouts they never meant to reach.

   The other trap in an attribute selector is an attribute that is not there.
   `<input>` with no type behaves as text and reports .type === "text", but
   `input[type="text"]` does not match it, so it gets none of this and renders
   as a 21px browser default beside 48px neighbours. Always write the type. */

label { display: block; font-size: 15.5px; font-weight: 600;
        color: var(--text); margin-bottom: 6px; }

input[type="text"], input[type="number"], input[type="date"],
input[type="time"], input[type="month"], input[type="search"],
select, textarea {
  width: 100%;
  min-height: var(--tap);
  padding: 11px 13px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 16px;   /* 16px exactly: anything less and iOS zooms on focus */
  /* `body` sets line-height:1.62 for prose. A <select> ignores an inherited
     line-height for its own box height, but Chromium's native date/time/month
     picker does not: it was inflating those three to ~52px next to a 47px
     select and text input, staggering every filter row that mixes them. */
  line-height: 1.2;
}
input:focus, select:focus, textarea:focus {
  border-color: var(--accent);
  outline: 2px solid var(--accent-soft);
  outline-offset: 0;
}
textarea { resize: vertical; min-height: 62px; }

/* A file input (the payout-QR upload). Bare, it renders as whatever the OS
   feels like; this makes it match every other field instead. */
input[type="file"] {
  width: 100%;
  min-height: var(--tap);
  padding: 6px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-muted);
  font: inherit;
  font-size: 15.5px;
}
input[type="file"]::file-selector-button {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  margin-right: 10px;
  padding: 9px 16px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 15.5px;
  font-weight: 600;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--surface-3); }

.field { margin-bottom: 13px; }
.hint { font-size: 15px; color: var(--text-muted); font-weight: 400; line-height: 1.5; }
.field .hint { margin-top: 5px; }

.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.row > * { flex: 1; min-width: 120px; }
.row > .narrow { flex: 0 0 auto; min-width: 0; }
/* A button beside labelled fields lines up with the input's box, not with
   the bottom of the field's margin. Two exclusions keep it honest: a narrow
   *field* (a short select beside a wide input) already carries the margin,
   and a field that has dropped its margin (.mb-0, the forms on a person's
   page) leaves the button nothing to make up for -- padding it anyway lifts
   the button 13px above the input it belongs to. */
.row > .field:not(.mb-0) ~ .narrow:not(.field),
.row > .narrow.beside-field:not(.field) { padding-bottom: 13px; }

.check { display: flex; align-items: center; gap: 8px; }

/* A fieldset groups the radios of one question. Left alone a browser draws
   it a border and notches the legend into the top of it, which looks like
   nothing else in this app -- every other group here is a label with its
   fields under it. This makes a fieldset look like one of those. */
fieldset { border: 0; padding: 0; margin: 0 0 var(--gap); min-width: 0; }
fieldset > legend {
  padding: 0; margin-bottom: 9px;
  font-size: 15.5px; font-weight: 600; color: var(--text);
}
/* Two choices set the row's own 10px apart read as one control with a stray
   dot in front of it. Answers to a question need more air than fields do. */
fieldset > .row { gap: 10px 22px; }

/* An answer is something you aim at. As a bare radio with a word beside it
   this was a 13px dot inside a 47x25 hit area, on a page whose every other
   control clears 44px -- and it is the answer that decides whether a
   customer is billed. The label wraps its own radio, so the whole thing
   toggles, and the chosen one is lit rather than marked with a dot the eye
   has to hunt for. */
fieldset .check {
  min-height: var(--tap);
  margin: 0;
  padding: 0 16px 0 13px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-size: 15.5px;
  font-weight: 500;
  cursor: pointer;
}
fieldset .check:hover { background: var(--surface-2); }
fieldset .check:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 650;
}

/* Two fields abreast; one column on a phone. */
.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
@media screen and (max-width: 640px) { .grid-2 { grid-template-columns: 1fr; } }
.max-420 { max-width: 420px; }
.max-260 { max-width: 260px; }
.tag { display: inline-block; font-size: 12.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; background: var(--accent-soft); color: var(--accent); padding: 2px 7px; border-radius: 999px; vertical-align: middle; white-space: nowrap; }
.tag-warn { background: var(--warn-soft); color: var(--warn); }
.tag-muted { background: var(--surface-3); color: var(--text-muted); }
form.inline { display: inline-block; margin: 0 4px 4px 0; }
.check input { width: auto; }
.check label { margin: 0; }

/* A row of day-of-week checkboxes, for setting several days at once. */
.weekday-picks { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 2px 0 14px; }
.weekday-picks label { display: flex; align-items: center; gap: 6px;
                        font-size: 15.5px; font-weight: 500; }
.weekday-picks input { width: auto; }

/* ----------------------------------------------------------------- buttons */

button, .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: var(--tap);
  padding: 11px 18px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
button:hover, .btn:hover { background: var(--surface-2); text-decoration: none; }

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}
.btn-primary:hover { background: var(--accent); filter: brightness(1.08); }

/* Destructive actions are deliberately plain until hovered: they should never
   be the thing your eye lands on first. */
.btn-danger { color: var(--danger); border-color: var(--border-strong); }
.btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }

.btn-sm { min-height: 44px; padding: 10px 16px; font-size: 15.5px; gap: 7px; }
/* A "Change" button whose editor is open reads as pressed, and says "Close",
   so the way back out is the same button and it looks like one. */
.btn[aria-expanded="true"], button[aria-expanded="true"] {
  background: var(--accent-soft, var(--surface-2));
  border-color: var(--accent);
  color: var(--accent);
}
.btn .ico, button .ico { opacity: .9; }
.btn-link { border: 0; background: none; color: var(--accent); padding: 0; }
.btn-link:hover { background: none; text-decoration: underline; }

.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

/* The way out of a table row: one quiet link at the end, never a button.
   Buttons are for things that change something. */
.rowlink {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 15px; font-weight: 600; color: var(--accent); white-space: nowrap;
  padding: 9px 11px; border-radius: var(--radius-sm); text-decoration: none;
}
.rowlink::after { content: "\203A"; opacity: .55; font-size: 17px; line-height: 1; }
.rowlink:hover { background: var(--accent-soft); text-decoration: none; }
.row-actions { display: flex; gap: 2px; justify-content: flex-end; flex-wrap: wrap; }
td.col-act, th.col-act { text-align: right; }

/* ---------------------------------------------------------- the platform */
body.pf { background: var(--bg); margin: 0; }
.pf-bar { background: #17181c; color: #ececea; }
.pf-bar-in { max-width: 1180px; margin: 0 auto; padding: 0 20px; display: flex; align-items: center; gap: 22px; min-height: 54px; flex-wrap: wrap; }
.pf-brand { display: inline-flex; align-items: center; gap: 9px; color: #fff; font-weight: 700; letter-spacing: -.015em; text-decoration: none; }
.pf-brand:hover { text-decoration: none; }
.pf-brand-tag { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: #93abec; margin-left: 2px; }
.pf-nav { display: flex; gap: 4px; align-self: stretch; }
.pf-nav a { display: inline-flex; align-items: center; padding: 0 14px; color: #b9b9b3; text-decoration: none; font-weight: 600; font-size: 15.5px; border-bottom: 2px solid transparent; }
.pf-nav a:hover { color: #fff; text-decoration: none; }
.pf-nav a[aria-current="page"] { color: #fff; border-bottom-color: #6f8ff0; }
.pf-right { margin-left: auto; display: flex; align-items: center; gap: 14px; }
.pf-link { color: #b9b9b3; font-size: 14.5px; text-decoration: none; }
.pf-link:hover { color: #fff; }
.pf-signout { background: none; border: 0; padding: 0; cursor: pointer; font: inherit; display: inline-flex; align-items: center; gap: 6px; }
.pf-dev { font-size: 11px; font-weight: 650; color: #e3b263; letter-spacing: .04em; text-transform: uppercase; }
.pf-main { max-width: 1180px; margin: 0 auto; padding: 24px 20px 64px; }
.pf-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 8px 16px; }
.pf-feature { display: flex; gap: 10px; align-items: flex-start; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: pointer; }
.pf-feature:hover { background: var(--surface-2); }
.pf-feature input { margin-top: 3px; width: auto; flex: none; }
.pf-feature strong { display: block; font-size: 15.5px; }
.pf-feature .small { display: block; line-height: 1.35; }
.pf-feature strong .tag { margin-left: 6px; font-size: 10px; }

/* A feature or limit that differs from the plan is drawn as the exception
   it is: an accent edge and a tag saying which way it differs. A row that
   matches the plan stays quiet, so the eye lands on the differences. */
.pf-feature.is-exception,
.field.is-exception input { border-color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
.field.is-exception label .tag { margin-left: 6px; }
.field label .tag { margin-left: 6px; vertical-align: 1px; }

/* The plan matrix: a column per plan, the current one lit, the picker in
   the column head. Reads as a price list, which is what it is. */
.plan-matrix th, .plan-matrix td { vertical-align: top; }
.plan-matrix td.center, .plan-matrix th.center { text-align: center; }
.plan-matrix thead th { text-transform: none; letter-spacing: 0; font-size: 14px; }
.plan-matrix .is-current { background: var(--accent-soft); }
.plan-matrix thead th.is-current { color: var(--accent); }
.plan-pick { display: flex; align-items: center; gap: 7px; cursor: pointer; font-weight: 650; margin: 0; }
.plan-pick input { width: auto; margin: 0; }
.plan-note { display: block; font-weight: 400; margin-top: 3px; }
.plan-yes { color: var(--ok); vertical-align: -3px; }
.plan-no { color: var(--text-faint); }
.plan-limit td { color: var(--text-muted); font-size: 14.5px; white-space: nowrap; }
.plan-limit td:first-child { font-weight: 600; color: var(--text); }
.plan-matrix tbody tr:first-child td { border-top: 2px solid var(--border); }
@media screen and (max-width: 720px) { .pf-nav { order: 3; width: 100%; } .pf-nav a { padding: 8px 10px; } }

/* On a phone every other table stacks into labelled cards (below); a
   matrix cannot, because a check mark on its own says nothing. It stays a
   table, and its wrap scrolls sideways if it must. */
@media screen and (max-width: 720px) {
  .plan-matrix { display: table; }
  .plan-matrix thead { display: table-header-group; position: static; left: auto; }
  .plan-matrix tbody { display: table-row-group; }
  .plan-matrix tr { display: table-row; border: 0; border-radius: 0; margin: 0; padding: 0; background: transparent; }
  .plan-matrix td { display: table-cell; width: auto; text-align: left; padding: 11px 14px; border-bottom: 1px solid var(--border); }
  .plan-matrix td.center { text-align: center; }
  .plan-matrix tbody tr:last-child td { border-bottom: 0; }
  .plan-matrix th { padding: 10px 12px; }
  /* ...and its first column is a row label, not the row's title. Wider
     than the screen rather than squeezed: a feature name wrapped one
     letter to a line is not a smaller table, it is no table. */
  .plan-matrix { width: auto; min-width: 100%; }
  .plan-matrix tbody td:first-child { font-size: 14px; font-weight: 400; padding: 11px 14px; white-space: nowrap; }
  .plan-matrix .plan-limit td:first-child { font-weight: 600; }
  .plan-note { display: none; }
  #plan .table-wrap { overflow-x: auto; }
  /* The row's title cell reads left to right, like a heading. */
  .pf tbody td:first-child { text-align: left; }
}

/* ---------------------------------------------------------- confirming */
dialog.confirm {
  border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px;
  max-width: 380px; width: calc(100% - 32px); box-shadow: var(--shadow-lg);
  background: var(--surface); color: var(--text);
}
dialog.confirm::backdrop { background: rgba(20, 20, 18, .35); }
dialog.confirm p { margin: 0 0 18px; font-size: 16.5px; line-height: 1.5; }
dialog.confirm .actions { justify-content: flex-end; }

/* ------------------------------------------------------ moving between pages */
/* No animation between pages: a crossfade of a whole page reads as a
   flicker, not as motion. What makes a click feel instant is the next page
   being ready before it is asked for (the speculation rules in base.html)
   and the ground never flashing white while it loads. */
html {
  background: var(--bg);
  /* Always reserve room for the scrollbar. Without this, anything that grows
     the page past the window height -- opening a shift table on Watch, a
     roster editor, a disclosure on a person's page -- makes the scrollbar
     appear, takes 15px off the viewport and shifts every centred thing on
     screen sideways by half of it. Closing it shifts back. Measured at
     7.5px each way, which reads as the whole app twitching. */
  scrollbar-gutter: stable;
}

/* ------------------------------------------------------------------ flashes */

.flash {
  padding: 15px 17px;
  border-radius: var(--radius);
  margin-bottom: var(--gap);
  font-size: 16px;
  border: 1px solid transparent;
  display: flex; gap: 9px; align-items: flex-start;
}
.flash .ico { margin-top: 1px; }
.flash.ok  { background: var(--ok-soft);     color: var(--ok);     border-color: var(--ok); }
.flash.err { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); }
.flash.warn{ background: var(--warn-soft);   color: var(--warn);   border-color: var(--warn); }

/* ------------------------------------------------------------- empty states */

.empty { text-align: center; padding: 34px 20px; color: var(--text-muted); }
.empty .ico-xl { color: var(--text-faint); opacity: .5; margin-bottom: 12px; }
.empty p { margin: 0 0 4px; }
.empty .hint { font-size: 15px; color: var(--text-muted); }

/* --------------------------------------------------------------- utilities */

.muted { color: var(--text-muted); }
.faint { color: var(--text-faint); }
.small { font-size: 14.5px; }
.tiny  { font-size: 13.5px; }
.right { text-align: right; }
.nowrap { white-space: nowrap; }
.stack > * + * { margin-top: 12px; }
.divider { height: 1px; background: var(--border); margin: 16px 0; }
hr { border: 0; border-top: 1px solid var(--border); margin: 16px 0; }

details.disclose > summary {
  cursor: pointer; font-size: 16px; font-weight: 600; color: var(--text);
  padding: 9px 0; list-style: none; min-height: var(--tap);
  display: flex; align-items: center; gap: 9px;
}
details.disclose > summary::-webkit-details-marker { display: none; }
details.disclose > summary:hover { color: var(--accent); }
details.disclose > summary .ico { color: var(--text-faint); }
/* The chevron turns to point down when open: the one universally understood
   signal that a section holds more. */
details.disclose > summary::after {
  content: ""; margin-left: auto;
  width: 8px; height: 8px; border-right: 2px solid var(--text-faint);
  border-bottom: 2px solid var(--text-faint); border-radius: 1px;
  transform: rotate(-45deg); transition: transform .15s;
}
details.disclose[open] > summary::after { transform: rotate(45deg); }

.banner-dev {
  background: var(--warn-soft); color: var(--warn);
  text-align: center; padding: 6px; font-size: 14px; font-weight: 600;
}

/* --------------------------------------------------------------- login page */

.login-wrap {
  min-height: 100vh; display: grid; place-items: center; padding: var(--gap);
}
.login-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; padding: 34px; max-width: 420px; width: 100%;
  box-shadow: var(--shadow); text-align: center;
}
.login-card h1 { margin-bottom: 8px; }
.login-card ol { text-align: left; padding-left: 20px; color: var(--text-muted);
                 font-size: 15.5px; line-height: 1.9; }
.login-card code {
  font-family: var(--mono); background: var(--surface-2);
  padding: 2px 6px; border-radius: 4px; color: var(--text); font-size: 14.5px;
}

.code-display {
  font-family: var(--mono); font-size: 30px; font-weight: 700;
  letter-spacing: .1em; padding: 20px; background: var(--surface-2);
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
  text-align: center; margin: 16px 0; user-select: all;
}

/* ------------------------------------------------- big friendly task cards */
/* The home screen is a set of jobs to do, not a set of tables to read. */

.tasks { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); }

.task {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: var(--text);
  text-align: left;
}
.task:hover {
  text-decoration: none;
  border-color: var(--accent);
  background: var(--accent-soft);
}
.task .icon { flex: 0 0 auto; color: var(--accent); display: flex; }
/* Title over description, always. As inline spans the two ran into one
   sentence on a phone: "Days that need a look Someone forgot to check out". */
.task > span:not(.icon):not(.badge) { flex: 1 1 auto; min-width: 0; }
.task .label { display: block; font-weight: 650; font-size: 18px; }
.task .desc  { display: block; font-size: 15px; color: var(--text-muted); margin-top: 3px; }
.task .badge {
  margin-left: auto; flex: 0 0 auto;
  background: var(--warn); color: #fff;
  border-radius: 100px; padding: 4px 12px;
  font-size: 15px; font-weight: 700;
}

/* ------------------------------------------------------------ mobile tables */
/* Below this width a table stops being a grid and becomes one card per row,
   each cell labelled by the column it came from. Sideways scrolling on a
   payroll table is how you misread a wage. */

@media screen and (max-width: 720px) {
  table, thead, tbody, tr, td { display: block; width: 100%; }

  thead { position: absolute; left: -9999px; }   /* kept for screen readers */

  tbody tr {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 10px;
    padding: 6px 2px;
    background: var(--surface);
  }
  tbody tr:hover { background: var(--surface); }

  td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 14px;
    border: 0;
    padding: 9px 14px;
    text-align: right;
  }
  td.num { text-align: right; }

  /* The column heading, carried down as data-label on each cell. */
  td[data-label]::before {
    content: attr(data-label);
    font-size: 14px;
    font-weight: 600;
    color: var(--text-muted);
    text-align: left;
    flex: 0 0 auto;
  }
  /* A cell with no label is the row's own actions: give it the full width. */
  td:not([data-label]) { justify-content: flex-end; }
  td:empty { display: none; }

  /* The "Change" row on Shops and the roster editor on a staff page are a
     colspan'd cell holding a whole embedded form, not a label/value pair:
     the flex/right-align treatment above squeezed the form's own rows into
     one horizontal line and pushed everything against the right edge. Block
     layout lets it stack normally, the same shape it already has on desktop,
     just full width. */
  td.bg-surface-2 {
    display: block;
    text-align: left;
  }

  tfoot { display: block; }
  tfoot td { justify-content: space-between; border-radius: var(--radius); }

  .table-wrap { overflow-x: visible; }
}

/* ------------------------------------------------------- the phone drawer */
/* Below this width the sidebar slides in over the page instead of taking a
   third of it. The page itself never shifts, so tapping a link does not make
   the content jump under your thumb. */

@media screen and (max-width: 900px) {
  .shell { display: block; }

  .mobilebar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 12px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 25;
  }

  /* A save redirects back to `#section-just-edited` rather than the page
     top (see routes/common.py go()). Without this, the sticky bar above
     covers the first inch of whatever it just scrolled to. */
  [id] { scroll-margin-top: 60px; }

  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: 264px;
    flex-basis: 264px;
    height: 100dvh;
    transform: translateX(-100%);
    transition: transform .18s ease-out;
    box-shadow: 0 0 0 rgba(0, 0, 0, 0);
  }
  .sidebar.open { transform: translateX(0); box-shadow: 0 0 40px rgba(0, 0, 0, .3); }

  .scrim {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, .45);
    z-index: 28;
  }
  .scrim[hidden] { display: none; }

  /* With scripting off the drawer cannot be opened, so the menu is simply
     part of the page rather than an inaccessible off-screen panel.

     Asked as a media query rather than through the no-js class. The class
     is removed by app.js at the very end of the page, so every page on a
     phone first drew the whole menu inline and then slid it away: a jump
     on every single click. The browser knows from the first paint. */
  @media (scripting: none) {
    .sidebar {
      position: static; transform: none; width: auto; flex-basis: auto;
      height: auto; border-right: 0; border-bottom: 1px solid var(--border);
    }
    .menu-btn, .scrim { display: none; }
  }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar { transition: none; }
  .live-dot { animation: none; }
  .qr-modal { transition: none; }
}

/* The quiet signal on /who that the board is keeping itself current, so a
   manager glancing at a shop-counter screen does not have to know the page
   polls every 30s to trust the number in front of them. */
.live-dot {
  display: inline-block;
  width: 7px; height: 7px;
  margin-left: 2px;
  border-radius: 50%;
  background: var(--ok);
  vertical-align: middle;
  animation: live-pulse 2s ease-in-out infinite;
}
@keyframes live-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .35; }
}

@media screen and (max-width: 640px) {
  main { padding: 16px 12px 60px; }
  h1 { font-size: 24px; }
  .row > * { flex: 1 1 100%; }        /* one field per line, never a squeeze */
  .actions { gap: 10px; }
  .actions > form, .actions > form > button { width: 100%; }
  /* ...except the watch page's row of decisions, where one of the four
     happens to be wrapped in a form of its own. Stretched, it stopped
     looking like one of four buttons and started looking like the answer. */
  .watch-actions > form, .watch-actions > form > button { width: auto; }
  .card { padding: 16px; }
  .stat .value { font-size: 26px; }
}

@media print {
  .topbar, .actions, form, .no-print { display: none !important; }
  body { background: #fff; }
  /* An app page sent to paper: the page alone, full width. Without this the
     sidebar prints down the left of every sheet and squeezes the board. */
  .sidebar, .mobilebar, .scrim, .flash { display: none !important; }
  .shell { display: block; min-height: 0; }
  /* The sheet's margin, as padding. @page has its margin at zero further
     down, which is what keeps the browser's own URL and date off the paper,
     so without this the board runs to the edge of the sheet. */
  main {
    max-width: none; padding: 12mm 13mm;
    /* The padding again at the top and foot of every sheet, not only the
       first and last: without it page two starts at the paper's edge. */
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }

  /* Paper is one surface. Screen cards separate sections by lifting them
     off the background; printed, that is a rounded box around everything,
     so a section is set apart by space and its own heading instead. */
  .shell .card {
    border: 0; border-radius: 0; box-shadow: none; background: none;
    padding: 0; margin: 0 0 7mm;
  }
  .shell .card:has(> .tl-head) { break-inside: avoid; }
  /* The list moves to the next sheet whole rather than leaving its last
     few rows alone on one; a list longer than a sheet still breaks. */
  .shell .card.flush { break-inside: avoid; }
  /* Tighter bars, so a day of two shops and ten people is one sheet. */
  .shell .tl-rows { gap: 3px; }
  .shell .tl-track { height: 19px; }
  .shell .tl-bar span { font-size: 8.5pt; }
  .shell .tl-who { font-size: 10.5pt; }
  .shell .legend { border-top: 0; padding: 0; font-size: 9pt; gap: 4px 14px; }
  .shell .tl-head h2 { font-size: 12.5pt; }
  /* All seven counts on one row. A sheet is narrow enough that auto-fit
     wraps them four and three, and the empty eighth cell prints as a grey
     block that looks like a count went missing. */
  .shell .grid.stats {
    grid-template-columns: repeat(7, minmax(0, 1fr));
    box-shadow: none; border-radius: 4px; margin-bottom: 6mm;
    /* On screen the dividers are the grid's background showing through a
       1px gap, and on paper a 1px gap at a fractional position rounds away,
       so two counts print as one box. A border does not round away. */
    gap: 0; background: none;
  }
  .shell .grid.stats > .stat + .stat { border-left: 1px solid var(--border); }
  /* The table's heading and first column on the same edge as everything
     else, now that the card they were inset from is gone. */
  .shell .card-head, .shell .card.flush > h2 { padding: 0 0 4px; }
  .shell .card.flush th:first-child,
  .shell .card.flush td:first-child { padding-left: 0; }
  .shell .stat { padding: 7px 10px; }
  .shell .stat .label { font-size: 8.5pt; }
  .shell .stat .value { font-size: 17pt; margin-top: 2px; }
  /* A name is a link on screen and only a name on paper. */
  .shell a { color: inherit; text-decoration: none; }
  .shell tr { break-inside: avoid; }
  .shell th, .shell td { padding-top: 3px; padding-bottom: 3px; font-size: 10pt; }
  .shell td .chip { padding-top: 1px; padding-bottom: 1px; font-size: 9pt; }
  .shell .wp-taken { margin-top: 5mm; }
  /* Fills carry meaning on a board, a chip's state and a bar's shift, and
     browsers drop them from paper unless asked. */
  .shell, .shell * {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .card { box-shadow: none; border-color: #ccc; }
}

/* ------------------------------------------------------------- timelines */
/* One person against their shop's opening hours, so a gap in cover is a gap
   you can actually see. Used by the live board and the weekly rota alike. */

.tl-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 4px;
}

/* Name column, bar, then the length. The name column width is shared by the
   ruler's left margin so the hour marks line up with the bars beneath. */
.tl-ruler { position: relative; height: 20px; margin: 0 46px 4px 116px; }
.tl-tick { position: absolute; top: 0; transform: translateX(-50%);
           display: flex; flex-direction: column; align-items: center; }
.tl-tick i { width: 1px; height: 5px; background: var(--border-strong); display: block; }
.tl-tick b { font: 400 10.5px var(--mono); color: var(--text-faint); }

.tl-rows { display: flex; flex-direction: column; gap: 5px; }
.tl-row { display: grid; grid-template-columns: 116px 1fr 46px; align-items: center; }
.tl-who { font-size: 15px; padding-right: 10px; overflow: hidden;
          text-overflow: ellipsis; white-space: nowrap; }
.tl-who a { color: var(--text); }

.tl-track { position: relative; height: 26px; background: var(--surface-2);
            border-radius: 5px; overflow: hidden; }
.tl-bar { position: absolute; top: 0; bottom: 0; border-radius: 5px;
          display: flex; align-items: center; justify-content: center;
          min-width: 42px; background: var(--accent); }
.tl-bar span { font: 500 11px var(--mono); color: var(--accent-text);
               white-space: nowrap; padding: 0 6px; }
.tl-len { font: 400 11.5px var(--mono); color: var(--text-faint);
          text-align: right; font-variant-numeric: tabular-nums; }

/* State colours the bar, so the board reads at a glance without the legend. */
.tl-bar.is-on_site  { background: var(--ok); }
.tl-bar.is-on_site span { color: #fff; }
.tl-bar.is-late     { background: var(--danger); }
.tl-bar.is-late span { color: #fff; }
.tl-bar.is-finished { background: var(--border-strong); }
.tl-bar.is-finished span { color: var(--text); }
.tl-bar.is-off, .tl-bar.is-not_working {
  background: transparent; border: 1px dashed var(--border-strong);
}
.tl-bar.is-off span, .tl-bar.is-not_working span { color: var(--text-muted); }
.tl-bar.is-due_later { background: var(--accent-soft); border: 1px dashed var(--accent); }
.tl-bar.is-due_later span { color: var(--accent); }
.tl-bar.is-on_leave { background: var(--leave-soft); border: 1px dashed var(--leave); }
.tl-bar.is-on_leave span { color: var(--leave); }

/* Somebody working a shop that is not their own. */
.tl-bar.covering { background: var(--visit-soft); border: 1px dashed var(--visit); }
.tl-bar.covering span { color: var(--visit); }

/* Still clocked in past when they were scheduled to stop. An inset ring
   rather than swapping the fill colour: they are still on site (green);
   this is an overlay fact on top of that, not a replacement for it. The
   .tl-who name column is only 116px, too narrow to also fit a chip
   reliably, so this is the one place overtime shows on the timeline. */
.tl-bar.has-ot { box-shadow: inset 0 0 0 3px var(--warn); }

/* Where "now" falls across the opening hours. */
.tl-now { position: absolute; top: -2px; bottom: -2px; width: 2px;
          background: var(--danger); z-index: 2; pointer-events: none; }
.tl-now::after {
  content: ""; position: absolute; top: -3px; left: -3px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--danger);
}

.tl-day { padding: 14px 0 4px; border-top: 1px solid var(--border); }
.tl-day:first-of-type { border-top: 0; }
.tl-daylabel { font-size: 15px; font-weight: 650; margin-bottom: 9px;
               display: flex; justify-content: space-between; gap: 10px; }
.tl-daylabel em { font: 400 11.5px var(--mono); color: var(--text-faint);
                  font-style: normal; }

.legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 14px;
          color: var(--text-muted); }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend i { width: 17px; height: 11px; border-radius: 3px; display: inline-block; }

@media screen and (max-width: 640px) {
  .tl-ruler { margin: 0 34px 4px 82px; }
  .tl-row { grid-template-columns: 82px 1fr 34px; }
  .tl-who { font-size: 12.5px; }
  .tl-bar span { font-size: 10px; padding: 0 3px; }
  .tl-bar { min-width: 30px; }
  .tl-tick b { font-size: 9.5px; }
  /* Every other hour mark below this width, or they collide into a smear. */
  .tl-tick:nth-child(even) { display: none; }
}

/* ================================================================ polish ===
   Refinements that change how the thing feels to use rather than how it looks
   in a screenshot. */

/* Page headings carry a one-line explanation. Somebody landing on a screen for
   the first time should not have to infer what it is for from the table. */
.page-head h1 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* A sticky action bar on long forms means Save is always reachable, instead of
   sitting below a screenful of fields. */
.card > form > .actions:last-child { margin-top: 4px; }

/* Tables: the first column is the subject and carries the weight. */
tbody td:first-child { font-weight: 500; }
tbody tr { transition: background .1s; }

/* Rows that lead somewhere show it. A table full of clickable rows with no
   affordance is a table people do not realise they can click. */
tbody tr:hover .btn-sm { border-color: var(--accent); color: var(--accent); }

/* Numbers line up under each other, always. Ragged decimals in a wage column
   are how a misread happens. */
td.num, th.num, .stat .value { font-variant-numeric: tabular-nums; }

/* Primary action is unmistakable; everything else recedes. */
.btn-primary { box-shadow: var(--shadow); }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); filter: none; }

/* Disabled state must look disabled, not merely unresponsive. */
button:disabled, .btn:disabled {
  opacity: .55; cursor: not-allowed; box-shadow: none;
}

/* Inputs that are about to receive a value the user must not fat-finger.
   :user-invalid only matches after the visitor has interacted with the
   field, unlike :invalid:not(:placeholder-shown), which never shows a
   placeholder on date/file inputs and so flagged them red on page load,
   before anyone had touched them. */
input:required:user-invalid { border-color: var(--danger); }

/* Cards group; sections inside them separate without another border. */
.card + .card { margin-top: var(--gap); }
/* ...except side by side in a grid, where `gap` already spaces them. Without
   this, a card that happens to be a `.grid`'s *second* child (e.g. Hourly pay
   / Normal working hours on /staff/{id}) picked up this margin on top of the
   grid's own gap; stretch then had to shrink that card to fit the extra
   space, so two equal-height cards rendered at different heights. */
.grid > .card + .card { margin-top: 0; }
h3 { display: flex; align-items: center; gap: 8px; }

/* Keyboard users get a route past the sidebar on every page. */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 60;
  background: var(--accent); color: var(--accent-text);
  padding: 12px 18px; border-radius: 0 0 var(--radius-sm) 0; font-weight: 600;
}
.skip:focus { left: 0; text-decoration: none; }

/* Status dot sizing sits with the chip it lives in. */
.chip .dot { width: 6px; height: 6px; margin-right: 5px; }
.chip.ok .dot { background: var(--ok); }

/* Long shop or staff names must not blow a table column apart. */
.tl-who, td { overflow-wrap: anywhere; }
.tl-who { overflow-wrap: normal; }

/* The payout QR on /payroll. One cell, stacked so the mobile card layout
   (label on the left, everything else as one block on the right) never
   splits the code from the account text beside it. */
.payout-cell {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
}
.payout-qr {
  display: block;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  /* A QR code needs a plain white quiet zone to stay scannable regardless of
     the row it sits in, not the page's own surface colour. */
  background: #fff;
  padding: 3px;
  cursor: zoom-in;
  /* Not every upload is a square code: a phone screenshot of a banking
     app's QR screen is usually a tall rectangle. The width/height attributes
     fix this thumbnail's box; without object-fit the image would stretch to
     fill it and mangle the actual code inside. `contain` scales it down
     evenly instead, letterboxed on the white background above. */
  object-fit: contain;
}
/* /payroll's table thumbnail: plain, not a link into the lightbox; the
   default zoom-in cursor from .payout-qr would promise a click that does
   nothing here. */
.payout-qr-static { cursor: default; }

/* The QR lightbox: a native <dialog>, so open/close, the backdrop, and
   Escape-to-close all come from the browser for free instead of hand-rolled
   JS. `position: fixed` + `inset: 0` + `margin: auto` reproduces the UA's
   own centering for `dialog:modal` explicitly; leaving position at its
   `relative` default here drops that centering, so the dialog renders at
   its in-flow spot in the document instead of over the backdrop, and
   showModal() then scrolls the page to put it in view. Sized clear enough
   to scan directly off the screen by default, no tap-to-zoom required.

   Sized by max-width/max-height rather than a fixed width: a QR is square,
   but plenty of what actually gets uploaded is a phone screenshot of a
   banking app's whole QR screen: tall, not square. Capping both axes and
   letting the dialog (and the image inside it, via object-fit) shrink to
   fit whichever is real means neither shape gets stretched or cut off. */
.qr-modal {
  position: fixed;
  inset: 0;
  margin: auto;
  border: 0;
  border-radius: var(--radius);
  padding: 24px 24px 22px;
  box-shadow: var(--shadow-lg);
  max-width: min(420px, 92vw);
  max-height: 88vh;
  text-align: center;
}
.qr-modal[open] {
  animation: qr-modal-in .12s ease-out;
}
@keyframes qr-modal-in {
  from { opacity: 0; transform: scale(.97); }
  to   { opacity: 1; transform: scale(1); }
}
.qr-modal::backdrop {
  background: rgba(20, 20, 18, .6);
  backdrop-filter: blur(2px);
}
.qr-modal-caption {
  margin: 0 0 16px;
  font-weight: 620;
  font-size: 15px;
}
.qr-modal img {
  display: block;
  /* max-width/max-height, not a flat 100%: a square code should still fill
     the dialog's width, but a tall screenshot needs to be capped by height
     instead or it pushes the dialog past the viewport. The reserved 110px
     is the caption line and the dialog's own padding above and below. */
  max-width: 100%;
  max-height: calc(88vh - 110px);
  width: auto; height: auto;
  margin: 0 auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: #fff;
  padding: 14px;
}
.qr-modal-close {
  position: absolute; top: 10px; right: 10px;
  width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid var(--border-strong); background: var(--surface);
  font-size: 20px; line-height: 1; cursor: pointer;
}
.qr-modal-close:hover { background: var(--surface-2); }

/* Per-person "mark paid" toggle on /payroll. Ticked state reuses the same
   ok/soft colouring every other "this is good" chip on the page already
   uses, so a scanned-and-paid row reads at a glance among the rest. */
.paid-toggle.is-paid {
  background: var(--ok-soft);
  border-color: var(--ok);
  color: var(--ok);
}

@media screen and (max-width: 720px) {
  /* On a phone the first cell is the row's title, so it reads as a heading
     rather than as another labelled field. */
  tbody td:first-child {
    font-size: 17px; font-weight: 650;
    border-bottom: 1px solid var(--border); margin-bottom: 4px; padding-bottom: 10px;
  }
  tbody td:first-child::before { display: none; }
  tbody td:first-child { justify-content: flex-start; }
}

/* ---------------------------------------------------------------- utilities
   One-off placement that used to live in a `style=""` attribute. The CSP is
   `style-src 'self'` with no `unsafe-inline`, so an inline style is not a
   convenience here; it is invisible: the browser drops the whole attribute
   and the element renders as if it were never written. Anything that is not
   a per-row computed value (the timeline bars below) belongs here instead. */

.mt-0    { margin-top: 0; }
.mt-4    { margin-top: 4px; }
.mt-8    { margin-top: 8px; }
.mt-9    { margin-top: 9px; }
.mt-10   { margin-top: 10px; }
.mt-12   { margin-top: 12px; }
.mt-14   { margin-top: 14px; }
.mt-16   { margin-top: 16px; }
.mt-18   { margin-top: 18px; }
.mt-20   { margin-top: 20px; }
.mt-26   { margin-top: 26px; }
.mt-neg4 { margin-top: -4px; }

.mb-8   { margin-bottom: 8px; }
.mb-10  { margin-bottom: 10px; }
.mb-12  { margin-bottom: 12px; }
.mb-gap { margin-bottom: var(--gap); }

.m-0 { margin: 0; }
.m-8-12  { margin: 8px 0 12px; }
.m-8-14  { margin: 8px 0 14px; }
.m-10-0  { margin: 10px 0 0; }

.text-left   { text-align: left; }
.text-right  { text-align: right; }
.text-danger { color: var(--danger); }
.text-accent { color: var(--accent); }
.text-ok     { color: var(--ok); }
.text-warn   { color: var(--warn); }

.border-warn   { border-color: var(--warn); }
.bg-surface-2  { background: var(--surface-2); }

.minw-220 { min-width: 220px; }

.max-620 { max-width: 620px; }
.max-540-center { max-width: 540px; margin: 0 auto; text-align: center; }
.max-520-center { max-width: 520px; margin: 0 auto; }
.wrap-420 { max-width: 420px; overflow-wrap: anywhere; }
.wrap-280 { max-width: 280px; overflow-wrap: anywhere; }

.jc-end    { justify-content: flex-end; }
.jc-center { justify-content: center; }

.fs-16-normal  { font-size: 17.5px; font-weight: 400; }
.fs-15-tight   { font-size: 16.5px; letter-spacing: 0; }
.note-inline   { text-align: left; color: var(--text-muted); font-size: 15.5px; }

/* Card header padding, for the `<h2>`/`<p class="sub">` pair that opens a
   `.card.flush`: the card itself has no padding, so its first children
   carry it instead. */
.card-head    { padding: 16px 18px 12px; }
.card-head-sm { padding: 16px 18px 8px; }
.card-head-x  { padding: 0 18px 12px; }

/* Week subtotal row inside the month's attendance table. The existing
   `tfoot td` rule gives the month's grand total below it a matching but
   heavier treatment, so the eye reads day rows, then a week, then the month. */
.tbl-week-total td { background: var(--surface-2); font-weight: 600;
                      padding-top: 8px; padding-bottom: 8px; }

/* Legend swatches on the presence timeline: a small block of the same
   colour or border the bar it explains uses. */
.swatch-ok      { background: var(--ok); }
.swatch-warn    { background: var(--warn); }
.swatch-danger  { background: var(--danger); }
.swatch-muted   { background: var(--border-strong); }
.swatch-dashed-muted  { background: var(--surface-2);  border: 1px dashed var(--border-strong); }
.swatch-dashed-accent { background: var(--accent-soft); border: 1px dashed var(--accent); }
.swatch-dashed-leave  { background: var(--leave-soft); border: 1px dashed var(--leave); }
.swatch-dashed-visit  { background: var(--visit-soft); border: 1px dashed var(--visit); }

/* A timeline bar that spans its whole row: the "off / on leave" case,
   which has no real start or end to place. */
.tl-bar-full { left: 0; width: 100%; }

/* ============================================= a person's page ===========
   /staff/{id} carries more controls than any other screen, most of which a
   manager touches once and never again. The layout below is built around
   that: the numbers that matter read off one strip at the top, the two
   everyday forms sit side by side, and the rest collapses into a single
   list of rows rather than a column of near-empty cards. */

/* The name, with an initial beside it so a page opened from a link is
   recognisable before the text is read. */
.id-head { display: flex; align-items: center; gap: 13px; min-width: 0; }
.avatar {
  flex: 0 0 auto;
  width: 42px; height: 42px;
  border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 18px; font-weight: 650;
  user-select: none;
}

/* The summary strip: four figures, one hairline between each, no gaps and
   no per-figure card. Reads as one instrument panel rather than four. */
.factbar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  margin-bottom: var(--gap);
  overflow: hidden;
}
.factbar > div { padding: 13px 16px; border-left: 1px solid var(--border); min-width: 0; }
.factbar > div:first-child { border-left: 0; }
.factbar .k {
  display: block;
  font-size: 13.5px; color: var(--text-muted); font-weight: 600;
}
.factbar .v {
  display: block;
  font-size: 24px; font-weight: 650; letter-spacing: -.02em;
  margin-top: 4px; font-variant-numeric: tabular-nums;
}
.factbar .v em { font-style: normal; font-size: 19px; font-weight: 600; }
.factbar .n { display: block; font-size: 14px; color: var(--text-muted); margin-top: 3px; }

/* Below the strip the grid can wrap onto one column on a narrow window; the
   hairlines would then be on the wrong edge, so they move to the top. */
@media screen and (max-width: 560px) {
  .factbar > div { border-left: 0; border-top: 1px solid var(--border); }
  .factbar > div:first-child { border-top: 0; }
}

/* A section label above a group of cards. */
.sec-head { margin: 26px 0 10px; }

/* The stacked "set up less often" list: one card, one row per section, a
   hairline between. Previously five separate cards, which cost five borders,
   five shadows and five 16px gaps to say five short words. */
.card.disclose-list { padding: 0; overflow: hidden; }
.disclose-list > details { border-top: 1px solid var(--border); }
.disclose-list > details:first-child { border-top: 0; }
.disclose-list > details > summary { padding: 0 18px; min-height: 54px; }
.disclose-list > details[open] > summary { background: var(--surface-2); }
.disclose-list > details > summary:hover { background: var(--surface-2); }
.disclose-list > details > .disclose-body { padding: 16px 18px 20px; }

/* The value on the right of a closed row: what the section currently holds,
   so it does not have to be opened to be checked. */
.sum-note {
  margin-left: auto;
  font-size: 14.5px; font-weight: 500; color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding-left: 12px; min-width: 0;
}
/* Scoped to this list: elsewhere a summary has nothing but its label, and the
   chevron's own `margin-left: auto` is what puts it on the right edge. Here
   the note already took that job, so the chevron only needs a gap. */
.disclose-list > details > summary::after { margin-left: 10px; flex: 0 0 auto; }
.disclose-list > details.is-caution > summary .ico { color: var(--warn); }

/* The setting itself, inside the note. On the staff page every row of a
   person's arrangement is one of these, so the value has to read as the
   answer and the qualifier after it as the aside; otherwise "$0.833 since
   15 Aug 2026" is one undifferentiated grey line and the page is no more
   readable closed than it was as a wall of open forms. */
.sum-note b {
  color: var(--text); font-weight: 600; font-size: 15.5px;
}
.sum-note b + * , .sum-note b ~ *:not(b) { font-weight: 400; }

/* A row in the same list that nobody may change. Same geometry as a summary,
   without the chevron or the pointer: a site viewer reads the identical
   facts and simply has nowhere to click. */
.disclose-list > .factrow {
  display: flex; align-items: center; gap: 9px;
  padding: 0 18px; min-height: 54px;
  border-top: 1px solid var(--border);
  font-size: 15.5px; font-weight: 600; color: var(--text);
}
.disclose-list > .factrow:first-child { border-top: 0; }
.disclose-list > .factrow .ico { color: var(--text-faint); }

/* A payout QR is recognised by sight faster than by any words about it, so
   the closed row carries the code itself at thumbnail size. */
.qr-mini {
  border-radius: 3px; vertical-align: -5px; margin-right: 7px;
  background: #fff; border: 1px solid var(--border);
}

.disclose-body h4 {
  font-size: 15px; font-weight: 650; margin: 0 0 10px; color: var(--text);
}

/* A table nested inside a disclosure has no card border of its own to sit
   against, so it gets one. */
.table-wrap.bordered {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.table-wrap.bordered table { font-size: 15px; }

/* ------------------------------------------------------------ rate ledger */
/* A financial record rather than a browsing list: denser rows, figures on a
   tabular grid, and the row's action kept to the width of one word so the
   money column does not drift from row to row. */

.ledger table { font-size: 14.5px; }
.ledger th { padding: 10px 13px; }
.ledger td { padding: 10px 13px; line-height: 1.45; }
.ledger td.num { font-variant-numeric: tabular-nums; font-weight: 600; }
.ledger .chip { margin-left: 6px; }

/* The reason column carries free text somebody typed. Capped so one long
   sentence cannot push the figures off a narrow screen. */
.ledger .cell-why { color: var(--text-muted); max-width: 30ch; }

/* How an entry stopped counting: label, reason, and when, on one line that
   degrades to two rather than stretching the row. */
.void-meta {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px;
  margin-top: 4px; font-size: 13px; color: var(--text-faint);
}
.void-meta .chip { margin-left: 0; }
.void-when { font-variant-numeric: tabular-nums; }

/* A cancelled entry stays in place and stays readable: struck through says
   "this did not stand" where hiding the row says nothing at all. The strike
   is on the date and the figure only, never on the reason: the explanation is
   the part somebody came to read. */
.row-void > td { background: var(--surface-2); }
.row-void > td:first-child,
.row-void > td.num { text-decoration: line-through; color: var(--text-faint); }

/* The per-row Void control. Closed it is one quiet word; open it is a single
   line of form, because the explanation it used to carry now sits once under
   the table instead of once per row. */
.void-pop > summary {
  cursor: pointer; list-style: none;
  font-size: 14px; font-weight: 600; color: var(--text-muted);
  padding: 6px 10px; border-radius: var(--radius-sm);
  border: 1px solid transparent;
}
.void-pop > summary::-webkit-details-marker { display: none; }
.void-pop > summary:hover { color: var(--danger); background: var(--danger-soft); }
.void-pop[open] > summary { color: var(--danger); background: var(--danger-soft); }

.void-pop form {
  display: flex; gap: 6px; align-items: center;
  margin-top: 8px; justify-content: flex-end;
}
.void-pop input[type="text"] {
  width: 200px; min-height: 40px; padding: 8px 11px; font-size: 15px; margin: 0;
}
.void-pop .btn-sm { min-height: 40px; }

/* On a phone every row is already a stacked card, so the form has the full
   width to use and a cramped 190px field would be the only narrow thing on
   the screen. */
@media screen and (max-width: 900px) {
  .ledger .cell-why { max-width: none; flex-wrap: wrap; }

  /* Each cell is a label/value flex row here, so the annotation has to be
     told to take a line of its own; otherwise it sits alongside the reason
     as a third item and the row reads as one run-on sentence. */
  .ledger .void-meta { flex-basis: 100%; justify-content: flex-end; margin-top: 2px; }

  .void-pop form { justify-content: flex-start; }
  .void-pop input[type="text"] { flex: 1 1 auto; width: auto; }
}

/* Card header with figures pinned to the right: the totals a manager came
   for, without scrolling to the foot of the table to find them. */
.card-head.split {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}
.head-figs { display: flex; gap: 26px; }
.head-figs .k {
  display: block;
  font-size: 13.5px; color: var(--text-muted); font-weight: 600;
}
.head-figs .v {
  display: block;
  font-size: 21px; font-weight: 650; margin-top: 2px;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}

/* Row actions live in their own column, sized to the button and pinned
   right. When the chips shared a cell with the button, a row flagged
   "forgot to check out" pushed its Open several centimetres away from the
   Open on the row above: a column of buttons that never lined up. */
th.col-act, td.col-act { width: 1%; white-space: nowrap; text-align: right; }
td.col-act > form { display: inline-block; }

/* The flags column: fixed to what the widest chip needs, so the money
   columns beside it stay where they are from row to row. */
td.col-tags { white-space: nowrap; }
td.col-tags .chip + .chip { margin-left: 5px; }

/* Clock-in access: state and its one-line explanation on the same baseline. */
.state-line { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; margin: 0; }

/* Payout QR: the code and the upload control side by side, rather than the
   code stranded above a full-width form. */
.qr-row { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.qr-side { flex: 1 1 220px; min-width: 200px; }

.mb-0 { margin-bottom: 0; }

@media screen and (max-width: 720px) {
  /* The phone card layout turns every cell into a labelled line. An action
     column is not a value, so it keeps the full width to itself. */
  td.col-act { width: auto; justify-content: flex-end; }
  .head-figs { gap: 20px; }
}


/* ------------------------------------------------------- the attendance strip
   A month of one person's days, one square each, so a habit is visible as a
   pattern rather than as a number you have to compare against everyone else's.
   Every square carries a title, because colour alone is not a label, and it
   is the only thing a screen reader or a colour-blind reader has here. */

.daystrip { display: flex; flex-wrap: wrap; gap: 2px; min-width: 200px; }

.dsq {
  width: 16px; height: 21px;
  border-radius: 3px;
  background: var(--surface-3);
  display: inline-block;
  flex: 0 0 auto;
}

.dsq.v-absent        { background: var(--danger-mid); }
.dsq.v-late          { background: var(--warn-mid); }
.dsq.v-on_time       { background: var(--ok-mid); }
.dsq.v-extra_day     { background: var(--accent-mid); }
.dsq.v-on_leave      { background: var(--leave-mid); }
.dsq.v-off           { background: var(--surface-3); }
.dsq.v-not_scheduled { background: var(--surface-3); }

/* Days with nothing to say yet read as absence of ink, not as a grey day off. */
.dsq.v-pending,
.dsq.v-none { background: transparent; box-shadow: inset 0 0 0 1px var(--border); }

/* A second fact on the same square: they came, but left early or never checked
   out. A bar rather than another colour, so the arrival verdict still reads. */
.dsq.flagged { box-shadow: inset 0 -4px 0 var(--danger); }

th.col-strip, td.col-strip { width: 40%; min-width: 210px; }

/* `td { overflow-wrap: anywhere }` exists so one unbreakable string -- a URL,
   a reason typed without spaces -- cannot blow a table off the page. A name is
   not that, and in a window around 1000px the strip column starved this one
   until "Saophea" broke across two lines mid-word. Below 720px the table
   becomes stacked cards and none of this applies. */
@media print, (min-width: 721px) {
  table.attendance { min-width: 700px; }
  table.attendance td[data-label="Person"] {
    overflow-wrap: normal;
    min-width: 128px;
  }
}

.legend {
  display: flex; flex-wrap: wrap; gap: 6px 16px;
  padding: 14px 18px 16px;
  border-top: 1px solid var(--border);
  font-size: 14px; color: var(--text-muted);
}
.legend-item { display: inline-flex; align-items: center; gap: 6px; }

@media screen and (max-width: 720px) {
  th.col-strip, td.col-strip { width: auto; }
}


/* ------------------------------------------------------------- join by QR */
/* A new hire is usually standing in front of you when you make their code, so
   the picture they can point a camera at is the primary control here and the
   typed code is the fallback -- the reverse of how this page used to read. */

.join-qr {
  display: flex; justify-content: center;
  margin: 18px 0 6px;
}
.join-qr img {
  /* White ground and a real border: a QR rendered straight onto the page
     background loses its quiet zone against anything but white, and scanners
     use that margin to find the edge of the code. */
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px;
  image-rendering: pixelated;   /* never blur the modules */
  max-width: 100%; height: auto;
}

.join-or {
  font-size: 12.5px; color: var(--text-faint);
  text-transform: uppercase; letter-spacing: .07em;
  margin: 14px 0 8px;
}

.code-display-sm {
  font-size: 15.5px; letter-spacing: 0; word-break: break-all;
}
.code-display-sm a { color: var(--accent); }

/* The fallback is a disclosure so it does not compete with the QR, but it is
   still one click away -- somebody's phone camera will fail eventually. */
.join-fallback { text-align: left; }
.join-fallback > summary { justify-content: center; font-size: 15px; }
.join-fallback > summary::after { margin-left: 8px; }

/* The change, not the destination: "09:00-17:00 -> a rest day" answers the
   manager's question in one glance where either half alone does not. An icon
   rather than an arrow character, because a template full of symbols renders
   differently on every device -- which is why this codebase has a test that
   refuses them. */
.arrow { color: var(--text-faint); margin: 0 3px; vertical-align: -.2em; }


/* ------------------------------------------------------ a person's own page */
/* No sidebar: there is nowhere else for this person to go, and a nav with one
   item in it is a nav that looks broken. Read on a phone far more often than
   on a desk, so the measure is narrow and the bar is small. */

.me-shell { min-height: 100vh; background: var(--bg); }

.me-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.me-bar .brand { font-size: 16.5px; }
.me-dev { margin: 0; padding: 4px 9px; font-size: 11px; }
.me-who {
  margin-left: auto; font-size: 14.5px; color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.me-signout { margin: 0; }

.me-main { max-width: 880px; margin: 0 auto; padding: 22px 18px 64px; }
.me-foot { margin-top: 26px; text-align: center; }

.plain-list { list-style: none; margin: 0; padding: 0; font-size: 15.5px; }
.plain-list li { padding: 4px 0; border-top: 1px solid var(--border); }
.plain-list li:first-child { border-top: 0; }


/* A payslip reads as a column of "what" against "how much", with the figure
   that matters ruled off at the foot. */
.payslip li { display: flex; gap: 12px; align-items: baseline; }
.payslip li > span:first-child { flex: 1 1 auto; min-width: 0; }
.payslip strong { font-variant-numeric: tabular-nums; white-space: nowrap; }
.payslip .faint { font-size: 12.5px; }
.payslip-total {
  border-top: 2px solid var(--border-strong) !important;
  margin-top: 4px; padding-top: 8px !important; font-size: 15px;
}
.payslip-total strong { font-size: 17px; }


/* ------------------------------------------------------------- the payslip */
/* A page whose real destination is paper. Everything below is written for the
   screen first and then survives print, rather than the other way round: a
   manager reads it once to check it before they ever press print, and a sheet
   that only made sense on paper would be checked less carefully. */

.print-body { background: var(--surface-2); }

.print-bar {
  display: flex; gap: 8px; justify-content: flex-end; align-items: center;
  max-width: 860px; margin: 0 auto; padding: 14px 16px 0;
}

.print-main { max-width: 860px; margin: 0 auto; padding: 14px 16px 40px; }

.slip {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 28px 30px 24px;
}

.slip-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 20px; flex-wrap: wrap;
  padding-bottom: 16px; border-bottom: 2px solid var(--text);
}
.slip-head h1 { font-size: 25px; margin: 2px 0 0; }
.slip-shop {
  margin: 0; font-size: 13px; color: var(--text-muted); font-weight: 600;
}
.slip-when { text-align: right; }
.slip-month { margin: 0; font-size: 17px; font-weight: 600; }
.slip-sub { margin: 3px 0 0; font-size: 14.5px; color: var(--text-muted); }

/* The answer, before the working. Somebody opens this to find one number. */
.slip-net {
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: 20px; flex-wrap: wrap; padding: 18px 0 16px;
}
.slip-net .k {
  display: block; font-size: 13px; color: var(--text-muted); font-weight: 600;
}
.slip-net .v {
  display: block; font-size: 34px; font-weight: 650; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; margin-top: 2px;
}
.slip-net-side { text-align: right; }

.slip-note {
  margin: 0 0 16px; padding: 10px 12px; border-radius: var(--radius-sm);
  background: var(--surface-2); font-size: 14.5px; color: var(--text-muted);
}
.slip-note-warn { background: var(--warn-soft); color: var(--warn); }

/* The subtraction. Row headers here are sentences, not column labels, so they
   opt out of the uppercase treatment every other th in the app gets. */
.slip-sum { margin-bottom: 26px; }
.slip-sum th {
  text-transform: none; letter-spacing: 0; font-size: 14px; font-weight: 500;
  color: var(--text); background: none; border-bottom: 0; white-space: normal;
  padding: 7px 0;
}
.slip-sum td { padding: 7px 0 7px 14px; border-bottom: 0; }
.slip-sum td:last-child { font-variant-numeric: tabular-nums; }
.slip-sum .slip-rule th, .slip-sum .slip-rule td {
  border-top: 1px solid var(--border); padding-top: 10px;
}
.slip-sum .slip-total th, .slip-sum .slip-total td {
  border-top: 2px solid var(--text); padding-top: 11px;
  font-size: 17px; font-weight: 650;
}
.slip-adj { font-weight: 500; }
.slip-why { display: block; font-size: 14px; color: var(--text-muted); }

/* "+1" against an out-time that landed the next day. Small, but it has to
   survive printing in black and white, so it carries no colour of its own. */
.slip-next {
  font-size: 0.72em; font-weight: 600; vertical-align: super;
  margin-left: 1px; color: var(--text-muted);
}

.slip-days { margin-bottom: 24px; }
.slip-days h2 {
  font-size: 14px; color: var(--text-muted); font-weight: 650; margin: 0 0 8px;
}
.slip-days table { border: 1px solid var(--border); border-radius: var(--radius-sm); }

.slip-foot {
  border-top: 1px solid var(--border); padding-top: 14px;
  font-size: 14px; color: var(--text-muted);
}
.slip-foot p { margin: 0 0 22px; }

/* Two ruled lines to sign against. A shop that hands over cash wants a
   signature on something, and drawing it here saves a biro and a ruler. */
.slip-sign { display: flex; gap: 40px; }
.slip-sign span {
  flex: 1; border-top: 1px solid var(--border-strong);
  padding-top: 5px; font-size: 13.5px; color: var(--text-muted);
}

/* Deliberately loud, and deliberately still there when the sheet prints: a
   payslip from the test instance that looked exactly like a real one is the
   one mistake this page could make that somebody gets handed. */
.slip-dev {
  margin: 0 0 12px; padding: 8px 12px; border-radius: var(--radius-sm);
  border: 1px dashed var(--warn); background: var(--warn-soft);
  color: var(--warn); font-size: 12.5px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase; text-align: center;
}

/* Row actions that stack rather than fight for width in a narrow cell. */
.row-actions { display: flex; gap: 6px; flex-wrap: wrap; }

@media screen and (max-width: 640px) {
  .slip { padding: 20px 18px 18px; }
  .slip-when, .slip-net-side { text-align: left; }
  .slip-net .v { font-size: 29px; }
}

@media print {
  /* One sheet, black on white, and no table split across a page fold:
     half a list of days on the back of the page is how a total stops
     looking like it belongs to the rows above it. */
  /* A zero page margin is what stops the browser stamping its own header
     and footer -- the page title, the URL, the date -- into that space. The
     sheet keeps its margin as padding instead, so the paper looks the same,
     and a document that gets handed over does not carry the address of a
     private page on it. */
  @page { margin: 0; }
  .print-body { background: #fff; }
  /* Browsers drop background fills when printing unless asked, and on a
     board the fill is the information: a state chip becomes bare text and a
     timeline bar an empty track. Asked for on the whole sheet rather than
     chip by chip, so a colour added later cannot be the one that goes
     missing. The payslip's rules below still take their own fills off. */
  .print-body, .print-body * {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .print-main { max-width: none; padding: 14mm; }
  .slip { border: 0; border-radius: 0; box-shadow: none; padding: 0; }
  .slip-days table, .slip-sum { page-break-inside: avoid; }
  .slip-foot { page-break-inside: avoid; }
  /* A heading stays with what it heads: alone at the foot of a sheet it
     names a table that starts on the next one. */
  .print-body h2, .print-body h3, .shell h2, .shell h3 { break-after: avoid; }
  .slip-note { border: 1px solid #999; background: none; color: #000; }
  .slip-dev { border-color: #000; background: none; color: #000; }
  th { background: none; }
}

/* ------------------------------------------------------- check-in photos */
/* Small on the Today table, a proper picture on the shift itself. Both open
   the same image; the thumbnail is a way in, not the evidence. */
.thumb {
  display: inline-block; vertical-align: middle;
  width: 32px; height: 32px; border-radius: 50%; overflow: hidden;
  border: 1px solid var(--border); margin-left: 6px;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.selfies { display: flex; flex-wrap: wrap; gap: 18px; }
.selfie { flex: 1 1 220px; max-width: 320px; }
.selfie .label { font-size: 15px; font-weight: 600; margin-bottom: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.selfie img {
  width: 100%; height: auto; display: block;
  border-radius: var(--radius-sm); border: 1px solid var(--border);
  background: var(--surface-2);
}

/* ------------------------------------------------------------ shop codes */
.code-panel-row { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
.code-panel img {
  background: #fff; border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 8px; image-rendering: pixelated;
}
.wrap-anywhere { overflow-wrap: anywhere; }

/* The tablet at the counter: one thing on the page, big enough to scan from
   arm's length, on a white ground so the quiet zone is real. */
.screen-body { background: #fff; margin: 0; min-height: 100vh; display: grid; place-items: center; }
.screen-main { text-align: center; padding: 24px; }
.screen-name { font-size: 28px; margin: 0 0 18px; }
.screen-qr img { width: min(70vmin, 420px); height: auto; image-rendering: pixelated; }
.screen-hint { font-size: 20px; color: var(--text-muted); margin: 18px 0 0; }

/* A line set in Khmer. The script stacks vowel signs above the letters and
   subscript consonants below them, so the Latin line-height clips both ends
   and two consecutive lines collide. Read from arm's length on the counter
   tablet, which is the one screen in the app a customer can also see. */
.km { line-height: 2; }

/* ============================================= the watch page ===========
   Each signal is a card holding the shifts behind it. Past a handful the
   table folds behind a summary line so the decisions stay within reach; the
   summary sits inside a flush card and carries the card's side padding. */
details.disclose.disclose-table > summary {
  /* Tight to the headline above it: this row is the card's meta line as well
     as the way into the shifts, and it still clears 44px because a summary is
     a tap target before it is a sentence. */
  padding: 2px 18px 8px; font-size: 15px; color: var(--text-muted);
  min-height: 44px;
}
details.disclose.disclose-table[open] > summary { color: var(--text); }
/* The decision row is buttons only. The one the manager pressed opens a
   panel underneath, on the quieter ground the roster editor uses, so the row
   itself never changes shape and the other buttons stay where they were. The
   panel is the card's last thing, so it needs no bottom padding of its own. */
/* The buttons carry their own 44px of height, so the band around them does
   not need a full card-head's worth on top of that. */
/* One row per signal: who and what on the left, the decisions on the right.
   On a phone the buttons wrap under the name instead. */
.watch-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px 16px; flex-wrap: wrap; padding-bottom: 10px;
}
.watch-who { min-width: 0; flex: 1 1 280px; }
.watch-actions { flex: 0 0 auto; gap: 6px; margin: 0; }
.watch-panel {
  background: var(--surface-2); border-top: 1px solid var(--border);
  padding: 16px 18px 18px;
}
.watch-panel > form { max-width: 480px; }
.watch-panel .field:last-of-type { margin-bottom: 12px; }

/* ========================================= managers, and what they may do ==
   The eight rights used to be a disclosure inside a table cell: each one a
   heading and a sentence, folded into a 220px column, and the sentence ran on
   from its own heading in a single grey line because the class meant to break
   it was never in this file. So the managers became rows, on the pattern the
   staff page already uses, and the eight jobs get the card's full width. */

/* A list nested inside a card has no card edge of its own to sit against, so
   it borrows one, the way a nested table does. */
.disclose-list.inset {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.disclose-list.inset > details > summary { padding: 0 14px; min-height: 60px; }
.disclose-list.inset > details > .disclose-body { padding: 14px; }

/* Two lines in the closed row: who, and which shop. The name carries the
   summary's own weight, the shop is the quiet answer underneath it. */
.mgr-who { display: flex; flex-direction: column; min-width: 0; }
.mgr-who .where { font-size: 13.5px; font-weight: 500; color: var(--text-muted); }

/* One right per block, as many abreast as fit. A bare checkbox is a 16px
   target for a decision that takes a sentence to explain, so the block is the
   target and the sentence sits inside it rather than beside it. */
.tick-grid {
  display: grid;
  /* min() on the track floor, not a bare 360px. auto-fit will not shrink a
     track below its own minimum, so on a phone a bare 360px lays one column
     360px wide inside a 289px card and the blocks hang over the edge. The
     min() lets the floor fall to the container width when there is less than
     360px to give it. */
  grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
  gap: 10px;
}
.tick {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 11px 13px; min-height: 44px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); cursor: pointer;
}
.tick:hover { border-color: var(--border-strong); background: var(--surface-2); }
.tick input { width: auto; flex: none; margin-top: 3px; }
.tick strong { display: block; font-size: 15.5px; font-weight: 600; }
.tick .what {
  display: block; margin-top: 2px;
  font-size: 13.5px; line-height: 1.4; color: var(--text-muted);
}
/* What somebody holds should read at a glance, not only from the state of
   eight small boxes. A browser without :has() shows the box alone, which is
   all there ever was. */
.tick:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
/* The risk of a right, under what it does, in the warning colour, so it
   reads as a separate point rather than more of the description. */
.tick .caution {
  display: flex; align-items: flex-start; gap: 6px; margin-top: 6px;
  font-size: 13px; line-height: 1.4; color: var(--warn);
}
.tick .caution .ico { flex: none; margin-top: 2px; }

/* The two ways out of an open row, on a rule of their own so neither is taken
   for one of the rights above. Save sits at the end, where the eye leaves the
   last block; taking somebody's access away stays at the other end, quiet. */
.mgr-foot {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 14px; padding-top: 14px;
  border-top: 1px solid var(--border);
}
.mgr-foot .save { margin-left: auto; }
/* The preset sits beside save, so the auto margin moves to it. */
.mgr-foot .preset { margin-left: auto; }
.mgr-foot .preset + .save { margin-left: 0; }
@media screen and (max-width: 560px) {
  /* Thumbs, not cursors: all full width, and save on top, so the one that
     cannot be undone is never where the finger already was. */
  .mgr-foot { flex-direction: column-reverse; align-items: stretch; }
  .mgr-foot > button { width: 100%; }
  .mgr-foot .save, .mgr-foot .preset { margin-left: 0; }
}


/* ------------------------------------------------------- the business page
   /settings. Every row is label and explanation on the left, the control on
   the right, hairline between, which is the same geometry as .disclose-list
   above without the disclosure: these rows are all open, all editable, and
   taller than 54px because each carries a sentence under its label.

   The column is capped at 880px rather than filling main's 1180px. A label
   at one end of 1180px and the input it names at the other reads worse than
   the empty space it replaces, because the eye has to cross the gap to pair
   them. Capping also keeps the control column adjacent to the label it
   belongs to at every width above the stacking breakpoint. */

.set-page { max-width: 880px; }

.set-card { padding: 0; overflow: hidden; }
.set-head { padding: 16px 18px 14px; }
.set-head h2 { margin: 0; }
.set-head .sub { margin-top: 4px; }

.set-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 4px 28px;
  align-items: center;
  padding: 14px 18px;
  border-top: 1px solid var(--border);
}

/* The label column. A bare <label> carries margin-bottom for the stacked
   form layout elsewhere; here the grid owns the spacing. */
.set-label label,
.set-label .set-name {
  display: block;
  margin: 0;
  font-size: 15.5px;
  font-weight: 600;
  color: var(--text);
}
.set-label .hint { margin-top: 3px; font-size: 14.5px; }

.set-control { min-width: 0; }
.set-control-end { display: flex; justify-content: flex-end; }

/* The unit reads after the box, so "Round paid time up to [5] minutes" is a
   sentence. It used to be a number over a separate "1-60 minutes" hint,
   which asked the reader to assemble it themselves. The range moved onto
   the input as min/max, where the browser enforces it. */
.set-input { display: flex; align-items: center; gap: 10px; }
/* Two selectors deep on purpose. `input[type="number"] { width: 100% }` in
   the base form block outranks a lone class, so .set-num alone left the
   number filling its column and pushed the unit past the card's edge, where
   overflow:hidden cut it to "18 d". */
.set-input .set-num { width: 104px; flex: 0 0 auto; }
.set-unit {
  flex: 0 0 auto;
  font-size: 15px; color: var(--text-muted);
  white-space: nowrap;
}

/* A whole toggle row is the label, so the target is the row and not a 16px
   native box. Stays two columns at every width: the switch on the right of
   its own name is legible on a phone, stacked under it is not. */
.set-row-toggle {
  grid-template-columns: minmax(0, 1fr) auto;
  cursor: pointer;
}
.set-row-toggle:hover { background: var(--surface-2); }

.set-switch {
  appearance: none; -webkit-appearance: none;
  position: relative; flex: 0 0 auto;
  width: 46px; height: 28px; margin: 0; padding: 0;
  border: 1px solid var(--border-strong); border-radius: 999px;
  background: var(--surface-3);
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease;
}
.set-switch::after {
  content: "";
  position: absolute; top: 2px; left: 2px;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(20, 20, 18, .28);
  transition: transform .15s ease;
}
.set-switch:checked { background: var(--accent); border-color: var(--accent); }
.set-switch:checked::after { transform: translateX(18px); }

/* The row is the tap target, so the ring belongs on the row rather than on a
   46px control inside it. */
.set-switch:focus-visible { outline: none; }
.set-row-toggle:has(.set-switch:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* A note that belongs to the whole card rather than to one row. */
.set-foot-note {
  margin: 0;
  padding: 13px 18px 15px;
  border-top: 1px solid var(--border);
  font-size: 14.5px;
  color: var(--text-muted);
}

.set-foot { padding: 14px 18px 16px; border-top: 1px solid var(--border); }
.set-foot form { margin: 0; }

/* Sticky, because Save belongs to the cards above it and not to the Owners
   card below, which saves itself. Loose between the two it was both
   ambiguous and off screen on a long form. */
.set-save {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 18px;
  margin-bottom: var(--gap);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.set-save-note {
  margin: 0 auto 0 0;
  font-size: 14.5px;
  color: var(--text-muted);
}

/* The owners table: three short columns had been stretched across the full
   card, putting a date a hand-span from the name it belongs to. */
.set-table th:last-child,
.set-table td:last-child { width: 1%; white-space: nowrap; text-align: right; }
.set-table th:nth-child(2),
.set-table td:nth-child(2) { width: 34%; }

@media screen and (max-width: 720px) {
  /* The two column widths above size the desktop table. Below this width
     every table in the app becomes one card per row, and a 1% column that
     may not wrap is a 28px box -- which is how "17 Sep 2026, 15:19" came to
     hang out over the edge of the Owners card. Widths off, wrapping back on. */
  .set-table td:nth-child(2),
  .set-table td:last-child { width: 100%; white-space: normal; text-align: right; }

  .set-row { grid-template-columns: 1fr; gap: 8px; }
  .set-row .set-control-end { justify-content: flex-start; }
  .set-row-toggle { grid-template-columns: minmax(0, 1fr) auto; gap: 14px; }
  .set-save { flex-direction: column-reverse; align-items: stretch; gap: 8px; }
  .set-save > button { width: 100%; }
  .set-save-note { margin: 0; text-align: center; }
}

@media (prefers-reduced-motion: reduce) {
  .set-switch, .set-switch::after { transition: none; }
}


/* ------------------------------------------------ the board, on paper
   The printed presence board. Deliberately almost nothing of its own: the
   objection to the old day report was that it was a second shape for the
   same day, and answering that with a third shape would be worse than
   leaving it alone. The board below is the same partial the screen draws.

   These repeat report.css's sheet head rather than importing it, on purpose.
   Linking that stylesheet would make this page depend on the very file that
   may be deleted with the day report, and a print view that breaks when an
   unrelated page is removed is a trap for whoever removes it. */

.wp-sheet-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; margin-bottom: 20px;
  padding-bottom: 12px; border-bottom: 2px solid var(--text);
}
.wp-sheet-head h1 { margin: 0; }
.wp-sheet-head .sub { margin: 2px 0 0; }

.wp-sheet-mark {
  display: inline-flex; align-items: center; gap: 8px;
  font-weight: 700; white-space: nowrap;
}
.wp-sheet-mark img { border-radius: 6px; display: block; }

/* On a page with the app's sidebar, these exist for the paper alone. The
   print shell has no sidebar and shows them on screen too, as its preview. */
@media screen { .shell .print-only { display: none; } }

.wp-taken {
  margin: 22px 0 0;
  font-size: 12.5px;
  color: var(--text-faint);
}
