/**
 * Workroom, a Digital Umbrella product.
 *
 * ── THE TOKENS ARE THE CATALOGUE'S ──────────────────────────────────────────
 *
 * Until 16 September 2026 this was ZynoStack's own warm-paper palette, because
 * it was ZynoStack's own tool. It is now product four, so the values below are
 * Digital Umbrella's, copied from `repos/brand/digital-umbrella/tokens.css` and
 * from the nearest SHIPPING implementation of that token set, the hub's
 * `web/index.html` :root block, rather than retyped out of a specification.
 * `du-platform/docs/07-DESIGN-SYSTEM.md` holds the four closed decisions:
 * dark-first on #08090D, IBM Plex Sans with JetBrains Mono, #4D9EFF as the
 * accent, and no build step.
 *
 * NO VALUE BELOW WAS CHOSEN HERE AND NO RATIO BELOW WAS ESTIMATED. Every pair
 * is recomputed from these declarations by `tests/web-design.test.js`, which
 * fails the build if one drops AND if a number in this header stops being the
 * number the tokens produce. Floors: 4.5 for text at any size (this product
 * uses no large-text exemption), 3.0 for a control boundary or the focus ring.
 *
 *   TEXT                      on --paper   on --bg   on --surface
 *   --ink     #F7F8FB            18.43       18.74       18.06
 *   --ink-2   #C8CEDA            12.39       12.60       12.15
 *   --ink-3   #99A1B2             7.54        7.67        7.39
 *   --accent  #4D9EFF             7.13        7.25        6.99
 *   --good    #48C78E             9.17        9.32        8.98
 *   --warn    #F5A524             9.59        9.75        9.40
 *   --bad     #FF6B6B             7.05        7.17        6.91
 *
 *   TEXT ON A TINTED CHIP     --accent-dim  --good-dim  --warn-dim  --bad-dim
 *   --ink                         15.62       14.46       15.13       16.31
 *   its own colour (accent/good/warn/bad)
 *                                  6.04        7.19        7.88        6.24
 *
 *   BOUNDARIES AND THE RING (floor 3.0)
 *   --edge       #5A6478 on --paper 3.29, --bg 3.34, --surface 3.22
 *   --accent     #4D9EFF on --paper 7.13, --bg 7.25, --surface 6.99   (the ring)
 *   --accent-hi  #7DB8FF on --paper 9.48, --bg 9.64, --surface 9.29   (hover fill)
 *   --ink-3      #99A1B2 on --good-dim 5.92, --warn-dim 6.20
 *                a control standing INSIDE a tinted notice, where --edge is
 *                2.58 and 2.70 and the control's own --paper fill is 1.27.
 *
 *   FILLS
 *   --on-accent on --accent 7.30, on --accent-hi 9.71
 *   --paper on --ink 18.43 (the toast)  |  --paper on --bad 7.05 (the failure)
 *
 * ── WHAT INVERTED WHEN THE PALETTE DID ──────────────────────────────────────
 *
 * THE ACCENT NOW CARRIES TEXT, and the old rule said the opposite. Brass
 * #c97a00 measured 3.26 on paper, so it was a fill and never a letter; #4D9EFF
 * measures 7.13 on --paper and clears the text floor on every ground, which is
 * why links, the "Doing" chip and the focus ring are all set in it. The rule
 * did not disappear, it moved: the two values below are the ones that are
 * never text now, and the design check fails on either of them as a `color`.
 *
 *   --line        #1C212C  1.21 on --paper. It separates REGIONS and must never
 *                 border an interactive control, which needs 3.0: that is
 *                 --edge. Five of the ten live accessibility failures in the
 *                 catalogue were this one confusion.
 *   --accent-line #123A6E  1.73 on --paper. A border and a tint edge only.
 *
 * SURFACE SEPARATION IS WEAKER THAN IT WAS, and that is stated rather than
 * hidden because it decides how the layout holds together: --bg to --paper is
 * 1.017 and --bg to --surface 1.038, against 1.138 and 1.076 on the old warm
 * palette. Neither reads as a boundary at all, so on this palette the BORDERS
 * are the layout: every card, column and panel draws a 1px --line rule, and
 * every control draws an --edge one. Remove them and the page is one flat
 * near-black rectangle.
 *
 * That is also why `.row` and `.task` moved from --line to --edge. Both are
 * real <button> elements, so WCAG 1.4.11 binds their boundary at 3:1, and on
 * the old palette a 1.38 hairline around a pressable row was already wrong and
 * merely harder to see. The design check now names both.
 *
 * ── THE TYPEFACE IS NAMED AND NOT SHIPPED ───────────────────────────────────
 *
 * --font leads with 'IBM Plex Sans' and --mono with 'JetBrains Mono', which is
 * what the hub declares and what 07-DESIGN-SYSTEM.md decided. NEITHER FILE IS
 * IN THIS REPOSITORY, so both fall through to the system stack exactly as the
 * hub's pages do today: the brand's colour has moved and its typography has
 * not. Shipping them is two woff2 files under `web/fonts/`, one @font-face
 * block here, the OFL text beside them, and a change to the design test's "no
 * font file" rule; `font-src 'self'` in the page's CSP already allows it and
 * `scripts/lib/deploy-web.mjs` already knows the media type. It is written up
 * in `docs/BACKLOG.md` rather than done here, because copying brand binaries
 * into a fourth repository with no sync is the second copy that drifts.
 *
 * No external resource of any kind: no font file, no icon set, no image. The
 * page's own Content-Security-Policy allows none, so anything added here that
 * fetches would fail silently rather than 404 visibly.
 */

