/* Snacko Express.
   Palette taken from the existing kiosk artwork and squadron branding:
   brick #8b3229, deep brown #2a110a, amber #fdac19. No framework — the markup
   is ours, so utility classes would mostly be indirection. */

:root {
  --brand:        #8b3229;
  --brand-dark:   #6d2720;
  --brand-deep:   #2a110a;
  --accent:       #fdac19;
  --accent-soft:  #fdd17f;

  --ink:          #1b1310;
  --ink-soft:     #6b5d57;
  --ink-faint:    #9a8d87;
  --line:         #e7e0dc;
  --surface:      #ffffff;
  --canvas:       #faf7f5;

  --ok:    #1b7f4d;
  --warn:  #a86112;
  --bad:   #b4231c;
  --ok-bg:   #e8f5ee;
  --warn-bg: #fdf3e3;
  --bad-bg:  #fcecea;

  --radius: 10px;
  --shadow: 0 1px 2px rgba(42,17,10,.06), 0 4px 12px rgba(42,17,10,.05);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

/* The hidden attribute, made to actually hide things.
 *
 * It works through the browser's own stylesheet, which ANY author rule with a
 * display beats -- so `.btn { display: inline-flex }` quietly defeated
 * `<button hidden>`, and the Stop buttons on the products sheet sat there in
 * plain sight with nothing running. The pattern had already been patched one
 * element at a time in six places across app.css and pos.css
 * (.modal-backdrop, .tab-panel, .pos-modal, .osk, .pos-item, .bal-qr), which
 * is the shape of a rule that should have been global from the start.
 *
 * !important because the point is to outrank whatever display the element's
 * own class sets; that is the whole job. Anything that needs to be laid out
 * should not be carrying `hidden`.
 */
[hidden] { display: none !important; }

body {
  background: var(--canvas);
  color: var(--ink);
  font: 15px/1.55 -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--brand); }
h1, h2, h3 { color: var(--brand-deep); margin: 0 0 .4em; line-height: 1.25; }
h1 { font-size: 1.6rem; font-weight: 650; }
h2 { font-size: 1.05rem; font-weight: 650; }
h3 { font-size: .95rem; font-weight: 600; }

/* ── Sign in ─────────────────────────────────────────────────────────── */
.auth-page {
  min-height: 100dvh;
  display: grid; place-items: center;
  padding: 24px;
  background: linear-gradient(160deg, var(--brand-deep), var(--brand) 140%);
}
.auth-card {
  width: 100%; max-width: 380px;
  background: var(--surface);
  border-radius: 14px;
  box-shadow: 0 18px 50px rgba(0,0,0,.28);
  padding: 32px 28px;
}
.auth-brand { text-align: center; margin-bottom: 22px; }
/* The wordmark, at its natural proportions. width/height are on the <img>
   itself so the card does not jump as it loads, and the sizing here is a
   max-width rather than a width so it shrinks on a narrow phone instead of
   pushing the card sideways. */
.auth-logo { display: block; margin: 0 auto; width: 100%; max-width: 240px; height: auto; }
.auth-brand h1 { font-size: 1.25rem; margin-top: 10px; }
.auth-note {
  margin: 18px 0 0; text-align: center;
  font-size: .8rem; color: var(--ink-faint);
}

/* ── Forms ───────────────────────────────────────────────────────────── */
.stack { display: grid; gap: 14px; }
label > span {
  display: block; margin-bottom: 5px;
  font-size: .78rem; font-weight: 600; color: var(--ink-soft);
}
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=date], input[type=search], select, textarea {
  width: 100%; padding: 9px 11px;
  border: 1px solid var(--line); border-radius: 8px;
  font: inherit; color: var(--ink); background: #fff;
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}
.hint { font-size: .78rem; color: var(--ink-faint); margin: 5px 0 0; }

/* The uploaded kiosk help picture, previewed on the Kiosks page. Bounded so a
   tall screenshot does not push the form it belongs to off the page. */
.help-preview {
  border: 1px solid var(--line); border-radius: 8px; padding: 8px;
  background: var(--canvas); display: inline-block; max-width: 100%;
}
.help-preview img {
  display: block; max-width: 420px; max-height: 300px; width: auto; height: auto;
  border-radius: 4px;
}

/* ── Reveal a password ───────────────────────────────────────────────────
   The eye sits inside the field rather than beside it, so a row of password
   boxes still lines up with the text boxes above and below it. The wrapper is
   added by password-eye.js; the markup itself is a plain input. */
.pw-field { position: relative; display: block; }
/* Room for the button, and the same on both types: the field becomes
   type="text" while it is revealed and must not resize as it does. */
.pw-field > input[type=password],
.pw-field > input[type=text] { padding-right: 42px; }

.pw-eye {
  position: absolute; top: 0; right: 0; height: 100%; width: 40px;
  display: flex; align-items: center; justify-content: center;
  padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--ink-faint); border-radius: 0 8px 8px 0;
}
.pw-eye:hover  { color: var(--ink-soft); }
.pw-eye:active { color: var(--brand); }
/* Pressed means the password is on screen, which is worth being obvious. */
.pw-eye[aria-pressed="true"] { color: var(--brand); }


/* ── Buttons ─────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 9px 16px; border: 1px solid transparent; border-radius: 8px;
  font: inherit; font-weight: 600; cursor: pointer; text-decoration: none;
  transition: filter .12s ease, background .12s ease;
}
.btn:disabled { opacity: .55; cursor: default; }
.btn-primary { background: var(--brand); color: #fff; }
.btn-primary:hover:not(:disabled) { filter: brightness(1.1); }
.btn-quiet { background: #fff; border-color: var(--line); color: var(--ink); }
.btn-quiet:hover:not(:disabled) { background: var(--canvas); }
/* Confirm and destroy stay green/red: a brand-coloured "Accept payment"
   reads worse than one that is unmistakably the positive action. */
