/* CrowdLatch console styles.
   One stylesheet, no CDN, no build step - same policy as the vendored htmx.
   Direction: an operations console. Every page is a dense table of state, money and time
   that someone scans and then acts on one row of. Density and alignment beat whitespace.

   The chip vocabulary at the bottom is the load-bearing idea: lifecycle position is encoded
   as colour, identically on every surface, so "rejected" is found by hue in an 11-column
   table rather than by reading. States are matched on [data-state], so this file - not a
   Python mapping - is where the vocabulary lives. */

:root {
  /* Warm near-black, after the MTurk worker console the owner supplied as reference: a dark
     neutral ground, light neutral body copy, and amber carrying every action and section title.
     Burlywood / goldenrod / darkgoldenrod are the owner's three colours (2026-08-28); goldenrod
     takes MTurk's amber role, burlywood takes its section headings.
     Measured, not eyeballed: body text 13.8:1 on a row, burlywood heading 10.1:1, goldenrod link
     7.8:1, text on a goldenrod fill 8.4:1. Every pair clears WCAG AA and most clear AAA; the
     worst is 5.67:1 (table-head captions). Re-measure before changing a token - the checker
     parses these declarations, so it cannot report a stale palette. */
  color-scheme: dark;  /* native selects, checkboxes, file inputs and scrollbars follow */

  --deck:   #14120f;   /* page ground - near-black, warmed */
  --paper:  #1c1a15;   /* surfaces, table body */
  --stripe: #211e18;   /* alternate table row */
  --raise:  #2b271f;   /* table heads and row hover - one step UP from a surface */
  --hull:   #37322a;   /* rules and borders */

  /* Body copy is light and neutral. Burlywood moved OFF the body text and onto headings, where
     it does the job the amber section titles do on MTurk - present, but not what you read. */
  --ink:    #e9e5dc;   /* primary text */
  --ink-hi: #faf7f1;   /* figures and strong values */
  --slate:  #a89f8e;   /* secondary text, labels, table-head captions */
  --gold-soft: #deb887;    /* burlywood - page headings and the wordmark */
  --beacon: #daa520;       /* goldenrod - links, focus, primary action */
  --beacon-hi: #f0c245;    /* lifted goldenrod - link hover */
  --beacon-deep: #b8860b;  /* darkgoldenrod - filled-button hover and active */
  --on-beacon: #14120f;    /* text sitting ON a goldenrod fill */

  /* Lifecycle tones: a warm-leaning text colour over a low wash of the same hue. Green/amber/red
     still mean what they meant; only the luminance flipped. */
  --ok:    #9ed48a;  --ok-wash:   #1e2a1a;
  --hold:  #9dbcd6;  --hold-wash: #1a232c;
  --wait:  #b5aa97;  --wait-wash: #262219;
  --warn:  #e8a355;  --warn-wash: #2e2211;
  --stop:  #e89184;  --stop-wash: #301c19;

  --sans: "Segoe UI Variable Text", -apple-system, BlinkMacSystemFont, "Segoe UI",
          Roboto, "Helvetica Neue", Arial, sans-serif;
  --display: "Segoe UI Variable Display", "Segoe UI Variable Text", -apple-system,
             BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono: "Cascadia Mono", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;

  --radius: 6px;
  --shell: 1120px;
}

/* --- base ---------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--deck);
  color: var(--ink);
  font: 400 14px/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--beacon); text-decoration-color: color-mix(in srgb, var(--beacon) 35%, transparent); text-underline-offset: 2px; }
/* On a dim ground a link has to brighten on hover, not deepen - darkgoldenrod would make it
   recede. It keeps its job on a FILLED goldenrod button, where receding reads as pressed. */
a:hover { color: var(--beacon-hi); text-decoration-color: currentColor; }

:focus-visible {
  outline: 2px solid var(--beacon);
  outline-offset: 2px;
  border-radius: 3px;
}

code, kbd, samp, pre { font-family: var(--mono); font-size: 12.5px; }
code { color: var(--slate); overflow-wrap: anywhere; }

hr { border: 0; border-top: 1px solid var(--hull); margin: 1.5rem 0; }

/* --- top bar ------------------------------------------------------------------------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--paper);
  border-bottom: 1px solid var(--hull);
}

.bar {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 1.25rem;
  min-height: 52px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .5rem .875rem;
}

.brand {
  font-family: var(--display);
  color: var(--gold-soft);
  font-size: 15px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.topbar nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .125rem;
  margin-left: .25rem;
}

.topbar nav a {
  display: inline-block;
  padding: .3rem .55rem;
  border-radius: 4px;
  color: var(--slate);
  text-decoration: none;
  white-space: nowrap;
}
.topbar nav a:hover { background: var(--raise); color: var(--ink); }

/* Active item. The <a> itself is never given a class - several tests assert nav anchors as
   exact strings - so the marker goes on a wrapping span. */