:root {
  /* ground. --paper is the catalogue's --panel (the main surface: cards,
     panels, the topbar, inputs) and --surface its --panel-2 (insets: column
     grounds, chips, table heads, hover). The names stay this product's because
     every rule below is written in them; the VALUES are the catalogue's. */
  --bg: #08090D;
  --paper: #0A0C10;
  --surface: #0C0F15;

  /* ink */
  --ink: #F7F8FB;
  --ink-2: #C8CEDA;
  --ink-3: #99A1B2;

  /* accent: the catalogue's neon blue */
  --accent: #4D9EFF;
  --accent-hi: #7DB8FF;
  --accent-dim: #0E1F35;
  --accent-line: #123A6E;
  --on-accent: #06080C;

  /* boundaries */
  --line: #1C212C;
  --edge: #5A6478;

  /* states */
  --good: #48C78E;
  --good-dim: #0F2A1B;
  --warn: #F5A524;
  --warn-dim: #2C1F08;
  --bad: #FF6B6B;
  --bad-dim: #2E1214;

  /* type. The brand's families lead and the system stack catches them: no font
     is downloaded here, so the first paint is the only paint and there is no
     reflow when a webfont arrives. See the header. */
  --font: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --t-page: 1.5rem;
  --t-h2: 1.125rem;
  --t-body: 0.9375rem;
  --t-small: 0.8125rem;
  --t-label: 0.75rem;

  /* spacing. THIS SCALE STAYS THIS PRODUCT'S OWN and deliberately does not
     become the hub's (4/8/12/18/24/36): it is not a brand token, nobody can see
     which one a page uses, and moving every padding by 2px to match a sibling
     is a diff across every screen that buys no brand at all. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;

  /* radius: the catalogue's 6px, which is what the siblings draw. */
  --r: 6px;
  --r-pill: 999px;

  /* control metrics. 44px is the touch target floor and applies to every
     control, including the ones only ever clicked with a mouse: a board is read
     on a phone on the way to a meeting. It is deliberately taller than the
     catalogue's 36px, which is a marketing site's number rather than a tool's. */
  --ctl-h: 44px;
  --maxw: 1180px;

  color-scheme: dark;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--bg);
  color: var(--ink);
  font: var(--t-body)/1.5 var(--font);
  min-height: 100vh;
}

/* The `hidden` attribute is only a UA-stylesheet `display:none`, so any author
   rule that sets display beats it at any specificity and the element simply
   stays on screen with no error. One `!important` here kills that whole family;
   per-selector patches grow one selector at a time and never finish. */
[hidden] { display: none !important; }

h1, h2, h3 { font-weight: 600; letter-spacing: -0.01em; line-height: 1.25; }
h1 { font-size: var(--t-page); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-body); }

a { color: var(--accent); }
a:hover { color: var(--ink); }