.btn-go     { background: var(--ok);  color: #fff; }
.btn-danger { background: var(--bad); color: #fff; }
.btn-block  { width: 100%; }
.btn-sm     { padding: 6px 11px; font-size: .85rem; }

/* ── Shell ───────────────────────────────────────────────────────────── */
.shell { display: grid; grid-template-columns: 232px 1fr; min-height: 100dvh; }
.sidebar {
  background: var(--brand); color: #fff;
  padding: 18px 0; display: flex; flex-direction: column;
}
/* The brand block at the top of the sidebar.
   A wordmark rather than the name in text: it is the same words, drawn. The
   name only appears underneath when a unit has set a different one, which is
   why this is a column. */
.sidebar-brand {
  padding: 2px 18px 16px; font-weight: 700; font-size: 1.05rem;
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
}
.brand-logo { width: 100%; max-width: 168px; height: auto; display: block; }
.brand-name { font-size: .82rem; font-weight: 600; color: rgba(255,255,255,.82); }
.sidebar a {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 18px; color: rgba(255,255,255,.88);
  text-decoration: none; font-size: .9rem; border-left: 3px solid transparent;
}
.sidebar a:hover { background: rgba(0,0,0,.14); color: #fff; }
.sidebar a.active {
  background: rgba(0,0,0,.2); color: #fff;
  border-left-color: var(--accent); font-weight: 600;
}
.sidebar-foot { margin-top: auto; padding: 14px 18px 0; font-size: .8rem; }
.sidebar-foot a { padding: 4px 0; border: 0; color: rgba(255,255,255,.7); }

.main { padding: 26px 30px 60px; max-width: 1120px; }
.page-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 22px;
}
.page-head p { margin: 4px 0 0; color: var(--ink-soft); font-size: .88rem; }

/* Sub-navigation within one sidebar area, for a subject that needs several
   pages but is one thing to the person using it. Links, not buttons: each tab
   is a real page with its own URL, so it can be bookmarked and reloaded. */
.subtabs {
  display: flex; gap: 2px; flex-wrap: wrap;
  border-bottom: 1px solid var(--line); margin-bottom: 20px;
}
.subtab {
  padding: 9px 15px; text-decoration: none; white-space: nowrap;
  color: var(--ink-soft); font-size: .88rem;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.subtab:hover { color: var(--ink); }
.subtab.is-active { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }

/* The accounting group is a wide set of peers rather than a short list
   hanging off one page, so it reads better centred. Still wraps to however
   many rows it needs on a narrow screen -- the tabs are navigation, and
   hiding some of them behind a scroll would hide whole pages. */
.subtabs-center { justify-content: center; }

/* Divides a page into named groups of cards, where a .card-head would put the
   label inside one card rather than over all of them. */
.section-head {
  margin: 4px 0 10px; padding-bottom: 6px;
  border-bottom: 1px solid var(--line); color: var(--ink-soft);
  font-size: .82rem; font-weight: 650;
  letter-spacing: .06em; text-transform: uppercase;
}

/* ── Cards, tables, tiles ────────────────────────────────────────────── */
.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: 20px;
}
.card-head {
  padding: 12px 16px; border-bottom: 1px solid var(--line);
  font-weight: 650; font-size: .9rem;
}
.card-body { padding: 16px; }
.empty { padding: 40px 16px; text-align: center; color: var(--ink-faint); font-size: .9rem; }

.grid { display: grid; gap: 16px; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

/* Six figures that have to be read as ONE row.
   The cash position is a sentence -- total, less what is committed, leaves
   available, less the next restock, leaves this, and we owe that -- and a row
   that wrapped after the fourth tile broke the sentence in half and read as
   two unrelated groups.
   An explicit six columns rather than auto-fit, because auto-fit's job is to
   decide how many fit and the whole point here is that the answer is six. The
   tiles inside are tightened to pay for it: at this width a 15px pad and a
   1.6rem figure put "$1,234.56" over the edge of its box.
   Below 1180px six no longer fit honestly, so it steps to 3 x 2 and then
   2 x 3 -- the pairs still sit beside the figure they relate to. */
.grid-6 { gap: 10px; grid-template-columns: repeat(6, minmax(0, 1fr)); }
.grid-6 .tile       { padding: 12px 11px; }
.grid-6 .tile-label { font-size: .68rem; letter-spacing: .04em; }
.grid-6 .tile-value { font-size: 1.25rem; }
.grid-6 .tile-hint  { font-size: .72rem; }

@media (max-width: 1180px) { .grid-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 620px)  { .grid-6 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.tile { background: var(--surface); border: 1px solid var(--line);
        border-radius: var(--radius); padding: 15px; box-shadow: var(--shadow); }
.tile-label { font-size: .72rem; letter-spacing: .07em; text-transform: uppercase;
              color: var(--ink-soft); }
.tile-value { font-size: 1.6rem; font-weight: 650; margin-top: 3px;
              font-variant-numeric: tabular-nums; }
.tile-hint  { font-size: .76rem; color: var(--ink-faint); margin-top: 3px; }

/* The same figure inside a card, where the card already supplies the surface
   and the border -- a tile in there would draw a box inside a box. Customers,
   Expenses and Automatic Fees all used these class names before anything
   defined them, so the numbers rendered as unstyled text. */
.stat       { padding: 2px 0; }
.stat-label { font-size: .72rem; letter-spacing: .07em; text-transform: uppercase;
              color: var(--ink-soft); }
.stat-value { font-size: 1.5rem; font-weight: 650; margin-top: 3px;
              font-variant-numeric: tabular-nums; }
.stat-value.small { font-size: 1rem; font-weight: 600; }

table { width: 100%; border-collapse: collapse; }
th {
  text-align: left; font-size: .74rem; text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink-soft); font-weight: 600;
  padding: 9px 16px; border-bottom: 1px solid var(--line);
}
td { padding: 11px 16px; border-bottom: 1px solid var(--line); font-size: .9rem; }
tr:last-child td { border-bottom: 0; }
.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ── Status ──────────────────────────────────────────────────────────── */
.alert { padding: 11px 14px; border-radius: 8px; font-size: .88rem; margin: 0 0 16px; }
/* An alert carrying a dismiss control needs room for it at the right. Marked
   with a class rather than selected with :has(), so the layout does not
   depend on a selector an older browser might not support -- and the gutter
   lands only on the alerts that have a button, not on the ones reporting
   something to fix, which cannot be dismissed at all. */
.alert.is-dismissable { position: relative; padding-right: 40px; }
.notice-dismiss { position: absolute; top: 4px; right: 6px; margin: 0; }
.notice-x {
  border: 0; background: none; cursor: pointer; line-height: 1;
  font-size: 1.25rem; padding: 2px 6px; border-radius: 6px;
  /* Inherits the alert's own colour, so one control works on all three tones
     rather than three rules that drift apart. Dimmed, because it is the least
     important thing in a box whose whole job is to be read. */
  color: inherit; opacity: .55;
}
.notice-x:hover, .notice-x:focus-visible { opacity: 1; background: rgba(0,0,0,.06); }
.notice-x:disabled { opacity: .3; cursor: default; }
.alert-error { background: var(--bad-bg);  color: var(--bad);  border: 1px solid #f3c9c6; }
.alert-warn  { background: var(--warn-bg); color: var(--warn); border: 1px solid #f3ddb8; }
.alert-ok    { background: var(--ok-bg);   color: var(--ok);   border: 1px solid #bfe3ce; }

.pill { display: inline-block; padding: 2px 8px; border-radius: 999px;
        font-size: .74rem; font-weight: 600; }
.pill-ok   { background: var(--ok-bg);   color: var(--ok); }
.pill-warn { background: var(--warn-bg); color: var(--warn); }
.pill-bad  { background: var(--bad-bg);  color: var(--bad); }
.pill-mute { background: #eee8e5;        color: var(--ink-soft); }

/* The reconciliation code is the thing an admin matches against a Venmo
   statement, so it is deliberately the loudest element on the row. */
.code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 700; letter-spacing: .18em;
  background: var(--accent-soft); color: var(--brand-deep);
  padding: 3px 9px; border-radius: 6px;
}
.code-xl { font-size: 2.1rem; letter-spacing: .3em; padding: 12px 18px; display: inline-block; }

.muted { color: var(--ink-soft); }
.small { font-size: .82rem; }
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Inputs default to full width for stacked forms; inside a .row they should
   size to their content instead of pushing everything onto its own line. */
.row > input, .row > select { width: auto; }
.page-head form { flex-wrap: nowrap; }
.right { margin-left: auto; }

@media (max-width: 860px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar { flex-direction: row; overflow-x: auto; padding: 10px; gap: 4px; }
  .sidebar-brand, .sidebar-foot { display: none; }
  .sidebar a { white-space: nowrap; border-left: 0; border-bottom: 3px solid transparent; }
  .sidebar a.active { border-left: 0; border-bottom-color: var(--accent); }
  .main { padding: 18px 16px 48px; }
}

/* ── Account modal ──────────────────────────────────────────────────────
   Opened from the user's name in the sidebar. */

.account-trigger {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 8px; margin: 0 0 4px -8px;
  background: transparent; border: 0; border-radius: 8px;
  color: #fff; font: inherit; font-size: .82rem; text-align: left;
  cursor: pointer;
}
.account-trigger:hover  { background: rgba(255,255,255,.10); }
.account-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.account-avatar {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  background: var(--accent); color: var(--brand-deep);
  display: grid; place-items: center;
  font-size: .68rem; font-weight: 700; letter-spacing: .02em;
}
.account-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.modal-backdrop {
  position: fixed; inset: 0; z-index: 100;
  background: rgba(42,17,10,.55);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal-backdrop[hidden] { display: none; }
body.modal-open { overflow: hidden; }

.modal {
  background: var(--surface); border-radius: var(--radius);
  box-shadow: 0 12px 40px rgba(42,17,10,.28);
  width: 100%; max-width: 560px; max-height: 90vh;
  display: flex; flex-direction: column; overflow: hidden;
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 15px 20px; border-bottom: 1px solid var(--line);
}
.modal-head h2 { margin: 0; font-size: 1.02rem; }
.modal-close {
  background: transparent; border: 0; cursor: pointer;
  font-size: 1.5rem; line-height: 1; color: var(--ink-faint); padding: 0 4px;
}
.modal-close:hover { color: var(--ink); }
.modal-body { padding: 18px 20px 22px; overflow-y: auto; }

.tabs { display: flex; gap: 2px; padding: 0 20px; border-bottom: 1px solid var(--line); }
.tab {
  background: transparent; border: 0; cursor: pointer;
  padding: 10px 14px; font: inherit; font-size: .88rem; color: var(--ink-soft);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab:hover { color: var(--ink); }
.tab.is-active { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }
.tab-panel[hidden] { display: none; }

.panel-title { margin: 0 0 4px; font-size: .82rem; text-transform: uppercase;
               letter-spacing: .06em; color: var(--ink-soft); }
.rule { border: 0; border-top: 1px solid var(--line); margin: 22px 0; }

@media (max-width: 860px) {
  /* The sidebar collapses on small screens, so surface the trigger there. */
  .sidebar-foot { display: block; }
  .modal { max-width: none; }
}

/* Switch between the administrator and shopper views. Only rendered for an
   account that has both, so it never appears as a dead end. */
.view-switch {
  display: block; margin: 10px 0 0; padding: 9px 12px;
  border: 1px solid rgba(255,255,255,.28); border-radius: 8px;
  color: #fff; text-decoration: none; font-size: .82rem; font-weight: 600;
  text-align: center;
}
.view-switch:hover { background: rgba(255,255,255,.12); }

/* ── Radar readout ──────────────────────────────────────────────────────
   Sits under the threshold fields it exists to inform, set apart so it reads
   as evidence rather than as another thing to fill in. */

.radar {
  margin-top: 16px; padding: 14px;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--canvas);
}
.radar-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; }
.radar-head .muted { margin-left: auto; }
.radar-figures {
  display: grid; gap: 12px; margin: 12px 0 0;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.radar-figure { display: flex; flex-direction: column; gap: 2px; }
.radar-value {
  font-size: 1.45rem; font-weight: 700; font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.radar .hint { margin-top: 10px; }
.radar code { font-size: .78rem; word-break: break-all; }
/* A kiosk measuring against thresholds nobody on this page can see. */
.radar-stale { color: var(--bad); }

/* ── Kiosk lighting rows ────────────────────────────────────────────────
   One row per presence state: what it is, the mode, and the two colours.
   Wraps to a stack on a narrow screen rather than crushing the selector. */

.led-row {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 16px;
  padding: 14px 0; border-bottom: 1px solid var(--line);
}
.led-what   { flex: 1 1 220px; min-width: 200px; }
.led-mode   { flex: 0 1 230px; display: flex; flex-direction: column; gap: 4px; }
.led-mode select { width: 100%; }
.led-colour { display: flex; flex-direction: column; gap: 4px; }
.led-colour input[type="color"] {
  width: 58px; height: 40px; padding: 2px; cursor: pointer;
}

/* A mode that generates its own colours does not read these. Dimmed rather
   than hidden, so the row does not reflow every time the mode changes and the
   stored colour is still visible. */
.led-colour.is-unused { opacity: .35; }
.led-colour.is-unused input[type="color"] { cursor: not-allowed; }

/* Speed sits last in the row and takes the slack, because a slider needs
   travel to be worth dragging. */
.led-speed { flex: 1 1 200px; min-width: 180px; display: flex; flex-direction: column; gap: 4px; }
.led-speed input[type="range"] { width: 100%; margin: 0; cursor: pointer; }
.led-speed output { font-variant-numeric: tabular-nums; }
.led-speed-scale {
  display: flex; justify-content: space-between;
  font-size: .72rem; font-weight: 400; margin: 0;
}
/* Solid has nothing to pace; the firmware never reads the number. */
.led-speed.is-unused { opacity: .35; }
.led-speed.is-unused input[type="range"] { cursor: not-allowed; }

/* ── The products spreadsheet ──────────────────────────────────────────────
   Dense on purpose: the whole point of the page is seeing many rows at once,
   so padding is smaller than the rest of the application and the cells carry
   no borders of their own -- the grid lines come from the table. */
.sheet-scroll {
  overflow-x: auto;
  /* Tall enough to be worth scrolling inside, short enough that Save stays
     reachable without leaving the sheet. */
  max-height: 62vh; overflow-y: auto;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.sheet { width: 100%; border-collapse: collapse; font-size: .86rem; }
.sheet thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--canvas); border-bottom: 1px solid var(--line);
  padding: 7px 8px; text-align: left; font-size: .74rem;
  text-transform: uppercase; letter-spacing: .03em; color: var(--ink-soft);
  white-space: nowrap;
}
.sheet tbody td { padding: 2px 4px; border-bottom: 1px solid var(--line); }
.sheet tbody tr:hover { background: var(--canvas); }

/* The row number and the thumbnail, narrow and out of the way. A photo here
   rather than in its own column: it is context for the row, not data in it. */
.sheet-gutter {
  width: 58px; white-space: nowrap; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; font-size: .74rem;
}
.sheet-n { display: inline-block; width: 22px; text-align: right; }
.sheet-thumb {
  width: 26px; height: 26px; object-fit: contain; vertical-align: middle;
  border: 1px solid var(--line); border-radius: 4px; background: #fff;
  margin-left: 4px;
}

/* Cells look like text until touched, so the grid reads as a table rather than
   as three hundred form fields. The focus ring is what says "you are editing". */
.sheet-cell {
  width: 100%; min-width: 0; box-sizing: border-box;
  border: 1px solid transparent; border-radius: 4px; background: transparent;
  padding: 5px 6px; font: inherit; color: inherit;
}
.sheet-cell:hover { border-color: var(--line); background: var(--surface); }
.sheet-cell:focus {
  outline: none; border-color: var(--brand); background: var(--surface);
  box-shadow: 0 0 0 2px rgba(157,47,38,.15);
}
.sheet-cell.num { text-align: right; font-variant-numeric: tabular-nums; }
.sheet-cell[type="checkbox"] { width: auto; }
.sheet-cell[aria-invalid="true"] { border-color: var(--warn); }
.sheet-order { max-width: 70px; }
.sheet .sheet-type { min-width: 130px; }

/* An unsaved row is marked down its left edge: visible while scrolling past,
   and it does not move anything. */
.sheet-row.is-dirty > td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
.sheet-row.is-inactive { opacity: .6; }

/* A type that does not track quantity has no cell to fill in. The dash says
   "deliberately nothing" where an empty cell would say "not typed yet". */
.sheet-untracked { color: var(--ink-faint); cursor: help; }

/* ── Found photos, awaiting approval ────────────────────────────────────── */
.candidate-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.candidate {
  margin: 0; padding: 10px; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--surface);
  display: flex; flex-direction: column; gap: 8px;
}
/* Contain, not cover: a packet photo cropped to fill a square loses the label,
   which is the only part a reviewer is actually checking. */
.candidate img {
  width: 100%; height: 120px; object-fit: contain; background: var(--canvas);
  border-radius: 6px;
}
.candidate figcaption { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.candidate figcaption strong { font-size: .84rem; line-height: 1.25; }
.candidate-actions { display: flex; gap: 6px; margin-top: auto; }
.candidate-actions .btn { flex: 1; }
.candidate.is-busy { opacity: .5; pointer-events: none; }

/* The prompt a generated picture came from. Clamped to two lines: it is
   diagnostic -- a poor drawing is usually a poor description -- and at full
   length it would be the tallest thing in the card. The full text is in the
   title attribute. */
.candidate-prompt {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; font-style: italic; cursor: help;
}

/* ── A progress bar with a mark on it ───────────────────────────────────────
   Used on the order-request rows: how much of an order has sold, against a
   tick showing where it stops losing money.

   The tick is absolutely positioned inside the track rather than drawn as
   part of the fill, because it has to be visible on BOTH sides of the fill
   edge -- an order at 30% with break-even at 70% needs to show how far it
   still has to go, and a gradient stop would vanish under the empty part of
   the bar. It overhangs the track top and bottom so it reads as a scale mark
   rather than as a gap in the fill. */
/* Two or three stacked scales -- items, money, and the funder's repayment
   where there is one -- each with its own name and figure. A grid rather
   than three flex rows so the tracks share a left and right edge: bars that
   start at different x positions cannot be read against each other, which
   is the entire reason they are stacked. */
.gauge {
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center; gap: 5px 8px; min-width: 190px;
}
.gauge-name {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .03em;
  color: var(--ink-faint); white-space: nowrap;
}
.gauge-figure {
  font-size: .76rem; color: var(--ink-soft); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.gauge-figure strong { color: var(--ink); }
.gauge-track {
  position: relative; height: 9px; border-radius: 999px;
  background: var(--line);
}
.gauge-fill {
  height: 100%; border-radius: 999px; background: var(--ok);
  /* Hairline so a 1% fill is still a visible sliver rather than nothing. */
  min-width: 2px;
}
/* Below break-even the bar is amber: the order has not yet paid for itself,
   which is a different state from selling slowly. Judged per bar -- the
   money can be back before the stock is gone. */
.gauge-fill.is-short { background: var(--warn); }
.gauge-fill.is-none  { background: transparent; min-width: 0; }

/* Nothing to measure YET: not approved, or not linked to a kiosk product.
   Hatched rather than empty, because a flat empty bar is indistinguishable
   from "none of it has sold" -- the opposite kind of fact. The break-even
   mark is still drawn: what it would take to wash its face is exactly what
   somebody deciding whether to approve it wants. */
.gauge-track.is-projection {
  background: repeating-linear-gradient(135deg,
    var(--line) 0 4px, transparent 4px 8px);
}
.gauge-tick {
  position: absolute; top: -3px; bottom: -3px; width: 2px;
  margin-left: -1px;             /* centred on its percentage, not beside it */
  background: var(--brand-deep); border-radius: 1px;
}
/* An order with too little stock left to ever break even. The mark would sit
   off the end of the scale, so it is pinned at the end and marked as out of
   reach instead of quietly clamping. */
.gauge-tick.is-unreachable { background: var(--bad); }
.gauge-legend {
  grid-column: 1 / -1; font-size: .72rem; color: var(--ink-faint);
}
.gauge-legend + .gauge-legend { margin-top: -2px; }

/* The repayment scale reads differently from the two above it: its good end
   is a full bar, and there is no break-even on a debt -- you either owe some
   of it or you do not. Marked off with a rule so nobody reads it as a third
   measure of how the stock is doing. */
.gauge-row-debt { border-top: 1px dashed var(--line); padding-top: 6px; }

@media (max-width: 560px) {
  /* The figures drop under their bar rather than squeezing the track to
     nothing on a phone. */
  .gauge { grid-template-columns: auto 1fr; }
  .gauge-figure { grid-column: 2; }
}

/* ── Clearing data ───────────────────────────────────────────────────────────
   One row per group, with the tickbox against a name, a count and the
   consequence in plain words. The consequence is the point: "clear the
   purchases" and "clear 26 purchases, which puts every member back to owing
   nothing" are different decisions, and only the second is really on offer. */
.reset-group {
  display: grid; grid-template-columns: auto 1fr; gap: 2px 12px;
  align-items: start; padding: 12px 0; border-bottom: 1px solid var(--line);
  cursor: pointer;
}
.reset-group:hover { background: var(--canvas); }
.reset-group input { margin-top: 3px; }
.reset-count { display: block; }
.reset-detail { display: block; margin-top: 3px; max-width: 62ch; }
/* A group with nothing in it stays on the list rather than disappearing, so
   the page reads the same every time and "already empty" is an answer. */
.reset-group.is-empty { opacity: .5; cursor: default; }
.reset-group.is-empty:hover { background: none; }

/* ── Surveys ─────────────────────────────────────────────────────────────────
   The link and its code side by side, because one of them is for a phone
   across the room and the other for somebody who would rather type. */
.survey-share { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
.survey-qr { flex: 0 0 auto; line-height: 0; }
.survey-qr svg { display: block; }
.mono { font-family: ui-monospace, monospace; font-size: .85rem; }

/* One question on the answering page. A fieldset rather than a div: the
   prompt is the legend, which is what a screen reader reads before each
   choice instead of leaving them floating unlabelled. */
.survey-question {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 14px; margin: 0;
}
.survey-question legend { font-weight: 650; padding: 0 6px; }
.survey-choice {
  display: flex; align-items: center; gap: 10px; padding: 8px 4px;
  cursor: pointer;
}
.survey-choice:hover { background: var(--canvas); }
.survey-choice input { margin: 0; }

/* Written answers, listed. Plain rows -- anything that looked like a quote
   card would invite reading them as individual people's voices, which is
   the one thing this screen must not encourage. */
.survey-answers { margin: 4px 0 0; padding-left: 18px; }
.survey-answers li { margin-bottom: 4px; overflow-wrap: anywhere; }

/* ── Reconciling a statement ─────────────────────────────────────────────────
   A queue of payments that arrived with nothing in the note to say who sent
   them. Each row opens the dialog that credits it, so the row is the control
   -- it carries role="button" and a tabindex, and has to look like something
   worth clicking without becoming a wall of buttons. */
.recon-row { cursor: pointer; }
.recon-row:hover { background: var(--canvas); }
.recon-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* The facts, above the decision. An administrator choosing who a payment
   belongs to should not be reading the note from one line of a table and the
   amount from another while a dropdown covers both. */
.recon-facts {
  display: grid; grid-template-columns: auto 1fr; gap: 6px 14px;
  margin: 0 0 18px; padding-bottom: 16px; border-bottom: 1px solid var(--line);
  font-size: .88rem;
}
.recon-facts dt {
  color: var(--ink-faint); font-size: .72rem; text-transform: uppercase;
  letter-spacing: .05em; font-weight: 650; align-self: center;
}
.recon-facts dd { margin: 0; overflow-wrap: anywhere; }
.recon-facts .mono { font-family: ui-monospace, monospace; font-size: .8rem; }

@media (max-width: 560px) {
  /* The labels go above their values rather than squeezing both into a
     phone's width, where a Venmo transfer id wraps over four lines. */
  .recon-facts { grid-template-columns: 1fr; gap: 2px; }
  .recon-facts dd { margin-bottom: 8px; }
}

/* ── Event planner ───────────────────────────────────────────────────────────
   Work on the left, the year on the right, and the year never goes away:
   planning is comparative -- what else is that weekend, has the quarter
   already been spent -- so the list of everything stays in view while an
   event is being edited.

   The side pane is a fixed 320px rather than a fraction, because its content
   is a list of short rows whose ideal width does not change with the window;
   the main area takes the rest. Below 980px they stack, main area first: on a
   phone the thing being worked on comes before the index of everything. */
.planner { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 18px; }
.planner-main { min-width: 0; }
.planner-side { min-width: 0; }

@media (max-width: 980px) {
  .planner { grid-template-columns: 1fr; }
}

/* The month. Whole weeks always -- seven columns, every cell present even
   outside the month -- so the 1st sits under the right weekday name and the
   grid keeps one shape all year.

   min-width with a scroll container rather than squeezing: seven columns of a
   usable size do not fit a phone, and a calendar compressed until the day
   numbers collide is not a calendar. */
.cal {
  display: grid; grid-template-columns: repeat(7, minmax(92px, 1fr));
  gap: 2px; min-width: 660px;
}
.cal-wrap { overflow-x: auto; }
.cal-dow {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-faint); font-weight: 650; text-align: center;
  padding-bottom: 4px;
}
.cal-day {
  min-height: 86px; padding: 4px 5px 6px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 6px; display: flex; flex-direction: column; gap: 3px;
}
/* The days either side of the month are present for alignment and recessive
   on purpose: they are context, not this month. */
.cal-day.is-outside { background: var(--canvas); opacity: .6; }
.cal-day.is-today { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand); }
.cal-date { font-size: .74rem; color: var(--ink-faint); font-weight: 650; }
.cal-day.is-today .cal-date { color: var(--brand); }

/* An event in a day cell. Wrapped rather than truncated with an ellipsis: two
   lines of a real name beats one line of "Squadron holiday pa…", and the cell
   grows to fit because a calendar with an event hidden under an overflow is
   worse than a slightly uneven row. */
.cal-event {
  display: block; font-size: .72rem; line-height: 1.25;
  padding: 3px 5px; border-radius: 4px; text-decoration: none;
  color: var(--ink); background: var(--canvas);
  border-left: 3px solid var(--ink-faint);
  overflow-wrap: anywhere;
}
.cal-event:hover { background: var(--line); }
.cal-event.is-selected { box-shadow: 0 0 0 2px var(--brand); }

/* Status by the left edge rather than by filling the chip: the fill would make
   the event name sit on a coloured ground at 11px, and the name is the thing
   somebody is reading. Every status also carries its label in the tooltip and
   in the side pane's pill, so the colour is never the only channel. */
.cal-draft     { border-left-color: var(--ink-faint); }
.cal-proposed  { border-left-color: var(--warn); }
.cal-approved  { border-left-color: var(--ok); }
.cal-completed { border-left-color: var(--brand); }
.cal-rejected,
.cal-cancelled { border-left-color: var(--bad); text-decoration: line-through;
                 color: var(--ink-faint); }

.cal-legend {
  display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 14px 0 0;
  font-size: .74rem; color: var(--ink-soft);
}
.cal-legend span { display: inline-flex; align-items: center; gap: 6px; }
.cal-key { width: 3px; height: 13px; border-radius: 2px; display: inline-block;
           background: var(--ink-faint); }
.cal-key.cal-proposed  { background: var(--warn); }
.cal-key.cal-approved  { background: var(--ok); }
.cal-key.cal-completed { background: var(--brand); }
.cal-key.cal-rejected,
.cal-key.cal-cancelled { background: var(--bad); }

/* One group of things an event needs -- the location, the equipment -- with
   its own + Add beside the heading. The button lives on the heading rather
   than in one shared form at the bottom, so "+ Add" next to Materials has
   already said what is being added and the form opens knowing its own kind.
   Nobody picks it from a list and nobody adds a tent under Location. */
.req-group + .req-group { margin-top: 22px; }
.req-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding-bottom: 6px; margin-bottom: 8px; border-bottom: 1px solid var(--line);
}
.req-title {
  margin: 0; font-size: .82rem; font-weight: 650;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft);
}
/* Pushed to the far right, so every group's button sits on one line down the
   page and the eye finds the next one without hunting. */
.req-add { margin-left: auto; }
/* The form opens inside the group it belongs to. Inset so it reads as part
   of that group rather than as the start of the next one. */
.req-form {
  margin-top: 12px; padding: 14px;
  background: var(--canvas); border: 1px solid var(--line); border-radius: var(--radius);
}

/* The funding figure. Big, because it is the number the whole screen exists
   to produce, and read-only because it is not typed -- it adds itself up
   from the list above it. */
.funding-total { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.funding-figure {
  font-size: 2rem; font-weight: 700; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.funding-total .muted { flex: 1 1 220px; }

/* The year list in the side pane. */
.year-month {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-faint); font-weight: 650;
  margin: 14px 0 4px; padding-bottom: 3px; border-bottom: 1px solid var(--line);
}
.year-row {
  display: grid; grid-template-columns: 30px 1fr; gap: 8px;
  padding: 7px 4px; border-radius: 6px; text-decoration: none; color: var(--ink);
}
.year-row:hover { background: var(--canvas); }
.year-row.is-selected { background: var(--canvas); box-shadow: inset 0 0 0 1px var(--brand); }
.year-day {
  font-size: 1.05rem; font-weight: 700; color: var(--ink-soft);
  text-align: right; font-variant-numeric: tabular-nums;
}
.year-what { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.year-what strong { font-size: .85rem; font-weight: 650; overflow-wrap: anywhere; }

/* ── Cash flow ───────────────────────────────────────────────────────────────
   Six money figures of three different KINDS, read as one picture.

   Colour carries the DIRECTION of the money and nothing else:

     grey  -- held. A balance, true today, which does not move when the page's
              date range changes.
     blue  -- money coming toward the squadron.
     red   -- money going away from it, hatched where it has not gone yet.

   Blue and red rather than the green and red used elsewhere in this
   application, because red against green is precisely the pair a colour-blind
   reader cannot separate, and which way the money went is this chart's entire
   job. The two hues were validated against the card surface: worst-pair
   separation 26.8 (CVD-simulated, OKLab x100, 8 is the floor) and both clear
   3:1 against white. The grey is the neutral of the pair, not a third series,
   which is why it is allowed to be grey.

   Every bar is labelled with its own name and its own figure, so identity
   never rests on colour, and the lengths share ONE ruler across all three
   groups -- two bars of the same length are the same amount of money wherever
   they sit. The groups are deliberately not summed into a single stacked bar:
   adding a balance to a flow produces a number that means nothing.

   Same grid as .gauge above for the same reason: the tracks share a left and a
   right edge, and bars that start at different x positions cannot be compared. */
.flow {
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center; gap: 7px 10px;
}
.flow-group + .flow-group { margin-top: 14px; }
.flow-group-head {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-soft); font-weight: 650; margin-bottom: 6px;
}
.flow-group-head .muted { text-transform: none; letter-spacing: 0; font-weight: 400; }
.flow-name  { font-size: .8rem; color: var(--ink-soft); }
.flow-figure {
  font-size: .82rem; font-weight: 650; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* The track is the ruler, not a container to be filled: it stays visible at
   its full width so a short bar reads as "little of the same scale" rather
   than as a bar of unknown length. */
.flow-track {
  position: relative; height: 18px; border-radius: 3px;
  background: var(--canvas); box-shadow: inset 0 0 0 1px var(--line);
}
/* Square at the baseline, 4px rounded at the data end: the end that carries
   the value is the end that gets the corner. min-width keeps a tiny-but-real
   figure visible instead of vanishing into the track. */
.flow-bar {
  position: absolute; inset: 0 auto 0 0; min-width: 3px;
  border-radius: 0 4px 4px 0;
}
.flow-held { background: #6b5d57; }
.flow-in   { background: #2a78d6; }
.flow-out  { background: #b4231c; }
/* Money that has not actually left yet. Hatched rather than given a fourth
   hue -- a new hue would claim it is a different kind of money, when it is the
   same money at an earlier moment. The same idiom as .gauge-track.is-projection
   above, which already means "not real yet" on the order screens. */
.flow-owed {
  background: repeating-linear-gradient(135deg,
    #b4231c 0 6px, #d9736e 6px 12px);
}
.flow-legend {
  display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 14px;
  font-size: .76rem; color: var(--ink-soft);
}
.flow-legend span { display: inline-flex; align-items: center; gap: 6px; }
.flow-key { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
.flow-key.flow-held { background: #6b5d57; }
.flow-key.flow-in   { background: #2a78d6; }
.flow-key.flow-out  { background: #b4231c; }
.flow-key.flow-owed {
  background: repeating-linear-gradient(135deg,
    #b4231c 0 4px, #d9736e 4px 8px);
}

@media (max-width: 560px) {
  /* The figure drops under its bar rather than squeezing the track to nothing,
     exactly as the gauges do. */
  .flow { grid-template-columns: auto 1fr; }
  .flow-figure { grid-column: 2; }
}

/* ── Percent remaining ──────────────────────────────────────────────────────
   A ring beside a product, showing how much of it is left against the level
   it was last filled to. One conic gradient and a punched-out middle; no
   image and no script, so it works in a table of three hundred rows.

   --pct carries a percentage from the markup. --ring is the colour, set by
   the state class, so the ring and the pill beside it can never disagree. */
.ring {
  --pct: 0%;
  --ring: var(--ok);
  width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%;
  background: conic-gradient(var(--ring) var(--pct), var(--line) 0);
  display: inline-grid; place-items: center; vertical-align: -4px;
}
.ring::after {
  content: ''; width: 11px; height: 11px; border-radius: 50%;
  background: var(--surface);
}
.ring.is-low  { --ring: var(--warn); }
.ring.is-out  { --ring: var(--bad); }
/* No baseline to measure against: a dashed outline rather than an empty ring,
   which would read as "none left". */
.ring.is-unknown {
  background: none; border: 1px dashed var(--line);
  width: 18px; height: 18px; flex-basis: 18px;
}
.ring.is-unknown::after { display: none; }
.stock-meter {
  display: inline-flex; align-items: center; gap: 6px;
  font-variant-numeric: tabular-nums;
}

/* ── Printing a list somebody carries round a shop ──────────────────────────
   The sidebar, the forms and the buttons are furniture for working on screen;
   on paper they are a wasted first page. Anything marked no-print goes, and
   the shopping list gets to use the full width. */
@media print {
  .sidebar, .subtabs, .page-head form, .no-print { display: none !important; }
  .shell, .main { display: block; padding: 0; background: #fff; }
  .card {
    box-shadow: none; border: 0; break-inside: avoid; margin: 0 0 10px;
  }
  .card-head { border-bottom: 1px solid #000; }
  a { text-decoration: none; color: inherit; }
  /* Scroll containers have no meaning on paper and would clip the table. */
  [style*="overflow-x"], .sheet-scroll { overflow: visible !important; max-height: none !important; }
}

/* ── One order, one row ─────────────────────────────────────────────────────
   The order-request rows: what it is, how it is selling, who is owed, and
   what can be done about it. A grid rather than a table because each row
   carries forms that open underneath it, and a form inside a table cell is
   stuck in one column's width. */
.order-row {
  display: grid; gap: 12px 20px; align-items: start;
  grid-template-columns: minmax(170px, 1fr) minmax(210px, 1.5fr) minmax(160px, 1fr);
}
.order-row > div { min-width: 0; }
@media (max-width: 760px) {
  .order-row { grid-template-columns: 1fr; }
}
.row-label {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .03em;
  color: var(--ink-faint); margin-bottom: 4px;
}

/* The controls sit under the row rather than in a fourth column, because
   each one opens a form: a form inside a grid cell is stuck in that cell's
   width, and a repayment form 150px wide is not usable. The strip stays with
   its own order because it is inside the same card. */
.row-actions {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
}
/* Closed, each control is just its button and sits beside the others.
   Opened, it claims a whole line so the form inside it has room -- and the
   remaining buttons wrap below rather than being squeezed. */
.row-actions > details { flex: 0 0 auto; }
.row-actions > details[open] { flex: 1 0 100%; }

/* A button that opens a panel. The disclosure triangle is removed: the
   element already looks like a button, and a button with a triangle on it
   reads as broken rather than as expandable. */
summary.btn { list-style: none; cursor: pointer; }
summary.btn::-webkit-details-marker { display: none; }
summary.btn::marker { content: ''; }
.row-panel {
  margin-top: 10px; padding: 12px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--canvas);
}

/* Month-by-month and year-by-year bars.
   Two tracks per row rather than one stacked bar: received and spent are two
   measures of the same month, and stacking them would draw a total that
   means nothing. Both are measured against one ruler, so two bars of the
   same length are the same amount of money wherever they sit. */
.months { display: flex; flex-direction: column; gap: 8px; }
.month-row {
  display: grid;
  grid-template-columns: 110px minmax(0, 1fr) 110px;
  align-items: center;
  gap: 12px;
}
.month-name { font-size: .8rem; color: var(--ink-soft); }
.month-bars { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.month-track {
  display: block;
  background: var(--canvas);
  border-radius: 3px;
  height: 11px;
  overflow: hidden;
}
.month-figure {
  text-align: right;
  font-size: .82rem;
  font-variant-numeric: tabular-nums;
}
/* A year that has not finished, drawn so it cannot be read as a short one. */
.flow-bar.is-partial {
  background-image: repeating-linear-gradient(135deg,
    rgba(255,255,255,.55) 0 3px, transparent 3px 7px);
}

@media (max-width: 640px) {
  .month-row { grid-template-columns: 84px minmax(0, 1fr) 88px; gap: 8px; }
  .month-name, .month-figure { font-size: .75rem; }
}