.topbar nav .here a { color: var(--beacon); font-weight: 600; }

.who {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: .625rem;
  color: var(--slate);
  font-size: 13px;
}
.who strong { color: var(--ink-hi); font-weight: 600; }

/* --- page ---------------------------------------------------------------------------- */

main {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 1.75rem 1.25rem 4rem;
}

h1 {
  font-family: var(--display);
  color: var(--gold-soft);
  font-size: 22px;
  font-weight: 650;
  letter-spacing: -0.012em;
  margin: 0 0 1.125rem;
}
h1 small { font-family: var(--sans); font-size: 12px; font-weight: 400; color: var(--slate); letter-spacing: 0; }

h2 {
  font-family: var(--display);
  font-size: 13px;
  font-weight: 650;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--slate);
  margin: 2rem 0 .625rem;
}
h2 small { text-transform: none; letter-spacing: 0; font-weight: 400; }

p { margin: 0 0 .875rem; }
small { font-size: 12px; color: var(--slate); }

/* A note line: the "Claim result: ...", "no longer open", ramp and duplicate messages. Every one
   of them is already written as a bare <p><em> in the templates, so no class is needed. */
main > p > em {
  font-style: normal;
  display: inline-block;
  background: var(--warn-wash);
  color: var(--warn);
  padding: .3rem .625rem;
  border-radius: 4px;
  font-size: 13px;
}

.error { color: var(--stop); }
.error em, .error strong { font-style: normal; }
.ok { color: var(--ok); }

/* --- surfaces ------------------------------------------------------------------------ */

/* A wide table scrolls inside its own box rather than taking the page sideways with it.
   This must NOT be scoped to the narrow media query. `main` is capped at `--shell` (820px),
   so a table wider than its column overflows the DOCUMENT at every viewport where the shell
   is capped but the window has not yet grown past the table - measured live 2026-08-31:
   /publisher took documentElement.scrollWidth past clientWidth at every width from 821 to
   847, and looked correct only from 848 up because the table happened to fit by then.
   Hoisting it costs nothing on desktop: `overflow-x: auto` makes this a block formatting
   context, so the table's 20px bottom margin stops collapsing through and is contained
   instead - body height measured identical before and after on all 5 pages that have one. */
.scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--paper);
  border: 1px solid var(--hull);
  border-radius: var(--radius);
  overflow: hidden;
  margin: 0 0 1.25rem;
  font-size: 13px;
  /* Every table here is money, counts or clock times. Lining figures everywhere means a column
     of numbers stacks even when the cell also carries words. */
  font-variant-numeric: tabular-nums;
}

th, td {
  text-align: left;
  padding: .38rem .7rem;
  border-bottom: 1px solid var(--hull);
  vertical-align: top;
}

/* The character move: table headers as small, wide-tracked utility labels rather than
   bold body text. Reads as an instrument panel, not a form. */
thead th {
  font-size: 10.5px;
  font-weight: 650;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--slate);
  background: var(--raise);
  border-bottom: 1px solid var(--hull);
  white-space: nowrap;
  vertical-align: bottom;
}
/* A sortable header is a control, and at rest it was pixel-identical to the dead headers beside
   it - same --slate on the same --raise, no decoration - so the only way to learn which of the
   seven columns sort was to sweep the mouse along them. The dotted rule under the label is the
   resting mark; the solid --hull line below belongs to the table, not to this control. Dotted
   rather than solid because six of seven headers carry it, and a solid row of them reads as a
   second border. The colour is spelled out on both states so affordance-contrast.py can parse
   it - currentColor would inherit correctly and measure as nothing. */
thead th a { color: var(--slate); text-decoration: none; border-bottom: 1px dotted var(--slate); }
thead th a:hover { color: var(--beacon); border-bottom-color: var(--beacon); }

tbody tr:nth-child(even) td, tbody tr:nth-child(even) th { background: var(--stripe); }
tbody tr:last-child td, tbody tr:last-child th { border-bottom: 0; }
tbody tr:hover td, tbody tr:hover th { background: var(--raise); }

/* Key/value tables (the dashboard summary) use a leading <th> per row. Without a width it takes
   half the shell and strands the value on the far side of the row. */