/* One ring, everywhere, and it is replaced rather than removed: a rule that
   strips the outline without offering anything back strands every keyboard
   user, and there is no way to see that in a screenshot. */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r);
}

.skip {
  position: absolute;
  left: -9999px;
  top: var(--s-2);
  background: var(--paper);
  color: var(--ink);
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--edge);
  border-radius: var(--r);
  z-index: 40;
}
.skip:focus { left: var(--s-4); }

/* -- header ---------------------------------------------------------------- */

.top {
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  border-top: 3px solid var(--accent);
}
.top-in {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--s-3) var(--s-4);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
}
/* The wordmark is Digital Umbrella's and the product name sits beside it, which
   is the catalogue's own order: the vendor owns the mark, the product owns the
   screen. `.mark` is the hex canopy from repos/brand/digital-umbrella/logo and
   it takes currentColor, so it is painted in the accent in both places it is
   drawn, here and on the gate, while the words beside it take the ink. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-body);
  font-weight: 700;
  letter-spacing: 0.02em;
  margin-right: auto;
  text-decoration: none;
  color: var(--ink);
}
.brand span { color: var(--ink-3); font-weight: 400; }
.brand:hover { color: var(--ink); }
.brand .mark { color: var(--accent); flex: none; }

.who {
  font-size: var(--t-small);
  color: var(--ink-2);
  display: flex;
  align-items: center;
  gap: var(--s-2);
}

.nav {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--s-4);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1);
}
.nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--ctl-h);
  padding: 0 var(--s-3);
  color: var(--ink-2);
  text-decoration: none;
  font-size: var(--t-small);
  font-weight: 600;
  border-bottom: 3px solid transparent;
}
.nav a:hover { color: var(--ink); background: var(--surface); }
.nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }

/* -- shell ----------------------------------------------------------------- */

main {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--s-5) var(--s-4) var(--s-6);
}

.head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}
.head p { color: var(--ink-2); font-size: var(--t-small); }
/* A control sharing the heading row sits at the far end and on its own
   baseline, not on the heading's. */
.head .field { margin-left: auto; align-self: end; }

.sub { color: var(--ink-2); font-size: var(--t-small); }
.muted { color: var(--ink-3); font-size: var(--t-small); }

/* -- controls -------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: var(--ctl-h);
  padding: 0 var(--s-4);
  font: 600 var(--t-small)/1 var(--font);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--edge);
  border-radius: 0;
  cursor: pointer;
}
.btn:hover { background: var(--surface); }
.btn[disabled] { cursor: not-allowed; color: var(--ink-3); background: var(--surface); }

/* Flat, as the catalogue draws it. The old palette gave this a 3px deeper
   bottom edge to read as a pressed key; on a near-black ground a bright fill
   at 7.30:1 is already the loudest thing on the page and the bevel only made
   it look like a different product. */
.btn-primary {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}
.btn-primary:hover { background: var(--accent-hi); border-color: var(--accent-hi); color: var(--on-accent); }

.btn-quiet { background: transparent; border-color: var(--edge); }
.btn-quiet:hover { background: var(--surface); }

.btn-danger { color: var(--bad); border-color: var(--edge); }
.btn-danger:hover { background: var(--bad-dim); color: var(--bad); }

/* -- fields ---------------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: var(--s-1); }
.field > label {
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.field input,
.field select,
.field textarea {
  min-height: var(--ctl-h);
  padding: var(--s-2) var(--s-3);
  font: var(--t-body)/1.4 var(--font);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--edge);
  border-radius: var(--r);
  width: 100%;
}
.field textarea { min-height: 88px; resize: vertical; }
.field input::placeholder,
.field textarea::placeholder { color: var(--ink-3); opacity: 1; }
.field .hint { font-size: var(--t-label); color: var(--ink-3); }
/* A single control standing on its own in a panel. Stretched to the full
   width of the page it reads as a search box for everything, not as a filter
   on the board beneath it. */
.field-narrow { max-width: 320px; }

.check {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-height: var(--ctl-h);
  font-size: var(--t-small);
  color: var(--ink-2);
}
.check input { width: 20px; height: 20px; accent-color: var(--accent); }