tbody th {
  width: 15rem;
  font-size: 13px;
  font-weight: 500;
  color: var(--slate);
  white-space: nowrap;
}

/* Empty states ("No batches yet.", "Queue empty.") are the only cells that span a whole row. */
td[colspan] { color: var(--slate); }

/* Column alignment. Text reads left; short readings centre; long-text columns (Note, Key,
   Params, Answers) stay left whatever the rule says.

   Money is the one split: the FIGURES are right-aligned so decimal points stack ($2.00 over
   $0.25 over $12.50), while the HEADING is centred over them (owner call 2026-08-28). The
   label names the column; the numbers are what you scan. So .money on a th and on a td do
   deliberately different things - don't "fix" one to match the other. */
.money { font-variant-numeric: tabular-nums; white-space: nowrap; }
td.money { text-align: right; }
th.money { text-align: center; }
td.mid, th.mid { text-align: center; }

/* Two actions in one cell need a gap; without it View and Claim collide. */
td a + form, td form + form { margin-left: .625rem; }
td small { display: inline-block; margin-right: .4rem; }
td code { font-size: 11.5px; }

progress {
  vertical-align: middle;
  width: 76px;
  height: 6px;
  border: 0;
  border-radius: 3px;
  background: var(--hull);
  accent-color: var(--beacon);
}
progress::-webkit-progress-bar { background: var(--hull); border-radius: 3px; }
progress::-webkit-progress-value { background: var(--beacon); border-radius: 3px; }

/* --- stat row ------------------------------------------------------------------------ */

/* Sized to its contents, not to the shell: three figures stretched across 1120px read as three
   unrelated panels rather than one balance. */
.stats {
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
  width: fit-content;
  max-width: 100%;
  background: var(--hull);
  border: 1px solid var(--hull);
  border-radius: var(--radius);
  overflow: hidden;
  margin: 0 0 1.25rem;
}
.stats > div {
  flex: 0 1 auto;
  min-width: 9.5rem;
  background: var(--paper);
  padding: .6rem .9rem .7rem;
}
.stats small {
  display: block;
  font-size: 10.5px;
  font-weight: 650;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--slate);
  margin-bottom: .3rem;
}
.stats strong {
  font-family: var(--display);
  color: var(--ink-hi);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.015em;
  font-variant-numeric: tabular-nums;
}

/* --- chips: the lifecycle vocabulary -------------------------------------------------- */

.chip {
  display: inline-block;
  padding: .1rem .45rem;
  border-radius: 3px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  white-space: nowrap;
  background: var(--wait-wash);
  color: var(--wait);
}

/* Every value here is an enum member that reaches a page: AssignmentState, BatchState,
   UnitState, MembershipState, PayoutState. An unmatched [data-state] silently inherits the
   neutral default below, so a state added without a rule gets the WRONG colour and nothing
   errors - tests/test_chips.py::test_every_state_has_a_chip_rule is what makes that go red. */
.chip[data-state="active"],
.chip[data-state="approved"],
.chip[data-state="complete"],
.chip[data-state="sent"]        { background: var(--ok-wash);   color: var(--ok); }

.chip[data-state="claimed"],
.chip[data-state="invited"],
.chip[data-state="pending"]     { background: var(--hold-wash); color: var(--hold); }

.chip[data-state="submitted"],
.chip[data-state="available"]   { background: var(--wait-wash); color: var(--wait); }

.chip[data-state="paused"],
.chip[data-state="returned"]    { background: var(--warn-wash); color: var(--warn); }

.chip[data-state="rejected"],
.chip[data-state="expired"],
.chip[data-state="expired_while_held"],
.chip[data-state="disqualified"],
.chip[data-state="failed"]      { background: var(--stop-wash); color: var(--stop); }

/* The one in the top bar, naming the deployment. */
.chip-demo {
  background: transparent;
  color: var(--slate);
  border: 1px solid var(--hull);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .08em;
  font-size: 9.5px;
  padding: .12rem .4rem;
}

/* --- forms and buttons ---------------------------------------------------------------- */

/* WCAG 1.4.11 wants 3:1 on the mark that identifies a control, against ADJACENT colour.
   The first pass gave buttons a --paper face on a --paper panel with a --hull edge, which
   measured 1.08:1 face and 1.47:1 edge - a control that was only findable by reading its
   label. --raise as a face does not fix it either (1.26:1); the ramp between --paper and
   --hull is too short, so the edge has to carry the criterion and only --slate (7.14:1)
   and --beacon-deep (5.75:1) clear it. --slate wins because goldenrod is already spoken
   for by :focus-visible below, and a hover that borrowed it would blur the focus ring.
   Resting state is now the STRONG one; hover escalates. Measured by
   docs/reviews/affordance-contrast.py, which parses these very declarations. */
button {
  font: 500 13px/1 var(--sans);
  color: var(--ink);
  background: var(--raise);
  border: 1px solid var(--slate);
  border-radius: 4px;
  padding: .4rem .7rem;
  cursor: pointer;
}
button:hover { background: var(--hull); border-color: var(--ink); }
button:active { background: var(--deck); }

/* The primary action is marked explicitly on the button. A `:last-of-type` heuristic was tried
   first and picked up "Log out" and "Remove authenticator"; the specificity needed to exclude
   those also outranked the quiet in-table style, so the class is the honest version. */
.primary {
  background: var(--beacon);
  border-color: var(--beacon);
  color: var(--on-beacon);
}
.primary:hover { background: var(--beacon-deep); border-color: var(--beacon-deep); }
td button.primary { background: var(--beacon); border-color: var(--beacon); color: var(--on-beacon); }
td button.primary:hover { background: var(--beacon-deep); border-color: var(--beacon-deep); }

/* Row actions inside tables stay quiet - many of them per page. Quiet is the FACE, not the
   edge: the flat --paper face keeps a review table calm, and the --slate edge is what makes
   each one identifiable as a control (6.63:1 against the row it sits on).
   The label never wraps. A wrappable label under-reports the button's min-content (its longest
   word), so a column at min-content - a `.fit` table at 360px - sizes to that word plus the
   sibling margin below, while at layout the inline-block button shrink-to-fits against the CELL
   and ignores its parent form's margin: "Revoke sessions" rendered two lines tall and 10px past
   its cell, 1.19px from the table's overflow clip, with no focus ring on its right side
   (measured 2026-09-01, /admin/requesters/1 at 360px). nowrap makes min-content the real width,
   so the column reserves margin + button and the overflow drops to 0.01px (clearance 11.19px).
   tests/test_stylesheet.py NOWRAP_REQUIRED guards it. */
td button { padding: .25rem .55rem; font-size: 12px; background: var(--paper); border-color: var(--slate); color: var(--ink); white-space: nowrap; }
td button:hover { border-color: var(--ink); background: var(--raise); }
td form { display: inline; }

/* A field keeps the RECESSED face on purpose - a well you type into, the opposite affordance
   from the raised key above - so its edge alone carries 1.4.11, same --slate as the buttons. */
input, select, textarea {
  font: 400 13px/1.4 var(--sans);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--slate);
  border-radius: 4px;
  padding: .38rem .5rem;
}
input:hover, select:hover, textarea:hover { border-color: var(--ink); }
::placeholder { color: var(--slate); opacity: 1; }
input[type="checkbox"] { accent-color: var(--beacon); width: 15px; height: 15px; }
input[type="file"] { border: 0; padding-left: 0; }

label { color: var(--slate); }
label input, label select { margin-left: .35rem; color: var(--ink); }

/* Stacked, labelled forms (sign-in, upload, account, invite): one control per line. */
form p { margin: 0 0 .75rem; display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .875rem; }
/* The label wraps like its parent. Without this it is a single nowrap flex line, and a wide
   `size=` input refuses to shrink below its intrinsic width (min-width:auto), so the siblings
   absorb the over-constraint by collapsing to MIN-CONTENT instead. For a text run that means
   breaking at spaces; for `code`, whose `overflow-wrap: anywhere` above puts its min-content
   at one character, it meant rendering `${column}` vertically on the upload form. */
form p label { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .4rem; min-width: 0; }
/* ...and wrapping alone is not enough: as a flex ITEM the label keeps min-width:auto, which floors
   it at its own min-content - and a `size=` input's min-content IS its intrinsic width. So the
   label overflowed its `p` and pushed the whole document wider than the viewport rather than
   crushing anything (measured 2026-08-31 at a 500px viewport: p 430, label 520, input 514,
   documentElement.scrollWidth 555). BOTH declarations below are load-bearing - probed live, each
   one alone leaves scrollWidth at 555; together they take it to 500. */
form input, form select, form textarea { max-width: 100%; }

/* A form that is the whole page gets a card. */
.panel {
  background: var(--paper);
  border: 1px solid var(--hull);
  border-radius: var(--radius);
  padding: 1.125rem 1.25rem;
  margin: 0 0 1.25rem;
  max-width: 640px;
}
.panel h2:first-child { margin-top: 0; }