/* Fields align at the TOP so their labels line up, and the submit button takes
   a row of its own. Bottom-aligning them instead lined the inputs up with the
   bottom of whichever field happened to carry a hint, which made a tidy form
   look like a mistake. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--s-3);
  align-items: start;
}
.form-grid > .btn { grid-column: 1 / -1; justify-self: start; }
/* A checkbox has no label above it, so left at the top of its cell it floats
   a line higher than every input beside it. */
.form-grid > .check { align-self: end; }

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

.card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s-4);
}
.card + .card { margin-top: var(--s-3); }

.panel {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s-4);
  margin-bottom: var(--s-5);
}
.panel > h2 { margin-bottom: var(--s-3); }
/* The board comes first on this page, so the panel beneath it needs the space
   above that the board's own margin does not provide. */
.panel-top { margin-top: var(--s-5); }

.empty {
  background: var(--paper);
  border: 1px dashed var(--edge);
  border-radius: var(--r);
  padding: var(--s-6) var(--s-4);
  text-align: center;
  color: var(--ink-2);
}
.empty strong { display: block; color: var(--ink); margin-bottom: var(--s-1); }

/* A notice is not a control, so its border is not held to 3:1 by WCAG 1.4.11.
   It is given the state's own colour anyway, because --edge measures 2.70 on
   --warn-dim and an outline fainter than the fill it surrounds reads as a
   rendering fault rather than as a frame. */
.notice {
  border: 1px solid var(--warn);
  border-left-width: 4px;
  border-radius: var(--r);
  padding: var(--s-4);
  background: var(--warn-dim);
  color: var(--ink);
  margin-bottom: var(--s-5);
}
/* The same box when the news is good: somebody has just joined a practice. */
.notice-good { border-color: var(--good); background: var(--good-dim); }
/* Inside a panel, under the control that produced it, rather than standing on
   its own above the page. */
.notice-inline { margin: var(--s-4) 0 0; }
/* EVERY child, not just the heading. The rule here used to be `h2 {
   margin-bottom }`, which was enough while the only notice on the page was two
   paragraphs under a heading. The joining link puts a field, a button and a
   footnote in one, and they arrived stacked against each other with the label
   touching the sentence above it: the global reset zeroes every margin, so
   anything not given one has none. Rendered at 1280 before this was fixed. */
.notice > * + * { margin-top: var(--s-3); }
.notice code {
  font-family: var(--mono);
  font-size: var(--t-small);
}
/* A CONTROL INSIDE A NOTICE IS STILL A CONTROL, so it takes --ink-3 and not
   --edge. The notice's own border is exempt from WCAG 1.4.11 because a notice
   is not pressable; the field and the button the joining link puts inside one
   are not exempt, and that is the single screen in this product that shows a
   bearer credential. On the tinted grounds --edge measures 2.58 on --good-dim
   and 2.70 on --warn-dim, and the control's own --paper fill measures 1.27 and
   1.22 against the same ground, so with --edge NEITHER the border nor the fill
   identifies the control at all. --ink-3 measures 5.92 and 6.20 there. Same
   precedent as the chips below: the ground decides the boundary token. */
.notice .btn,
.notice .field input { border-color: var(--ink-3); }

/* -- chips ----------------------------------------------------------------- */

/* A chip always carries its own word. Colour is a second signal and never the
   only one: on a monochrome screen, or to anyone who does not separate red from
   green, the label is what is left.

   EACH TINTED CHIP IS OUTLINED IN ITS OWN COLOUR rather than in --edge, and
   that is arithmetic rather than taste: --edge measures 2.79 on --accent-dim,
   2.58 on --good-dim, 2.70 on --warn-dim and 2.91 on --bad-dim, all under the
   3.0 floor, while each state's own colour clears 6.04 or better against its
   own fill. The neutral chip keeps --edge, which clears on all three grounds. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--edge);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  background: var(--surface);
  color: var(--ink-2);
}
.chip-doing { background: var(--accent-dim); color: var(--accent); border-color: var(--accent); }
.chip-blocked { background: var(--bad-dim); color: var(--bad); border-color: var(--bad); }
.chip-done { background: var(--good-dim); color: var(--good); border-color: var(--good); }
.chip-high { background: var(--warn-dim); color: var(--warn); border-color: var(--warn); }
.chip-overdue { background: var(--bad-dim); color: var(--bad); border-color: var(--bad); }
.chip-soon { background: var(--warn-dim); color: var(--warn); border-color: var(--warn); }

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--edge);
  color: var(--ink-2);
  font-size: var(--t-label);
  font-weight: 700;
  flex: none;
}

/* -- lists ----------------------------------------------------------------- */

.rows { list-style: none; display: flex; flex-direction: column; gap: var(--s-2); }
.row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  text-align: left;
  background: var(--paper);
  /* --edge, not --line: a row IS a <button>, so WCAG 1.4.11 wants 3:1 on the
     boundary that identifies it, and on this ground the border is the only
     thing that does (--bg to --paper is 1.017). */
  border: 1px solid var(--edge);
  border-radius: var(--r);
  padding: var(--s-3) var(--s-4);
  min-height: var(--ctl-h);
  font: var(--t-body)/1.4 var(--font);
  /* A row is a surface you press, not a sentence with a link in it, so it
     takes the body's ink and drops the underline the anchor rule gives it. */
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
}
.row:hover { background: var(--surface); color: var(--ink); }
.row:hover .row-title { text-decoration: underline; }
.row-main { flex: 1 1 260px; min-width: 0; }
.row-title { font-weight: 600; }
.row-meta { color: var(--ink-2); font-size: var(--t-small); }
.row-tags { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; }

table { border-collapse: collapse; width: 100%; font-size: var(--t-small); }
caption { text-align: left; color: var(--ink-2); padding-bottom: var(--s-2); }
th, td {
  text-align: left;
  padding: var(--s-3);
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
thead th {
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: var(--surface);
}
/* A row header is DATA that happens to name its row. Given the column-header
   treatment it shouts a person's name in tracked capitals, which reads as a
   heading rather than as the row it labels. */
th[scope="row"] { font-weight: 600; color: var(--ink); }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
th.num { text-align: right; }
.table-wrap { overflow-x: auto; background: var(--paper); border: 1px solid var(--line); border-radius: var(--r); }

/* -- the board ------------------------------------------------------------- */

.board {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-3);
  align-items: start;
}
.col {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s-2);
  min-width: 0;
}
.col > h3 {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
  padding: var(--s-2);
}
.col-n { font-variant-numeric: tabular-nums; color: var(--ink-2); }
.col ul { list-style: none; display: flex; flex-direction: column; gap: var(--s-2); }
.col .kids { margin: var(--s-2) 0 0 var(--s-4); border-left: 2px solid var(--line); padding-left: var(--s-2); }

.task {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--paper);
  /* A card is a <button> too: 3:1 on every side, and the left rail carries the
     state on top of that. */
  border: 1px solid var(--edge);
  border-left: 3px solid var(--edge);
  border-radius: var(--r);
  padding: var(--s-3);
  font: var(--t-body)/1.35 var(--font);
  color: var(--ink);
  cursor: pointer;
}
.task:hover { background: var(--accent-dim); }
.task-title { font-weight: 600; }
.task-line {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
  margin-top: var(--s-2);
  font-size: var(--t-small);
  color: var(--ink-2);
}
.task-of { font-size: var(--t-label); color: var(--ink-3); }
.task-overdue { border-left-color: var(--bad); }
.task-soon { border-left-color: var(--warn); }
/* Full-strength --accent: the marker sits between the card (--paper) and the
   column ground (--surface) and measures 7.13 and 6.99 against them. On the old
   warm palette the same token measured 2.67 against the darker of the two and a
   deeper variant had to be used instead. The chip beside the title is still the
   signal that carries; this edge only has to be visible. */
.task-high { border-left-color: var(--accent); }

/* -- the side panel -------------------------------------------------------- */

/* A native <dialog> opened with showModal(). The focus trap, the inert page
   behind it and Escape are the browser's, which is both less code and more
   correct than a hand-rolled trap that has to be right in every browser. */
dialog {
  border: none;
  padding: 0;
  background: transparent;
  color: var(--ink);
  max-width: none;
  max-height: none;
}
/* Darker and denser than the 0.45 the light palette used: a scrim has to darken
   what is behind it, and on a #08090D page a light veil does nothing at all. */
dialog::backdrop { background: rgba(2, 3, 5, 0.72); }