/* Review page: the per-row approve/reject controls. Selected structurally because the
   markup of those buttons is asserted byte-for-byte by tests/test_requester.py. */
td button[value="reject"] { color: var(--stop); }
td button[value="reject"]:hover { border-color: var(--stop); }
td button[value="approve"] { color: var(--ok); }
td button[value="approve"]:hover { border-color: var(--ok); }
td input[name="reason"] { width: 11rem; font-size: 12px; padding: .25rem .45rem; }
td > button + input, td > input + button { margin-left: .25rem; }

/* --- task frame ------------------------------------------------------------------------ */

/* Label/value pairs spread across the strip, after the reference: the batch title anchors the
   left, the readings sit to the right of it, and the action ends the row. */
.taskbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: .25rem 1.5rem;
  background: var(--paper);
  border: 1px solid var(--hull);
  border-radius: var(--radius) var(--radius) 0 0;
  border-bottom: 0;
  padding: .4rem .875rem;
  font-size: 12.5px;
  color: var(--ink);
}
.taskbar strong { color: var(--ink-hi); font-weight: 600; }
.taskbar .meta { white-space: nowrap; }
.taskbar .meta b { font-weight: 400; color: var(--slate); margin-right: .4rem; }
.taskbar form { display: inline; }
.taskbar button { padding: .2rem .6rem; font-size: 12px; }
.taskbar .left { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink-hi); }

.frame {
  display: block;
  width: 100%;
  height: 70vh;
  background: var(--paper);
  border: 1px solid var(--hull);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
}

/* --- filters ---------------------------------------------------------------------------- */

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .375rem;
  margin: 0 0 1rem;
}
.filters a, .filters strong {
  padding: .3rem .6rem;
  border-radius: 4px;
  font-size: 13px;
  text-decoration: none;
  border: 1px solid transparent;
}
/* The shared rule above sets a TRANSPARENT edge, so the three filters you are not currently on
   were unmarked text until hovered - the same rest-vs-hover inversion the buttons had. They take
   the same --slate resting edge and the same escalation to --ink. The selected filter keeps its
   --paper face and bold weight to stay distinct, and moves off --hull (1.47:1) so the CURRENT
   state is not the weakest-bounded thing in the row. */
.filters a { color: var(--slate); border-color: var(--slate); }
.filters a:hover { background: var(--paper); color: var(--ink); border-color: var(--ink); }
.filters strong { background: var(--paper); border-color: var(--slate); color: var(--ink); font-weight: 600; }
.filters .sep { color: var(--hull); }

/* --- task workspace ---------------------------------------------------------------------- */

/* The work and view pages give the whole viewport to the requester's task: the two bars sit
   at the top and the iframe takes everything under them, edge to edge. Nothing on this page
   scrolls - the task scrolls inside its own frame - so the column is sized in dvh and every
   child is allowed to shrink (min-height: 0) rather than forcing the body taller. */
body.taskframe {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
body.taskframe .bar { max-width: none; min-height: 36px; padding: 0 .875rem; }
body.taskframe main {
  flex: 1;
  min-height: 0;
  max-width: none;
  /* `margin: 0 auto` from the base rule must go too: an auto margin on a flex item beats
     align-items: stretch, so the column stayed shrink-to-fit and centred. */
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}
/* Both bars run lean here - every pixel they give up goes to the task. */
body.taskframe .taskbar {
  border-radius: 0;
  border-left: 0;
  border-right: 0;
  border-top: 0;
  padding: .3rem .875rem;
}
body.taskframe .frame {
  flex: 1;
  min-height: 0;
  height: auto;
  border-radius: 0;
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
}

/* --- small screens ---------------------------------------------------------------------- */

@media (max-width: 820px) {
  .bar { padding: .5rem .875rem; }
  .who { margin-left: 0; width: 100%; }
  main { padding: 1.25rem .875rem 3rem; }
  /* `.scroll { overflow-x }` is deliberately NOT here - see the surfaces section. Only the
     narrow-screen minimum belongs in this block: it is what forces the scroll below 820px. */
  .scroll table { min-width: 700px; }
  /* `.fit` opts a table out of that minimum: it wants the scroller as an overflow guard, not a
     700px floor it does not need. The Workers and digest tables are 445-480px wide and read
     fine at 500px - the minimum above would push them into a scrollbar to solve nothing, while
     without a scroller at all they took the whole document sideways at 360px (measured
     2026-08-31: /publisher/workers scrollWidth 480 against a 360 viewport). */
  .scroll.fit table { min-width: 0; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