.side {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(480px, 100%);
  height: 100%;
  background: var(--paper);
  border-left: 1px solid var(--edge);
  display: flex;
  flex-direction: column;
}
.side-top {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4);
  border-bottom: 1px solid var(--line);
}
.side-top h2 { flex: 1; min-width: 0; }
.side-body { flex: 1; overflow-y: auto; padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-4); }
/* One column in a 480px panel. The page's auto-fit grid gives two here, and a
   half-width date field beside a half-width title is harder to fill in than
   either would be on its own. */
.side .form-grid { grid-template-columns: minmax(0, 1fr); }
.side-foot { padding: var(--s-4); border-top: 1px solid var(--line); display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* Fields inside one form still need air between them: the side panel's own
   gap only separates its top-level children, and a form is one of those. */
.side-body form .field + .field,
.side-body form .mid + .field { margin-top: var(--s-3); }

.mid {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-4) 0 var(--s-2);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.mid::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.comments { list-style: none; display: flex; flex-direction: column; gap: var(--s-2); }
.comments li {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: var(--s-3);
  font-size: var(--t-small);
}
.comments .by { color: var(--ink-2); font-size: var(--t-label); }

.feed { list-style: none; display: flex; flex-direction: column; gap: var(--s-2); }
.feed li { font-size: var(--t-small); color: var(--ink-2); padding-left: var(--s-3); border-left: 2px solid var(--line); }
.feed time { color: var(--ink-3); font-size: var(--t-label); display: block; }

/* -- the live region ------------------------------------------------------- */

/* Both regions live in one fixed stack so an error and a save cannot land on
   top of each other. Empty regions occupy nothing, so the stack is invisible
   until there is something to say. */
.say {
  position: fixed;
  left: 50%;
  bottom: var(--s-4);
  transform: translateX(-50%);
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  max-width: calc(100% - 2 * var(--s-4));
  pointer-events: none;
}
.say p {
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--r);
  padding: var(--s-2) var(--s-4);
  font-size: var(--t-small);
}
.say p.bad { background: var(--bad); color: var(--paper); }

/* Announced, never shown. Not `display:none`, which removes it from the
   accessibility tree along with the announcement. */
.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* -- narrow ---------------------------------------------------------------- */

/* The board stacks to one column. Four columns squeezed into 400px is four
   unreadable columns, and a horizontally scrolling board hides the two columns
   that matter most on the day something is blocked. */
@media (max-width: 860px) {
  .board { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 460px) {
  main { padding: var(--s-4) var(--s-3) var(--s-5); }
  .top-in, .nav { padding-left: var(--s-3); padding-right: var(--s-3); }
  h1 { font-size: 1.25rem; }
  .head .field { margin-left: 0; flex: 1 1 100%; max-width: none; }
  .side { width: 100%; }
  /* Form buttons go full width because they end a stacked column of fields and
     a narrow button under a wide field reads as unfinished. The header's
     buttons deliberately do NOT: a full-width bar there pushes the first task
     off the screen, which is the whole page on a phone. */
  .side-foot .btn, .form-grid .btn { width: 100%; }
}

@media (prefers-reduced-motion: no-preference) {
  .btn, .row, .task, .nav a { transition: background-color 120ms ease; }
}

/* The signed-out gate: the one thing a visitor sees. Centred, the wordmark, the
   product name, one control, and no copy about what is behind it (Hisham, 16
   Sep 2026). The same shape the hub's gate has, so a customer who signs into
   two Digital Umbrella products meets one door twice. */
.gate {
  /* The gate is the whole page: app.js hides the topbar and the navigation on
     this one screen, so nothing else is competing for the height. 50vh, which
     is what this was while the topbar stayed, parked the wordmark in the top
     half with an empty half beneath it. 76vh plus main's own padding still
     clears the viewport at any height, so the page never gains a scrollbar for
     the sake of centring three lines. */
  min-height: 76vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  text-align: center;
}
.gate-brand {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  color: var(--ink);
  font-size: var(--t-h2);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.gate-brand .mark { color: var(--accent); flex: none; }
.gate-name { color: var(--ink-3); font-size: var(--t-small); letter-spacing: 0.08em; text-transform: uppercase; }
.gate .btn { min-width: 12rem; }
